Skip to content

Instantly share code, notes, and snippets.

@ThomasRohde
Last active July 27, 2023 06:53
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ThomasRohde/c3b895a263b334b9e908d798802e1432 to your computer and use it in GitHub Desktop.
Save ThomasRohde/c3b895a263b334b9e908d798802e1432 to your computer and use it in GitHub Desktop.
Layout ArchiMate views using ELK (Eclipse Layout Kernel). #jArchi #Archi #ArchiMate
/*
Author: Thomas Klok Rohde
Description: Layout ArchiMate views using ELK (Eclipse Layout Kernel) compiled to JavaScript. The current implementation does not consider edges with edges (sections).
WARNING: This has only been tested on Windows 10 with Edge (WebView2)
History:
October 11, 2022 : Created with base set of scripts
*/
console.show();
console.clear();
let view;
const viewAsObject = {
visitedEdges: $("#null"),
visitNode: function (n) {
let children = [];
let edges = [];
let node = { id: n.id };
if ($(n).is("element")) {
node.width = n.bounds.width;
node.height = n.bounds.height;
}
// Add any ELK properties defined in object properties. They are prefixed with ":"
// Properties set on nodes, specifically the root view node, will overide any ELK properties defined in the dialog
let props = n.prop();
let hasprops = false;
let propertyElement = {};
props.forEach(p => {
let domain = p.split(":")[0];
if (domain == "ELK") {
hasprops = true;
propertyElement[p.split(":")[1]] = n.prop(p);
}
});
if (hasprops) node.properties = propertyElement;
// Recursively add all child elements
$(n).children().not("relationship").each(element => {
children.push(viewAsObject.visitNode(element));
});
if (children.length) node.children = children;
// Now, add all relationships
let relationships = $("#null");
$(n).children().not("relationship").rels().each(rel => {
if (rel) {
srcParent = $(rel.source).parent().first();
tgtParent = $(rel.target).parent().first();
if (!(srcParent.id == rel.target.id || tgtParent.id == rel.source.id) && (!relationships.contains(rel)))
relationships.add(rel);
}
});
relationships.each(rel => {
if (!viewAsObject.visitedEdges.contains(rel)) {
edge = {
id: rel.id,
source: rel.source.id,
target: rel.target.id
}
edges.push(edge);
viewAsObject.visitedEdges.add(rel);
}
});
if (edges.length) node.edges = edges;
return node;
},
create: function () {
view = $(selection).filter("archimate-diagram-model").add($(selection).filter("sketch-model")).add($(selection).filter("canvas-model")).first();
if (view) return this.visitNode(view)
else return undefined;
}
}
let viewObject = JSON.stringify(viewAsObject.create());
if (viewObject == undefined)
throw new Error("No view selected");
const SWT = Java.type('org.eclipse.swt.SWT');
const FillLayout = Java.type('org.eclipse.swt.layout.FillLayout');
const Shell = Java.type('org.eclipse.swt.widgets.Shell');
const Browser = Java.type('org.eclipse.swt.browser.Browser');
const ProgressAdapter = Java.extend(Java.type('org.eclipse.swt.browser.ProgressAdapter'));
const LocationAdapter = Java.extend(Java.type('org.eclipse.swt.browser.LocationAdapter'));
const CustomFunction = Java.extend(Java.type('org.eclipse.swt.browser.BrowserFunction'));
const IArchiImages = Java.type('com.archimatetool.editor.ui.IArchiImages');
const ImageFactory = Java.type('com.archimatetool.editor.ui.ImageFactory');
let display = shell.getDisplay();
let newShell = new Shell(display, SWT.MODAL | SWT.TITLE | SWT.ON_TOP);
newShell.setText("Eclipse Layout Kernel");
newShell.setLayout(new FillLayout());
html = `<html>
<title>Eclipse browser</title>
<style>
*,
*:before,
*:after {
box-sizing: border-box;
}
input[type=text],
input[type=number],
input[type=email],
select {
padding: 5px;
margin: 5px 0;
border-radius: 5px;
border-width: 2px;
width: 100%;
}
fieldset {
margin: 10px;
border-width: 2px;
border-radius: 5px;
}
button {
margin: 10px;
padding: 5px;
width: 100px;
}
</style>
<body>
<fieldset>
<legend>Select layout options</legend>
<select id="algorithm" size="1" placeholder="Select layout algorithm">
<option value="layered">Layered</option>
<option value="stress">Stress</option>
<option value="mrtree">Mr Tree</option>
<option value="radial">Radial</option>
<option value="force">Force</option>
<option value="disco">Disco</option>
<option value="sporeOverlap">Spore Overlap</option>
<option value="sporeCompaction">Spore Compaction</option>
<option value="rectpacking">Rect Packing</option>
</select>
<select id="direction" size="1" placeholder="Select layout direction">
<option value="UP">Up</option>
<option value="DOWN">Down</option>
<option value="LEFT">Left</option>
<option value="RIGH">Right</option>
</select>
<input type="number" id="spacing" placeholder="Enter node spacing"><br>
<input type="number" id="layerspacing" placeholder="Enter node spacing between layers"><br>
</fieldset>
<button onclick="okPressed()">Ok</button><button onclick="cancelPressed()">Cancel</button>
<script type="text/javascript" src="${__DIR__ + "/lib/elk.bundled.js"}"></script>
<script type="text/javascript">
function okPressed() {
const graph = ${viewObject}
const elk = new ELK();
const algorithm = document.getElementById("algorithm").value;
const direction = document.getElementById("direction").value;
const properties = {
'elk.algorithm': (algorithm == "") ? "layered" : algorithm,
'elk.spacing.nodeNode': (document.getElementById("spacing").value == "") ? 100:parseInt(document.getElementById("spacing").value),
'elk.layered.spacing.nodeNodeBetweenLayers': (document.getElementById("layerspacing").value == "") ? 100:parseInt(document.getElementById("layerspacing").value),
'elk.direction': (direction == "") ? "DOWN" : direction
};
graph.properties = { ...properties, ...graph.properties};
elk.layout(graph).then(function (g) {
okPressedEvent(JSON.stringify(g));
})
}
function cancelPressed() {
cancelPressedEvent();
}
</script>
</body>`;
let okPressed = false;
let cancelPressed = false;
// let browser = new Browser(newShell, SWT.NONE);
let browser = new Browser(newShell, SWT.EDGE);
let graph = {};
browser.addProgressListener(new ProgressAdapter({
completed: function (event) {
let fncOk = new CustomFunction(browser, "okPressedEvent", {
function: function (args) {
okPressed = true;
graph = JSON.parse(args[0]);
}
});
let fncCancel = new CustomFunction(browser, "cancelPressedEvent", {
function: function (args) {
cancelPressed = true;
}
});
browser.addLocationListener(new LocationAdapter({
changed: function (e) {
browser.removeLocationListener(this);
fncOk.dispose();
fncCancel.dispose();
}
}));
}
}));
// Write the HTML to a temporary file, so we are allowed to execute a local script
let System = Java.type('java.lang.System');
let tmpfile = System.getProperty("java.io.tmpdir") + "layout.html";
$.fs.writeFile(tmpfile, html);
browser.setUrl("file:///" + tmpfile);
// Set icon to Archi icon, in case shell has a style which displays icons
newShell.setImage(IArchiImages.ImageFactory.getImage(IArchiImages.ICON_APP));
newShell.setSize(500, 475);
newShell.open();
while (!newShell.isDisposed() && !okPressed && !cancelPressed) {
if (!display.readAndDispatch()) display.sleep();
}
if (okPressed) {
tmpfile = System.getProperty("java.io.tmpdir") + "graph.js";
$.fs.writeFile(tmpfile, JSON.stringify(graph, null, 3));
console.log('Layouting done. Proceeding to apply to view.');
// Now, use the layout on the view
function layoutNode(node) {
object = $("#" + node.id).first();
if ($(object).is("element")) {
object.bounds = {
x: node.x,
y: node.y,
width: node.width,
height: node.height
};
}
let children = node.children;
if (children) {
children.forEach(c => {
layoutNode(c);
});
}
let edges = node.edges;
if (edges) {
edges.forEach(r => {
const edge = $("#" + r.id).first();
edge.deleteAllBendpoints();
const srcX = edge.source.bounds.x;
const srcY = edge.source.bounds.y;
const tgtX = edge.target.bounds.x;
const tgtY = edge.target.bounds.y;
const sections = r.sections;
if (sections) {
// We don't care about junctions for now, so we assume only 1 section
const section = sections[0];
const bendpoints = section.bendPoints;
if (bendpoints) {
for (let i = 0; i < bendpoints.length; i++) {
let x = bendpoints[i].x;
let y = bendpoints[i].y;
let bp = {
startX: x - srcX,
startY: y - srcY,
endX: x - tgtX,
endY: y - tgtY
}
edge.addRelativeBendpoint(bp, i);
}
}
}
})
}
}
layoutNode(graph);
}
else if (cancelPressed)
console.log('Dialog cancelled.')
newShell.dispose();
@multiduplikator
Copy link

