org.eclipse.elk.core.service.DiagramLayoutEngine.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.elk.core.service.DiagramLayoutEngine.java

Source

/*******************************************************************************
 * Copyright (c) 2011, 2015 Kiel University and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Kiel University - initial API and implementation
 *******************************************************************************/
package org.eclipse.elk.core.service;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.elk.core.GraphIssue;
import org.eclipse.elk.core.GraphValidationException;
import org.eclipse.elk.core.IGraphLayoutEngine;
import org.eclipse.elk.core.LayoutConfigurator;
import org.eclipse.elk.core.LayoutOptionValidator;
import org.eclipse.elk.core.data.LayoutMetaDataService;
import org.eclipse.elk.core.data.LayoutOptionData;
import org.eclipse.elk.core.klayoutdata.KShapeLayout;
import org.eclipse.elk.core.options.CoreOptions;
import org.eclipse.elk.core.options.PortConstraints;
import org.eclipse.elk.core.options.SizeConstraint;
import org.eclipse.elk.core.service.util.MonitoredOperation;
import org.eclipse.elk.core.util.BasicProgressMonitor;
import org.eclipse.elk.core.util.ElkUtil;
import org.eclipse.elk.core.util.IElkCancelIndicator;
import org.eclipse.elk.core.util.IElkProgressMonitor;
import org.eclipse.elk.core.util.IGraphElementVisitor;
import org.eclipse.elk.core.util.IValidatingGraphElementVisitor;
import org.eclipse.elk.core.util.Maybe;
import org.eclipse.elk.core.util.Pair;
import org.eclipse.elk.graph.KEdge;
import org.eclipse.elk.graph.KGraphElement;
import org.eclipse.elk.graph.KLabel;
import org.eclipse.elk.graph.KNode;
import org.eclipse.elk.graph.KPort;
import org.eclipse.elk.graph.properties.IProperty;
import org.eclipse.elk.graph.properties.IPropertyHolder;
import org.eclipse.elk.graph.properties.MapPropertyHolder;
import org.eclipse.elk.graph.properties.Property;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.statushandlers.StatusManager;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterators;
import com.google.common.collect.Multimap;
import com.google.inject.ConfigurationException;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.Singleton;

/**
 * The entry class for automatic layout of graphical diagrams.
 * Use this class to perform automatic layout on the content of a workbench part that contains
 * a graph-based diagram. The mapping between the diagram and the layout graph structure is managed
 * by a {@link IDiagramLayoutConnector} implementation, which has to be registered using the
 * {@code layoutConnectors} extension point.
 * 
 * <p>Subclasses of this class can be bound in an {@link ILayoutSetup} injector for customization.
 * Note that this class is marked as {@link Singleton}, which means that exactly one instance is
 * created for each injector, i.e. for each registered {@link ILayoutSetup}.</p>
 * 
 * @author msp
 * @kieler.design proposed by msp
 * @kieler.rating yellow 2012-07-05 review KI-18 by cmot, sgu
 */
@Singleton
public class DiagramLayoutEngine {

    /**
     * Configuration class for invoking the {@link DiagramLayoutEngine}.
     * Use a {@link LayoutConfigurator} to configure layout options:
     * <pre>
     * DiagramLayoutEngine.Parameters params = new DiagramLayoutEngine.Parameters();
     * params.addLayoutRun().configure(KNode.class)
     *         .setProperty(LayoutOptions.ALGORITHM, "org.eclipse.elk.algorithm.layered")
     *         .setProperty(LayoutOptions.SPACING, 30.0f)
     *         .setProperty(LayoutOptions.ANIMATE, true);
     * DiagramLayoutEngine.invokeLayout(workbenchPart, diagramPart, params);
     * </pre>
     * If multiple configurators are given, the layout is computed multiple times:
     * once for each configurator. This behavior can be used to apply different layout algorithms
     * one after another, e.g. first a node placer algorithm and then an edge router algorithm.
     * Example:
     * <pre>
     * DiagramLayoutEngine.Parameters params = new DiagramLayoutEngine.Parameters();
     * params.addLayoutRun().configure(KNode.class)
     *         .setProperty(LayoutOptions.ALGORITHM, "org.eclipse.elk.algorithm.force");
     * params.addLayoutRun().setClearLayout(true).configure(KNode.class)
     *         .setProperty(LayoutOptions.ALGORITHM, "de.cau.cs.kieler.kiml.libavoid");
     * DiagramLayoutEngine.invokeLayout(workbenchPart, diagramPart, params);
     * </pre>
     * <b>Note:</b> By using the {@link LayoutConfigurator} approach as shown above, the Layout
     * view does not have any access to the configured values and hence will not work correctly.
     * In order to support the Layout view, use the {@link ILayoutConfigurationStore} interface instead.
     */
    public static final class Parameters {

