Skip to content

Commit

Permalink
#514 Refactor GlspElkLayoutEngine
Browse files Browse the repository at this point in the history
- Refactor the `GlspElkLayoutEngine` and related concepts to properly support layout computation for ports. In this process we also opted out of trying to reuse the existing `SprottyElkLayoutEngine` and instead implement a new layout engine specifically for the GLSP graphmodel. This implementation is more in line with the Java ELK-Layout Engine and implements similar features like automatic detection of common ancestors of edge source/targets to correctly transform them in the ELK graph.

- Removes the no longer needed `BasicTypeMapper`

Also: 
- Provide utility functions for querying parent elements of a given element that match a predicate. (gmodel-util.ts)
- Only rerexport the the subset of types that are really needed from "sprotty-elk" to avoid unncessary naming clashes.
- Add jsdoc for `GModelSerializer`
- Fix implementation of `getAllEdges` in `GModelIndex`
- Remove unused (and unset) `source` and `target` references in `GEdge`

Contributed on behalf of STMicroelectronics
  • Loading branch information
tortmayr committed Feb 3, 2022
1 parent 397cddd commit 54b5138
Show file tree
Hide file tree
Showing 21 changed files with 662 additions and 345 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ The initial initial implementation was contributed on behalf of STMicroelectroni

### Changes

- [diagram] Implement LayoutEngine API for server-side autolayouting & provide an integration package for layout engines based on ELK. [#509](https://github.com/eclipse-glsp/glsp-server-node/pull/2) - Contributed on behalf of STMicroelectronics
- [diagram] Implement LayoutEngine API for server-side autolayouting & provide an integration package for layout engines based on ELK. [#509](https://github.com/eclipse-glsp/glsp-server-node/pull/2) [#514](https://github.com/eclipse-glsp/glsp-server-node/pull/5) - Contributed on behalf of STMicroelectronics
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Below is a list of features that are currently supported.
| Model Saving |||
| Model Dirty State | ||
| Model SVG Export |||
| Model Layout | ||
| Model Layout | ||
| Model Edit Modes<br>- Edit<br>- Read-only | <br>✓<br>&nbsp; | <br>✓<br>✓ |
| Client View Port<br>- Center<br>- Fit to Screen | <br>✓<br>✓ | <br>✓<br>✓ |
| Client Status Notification |||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { GlspLayoutConfigurator, SModelIndex } from '@eclipse-glsp/layout-elk';
import { AbstractLayoutConfigurator, LayoutOptions } from '@eclipse-glsp/layout-elk';
import { GGraph } from '@eclipse-glsp/server-node';
import { LayoutOptions } from 'elkjs';
import { injectable } from 'inversify';

@injectable()
export class WorkflowLayoutConfigurator extends GlspLayoutConfigurator {
protected graphOptions(sgraph: GGraph, index: SModelIndex): LayoutOptions | undefined {
export class WorkflowLayoutConfigurator extends AbstractLayoutConfigurator {
protected graphOptions(graph: GGraph): LayoutOptions | undefined {
return {
'elk.algorithm': 'layered'
};
Expand Down
3 changes: 0 additions & 3 deletions examples/workflow-server/src/workflow-diagram-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
DiagramConfiguration,
GLSPServer,
GModelDiagramModule,
GModelIndex,
InstanceMultiBinding,
JsonRpcGLSPServer,
LabelEditValidator,
Expand Down Expand Up @@ -53,7 +52,6 @@ import { WorkflowCommandPaletteActionProvider } from './provider/workflow-comman
import { WorkflowContextMenuItemProvider } from './provider/workflow-context-menu-item-provider';
import { WorkflowDiagramConfiguration } from './workflow-diagram-configuration';
import { WorkflowGLSPServer } from './workflow-glsp-server';
import { WorkflowModelIndex } from './workflow-model-index';
import { WorkflowPopupFactory } from './workflow-popup-factory';

@injectable()
Expand Down Expand Up @@ -82,7 +80,6 @@ export class WorkflowDiagramModule extends GModelDiagramModule {
bind(PopupModelFactory).to(WorkflowPopupFactory).inSingletonScope();
bind(ModelValidator).to(WorkflowModelValidator).inSingletonScope();
bind(ToolPaletteItemProvider).to(DefaultToolPaletteItemProvider).inSingletonScope();
rebind(GModelIndex).to(WorkflowModelIndex).inSingletonScope();
}

protected configureOperationHandlers(binding: InstanceMultiBinding<OperationHandlerConstructor>): void {
Expand Down
24 changes: 2 additions & 22 deletions packages/graph/src/gedge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,6 @@ export class GEdge extends GModelElement {
sourceId: string;
targetId: string;
routerKind?: string;
private _source: GModelElement;
private _target: GModelElement;

get source(): GModelElement {
return this._source;
}

set source(value: GModelElement) {
this._source = value;
this.sourceId = value.id;
}

get target(): GModelElement {
return this._target;
}

set target(value: GModelElement) {
this._target = value;
this.targetId = value.id;
}
}

export namespace GEdge {
Expand All @@ -56,7 +36,7 @@ export namespace GEdge {

export class GEdgeBuilder<G extends GEdge = GEdge> extends GModelElementBuilder<G> {
source(source: GModelElement): this {
this.proxy.source = source;
this.proxy.sourceId = source.id;
return this;
}

Expand All @@ -66,7 +46,7 @@ export class GEdgeBuilder<G extends GEdge = GEdge> extends GModelElementBuilder<
}

target(target: GModelElement): this {
this.proxy.target = target;
this.proxy.targetId = target.id;
return this;
}

Expand Down
7 changes: 4 additions & 3 deletions packages/graph/src/gmodel-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
Dimension,
flatPush,
isSModelElementSchema,
isSModelRootSchema,
JsonPrimitive,
MaybeArray,
Point,
Expand All @@ -28,7 +27,6 @@ import {
} from '@eclipse-glsp/protocol';
import * as uuid from 'uuid';
export type GModelElementConstructor<G extends GModelElement = GModelElement> = new () => G;

/**
* Represents a `GModeElement` serialized as plain JSON object.
*/
Expand Down Expand Up @@ -75,6 +73,9 @@ export abstract class GModelElement implements GModelElementSchema {
*/
args?: Args;

/**
* Retrieve the {@link GModelRoot} element by traversing up the parent hierachy.
*/
get root(): GModelRoot {
let current: GModelElement = this;
while (!(current instanceof GModelRoot)) {
Expand Down Expand Up @@ -183,7 +184,7 @@ export abstract class GModelElementBuilder<G extends GModelElement> {
export type GModelRootSchema = SModelRootSchema;

export function isGModelRootSchema(schema: any): schema is GModelRootSchema {
return isSModelRootSchema(schema);
return isSModelElementSchema(schema);
}

export class GModelRoot extends GModelElement implements SModelRootSchema {
Expand Down
58 changes: 58 additions & 0 deletions packages/graph/src/gmodel-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/********************************************************************************
* Copyright (c) 2022 STMicroelectronics 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
********************************************************************************/

import { GModelElement, GModelElementConstructor } from '.';

export type Predicate<T> = (element: T) => boolean;

/**
* Returns the first element matching the search predicate starting from the given
* element and walking up the parent hierarchy.
*
* @param element The element to start the search from.
* @param searchPredicate The predicate which the element should match.
* @returns The first matching element or `undefined`.
*/
export function findParent(element: GModelElement, searchPredicate: Predicate<GModelElement>): GModelElement | undefined {
if (!element) {
return undefined;
}
if (searchPredicate(element)) {
return element;
}
const parent = element.parent;
return parent ? findParent(parent, searchPredicate) : undefined;
}

/**
* Returns the first parent element that is an instance of the given {@link GModelElementConstructor} starting from the given element.
* (recursively walking up the parent hierarchy).
*
* @param element The element to start the search from.
* @param constructor The queried parent class ({@link GModelElementConstructor}).
* @returns The first matching parent element or `undefined`.
*/
export function findParentByClass<G extends GModelElement>(
element: GModelElement,
constructor: GModelElementConstructor<G>
): G | undefined {
const predicate: Predicate<GModelElement> = element => element instanceof constructor;
const result = findParent(element, predicate);
if (result) {
return result as G;
}
return undefined;
}
3 changes: 2 additions & 1 deletion packages/graph/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ export * from './gissue-marker';
export * from './glabel';
export * from './glayout-container';
export * from './glayoutable';
export * from './gpre-rendered-element';
export * from './gmodel-element';
export * from './gmodel-util';
export * from './gnode';
export * from './gport';
export * from './gpre-rendered-element';
export * from './gshape-element';
73 changes: 0 additions & 73 deletions packages/layout-elk/src/basic-type-mapper.ts

This file was deleted.

33 changes: 15 additions & 18 deletions packages/layout-elk/src/di.config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,21 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { DefaultModelState, DiagramConfiguration, GGraph, GModelElementConstructor, ModelState } from '@eclipse-glsp/server-node';
import { DefaultModelState, GGraph, GModelElementConstructor, ModelState } from '@eclipse-glsp/server-node';
import { StubDiagramConfiguration } from '@eclipse-glsp/server-node/lib/test/mock-util';
import { expect } from 'chai';
import { Container, ContainerModule, injectable } from 'inversify';
import * as sinon from 'sinon';
import { IElementFilter, ILayoutConfigurator } from 'sprotty-elk';
import { SModelIndex } from 'sprotty-protocol';
import { GlspLayoutConfigurator } from '.';
import { AbstractLayoutConfigurator } from '.';
import { configureELKLayoutModule } from './di.config';
import { GlspElementFilter } from './glsp-element-filter';
import { FallbackGlspLayoutConfigurator } from './glsp-layout-configurator';
import { DefaultElementFilter, ElementFilter } from './element-filter';
import { FallbackLayoutConfigurator, LayoutConfigurator } from './layout-configurator';

@injectable()
class CustomLayoutConfigurator extends GlspLayoutConfigurator {}
class CustomLayoutConfigurator extends AbstractLayoutConfigurator {}

@injectable()
class CustomElementFilter extends GlspElementFilter {}
class CustomElementFilter extends DefaultElementFilter {}

describe('test configureELKLayoutModule', () => {
const sandbox = sinon.createSandbox();
Expand All @@ -40,19 +38,18 @@ describe('test configureELKLayoutModule', () => {
sandbox.stub(mockDiagramConfiguration, 'typeMapping').value(typeMappings);
const modelState = new DefaultModelState();
const baseModule = new ContainerModule(bind => {
bind(DiagramConfiguration).toConstantValue(mockDiagramConfiguration);
bind(ModelState).toConstantValue(modelState);
});
it('configure with minimal options', () => {
const algorithm = 'layered';
const elkModule = configureELKLayoutModule({ algorithms: [algorithm] });
const container = new Container();
container.load(baseModule, elkModule);
const filter = container.get<IElementFilter>(IElementFilter);
expect(filter).to.be.instanceOf(GlspElementFilter);
const configurator = container.get<ILayoutConfigurator>(ILayoutConfigurator);
expect(configurator).to.be.instanceOf(FallbackGlspLayoutConfigurator);
const graphOptions = configurator.apply(new GGraph(), new SModelIndex());
const filter = container.get<ElementFilter>(ElementFilter);
expect(filter).to.be.instanceOf(DefaultElementFilter);
const configurator = container.get<LayoutConfigurator>(LayoutConfigurator);
expect(configurator).to.be.instanceOf(FallbackLayoutConfigurator);
const graphOptions = configurator.apply(new GGraph());
expect(graphOptions).not.to.be.undefined;
expect(graphOptions!['elk.algorithm']).to.equal(algorithm);
});
Expand All @@ -66,8 +63,8 @@ describe('test configureELKLayoutModule', () => {
const elkModule = configureELKLayoutModule({ algorithms: [algorithm], defaultLayoutOptions });
const container = new Container();
container.load(baseModule, elkModule);
const configurator = container.get<ILayoutConfigurator>(ILayoutConfigurator);
const graphOptions = configurator.apply(new GGraph(), new SModelIndex());
const configurator = container.get<LayoutConfigurator>(LayoutConfigurator);
const graphOptions = configurator.apply(new GGraph());
expect(graphOptions).not.to.be.undefined;
expect(graphOptions).to.include(defaultLayoutOptions);
});
Expand All @@ -77,7 +74,7 @@ describe('test configureELKLayoutModule', () => {
const elkModule = configureELKLayoutModule({ algorithms: [algorithm], layoutConfigurator: CustomLayoutConfigurator });
const container = new Container();
container.load(baseModule, elkModule);
const configurator = container.get<ILayoutConfigurator>(ILayoutConfigurator);
const configurator = container.get<LayoutConfigurator>(LayoutConfigurator);
expect(configurator).to.be.instanceOf(CustomLayoutConfigurator);
});

Expand All @@ -86,7 +83,7 @@ describe('test configureELKLayoutModule', () => {
const elkModule = configureELKLayoutModule({ algorithms: [algorithm], elementFilter: CustomElementFilter });
const container = new Container();
container.load(baseModule, elkModule);
const filter = container.get<IElementFilter>(IElementFilter);
const filter = container.get<ElementFilter>(ElementFilter);
expect(filter).to.be.instanceOf(CustomElementFilter);
});
});
Loading

0 comments on commit 54b5138

Please sign in to comment.