@eclipse-glsp/layout-elk
Version:
Integration of ELK graph layout algorithms in GLSP Node Server
290 lines • 12.2 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GlspElkLayoutEngine = exports.ElkFactory = void 0;
/********************************************************************************
* Copyright (c) 2018-2025 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
********************************************************************************/
const server_1 = require("@eclipse-glsp/server");
const inversify_1 = require("inversify");
const element_filter_1 = require("./element-filter");
const layout_configurator_1 = require("./layout-configurator");
exports.ElkFactory = Symbol('ElkFactory');
/**
* An implementation of GLSP's {@link LayoutEngine} interface that retrieves the graphical model from the {@link ModelState},
* transforms this model into an ELK graph and then invokes the underlying ELK instance for layout computation.
*/
let GlspElkLayoutEngine = class GlspElkLayoutEngine {
constructor(elkFactory, filter, configurator, modelState) {
this.filter = filter;
this.configurator = configurator;
this.modelState = modelState;
this.elk = elkFactory();
}
layout(operation) {
const root = this.modelState.root;
if (!(root instanceof server_1.GGraph)) {
return root;
}
this.elkEdges = [];
this.idToElkElement = new Map();
const elkGraph = this.transformToElk(root);
return this.elk.layout(elkGraph).then(result => {
this.applyLayout(result);
return root;
});
}
transformToElk(model) {
if (model instanceof server_1.GGraph) {
const graph = this.transformGraph(model);
this.elkEdges.forEach(elkEdge => {
const parent = this.findCommonAncestor(elkEdge);
if (parent) {
parent.edges.push(elkEdge);
}
});
return graph;
}
else if (model instanceof server_1.GNode) {
return this.transformNode(model);
}
else if (model instanceof server_1.GEdge) {
return this.transformEdge(model);
}
else if (model instanceof server_1.GLabel) {
return this.transformLabel(model);
}
else if (model instanceof server_1.GPort) {
return this.transformPort(model);
}
throw new Error('Type not supported: ' + model.type);
}
/**
* Searches for all children of the given element that are an instance of the given {@link GModelElementConstructor}
* and are included by the {@link ElementFilter}. Also considers children that are nested inside of {@link GCompartment}s.
* @param element The element whose children should be queried.
* @param constructor The class instance that should be matched
* @returns A list of all matching children.
*/
findChildren(element, constructor) {
const result = [];
element.children.forEach(child => {
if (child instanceof constructor && this.filter.apply(element)) {
result.push(child);
}
else if (child instanceof server_1.GCompartment) {
result.push(...this.findChildren(child, constructor));
}
});
return result;
}
findCommonAncestor(elkEdge) {
if (elkEdge.sources.length === 0 || elkEdge.targets.length === 0) {
this.logger.warn('Edges with multiple sources or targets are not supported by the GLSPElkLayoutEngine', elkEdge);
return undefined;
}
const source = this.modelState.index.get(elkEdge.sources[0]);
const target = this.modelState.index.get(elkEdge.targets[0]);
if (!source || !target) {
return undefined;
}
const sourceParent = (0, server_1.findParent)(source.parent, parent => parent instanceof server_1.GNode || parent instanceof server_1.GGraph);
const targetParent = (0, server_1.findParent)(target.parent, parent => parent instanceof server_1.GNode || parent instanceof server_1.GGraph);
if (!sourceParent || !targetParent) {
return undefined;
}
let ancestor;
if (sourceParent === targetParent) {
ancestor = this.idToElkElement.get(sourceParent.id);
}
else if (source === targetParent) {
ancestor = this.idToElkElement.get(source.id);
}
else if (target === sourceParent) {
ancestor = this.idToElkElement.get(target.id);
}
return ancestor;
}
transformGraph(graph) {
const elkGraph = {
id: graph.id,
layoutOptions: this.configurator.apply(graph)
};
if (graph.children) {
elkGraph.children = this.findChildren(graph, server_1.GNode).map(child => this.transformToElk(child));
elkGraph.edges = [];
this.elkEdges.push(...this.findChildren(graph, server_1.GEdge).map(child => this.transformToElk(child)));
}
this.idToElkElement.set(graph.id, elkGraph);
return elkGraph;
}
transformNode(node) {
const elkNode = {
id: node.id,
layoutOptions: this.configurator.apply(node)
};
if (node.children) {
elkNode.children = this.findChildren(node, server_1.GNode).map(child => this.transformToElk(child));
elkNode.edges = [];
this.elkEdges.push(...this.findChildren(node, server_1.GEdge).map(child => this.transformToElk(child)));
elkNode.labels = this.findChildren(node, server_1.GLabel).map(child => this.transformToElk(child));
elkNode.ports = this.findChildren(node, server_1.GPort).map(child => this.transformToElk(child));
}
this.transformShape(elkNode, node);
this.idToElkElement.set(node.id, elkNode);
return elkNode;
}
transformShape(elkShape, shape) {
if (shape.position) {
elkShape.x = shape.position.x;
elkShape.y = shape.position.y;
}
if (shape.size) {
elkShape.width = shape.size.width;
elkShape.height = shape.size.height;
}
}
transformEdge(edge) {
const elkEdge = {
id: edge.id,
sources: [edge.sourceId],
targets: [edge.targetId],
layoutOptions: this.configurator.apply(edge)
};
if (edge.children) {
elkEdge.labels = this.findChildren(edge, server_1.GLabel).map(child => this.transformToElk(child));
}
const points = edge.routingPoints;
if (points && points.length >= 2) {
elkEdge.sections = [
{
id: edge.id + ':section',
startPoint: points[0],
bendPoints: points.slice(1, points.length - 1),
endPoint: points[points.length - 1]
}
];
}
this.idToElkElement.set(edge.id, elkEdge);
return elkEdge;
}
transformLabel(label) {
const elkLabel = {
id: label.id,
text: label.text,
layoutOptions: this.configurator.apply(label)
};
this.transformShape(elkLabel, label);
this.idToElkElement.set(label.id, elkLabel);
return elkLabel;
}
transformPort(port) {
const elkPort = {
id: port.id,
layoutOptions: this.configurator.apply(port)
};
if (port.children) {
elkPort.labels = this.findChildren(port, server_1.GLabel).map(child => this.transformToElk(child));
this.elkEdges.push(...this.findChildren(port, server_1.GEdge).map(child => this.transformToElk(child)));
}
this.transformShape(elkPort, port);
this.idToElkElement.set(port.id, elkPort);
return elkPort;
}
applyLayout(elkNode) {
const element = this.modelState.index.get(elkNode.id);
if (element instanceof server_1.GNode) {
this.applyShape(element, elkNode);
}
if (elkNode.children) {
for (const child of elkNode.children) {
this.applyLayout(child);
}
}
if (elkNode.edges) {
for (const elkEdge of elkNode.edges) {
const edge = this.modelState.index.get(elkEdge.id);
if (edge instanceof server_1.GEdge) {
this.applyEdge(edge, elkEdge);
}
}
}
if (elkNode.ports) {
for (const elkPort of elkNode.ports) {
const port = this.modelState.index.findByClass(elkPort.id, server_1.GPort);
if (port) {
this.applyShape(port, elkPort);
}
}
}
}
applyShape(shape, elkShape) {
if (elkShape.x !== undefined && elkShape.y !== undefined) {
shape.position = { x: elkShape.x, y: elkShape.y };
}
if (elkShape.width !== undefined && elkShape.height !== undefined) {
shape.size = { width: elkShape.width, height: elkShape.height };
}
if (elkShape.labels) {
for (const elkLabel of elkShape.labels) {
const label = elkLabel.id ? this.modelState.index.findByClass(elkLabel.id, server_1.GLabel) : undefined;
if (label) {
this.applyShape(label, elkLabel);
}
}
}
}
applyEdge(edge, elkEdge) {
const points = [];
if (elkEdge.sections && elkEdge.sections.length > 0) {
const section = elkEdge.sections[0];
if (section.startPoint) {
points.push(section.startPoint);
}
if (section.bendPoints) {
points.push(...section.bendPoints);
}
if (section.endPoint) {
points.push(section.endPoint);
}
}
edge.routingPoints = points;
if (elkEdge.labels) {
elkEdge.labels.forEach(elkLabel => {
const label = elkLabel.id ? this.modelState.index.findByClass(elkLabel.id, server_1.GLabel) : undefined;
if (label) {
this.applyShape(label, elkLabel);
}
});
}
}
};
exports.GlspElkLayoutEngine = GlspElkLayoutEngine;
__decorate([
(0, inversify_1.inject)(server_1.Logger),
__metadata("design:type", server_1.Logger)
], GlspElkLayoutEngine.prototype, "logger", void 0);
exports.GlspElkLayoutEngine = GlspElkLayoutEngine = __decorate([
(0, inversify_1.injectable)(),
__metadata("design:paramtypes", [Function, Object, Object, Object])
], GlspElkLayoutEngine);
//# sourceMappingURL=glsp-elk-layout-engine.js.map