        private List<LayoutConfigurator> configurators = new LinkedList<LayoutConfigurator>();
        private MapPropertyHolder globalSettings = new MapPropertyHolder();
        private boolean overrideDiagramConfig = true;

        /**
         * Set whether to override the configuration from the {@link ILayoutConfigurationStore}
         * provided by the diagram layout manager with the configuration provided by this setup.
         * The default is {@code true}.
         */
        public Parameters setOverrideDiagramConfig(final boolean override) {
            this.overrideDiagramConfig = override;
            return this;
        }

        /**
         * Returns the global settings, i.e. options that are not processed by layout algorithms,
         * but by layout managers or the layout engine itself.
         */
        public IPropertyHolder getGlobalSettings() {
            return globalSettings;
        }

        /**
         * Add a layout run with the given configurator. Each invocation of this method corresponds
         * to a separate layout execution on the whole input graph. This can be used to apply
         * multiple layout algorithms one after another, where each algorithm execution can reuse
         * results from previous executions.
         * 
         * @return the given configurator
         */
        public LayoutConfigurator addLayoutRun(final LayoutConfigurator configurator) {
            configurators.add(configurator);
            configurator.setFilter(OPTION_TARGET_FILTER);
            return configurator;
        }

        /**
         * Convenience method for {@code addLayout(new LayoutConfigurator())}.
         */
        public LayoutConfigurator addLayoutRun() {
            return addLayoutRun(new LayoutConfigurator());
        }
    }

    /** preference identifier for debug graph output. */
    public static final String PREF_DEBUG_OUTPUT = "elk.debug.graph";
    /** preference identifier for execution time measurement. */
    public static final String PREF_EXEC_TIME_MEASUREMENT = "elk.exectime.measure";

    /**
     * Filter for {@link LayoutConfigurator} that checks for each option whether its configured targets
     * match the input element.
     */
    public static final Predicate<Pair<KGraphElement, IProperty<?>>> OPTION_TARGET_FILTER = (
            Pair<KGraphElement, IProperty<?>> input) -> {
        LayoutOptionData optionData = LayoutMetaDataService.getInstance().getOptionData(input.getSecond().getId());
        if (optionData != null) {
            KGraphElement e = input.getFirst();
            Set<LayoutOptionData.Target> targets = optionData.getTargets();
            if (e instanceof KNode) {
                if (((KNode) e).getChildren().isEmpty()) {
                    return targets.contains(LayoutOptionData.Target.NODES);
                } else {
                    return targets.contains(LayoutOptionData.Target.NODES)
                            || targets.contains(LayoutOptionData.Target.PARENTS);
                }
            } else if (e instanceof KEdge) {
                return targets.contains(LayoutOptionData.Target.EDGES);
            } else if (e instanceof KPort) {
                return targets.contains(LayoutOptionData.Target.PORTS);
            } else if (e instanceof KLabel) {
                return targets.contains(LayoutOptionData.Target.LABELS);
            }
        }
        return true;
    };