@ThomasRohde: I am trying to get this script to work. I downloaded the elkjs from here: https://github.com/kieler/elkjs/releases. Then I adjusted line 146 to point to the location of the js file.

What else is needed to get this running?

Thanks for your help. This is really the script I need...

@ThomasRohde
Copy link
Author

Have you tried this advice from the Archi JArchi forum, to change line 173 to:

let browser = new Browser(newShell, SWT.EDGE);

https://forum.archimatetool.com/index.php?topic=1264.0

@multiduplikator
Copy link

I changed line 146 back to original and then changed line 173 as per advice.

This works like a charm! Thank you so much!!!

Where is the donate button?

@multiduplikator
Copy link

@ThomasRohde Last question (I hope). Which layout option would you recommend to get close to dot style (graphviz)?

And thank you again for support. Highly appreciated.

@multiduplikator
Copy link

Somehow, its hard to control the layout to account for the labels of the relationships. They are very likely to overlap and make the layout unreadable if we have many connections.

Do I miss an option to control this?

@ThomasRohde
Copy link
Author

I actually haven't played much with it - I've spent a lot of time just finding a layout library and getting it to work with Archi. I'm still having an issue with bending points. And compound objects doesn't work right, the labels aren't considered. Although I can center+top the labels, there is still not enough room to fit the label nicely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment