Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[layout] introduced 'interactive' layout #36

Merged
merged 1 commit into from
Sep 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected void handle(LayoutSelectionAction action) {
SGraph model = (SGraph) getModel();
GraphLayoutEngine layoutEngine = (GraphLayoutEngine) getLayoutEngine();
layoutEngine.setSelection(selection.getSelectedElementsIDs());
layoutEngine.layout(model);
layoutEngine.layout(model, action);
dispatch(new UpdateModelAction(model));
dispatch(new FitToScreenAction(fitToScreen -> {
fitToScreen.setMaxZoom(1.0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,24 @@
import org.eclipse.sprotty.Dimension;
import org.eclipse.sprotty.SEdge;
import org.eclipse.sprotty.SGraph;
import org.eclipse.sprotty.SModelElement;
import org.eclipse.sprotty.SNode;
import org.eclipse.sprotty.util.IdCache;

import com.google.inject.Inject;
import com.google.inject.Provider;

public class GraphGenerator {

private static final int NODE_COUNT = 50;
private static final int ADD_EDGE_COUNT = 50;
private static final double NODE_SIZE = 60;

@Inject
private Provider<IdCache<SModelElement>> idCacheProvider;

private Random random = new Random();

public SGraph generateGraph() {
Context ctx = new Context();
ctx.idCache = idCacheProvider.get();
return new SGraph(graph -> {
graph.setId("graph");
graph.setChildren(new ArrayList<>(2 * NODE_COUNT + ADD_EDGE_COUNT));

// Generate nodes
for (int n = 0; n < NODE_COUNT; n++) {
graph.getChildren().add(generateNode(ctx));
graph.getChildren().add(generateNode(n));
}

// Generate one connected edge per node
Expand All @@ -58,9 +48,9 @@ public SGraph generateGraph() {
n2 = random.nextInt(NODE_COUNT);
} while (n1 == n2);
graph.getChildren().add(generateEdge(
graph.getChildren().get(n1).getId(),
graph.getChildren().get(n2).getId(),
ctx));
"node" + n1,
"node" + n2,
n1));
}

// Generate additional edges
Expand All @@ -71,30 +61,26 @@ public SGraph generateGraph() {
n2 = random.nextInt(NODE_COUNT);
} while (n1 == n2);
graph.getChildren().add(generateEdge(
graph.getChildren().get(n1).getId(),
graph.getChildren().get(n2).getId(),
ctx));
"node" + n1,
"node" + n2,
NODE_COUNT + e));
}
});
}

private SNode generateNode(Context ctx) {
private SNode generateNode(int n) {
return new SNode(node -> {
node.setId(ctx.idCache.uniqueId(node, "node", 1));
node.setId("node" + n);
node.setSize(new Dimension(NODE_SIZE, NODE_SIZE));
});
}

private SEdge generateEdge(String sourceId, String targetId, Context ctx) {
private SEdge generateEdge(String sourceId, String targetId, int n) {
return new SEdge(edge -> {
edge.setId(ctx.idCache.uniqueId(edge, "edge", 1));
edge.setId("edge" + n);
edge.setSourceId(sourceId);
edge.setTargetId(targetId);
});
}

private static class Context {
IdCache<SModelElement> idCache;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.eclipse.elk.alg.force.options.ForceOptions;
import org.eclipse.elk.core.options.CoreOptions;
import org.eclipse.elk.graph.ElkGraphElement;
import org.eclipse.sprotty.Action;
import org.eclipse.sprotty.SGraph;
import org.eclipse.sprotty.SModelElement;
import org.eclipse.sprotty.SModelRoot;
Expand All @@ -34,14 +35,14 @@ public class GraphLayoutEngine extends ElkLayoutEngine {
private final Set<String> selection = new HashSet<>();

@Override
public void layout(SModelRoot root) {
public void layout(SModelRoot root, Action cause) {
if (root instanceof SGraph) {
SprottyLayoutConfigurator configurator = new SprottyLayoutConfigurator();
configurator.configureByType("graph")
.setProperty(CoreOptions.ALGORITHM, ForceOptions.ALGORITHM_ID)
.setProperty(CoreOptions.RANDOM_SEED, 0)
.setProperty(ForceOptions.ITERATIONS, 1000);
layout((SGraph) root, configurator);
layout((SGraph) root, configurator, cause);
}
}

Expand Down
2 changes: 1 addition & 1 deletion examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"bootstrap": "^4.3.1",
"reflect-metadata": "^0.1.13",
"sprotty": "file:/Users/spoenemann/github/sprotty/sprotty"
"sprotty": "next"
},
"devDependencies": {
"babel-plugin-syntax-dynamic-import": "^6.18.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

import com.google.common.collect.Maps;

import org.eclipse.sprotty.Action;
import org.eclipse.sprotty.BoundsAware;
import org.eclipse.sprotty.Dimension;
import org.eclipse.sprotty.EdgeLayoutable;
Expand Down Expand Up @@ -86,16 +87,16 @@ public static void initialize(ILayoutMetaDataProvider ...providers) {
* {@link SprottyLayoutConfigurator}.
*/
@Override
public void layout(SModelRoot root) {
public void layout(SModelRoot root, Action cause) {
if (root instanceof SGraph) {
layout((SGraph) root, null);
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) {
public void layout(SGraph sgraph, SprottyLayoutConfigurator configurator, Action cause) {
LayoutContext context = transformGraph(sgraph);
if (configurator != null) {
ElkUtil.applyVisitors(context.elkGraph, configurator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
package org.eclipse.sprotty.layout.test

import com.google.inject.Inject
import org.eclipse.elk.core.math.ElkPadding
import org.eclipse.elk.core.math.KVector
import org.eclipse.elk.core.math.KVectorChain
import org.eclipse.elk.core.options.CoreOptions
import org.eclipse.elk.graph.ElkNode
import org.eclipse.sprotty.Action
import org.eclipse.sprotty.Dimension
import org.eclipse.sprotty.Point
import org.eclipse.sprotty.SCompartment
Expand All @@ -27,11 +33,6 @@ import org.eclipse.sprotty.SNode
import org.eclipse.sprotty.SPort
import org.eclipse.sprotty.layout.ElkLayoutEngine
import org.eclipse.sprotty.layout.SprottyLayoutConfigurator
import org.eclipse.elk.core.math.ElkPadding
import org.eclipse.elk.core.math.KVector
import org.eclipse.elk.core.math.KVectorChain
import org.eclipse.elk.core.options.CoreOptions
import org.eclipse.elk.graph.ElkNode
import org.junit.Test

import static extension org.eclipse.sprotty.SModelUtil.*
Expand All @@ -48,14 +49,14 @@ class ElkLayoutEngineTest extends AbstractElkTest {
return config
}

override layout(SModelRoot model) {
layout(model as SGraph, configurator)
override layout(SModelRoot model, Action cause) {
layout(model as SGraph, configurator, cause)
}

def layout(SGraph model, (SprottyLayoutConfigurator)=>void initialize) {
val config = getConfigurator
initialize.apply(config)
layout(model, config)
layout(model, config, null)
}

def getTransformedGraph(SGraph model) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,12 @@ protected CompletableFuture<Void> submitModel(SModelRoot newRoot, boolean update
}

private void doSubmitModel(SModelRoot newRoot, boolean update, Action cause) {
if (getServerLayoutKind(newRoot) == ServerLayoutKind.AUTOMATIC) {
ServerLayoutKind layoutKind = getServerLayoutKind(newRoot);
if (layoutKind == ServerLayoutKind.AUTOMATIC
|| layoutKind == ServerLayoutKind.INTERACTIVE) {
ILayoutEngine layoutEngine = getLayoutEngine();
if (layoutEngine != null) {
layoutEngine.layout(newRoot);
layoutEngine.layout(newRoot, cause);
}
}
synchronized (modelLock) {
Expand Down Expand Up @@ -514,7 +516,8 @@ protected void handle(OpenAction action) {
* Called when a {@link LayoutAction} is received.
*/
protected void handle(LayoutAction action) {
if (getServerLayoutKind(getModel()) != ServerLayoutKind.NONE) {
ServerLayoutKind layoutKind = getServerLayoutKind(currentRoot);
if (layoutKind != ServerLayoutKind.NONE) {
ILayoutEngine layoutEngine = getLayoutEngine();
if (layoutEngine != null) {
// Clone the current model, as it has already been sent to the client with the old revision
Expand All @@ -526,7 +529,7 @@ protected void handle(LayoutAction action) {
}
// Don't execute layout twice
if (getServerLayoutKind(newRoot) != ServerLayoutKind.AUTOMATIC) {
layoutEngine.layout(newRoot);
layoutEngine.layout(newRoot, action);
}
doSubmitModel(newRoot, true, action);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@ public interface ILayoutEngine {
/**
* Compute a layout for the given model and modify the model accordingly.
*/
public void layout(SModelRoot root);
public void layout(SModelRoot root, Action cause);

/**
* An implementation that does nothing.
*/
public static class NullImpl implements ILayoutEngine {
@Override
public void layout(SModelRoot root) {
public void layout(SModelRoot root, Action cause) {
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.eclipse.sprotty.util.IdCache
* Convenient functions for generating sprotty diagrams.
*
* @deprecated Use the constructors with the lambda parameter for convenient and
* readable construction and the {@link IdCache} to provide unique id's.
* readable construction and the {@link IdCache} to guarantee unique id's.
*/
@Deprecated
final class SModelUtil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ public enum ServerLayoutKind {
* Layout information stored in the model will be overwritten.
*/
AUTOMATIC,

/**
* The server re-layouts the diagram interactively. In ELK, <em>interactive</em> means
* that the order is not calculated but derived from the positions of the nodes. This
* is close to a partial layout.
* The layout information must be stored in the model and will be overwritten on layout.
*/
INTERACTIVE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value is not used anywhere in the framework. How would an application use it?


/**
* The server re-layouts the diagram only if manually triggered by a <code>LayoutAction</code>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,52 @@ package org.eclipse.sprotty.util
import com.google.common.collect.BiMap
import com.google.common.collect.HashBiMap
import java.util.Set
import org.eclipse.sprotty.SModelElement
import org.apache.log4j.Logger

/**
* Helps to create unique IDs for {@link SModelElement}s.
* Assert unique IDs for model elements and and allow to look them up.
*
* In Sprotty, it's the diagram implementor's responsibility to create unique
* IDs for SModel elements. For consistent animations on model updates, these
* IDs should be based on properties of the underlying model in a way that
* they are resilient to reordering and if possible renaming.
*
* This class makes sure these IDs are unique, and allows to look them up for
* a given model element in order to establish cross references in the SModel,
* e.g. for <code>sourceId</code> and <code>target</code> of an <code>SEdge</code>.
*
* @param T the type of the underling model element
*/
class IdCache <T> {

static val LOG = Logger.getLogger(IdCache)

val BiMap<String, T> id2element = HashBiMap.create
val Set<String> otherIds = newHashSet

def String uniqueId(T element, String idProposal) {
uniqueId(element, idProposal, 0)
createUniqueId(element, idProposal)
}

def String uniqueId(String idProposal) {
uniqueId(null, idProposal, 0)
createUniqueId(null, idProposal)
}

def boolean isIdAlreadyUsed(String id) {
id2element.containsKey(id) || otherIds.contains(id)
}

def String uniqueId(T element, String idPrefix, int countStart) {
var String proposedId
var count = countStart
protected def String createUniqueId(T element, String idPrefix) {
var String proposedId = idPrefix
var count = 0
do {
proposedId = if (count == 0) idPrefix else idPrefix + count
proposedId = idPrefix + if (count === 0) '' else count
if (element !== null && id2element.get(proposedId) == element)
return proposedId
count++
} while (id2element.containsKey(proposedId) || otherIds.contains(proposedId))

count++
} while (proposedId.idAlreadyUsed)
if (count > 1)
LOG.error('''Duplicate ID '«idPrefix»'. Using «proposedId» instead''')
if (element === null) {
otherIds.add(proposedId)
} else {
Expand All @@ -58,5 +76,4 @@ class IdCache <T> {
def getId(T element) {
id2element.inverse.get(element)
}

}