    /**
     * Property for the diagram layout connector used for automatic layout. This property is
     * attached to layout mappings created by the {@code layout} methods.
     */
    public static final IProperty<IDiagramLayoutConnector> MAPPING_CONNECTOR = new Property<IDiagramLayoutConnector>(
            "layoutEngine.diagramLayoutConnector");
    /**
     * Property for the status result of automatic layout. This property is attached to layout
     * mappings created by the {@code invokeLayout} methods.
     */
    public static final IProperty<IStatus> MAPPING_STATUS = new Property<IStatus>("layoutEngine.status");

    /**
     * Perform layout on the given workbench part with the given global options.
     * 
     * @param workbenchPart
     *            the workbench part for which layout is performed
     * @param diagramPart
     *            the parent diagram part for which layout is performed, or {@code null} if the whole
     *            diagram shall be arranged
     * @param animate
     *            if true, animation is activated (if supported by the diagram connector)
     * @param progressBar
     *            if true, a progress bar is displayed
     * @param layoutAncestors
     *            if true, layout is not only performed for the selected diagram part, but also for
     *            its ancestors
     * @param zoomToFit
     *            if true, automatic zoom-to-fit is activated (if supported by the diagram connector)
     * @return the layout mapping used in this operation
     */
    public static LayoutMapping invokeLayout(final IWorkbenchPart workbenchPart, final Object diagramPart,
            final boolean animate, final boolean progressBar, final boolean layoutAncestors,
            final boolean zoomToFit) {
        Parameters params = new Parameters();
        params.getGlobalSettings().setProperty(CoreOptions.ANIMATE, animate)
                .setProperty(CoreOptions.PROGRESS_BAR, progressBar)
                .setProperty(CoreOptions.LAYOUT_ANCESTORS, layoutAncestors)
                .setProperty(CoreOptions.ZOOM_TO_FIT, zoomToFit);
        return invokeLayout(workbenchPart, diagramPart, params);
    }

    /**
     * Perform layout on the given workbench part with the given setup.
     * 
     * @param workbenchPart
     *            the workbench part for which layout is performed
     * @param diagramPart
     *            the parent diagram part for which layout is performed, or {@code null} if the whole
     *            diagram shall be layouted
     * @param params
     *            layout parameters, or {@code null} to use default values
     * @return the layout mapping used in this operation
     */
    public static LayoutMapping invokeLayout(final IWorkbenchPart workbenchPart, final Object diagramPart,
            final Parameters params) {
        return invokeLayout(workbenchPart, diagramPart, (IElkCancelIndicator) null, params);
    }

    /**
     * Perform layout on the given workbench part and diagram part. This static method creates an instance
     * of {@link DiagramLayoutEngine} using a {@link LayoutConnectorsService} injector and delegates the
     * operation to that instance.
     * 
     * <p>Depending on the {@code cancelIndicator} argument, different methods of the created engine
     * may be used: either the one taking an {@link IElkCancelIndicator} as argument, or the one taking
     * an {@link IElkProgressMonitor} as argument (the latter inherits from the former).</p>
     * 
     * <p>{@code workbenchPart} and {@code diagramPart} must not be {@code null} at the same time.</p>
     * 
     * @param workbenchPart
     *            the workbench part for which layout is performed, or {@code null}
     * @param diagramPart
     *            the parent diagram part for which layout is performed, or {@code null} if the whole
     *            diagram shall be layouted
     * @param cancelIndicator
     *            an {@link IElkCancelIndicator} to be evaluated repeatedly during the layout operation,
     *            or an {@link IElkProgressMonitor} to which progress is reported, or {@code null}
     * @param params
     *            layout parameters, or {@code null} to use default values
     * @return the layout mapping used in this operation, or {@code null}if the workbench part and diagram
     *            part cannot be identified by the {@link LayoutConnectorsService}
     */
    public static LayoutMapping invokeLayout(final IWorkbenchPart workbenchPart, final Object diagramPart,
            final IElkCancelIndicator cancelIndicator, final Parameters params) {
        Injector injector = LayoutConnectorsService.getInstance().getInjector(workbenchPart, diagramPart);

        if (injector != null) {
            try {
                DiagramLayoutEngine engine = injector.getInstance(DiagramLayoutEngine.class);
                if (cancelIndicator instanceof IElkProgressMonitor) {
                    return engine.layout(workbenchPart, diagramPart, (IElkProgressMonitor) cancelIndicator, params);
                } else {
                    return engine.layout(workbenchPart, diagramPart, cancelIndicator, params);
                }
            } catch (ConfigurationException exception) {
                IStatus status = new Status(IStatus.ERROR, ElkServicePlugin.PLUGIN_ID,
                        workbenchPart == null ? "The Guice configuration for the given selection is inconsistent."
                                : "The Guice configuration for " + workbenchPart.getTitle() + " is inconsistent.",
                        exception);
                StatusManager.getManager().handle(status, StatusManager.SHOW);
            }
        } else {
            IStatus status = new Status(IStatus.ERROR, ElkServicePlugin.PLUGIN_ID,
                    workbenchPart == null ? "No layout connector is available for the given selection."
                            : "No layout connector is available for " + workbenchPart.getTitle() + ".");
            StatusManager.getManager().handle(status, StatusManager.SHOW);
        }
        return null;
    }

    //------- NON-STATIC PART (customizable via dependency injection) -------//

    /**
     * The diagram layout connector used to import the layout graph and apply the resulting layout.
     */
    @Inject
    private IDiagramLayoutConnector connector;

    /**
     * The layout configuration manager for handling layout options.
     */
    @Inject
    private LayoutConfigurationManager configManager;

    /**
     * The graph layout engine that does the actual automatic layout computation.
     */
    @Inject
    private IGraphLayoutEngine graphLayoutEngine;

    /**
     * Provider for validators of layout options.
     */
    @Inject
    private Provider<LayoutOptionValidator> layoutOptionValidatorProvider;

    /**
     * Perform layout on the given workbench part and diagram part. The layout operation is wrapped in a
     * {@link MonitoredOperation} in order to ensure that building the layout graph and applying the layout
     * is both done in the UI thread. In case of a problem a message is shown to the user with the default
     * {@link StatusManager}.
     * 
     * <p>{@code workbenchPart} and {@code diagramPart} must not be {@code null} at the same time.</p>
     * 
     * @param workbenchPart
     *            the workbench part for which layout is performed, or {@code null}
     * @param diagramPart
     *            the parent diagram part for which layout is performed, or {@code null} if the whole
     *            diagram shall be layouted
     * @param cancelIndicator
     *            an {@link IElkCancelIndicator} to be evaluated repeatedly during the layout operation,
     *            or {@code null}
     * @param params
     *            layout parameters, or {@code null} to use default values
     * @return the layout mapping used in this operation
     */
    public LayoutMapping layout(final IWorkbenchPart workbenchPart, final Object diagramPart,
            final IElkCancelIndicator cancelIndicator, final Parameters params) {
        if (workbenchPart == null && diagramPart == null) {
            throw new NullPointerException();
        }

        final Parameters finalParams = params != null ? params : new Parameters();
        final Maybe<LayoutMapping> layoutMapping = Maybe.create();
        final Pair<IWorkbenchPart, Object> target = Pair.of(workbenchPart, diagramPart);
        final ExecutorService executorService = ElkServicePlugin.getInstance().getExecutorService();
        final MonitoredOperation monitoredOperation = new MonitoredOperation(executorService, cancelIndicator) {

            // First phase: build the layout graph
            @Override
            protected void preUIexec() {
                boolean layoutAncestors = finalParams.getGlobalSettings().getProperty(CoreOptions.LAYOUT_ANCESTORS);
                LayoutMapping mapping;
                if (layoutAncestors && workbenchPart != null) {
                    mapping = connector.buildLayoutGraph(workbenchPart, null);
                    mapping.setParentElement(diagramPart);
                } else {
                    mapping = connector.buildLayoutGraph(workbenchPart, diagramPart);
                }
                layoutMapping.set(mapping);
            }

            // Second phase: execute layout algorithms
            @Override
            protected IStatus execute(final IElkProgressMonitor monitor) {
                if (monitor.isCanceled()) {
                    return Status.CANCEL_STATUS;
                }

                LayoutMapping mapping = layoutMapping.get();
                IStatus status;
                if (mapping != null && mapping.getLayoutGraph() != null) {
                    // Extract the diagram configuration
                    addDiagramConfig(finalParams, mapping);

                    // Perform the actual layout
                    status = layout(mapping, monitor, finalParams);

                    // Stop earlier layout operations that are still running
                    if (!monitor.isCanceled()) {
                        stopEarlierOperations(target, getTimestamp());
                    }
                } else {
                    status = new Status(Status.WARNING, ElkServicePlugin.PLUGIN_ID,
                            "Unable to build the layout graph from the given selection.");
                }

                monitor.done();
                return status;
            }

            // Third phase: apply layout with animation
            @Override
            protected void postUIexec() {
                if (layoutMapping.get() != null) {
                    connector.applyLayout(layoutMapping.get(), finalParams.getGlobalSettings());
                }
            }
        };

        Multimap<Pair<IWorkbenchPart, Object>, MonitoredOperation> runningOperations = ElkServicePlugin
                .getInstance().getRunningOperations();
        synchronized (runningOperations) {
            runningOperations.put(target, monitoredOperation);
        }

        try {
            boolean progressBar = finalParams.getGlobalSettings().getProperty(CoreOptions.PROGRESS_BAR);
            if (progressBar) {
                // Perform layout with a progress bar
                monitoredOperation.runMonitored();
            } else {
                // Perform layout without a progress bar
                monitoredOperation.runUnmonitored();
            }
        } finally {
            synchronized (runningOperations) {
                runningOperations.remove(target, monitoredOperation);
            }
        }

        return layoutMapping.get();
    }

    /**
     * Stop all running operations whose timestamp is earlier than the given one.
     * 
     * @param target the layout target
     * @param time operations with a timestamp that is less than this are stopped
     */
    protected void stopEarlierOperations(final Pair<IWorkbenchPart, Object> target, final long time) {
        Multimap<Pair<IWorkbenchPart, Object>, MonitoredOperation> runningOperations = ElkServicePlugin
                .getInstance().getRunningOperations();
        synchronized (runningOperations) {
            for (MonitoredOperation operation : runningOperations.get(target)) {
                if (operation.getTimestamp() < time) {
                    operation.cancel();
                }
            }
        }
    }

    /**
     * Perform layout on the given workbench part and diagram part with a given progress monitor.
     * The three steps of the layout operation (build layout graph, invoke algorithms, apply layout)
     * are all executed in the same thread that calls this method. In case of a problem the resulting
     * {@link IStatus} is attached to the returned {@link LayoutMapping}, but is not reported to the user.
     * 
     * <p>{@code workbenchPart} and {@code diagramPart} must not be {@code null} at the same time.</p>
     * 
     * @param workbenchPart
     *            the workbench part for which layout is performed, or {@code null}
     * @param diagramPart
     *            the parent diagram part for which layout is performed, or {@code null} if the whole
     *            diagram shall be layouted
     * @param progressMonitor
     *            a progress monitor to which progress of the layout algorithm is reported,
     *            or {@code null} if no progress reporting is required
     * @param params
     *            layout parameters
     * @return the layout mapping used in this operation
     */
    public LayoutMapping layout(final IWorkbenchPart workbenchPart, final Object diagramPart,
            final IElkProgressMonitor progressMonitor, final Parameters params) {
        if (workbenchPart == null && diagramPart == null) {
            throw new NullPointerException();
        }

        final IElkProgressMonitor finalMonitor;
        if (progressMonitor == null) {
            finalMonitor = new BasicProgressMonitor(0,
                    ElkServicePlugin.getInstance().getPreferenceStore().getBoolean(PREF_EXEC_TIME_MEASUREMENT));
        } else {
            finalMonitor = progressMonitor;
        }
        // SUPPRESS CHECKSTYLE NEXT MagicNumber
        finalMonitor.begin("Layout Diagram", 3);

        // Build the layout graph
        IElkProgressMonitor submon1 = finalMonitor.subTask(1);
        submon1.begin("Build layout graph", 1);
        LayoutMapping mapping = connector.buildLayoutGraph(workbenchPart, diagramPart);

        if (mapping != null && mapping.getLayoutGraph() != null) {
            // Extract the diagram configuration
            addDiagramConfig(params, mapping);
            submon1.done();

            // Perform the actual layout
            layout(mapping, finalMonitor.subTask(1), params);

            // Apply the layout to the diagram
            IElkProgressMonitor submon3 = finalMonitor.subTask(1);
            submon3.begin("Apply layout to the diagram", 1);
            connector.applyLayout(mapping, params.getGlobalSettings());
            submon3.done();
        } else {
            if (mapping == null) {
                mapping = new LayoutMapping(workbenchPart);
            }
            IStatus status = new Status(Status.WARNING, ElkServicePlugin.PLUGIN_ID,
                    "Unable to build the layout graph from the given selection.");
            mapping.setProperty(MAPPING_STATUS, status);
        }

        finalMonitor.done();
        return mapping;
    }

    /**
     * Create a diagram layout configuration and add it to the setup.
     */
    protected void addDiagramConfig(final Parameters params, final LayoutMapping layoutMapping) {
        LayoutConfigurator diagramConfig = configManager.createConfigurator(layoutMapping);
        if (params.configurators.isEmpty()) {
            params.addLayoutRun(diagramConfig);
        } else {
            ListIterator<LayoutConfigurator> configIter = params.configurators.listIterator();
            while (configIter.hasNext()) {
                boolean isFirstConfig = !configIter.hasPrevious();
                LayoutConfigurator setupConfig = configIter.next();
                if (params.overrideDiagramConfig) {
                    if (isFirstConfig || setupConfig.isClearLayout()) {
                        LayoutConfigurator newConfig;
                        if (configIter.hasNext()) {
                            newConfig = new LayoutConfigurator().overrideWith(diagramConfig);
                        } else {
                            newConfig = diagramConfig;
                        }
                        configIter.set(newConfig.overrideWith(setupConfig));
                    }
                } else {
                    setupConfig.overrideWith(diagramConfig);
                }
            }
        }
    }

    /**
     * Perform layout on the given layout graph mapping. If zero or one layout configurator is
     * passed, the layout engine is executed exactly once. If multiple layout configurators are
     * passed, the layout engine is executed accordingly often, but the resulting layout is applied
     * only once. This is useful for composition of multiple algorithms that process only parts of
     * the graph. Layout listeners are notified after the layout has been computed.
     * 
     * @param mapping
     *            a mapping for the layout graph
     * @param progressMonitor
     *            a progress monitor to which progress of the layout algorithm is reported
     * @param params
     *            layout parameters
     * @return a status indicating success or failure
     */
    public IStatus layout(final LayoutMapping mapping, final IElkProgressMonitor progressMonitor,
            final Parameters params) {
        mapping.setProperty(MAPPING_CONNECTOR, connector);

        boolean layoutAncestors = params.getGlobalSettings().getProperty(CoreOptions.LAYOUT_ANCESTORS);
        if (layoutAncestors) {
            // Mark all parallel areas for exclusion from layout
            KGraphElement graphElem = mapping.getGraphMap().inverse().get(mapping.getParentElement());
            if (graphElem instanceof KNode && ((KNode) graphElem).getParent() != null) {
                if (params.configurators.isEmpty()) {
                    params.configurators.add(new LayoutConfigurator());
                }
                KNode node = (KNode) graphElem;
                do {
                    KNode parent = node.getParent();
                    for (KNode child : parent.getChildren()) {
                        if (child != node) {
                            for (LayoutConfigurator c : params.configurators) {
                                IPropertyHolder childConfig = c.configure(child);
                                // Do not layout the content of the child node
                                childConfig.setProperty(CoreOptions.NO_LAYOUT, true);
                                // Do not change the size of the child node
                                childConfig.setProperty(CoreOptions.NODE_SIZE_CONSTRAINTS, SizeConstraint.fixed());
                                // Do not move the ports of the child node
                                childConfig.setProperty(CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_POS);
                            }
                        }
                    }
                    node = parent;
                } while (node.getParent() != null);
            }
        }

        // Set up graph validators
        LinkedList<IGraphElementVisitor> visitors = new LinkedList<IGraphElementVisitor>();
        if (params.getGlobalSettings().getProperty(CoreOptions.VALIDATE_OPTIONS)) {
            visitors.add(layoutOptionValidatorProvider.get());
        }

        // Notify listeners of the to-be-executed layout
        LayoutConnectorsService.getInstance().fireLayoutAboutToStart(mapping, progressMonitor);

        IStatus status = null;
        if (params.configurators.isEmpty()) {
            // Perform layout without any extra configuration
            IGraphElementVisitor[] visitorsArray = visitors.toArray(new IGraphElementVisitor[visitors.size()]);
            status = layout(mapping, progressMonitor, visitorsArray);
        } else if (params.configurators.size() == 1) {
            // Perform layout once with an extra configuration
            visitors.addFirst(params.configurators.get(0));
            IGraphElementVisitor[] visitorsArray = visitors.toArray(new IGraphElementVisitor[visitors.size()]);
            status = layout(mapping, progressMonitor, visitorsArray);
        } else {
            // Perform layout multiple times with different configurations
            progressMonitor.begin("Diagram layout engine", params.configurators.size());
            ListIterator<LayoutConfigurator> configIter = params.configurators.listIterator();
            while (configIter.hasNext()) {
                visitors.addFirst(configIter.next());
                IGraphElementVisitor[] visitorsArray = visitors.toArray(new IGraphElementVisitor[visitors.size()]);
                status = layout(mapping, progressMonitor, visitorsArray);
                if (!status.isOK()) {
                    break;
                }
                visitors.removeFirst();

                // If an additional layout configurator is attached to the graph, consider it in the future
                LayoutConfigurator addConfig = mapping.getLayoutGraph().getData(KShapeLayout.class)
                        .getProperty(LayoutConfigurator.ADD_LAYOUT_CONFIG);
                if (addConfig != null) {
                    ListIterator<LayoutConfigurator> configIter2 = params.configurators
                            .listIterator(configIter.nextIndex());
                    while (configIter2.hasNext()) {
                        configIter2.next().overrideWith(addConfig);
                    }
                }
            }
            progressMonitor.done();
        }

        mapping.setProperty(MAPPING_STATUS, status);
        // Notify listeners of the executed layout
        LayoutConnectorsService.getInstance().fireLayoutDone(mapping, progressMonitor);
        return status;
    }

    /**
     * Perform layout on the given layout graph mapping. Layout listeners are <em>not</em> notified
     * in this method.
     * 
     * @param mapping
     *            a mapping for the layout graph
     * @param progressMonitor
     *            a progress monitor to which progress of the layout algorithm is reported; if the
     *            given monitor is not yet started, it is started in this method
     * @param visitors
     *            an optional array of graph element visitors to apply
     * @return a status indicating success or failure
     */
    public IStatus layout(final LayoutMapping mapping, final IElkProgressMonitor progressMonitor,
            final IGraphElementVisitor... visitors) {

        if (progressMonitor.isCanceled()) {
            return Status.CANCEL_STATUS;
        }
        boolean newTask = progressMonitor.begin("Diagram layout engine", 1);

        try {
            // Configure the layout graph by applying the given visitors
            if (visitors.length > 0) {
                applyVisitors(mapping.getLayoutGraph(), visitors);
            }

            // Export the layout graph for debugging
            if (ElkServicePlugin.getInstance().getPreferenceStore().getBoolean(PREF_DEBUG_OUTPUT)) {
                exportLayoutGraph(mapping.getLayoutGraph());
            }

            // Perform layout on the layout graph
            graphLayoutEngine.layout(mapping.getLayoutGraph(), progressMonitor.subTask(1));

            if (newTask) {
                progressMonitor.done();
            }
            if (progressMonitor.isCanceled()) {
                return Status.CANCEL_STATUS;
            }

            // return a positive status
            return Status.OK_STATUS;

        } catch (Throwable exception) {
            return new Status(IStatus.ERROR, ElkServicePlugin.PLUGIN_ID, "Failed to perform diagram layout.",
                    exception);
        }
    }

    /**
     * Apply the given graph element visitors to the content of the given graph. If validators are involved
     * and at least one error is found, a {@link GraphValidationException} is thrown.
     * 
     * @throws GraphValidationException if an error is found while validating the graph
     */
    protected void applyVisitors(final KNode graph, final IGraphElementVisitor... visitors)
            throws GraphValidationException {
        for (int i = 0; i < visitors.length; i++) {
            visitors[i].visit(graph);
        }
        Iterator<KGraphElement> allElements = Iterators.filter(graph.eAllContents(), KGraphElement.class);
        while (allElements.hasNext()) {
            KGraphElement element = allElements.next();
            for (int i = 0; i < visitors.length; i++) {
                visitors[i].visit(element);
            }
        }

        // Gather validator results and generate an error message
        List<GraphIssue> allIssues = null;
        for (int i = 0; i < visitors.length; i++) {
            if (visitors[i] instanceof IValidatingGraphElementVisitor) {
                Collection<GraphIssue> issues = ((IValidatingGraphElementVisitor) visitors[i]).getIssues();
                if (!issues.isEmpty()) {
                    if (allIssues == null) {
                        allIssues = new ArrayList<GraphIssue>(issues);
                    } else {
                        allIssues.addAll(issues);
                    }
                }
            }
        }
        if (allIssues != null) {
            StringBuilder message = new StringBuilder();
            for (GraphIssue issue : allIssues) {
                if (message.length() > 0) {
                    message.append("\n");
                }
                message.append(issue.getSeverity()).append(": ").append(issue.getMessage()).append("\n\tat ");
                ElkUtil.printElementPath(issue.getElement(), message);
            }
            throw new GraphValidationException(message.toString(), allIssues);
        }
    }

    /**
     * Export the given layout graph in KGraph format.
     * 
     * @param graph the parent node of the layout graph
     */
    protected void exportLayoutGraph(final KNode graph) {
        URI exportUri = getExportURI(graph);
        if (exportUri != null) {
            // serialize all properties of the graph
            ElkUtil.persistDataElements(graph);

            // save the KGraph to a file
            ResourceSet resourceSet = new ResourceSetImpl();
            Resource resource = resourceSet.createResource(exportUri);
            resource.getContents().add(graph);
            try {
                resource.save(Collections.emptyMap());
            } catch (IOException e) {
                // ignore the exception and abort the layout graph exporting
            }
        }
    }

    /**
     * Return a file URI to use for exporting graphs.
     * 
     * @param graph the parent node of the layout graph
     */
    protected URI getExportURI(final KNode graph) {
        String path = System.getProperty("user.home");
        if (path != null) {
            if (path.endsWith(File.separator)) {
                path += "tmp" + File.separator + "layout" + File.separator + Integer.toHexString(graph.hashCode())
                        + ".kgraph";
            } else {
                path += File.separator + "tmp" + File.separator + "layout" + File.separator
                        + Integer.toHexString(graph.hashCode()) + ".kgraph";
            }
            return URI.createFileURI(path);
        }
        return null;
    }

}