|
|
@@ -0,0 +1,839 @@
|
|
|
+Draw.loadPlugin(function(ui) {
|
|
|
+
|
|
|
+ const model = ui.editor.graph.model;
|
|
|
+
|
|
|
+ const defaultConfig = {
|
|
|
+ // When making an activity wider, the x-position of ports will be scaled proportionally to the new width, and vice-versa with height and y-position. This may be annoying when simply resizing an activity to add an extra port.
|
|
|
+ movePortsOnResizeActivity: false,
|
|
|
+ // The new algorithm probably gives better results under all circumstances.
|
|
|
+ newPortSnappingAlgorithm: true,
|
|
|
+
|
|
|
+ // Edge styles.
|
|
|
+ edgeStyleCtrlFlow: "edgeStyle=orthogonalEdgeStyle;endArrow=classic;rounded=0;html=1;strokeWidth=2;fontSize=14;strokeColor=#004C99;jumpStyle=gap;",
|
|
|
+ edgeStyleDataFlow: "edgeStyle=orthogonalEdgeStyle;endArrow=open;rounded=0;html=1;strokeWidth=1;fontSize=14;fontColor=#000000;fillColor=#d5e8d4;strokeColor=#6D9656;endFill=0;jumpStyle=gap;",
|
|
|
+ edgeStyleTypedBy: "edgeStyle=0;endArrow=blockThin;html=1;strokeWidth=1;fontSize=14;fontColor=#000000;dashed=1;endFill=1;endSize=6;strokeColor=#999999;",
|
|
|
+ edgeStyleParentVersion: "edgeStyle=orthogonalEdgeStyle;endArrow=classic;rounded=0;html=1;fontSize=11;fontColor=#000000;fillColor=#fff2cc;strokeColor=#d6b656;rounded=0;jumpStyle=gap;",
|
|
|
+ edgeStyleSSLink: "edgeStyle=0;endArrow=blockThin;html=1;fontSize=11;strokeColor=#694B2E;rounded=0;dashed=1;dashPattern=1 4;endFill=1;",
|
|
|
+ edgeStyleComment: "edgeStyle=0;endArrow=none;dashed=1;html=1;",
|
|
|
+
|
|
|
+
|
|
|
+ portLabelOffsetEdgeDirection: 22,
|
|
|
+ portLabelOffsetPerpendicularToEdgeDirection: 24,
|
|
|
+ };
|
|
|
+
|
|
|
+ let currentConfig;
|
|
|
+
|
|
|
+ const existingConfig = window.localStorage.getItem('ftgpmConfig');
|
|
|
+ if (existingConfig === null) { // unlike Object and Map, localStorage returns 'null' when item doesn't exist...
|
|
|
+ currentConfig = {};
|
|
|
+ } else {
|
|
|
+ console.log("Have existing ftgpm config...", existingConfig)
|
|
|
+ currentConfig = JSON.parse(existingConfig);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Configuration dialog window:
|
|
|
+ function createFtgpmConfigWindow() { // only called once - this is only a function in order not to pollute namespace
|
|
|
+
|
|
|
+ // Drawio has no decent abstraction for UI elements or even displaying a dialog window.
|
|
|
+ // The following is based on code from src/main/webapp/js/diagramly/Dialogs.js
|
|
|
+ const wndDiv = document.createElement('div');
|
|
|
+ wndDiv.style.userSelect = 'none';
|
|
|
+ wndDiv.style.overflow = 'hidden';
|
|
|
+ wndDiv.style.padding = '10px';
|
|
|
+ wndDiv.style.height = '100%';
|
|
|
+ const wnd = new mxWindow("FTG+PM Plugin Configuration",
|
|
|
+ wndDiv, 100, 100, 640, 570, true, true);
|
|
|
+ wnd.destroyOnClose = false;
|
|
|
+ wnd.setMaximizable(false);
|
|
|
+ wnd.setResizable(false);
|
|
|
+ wnd.setClosable(true);
|
|
|
+ wnd.addListener('show', mxUtils.bind(this, function() {
|
|
|
+ statusText.innerHTML = "";
|
|
|
+ }));
|
|
|
+
|
|
|
+ function createConfig(labelText, json, readonly) {
|
|
|
+ const columnDiv = document.createElement('div');
|
|
|
+ columnDiv.style.display = 'inline-block';
|
|
|
+
|
|
|
+ const label = document.createElement('label');
|
|
|
+ const textarea = document.createElement('textarea');
|
|
|
+ label.setAttribute('for', textarea);
|
|
|
+ label.innerHTML = labelText;
|
|
|
+ textarea.setAttribute('wrap', 'off');
|
|
|
+ textarea.setAttribute('spellcheck', 'false');
|
|
|
+ textarea.setAttribute('autocorrect', 'off');
|
|
|
+ textarea.setAttribute('autocomplete', 'off');
|
|
|
+ textarea.setAttribute('autocapitalize', 'off');
|
|
|
+ textarea.setAttribute('wrap', 'hard');
|
|
|
+ if (readonly) {
|
|
|
+ textarea.setAttribute('readonly', 'true');
|
|
|
+ }
|
|
|
+ textarea.value = JSON.stringify(json, null, 2);
|
|
|
+ textarea.style.overflow = 'auto';
|
|
|
+ textarea.style.resize = 'none';
|
|
|
+ textarea.style.width = '300px';
|
|
|
+ textarea.style.height = '360px';
|
|
|
+ textarea.style.marginBottom = '16px';
|
|
|
+
|
|
|
+ columnDiv.appendChild(label);
|
|
|
+ mxUtils.br(columnDiv);
|
|
|
+ columnDiv.appendChild(textarea);
|
|
|
+ return [columnDiv, textarea];
|
|
|
+ }
|
|
|
+
|
|
|
+ const [defaultColumn] = createConfig("Default config (hardcoded & read-only):", defaultConfig, true);
|
|
|
+ const [customColumn, customTextArea] = createConfig("Custom config (acts as an 'overlay' on top of default config):", currentConfig, false);
|
|
|
+ wndDiv.appendChild(defaultColumn);
|
|
|
+ wndDiv.appendChild(customColumn);
|
|
|
+
|
|
|
+ mxUtils.br(wndDiv);
|
|
|
+
|
|
|
+ const buttonsDiv = document.createElement('div');
|
|
|
+ buttonsDiv.style.textAlign = 'right';
|
|
|
+ const statusText = document.createElement('div');
|
|
|
+ statusText.style.display = 'inline-block';
|
|
|
+
|
|
|
+ // attempts to save user configuration and displays status text as a side effect
|
|
|
+ // returns true if all went well
|
|
|
+ function saveConfigurationAndDisplayStatus() {
|
|
|
+ let parsed;
|
|
|
+ try {
|
|
|
+ parsed = JSON.parse(customTextArea.value); // may throw
|
|
|
+ } catch (parseErr) {
|
|
|
+ statusText.innerHTML = "Parse error: " + parseErr.toString();
|
|
|
+ statusText.style.color = 'red';
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
|
|
|
+ statusText.innerHTML = "JSON value is not an object";
|
|
|
+ statusText.style.color = 'red';
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ currentConfig = parsed;
|
|
|
+ window.localStorage.setItem('ftgpmConfig', JSON.stringify(parsed));
|
|
|
+ customTextArea.value = JSON.stringify(parsed, null, 2); // prettify
|
|
|
+ statusText.innerHTML = "Configuration successfully set and saved (in window.localStorage)";
|
|
|
+ statusText.style.color = 'green';
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ const applyButton = mxUtils.button("Apply", saveConfigurationAndDisplayStatus);
|
|
|
+ applyButton.className = 'geBtn gePrimaryBtn';
|
|
|
+ applyButton.style.float = 'none'; // override geBtn style
|
|
|
+
|
|
|
+ const resetButton = mxUtils.button("Reset", function() {
|
|
|
+ customTextArea.value = JSON.stringify(currentConfig, null, 2);
|
|
|
+ });
|
|
|
+ resetButton.className = 'geBtn';
|
|
|
+ resetButton.style.float = 'none'; // override geBtn style
|
|
|
+
|
|
|
+ buttonsDiv.appendChild(statusText);
|
|
|
+ buttonsDiv.appendChild(applyButton);
|
|
|
+ buttonsDiv.appendChild(resetButton);
|
|
|
+
|
|
|
+ // hacks buttons:
|
|
|
+ const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
|
|
|
+ if (saveConfigurationAndDisplayStatus()) {
|
|
|
+ model.beginUpdate();
|
|
|
+ for (const [cellId, cell] of Object.entries(model.cells)) {
|
|
|
+ applyCellStyleFromConfig(cell);
|
|
|
+ }
|
|
|
+ model.endUpdate();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
|
|
|
+ if (saveConfigurationAndDisplayStatus()) {
|
|
|
+ model.beginUpdate();
|
|
|
+ for (const [cellId, cell] of Object.entries(model.cells)) {
|
|
|
+ if (cell.geometry) {
|
|
|
+ // we'll force a geometry update:
|
|
|
+ const newGeometry = cell.geometry.clone();
|
|
|
+ snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
|
|
|
+ model.setGeometry(cell, newGeometry);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ model.endUpdate();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ forceEdgeStyleButton.className = 'geBtn';
|
|
|
+ forceEdgeStyleButton.style.float = 'none'; // override geBtn style
|
|
|
+ forcePortSnapButton.className = 'geBtn';
|
|
|
+ forcePortSnapButton.style.float = 'none'; // override geBtn style
|
|
|
+
|
|
|
+ hacksDiv = document.createElement('div');
|
|
|
+ hacksDiv.appendChild(document.createTextNode("Hacks: (use undo if you fuck things up)"));
|
|
|
+ mxUtils.br(hacksDiv);
|
|
|
+ hacksDiv.appendChild(forceEdgeStyleButton);
|
|
|
+ mxUtils.br(hacksDiv);
|
|
|
+ hacksDiv.appendChild(forcePortSnapButton);
|
|
|
+
|
|
|
+ wndDiv.appendChild(buttonsDiv);
|
|
|
+ mxUtils.br(wndDiv);
|
|
|
+ wndDiv.appendChild(hacksDiv);
|
|
|
+
|
|
|
+ return wnd;
|
|
|
+ }
|
|
|
+
|
|
|
+ const ftgpmConfigWindow = createFtgpmConfigWindow();
|
|
|
+
|
|
|
+ function getFromConfig(parameter) {
|
|
|
+ if (currentConfig.hasOwnProperty(parameter)) {
|
|
|
+ return currentConfig[parameter];
|
|
|
+ } else {
|
|
|
+ return defaultConfig[parameter];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // // Force style window:
|
|
|
+ // const forceStyleDiv = document.createElement('div');
|
|
|
+
|
|
|
+ // const forceStyleWindow = new mxWindow("FTG+PM: Force Style Rules",
|
|
|
+ // forceStyleDiv, 200, 200, 400, 90, true, true);
|
|
|
+ // forceStyleWindow.destroyOnClose = false;
|
|
|
+ // forceStyleWindow.setMaximizable(false);
|
|
|
+ // forceStyleWindow.setResizable(false);
|
|
|
+ // forceStyleWindow.setClosable(true);
|
|
|
+
|
|
|
+ // const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
|
|
|
+ // for (const [cellId, cell] of Object.entries(model.cells)) {
|
|
|
+ // applyCellStyleFromConfig(cell);
|
|
|
+ // }
|
|
|
+ // });
|
|
|
+ // const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
|
|
|
+ // for (const [cellId, cell] of Object.entries(model.cells)) {
|
|
|
+ // if (cell.geometry) {
|
|
|
+ // // we'll force a geometry update:
|
|
|
+ // const newGeometry = cell.geometry.clone();
|
|
|
+ // snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
|
|
|
+ // model.setGeometry(cell, newGeometry);
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // })
|
|
|
+ // forceEdgeStyleButton.className = 'geBtn';
|
|
|
+ // forcePortSnapButton.className = 'geBtn';
|
|
|
+ // forceStyleDiv.appendChild(forceEdgeStyleButton);
|
|
|
+ // mxUtils.br(forceStyleDiv);
|
|
|
+ // forceStyleDiv.appendChild(forcePortSnapButton);
|
|
|
+
|
|
|
+ window.onkeyup = function(e) {
|
|
|
+ // Shortcut to show FTG+PM config:
|
|
|
+ if (e.ctrlKey && e.key === '.') {
|
|
|
+ ftgpmConfigWindow.show();
|
|
|
+ }
|
|
|
+ // // Shortcut to apply style from config to all edges:
|
|
|
+ // if (e.ctrlKey && e.key === 'Enter') {
|
|
|
+ // forceStyleWindow.show();
|
|
|
+ // }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // For auto-updating port orientation
|
|
|
+ // Note: this is only the 'shape', not the full style
|
|
|
+ const encodedPortShapes = {
|
|
|
+ in: {
|
|
|
+ l: `stencil(pZNdD4IgFIZ/DbcNYa3rZvU/yE7JRHBAX/++Y0c3zXC2btzO+4wHfFEm81CqBpjgJZM7JoRc4wPHO42C06hCA0Wk7Ka8VkcDREL0roK7PsXOoG0JXseWyj3jW9Y6OJN54axFiXY2jMiAo0xpi2v5g2Td9s9uWnWna3CHGiJ4yjNKmTgsFmf/iGWOSep1ZH5URXXx7mpP01VJdnYevoAeN6rtdwJ6XLsbJFuba6cXGG0Hgg9Dtun7+UUydshFhsK4ADNXiXmiCWxQG0NfY+rKJh2/U/oD3sEL)`,
|
|
|
+ r: `stencil(pZNdD4IgFIZ/DbcNYa3rZvU/yE7JQnBAVv++o0eXH9ls3bid9xkP8qJMpiFXJTDBcyZ3TAi5xgeOdxoFp1GFErJIWaW8VkcDREL07gp3fYqtQdscvI41lXvGt6x2cCbTzFmLEu1sGJAeR5nSFtfyB8na7Z/ttGrfrsQdCojgKU8oZeKwWJz8I5YpJnPHkelRZdeLdzd7mq6aZWfn4QPocKnqfiegw4Wr4H24cW3f6ukMRtueYShINl0/vzhGErlIkRkX4MtdYj5TBVaojaHPce7OJiU3Kf0CTfAC)`,
|
|
|
+ t: `stencil(nZPRDoIgGIWfhtuGsNZ1s3oP0r9kIjggrbfv1183S23VDds5Z3zAAZhMQ6FqYIIXTB6YEILjgLIlKbckVaghi+Q1ymt1NkBJiN6V0Oo8DgRtC/A6dqk8Mr5nHZIzmWbOWoRoZ8NLMskRprTFufxOML4ZNvAYNKkaV6gggic3IZeJ09/g5BewTNFZO45Mzyorr97dbD6ftZpdnIeFYIxr1fU7C8a4cg1MDrfU2XI7I8BoOwHIxdZ/ISS7t4K/g2TGBfhwleivNIENamPoNa5d2azj3qUf0BtP)`,
|
|
|
+ b: `stencil(nZNRD4IgFIV/Da8NYa3nZvU/EG/JQnBAWv++q1c3m9pWL27nnPHJPQCTeaxUA0zwiskTE0Jw/KDsSMo9SRUb0Im8VgWjCguUxBT8HTpTppFgXAXBpD6VZ8aPrEdyJnPtnUOI8S5+JLMcYco4XMufBOO7cQOvUZNq8A81JAjkZuQycfkbnP0Cljk6W+PIvFD6fgv+4crlqs3s6gOsBFPcqL7fRTDFtW9hNtzHaJNcr2ciWONmhOywXvwvDLn/Yxva+ghfzhL9jSqwQmMtXcetM1uUPLj0BAbjDQ==)`,
|
|
|
+ }
|
|
|
+ };
|
|
|
+ encodedPortShapes.out = {
|
|
|
+ r: encodedPortShapes.in.l,
|
|
|
+ l: encodedPortShapes.in.r,
|
|
|
+ t: encodedPortShapes.in.b,
|
|
|
+ b: encodedPortShapes.in.t,
|
|
|
+ }
|
|
|
+
|
|
|
+ // Styles for our different edge types
|
|
|
+ // Mapping from 'pmRole' attribute to key in config.
|
|
|
+ const edgeStyles = {
|
|
|
+ ctrl_flow: 'edgeStyleCtrlFlow',
|
|
|
+ data_flow: 'edgeStyleDataFlow',
|
|
|
+ typed_by: 'edgeStyleTypedBy',
|
|
|
+ parent_version: 'edgeStyleParentVersion',
|
|
|
+ comment_edge: 'edgeStyleComment',
|
|
|
+ };
|
|
|
+
|
|
|
+ function applyCellStyleFromConfig(cell) {
|
|
|
+ const ftgpmType = cell.getAttribute(TYPE_ATTR);
|
|
|
+ if (ftgpmType) {
|
|
|
+ if (edgeStyles.hasOwnProperty(ftgpmType)) {
|
|
|
+ setEdgeStyle(cell, ftgpmType);
|
|
|
+ }
|
|
|
+ // TODO: do the same on nodes.
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ const TYPE_ATTR = "pmRole"; // cell attribute holding the ftgpm type
|
|
|
+
|
|
|
+ function parseStyle(str) {
|
|
|
+ const pairs = str.split(';').filter(s => s !== '');
|
|
|
+ const map = new Map();
|
|
|
+ pairs.map(pair => {
|
|
|
+ const [key,value] = pair.split('=');
|
|
|
+ map.set(key, value);
|
|
|
+ });
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ function unparseStyle(map) {
|
|
|
+ let str = "";
|
|
|
+ for (const [key, value] of map.entries()) {
|
|
|
+ if (value === undefined) {
|
|
|
+ str += key + ';';
|
|
|
+ } else {
|
|
|
+ str += key + '=' + value + ';';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return str;
|
|
|
+ }
|
|
|
+
|
|
|
+ // copy selected keys (=array) from fromMap to toMap
|
|
|
+ function mapAssign(toMap, fromMap, keys) {
|
|
|
+ for (const key of keys) {
|
|
|
+ const value = fromMap.get(key);
|
|
|
+ if (value !== undefined) {
|
|
|
+ toMap.set(key, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function reverseEdge(edge, sourceCell, targetCell) {
|
|
|
+ // Reverse source and target
|
|
|
+ ui.editor.graph.model.setTerminals(edge, targetCell, sourceCell);
|
|
|
+
|
|
|
+ // Reverse 'waypoints'
|
|
|
+ if (edge.geometry.points) {
|
|
|
+ const newGeometry = edge.geometry.clone();
|
|
|
+ newGeometry.points.reverse();
|
|
|
+ ui.editor.graph.model.setGeometry(edge, newGeometry);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Reverse entry and exit points
|
|
|
+ const oldStyle = parseStyle(model.getStyle(edge));
|
|
|
+ const newStyle = new Map(oldStyle);
|
|
|
+ // entry becomes exit
|
|
|
+ newStyle.set('entryX', oldStyle.get('exitX'));
|
|
|
+ newStyle.set('entryY', oldStyle.get('exitY'));
|
|
|
+ // exit becomes entry
|
|
|
+ newStyle.set('exitX', oldStyle.get('entryX'));
|
|
|
+ newStyle.set('exitY', oldStyle.get('entryY'));
|
|
|
+ // entryD becomes exitD
|
|
|
+ newStyle.set('entryDx', oldStyle.get('exitDx'));
|
|
|
+ newStyle.set('entryDy', oldStyle.get('exitDy'));
|
|
|
+ // exitD becomes entryD
|
|
|
+ newStyle.set('exitDx', oldStyle.get('entryDx'));
|
|
|
+ newStyle.set('exitDy', oldStyle.get('entryDy'));
|
|
|
+ model.setStyle(edge, unparseStyle(newStyle));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Simply sets the data attribute TYPE_ATTR to ftgpm_type.
|
|
|
+ function setFtgpmType(cell, ftgpm_type) {
|
|
|
+ // Set type attribute
|
|
|
+ let value = model.getValue(cell);
|
|
|
+ if (!value) {
|
|
|
+ // Workaround: 'value' must be an XML element
|
|
|
+ value = mxUtils.createXmlDocument().createElement('object');
|
|
|
+ value.setAttribute('label', '');
|
|
|
+ }
|
|
|
+ value.setAttribute(TYPE_ATTR, ftgpm_type);
|
|
|
+ model.setValue(cell, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ function setEdgeStyle(edge, style_type) {
|
|
|
+ // Update style
|
|
|
+
|
|
|
+ // Workaround: don't overwrite connection points
|
|
|
+ const oldstyle = parseStyle(model.getStyle(edge));
|
|
|
+ const newstyle = new Map(parseStyle(getFromConfig(edgeStyles[style_type])));
|
|
|
+ mapAssign(newstyle, oldstyle, [
|
|
|
+ // retain these properties from oldstyle:
|
|
|
+ "entryX", "entryY", "entryDx", "entryDy",
|
|
|
+ "exitX", "exitY", "exitDx", "exitDy"
|
|
|
+ ]);
|
|
|
+ model.setStyle(edge, unparseStyle(newstyle));
|
|
|
+ }
|
|
|
+
|
|
|
+ function isArtifact(type) {
|
|
|
+ return type === "artifact";
|
|
|
+ }
|
|
|
+ function isFormalism(type) {
|
|
|
+ return type === "formalism";
|
|
|
+ }
|
|
|
+ function isTransformation(type) {
|
|
|
+ return type === "auto_transformation" || type === "transformation" || type === "comp_transformation";
|
|
|
+ }
|
|
|
+ function isActivityNode(type) {
|
|
|
+ return type === "autom_activity" || type === "activity" || type === "comp_activity";
|
|
|
+ }
|
|
|
+ function isControlFlowPort(type) {
|
|
|
+ return type === "ctrl_in" || type === "ctrl_out";
|
|
|
+ }
|
|
|
+ function isDataPort(type) {
|
|
|
+ return type === "data_in" || type === "data_out";
|
|
|
+ }
|
|
|
+ function isInport(type) {
|
|
|
+ return type === "data_in" || type === "ctrl_in";
|
|
|
+ }
|
|
|
+ function isOutport(type) {
|
|
|
+ return type === "data_out" || type === "ctrl_out";
|
|
|
+ }
|
|
|
+ function isPort(type) {
|
|
|
+ return ["data_in", "data_out", "ctrl_in", "ctrl_out"].includes(type);
|
|
|
+ }
|
|
|
+ function isControlFlowNode(type) {
|
|
|
+ return isControlFlowPort(type) || type === "initial" || type === "final" || type === "fork_join";
|
|
|
+ }
|
|
|
+ function isDataFlowNode(type) {
|
|
|
+ return isDataPort(type) || isArtifact(type) || isFormalism(type);
|
|
|
+ }
|
|
|
+ function isTraceEvent(type) {
|
|
|
+ return type === "traceevent_begin" || type === "traceevent_end";
|
|
|
+ }
|
|
|
+ function isArtifactVersion(type) {
|
|
|
+ return type === "artifact_version";
|
|
|
+ }
|
|
|
+ function getFlowType(type) {
|
|
|
+ if (isDataFlowNode(type)) {
|
|
|
+ return "data";
|
|
|
+ }
|
|
|
+ if (isControlFlowNode(type)) {
|
|
|
+ return "ctrl";
|
|
|
+ }
|
|
|
+ // throw new Error("unknown flow type");
|
|
|
+ console.log("unknown flow type");
|
|
|
+ }
|
|
|
+
|
|
|
+ // [ condition, flow-type ]
|
|
|
+ const edgeTypeRules = [
|
|
|
+ // PM control flow
|
|
|
+ [ (src, tgt) => isControlFlowNode(src) && isControlFlowNode(tgt), "ctrl_flow" ],
|
|
|
+
|
|
|
+ // PM data flow
|
|
|
+ [ (src, tgt) => isDataFlowNode(src) && isDataFlowNode(tgt), "data_flow" ],
|
|
|
+
|
|
|
+ // FTG data flow
|
|
|
+ [ (src, tgt) => isTransformation(src) && isFormalism(tgt), "data_flow" ],
|
|
|
+ [ (src, tgt) => isFormalism(src) && isTransformation(tgt), "data_flow" ],
|
|
|
+
|
|
|
+ [ (src,tgt) => isActivityNode(src) && isTransformation(tgt), "typed_by" ],
|
|
|
+ [ (src,tgt) => isArtifact(src) && isFormalism(tgt), "typed_by" ],
|
|
|
+
|
|
|
+ [ (src,tgt) => isTraceEvent(src) && isArtifactVersion(tgt), "data_flow"],
|
|
|
+ [ (src,tgt) => isArtifactVersion(src) && isTraceEvent(tgt), "data_flow"],
|
|
|
+ [ (src,tgt) => isTraceEvent(src) && isTraceEvent(tgt), "ctrl_flow"],
|
|
|
+
|
|
|
+ [ (src,tgt) => src === "traceevent_begin" && tgt === "ctrl_in", "typed_by"],
|
|
|
+ [ (src,tgt) => src === "traceevent_end" && tgt === "ctrl_out", "typed_by"],
|
|
|
+ [ (src,tgt) => isArtifactVersion(src) && isArtifact(tgt), "typed_by"],
|
|
|
+
|
|
|
+ [ (src,tgt) => isArtifactVersion(src) && isArtifactVersion(tgt), "parent_version"],
|
|
|
+
|
|
|
+ [ (src,tgt) => isArtifactVersion(src) && tgt === "storage", "ss_link"],
|
|
|
+ [ (src,tgt) => isArtifactVersion(src) && tgt === "real_object", "ss_link"],
|
|
|
+ [ (src,tgt) => src === "traceevent_begin" && tgt === "service", "ss_link"],
|
|
|
+
|
|
|
+ [ (src,tgt) => src === "comment" || tgt === "comment", "comment_edge"],
|
|
|
+ ];
|
|
|
+
|
|
|
+ // Very specific: This function is used to determine the correct direction of an arrow automatically
|
|
|
+ // An activity or artifact at the highest level (= a direct child of a layer) has level 0
|
|
|
+ // Every time something is nested in an activity, the level increases by 1.
|
|
|
+ // Ports are special: they can only be a child of an activity, but they have the same level as the activity.
|
|
|
+ function getNestedLevel(cell) {
|
|
|
+ function getLvlRecursive(cell) {
|
|
|
+ if (!cell.parent) {
|
|
|
+ throw Error("getNestedLevel called with parentless cell (i.e. cell is root, or is deleted)");
|
|
|
+ }
|
|
|
+ if (model.isLayer(cell)) {
|
|
|
+ return 0;
|
|
|
+ // throw Error("getNestedLevel called with layer cell");
|
|
|
+ }
|
|
|
+ if (cell.parent === model.isLayer(cell.parent)) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ else if (isActivityNode(cell.parent.getAttribute(TYPE_ATTR))) {
|
|
|
+ return getLvlRecursive(cell.parent) + 1;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ return getLvlRecursive(cell.parent);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const baseLevel = getLvlRecursive(cell);
|
|
|
+ if (isPort(cell.getAttribute(TYPE_ATTR))) {
|
|
|
+ return baseLevel - 1;
|
|
|
+ } else {
|
|
|
+ return baseLevel;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function isDirectionless(type) {
|
|
|
+ return isArtifact(type) || type === "fork_join";
|
|
|
+ }
|
|
|
+
|
|
|
+ ui.editor.graph.addListener(mxEvent.CELL_CONNECTED, (_, eventObj) => {
|
|
|
+ // Happens whenever an edge is (dis)connected.
|
|
|
+
|
|
|
+ const edge = eventObj.properties.edge;
|
|
|
+ const sourceCell = edge.source;
|
|
|
+ const targetCell = edge.getTerminal();
|
|
|
+
|
|
|
+ // This will change the edge style WITHIN the transaction of the edit operation.
|
|
|
+ // The terminal-change and style-change will be one edit operation from point of view of undo manager.
|
|
|
+
|
|
|
+ const sourceType = sourceCell ? sourceCell.getAttribute(TYPE_ATTR) : null;
|
|
|
+ const targetType = targetCell ? targetCell.getAttribute(TYPE_ATTR) : null;
|
|
|
+
|
|
|
+ // Update style if necessary
|
|
|
+ for (const [cond, linkType] of edgeTypeRules) {
|
|
|
+ if (cond(sourceType, targetType)) {
|
|
|
+ setFtgpmType(edge, linkType);
|
|
|
+ setEdgeStyle(edge, linkType);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (sourceCell && targetCell) {
|
|
|
+ // Auto-correct edge direction in certain cases.
|
|
|
+ // We call 'reverseEdge' if we have an illegal situation, but reversing the arrow gives a legal situation.
|
|
|
+ const sourceLvl = getNestedLevel(sourceCell);
|
|
|
+ const targetLvl = getNestedLevel(targetCell);
|
|
|
+
|
|
|
+ // We have 2 flow types: ctrl and data. They remain segregated.
|
|
|
+ if (getFlowType(sourceType) === getFlowType(targetType)) {
|
|
|
+ if (sourceLvl === targetLvl) {
|
|
|
+ // Equally nested ...
|
|
|
+ if (isInport(sourceType) && isOutport(targetType)
|
|
|
+ || isInport(sourceType) && isDirectionless(targetType)
|
|
|
+ || isDirectionless(sourceType) && isOutport(targetType)) {
|
|
|
+ reverseEdge(edge, sourceCell, targetCell);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (sourceLvl === targetLvl + 1) {
|
|
|
+ // Source of arrow is nested deeper ...
|
|
|
+ if (isInport(sourceType) && isInport(targetType)
|
|
|
+ || isDirectionless(sourceType) && isInport(targetType)) {
|
|
|
+ // Should flow from shallowly nested input port to deeply nested input port:
|
|
|
+ reverseEdge(edge, sourceCell, targetCell);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (sourceLvl + 1 === targetLvl) {
|
|
|
+ // Target of arrow is nested deeper ...
|
|
|
+ if (isOutport(sourceType) && isOutport(targetType)
|
|
|
+ || isOutport(sourceType) && isDirectionless(targetType)) {
|
|
|
+ // Should flow from deeply nested output port to shallowly nested output port
|
|
|
+ reverseEdge(edge, sourceCell, targetCell);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ function outsideOffset(wOrH) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ function centerOffset(wOrH) {
|
|
|
+ return -wOrH / 2;
|
|
|
+ }
|
|
|
+ function insideOffset(wOrH) {
|
|
|
+ return -wOrH;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Change this to position ports outside, inside or centered at the edge.
|
|
|
+ const borderOffset = centerOffset;
|
|
|
+
|
|
|
+ function rightOrBottomBorder(wOrH, parentWorH) {
|
|
|
+ return parentWorH + borderOffset(wOrH);
|
|
|
+ }
|
|
|
+ function leftOrTopBorder(wOrH) {
|
|
|
+ return -wOrH - borderOffset(wOrH);
|
|
|
+ }
|
|
|
+
|
|
|
+ function cellCenter(cellGeometry) {
|
|
|
+ // Coordinates are relative to topleft corner of parent shape
|
|
|
+ const {x,y, width, height} = cellGeometry;
|
|
|
+ // Center of cell
|
|
|
+ return {
|
|
|
+ x: x + width/2,
|
|
|
+ y: y + height/2,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // Deprecated snapping algorithm
|
|
|
+ function closestBorder(cellGeometry, parentGeometry) {
|
|
|
+ // Cell center is relative to parent: (0,0) is topleft
|
|
|
+ const c = cellCenter(cellGeometry);
|
|
|
+
|
|
|
+ // We draw two imaginary diagonals through the parent shape, to determine whether the child shape is more close to the top, right, bottom or left side.
|
|
|
+ //
|
|
|
+ // 2
|
|
|
+ // /
|
|
|
+ // +----+
|
|
|
+ // |\ t/|
|
|
|
+ // |l\/ |
|
|
|
+ // | /\r|
|
|
|
+ // |/b \|
|
|
|
+ // +----+
|
|
|
+ // \
|
|
|
+ // 1
|
|
|
+ const slope = parentGeometry.height / parentGeometry.width;
|
|
|
+ const above1 = c.y < slope * c.x;
|
|
|
+ const above2 = c.y < parentGeometry.height - slope * c.x;
|
|
|
+
|
|
|
+ if (above1)
|
|
|
+ if (above2)
|
|
|
+ return 't';
|
|
|
+ else
|
|
|
+ return 'r';
|
|
|
+ else
|
|
|
+ if (above2)
|
|
|
+ return 'l';
|
|
|
+ else
|
|
|
+ return 'b';
|
|
|
+ }
|
|
|
+
|
|
|
+ // Improved snapping algorithm
|
|
|
+ function closestBorder2(cellGeometry, parentGeometry) {
|
|
|
+ // Cell center is relative to parent: (0,0) is topleft
|
|
|
+ const c = cellCenter(cellGeometry);
|
|
|
+
|
|
|
+ const slope = 1; // fixed slope, 45 degrees
|
|
|
+
|
|
|
+ const above1 = c.y < slope * c.x;
|
|
|
+ const above3 = c.y < (-slope) * c.x + parentGeometry.height ;
|
|
|
+
|
|
|
+ const above4 = c.y < slope * (c.x - parentGeometry.width) + parentGeometry.height;
|
|
|
+ const above2 = c.y < (-slope) * (c.x - parentGeometry.width);
|
|
|
+
|
|
|
+ if (parentGeometry.width >= parentGeometry.height) {
|
|
|
+ // Parent's width > height
|
|
|
+ //
|
|
|
+ // 1 2
|
|
|
+ // \ /
|
|
|
+ // +------+ --> x
|
|
|
+ // |\ t /|
|
|
|
+ // |l\__/ |__ 5
|
|
|
+ // | / \r|
|
|
|
+ // |/ b \|
|
|
|
+ // +------+
|
|
|
+ // / \
|
|
|
+ // 3 4
|
|
|
+ // |
|
|
|
+ // V
|
|
|
+ // y
|
|
|
+
|
|
|
+ const above5 = c.y < parentGeometry.height / 2;
|
|
|
+
|
|
|
+ if (!above1 && above3) {
|
|
|
+ return 'l';
|
|
|
+ }
|
|
|
+ if (!above2 && above4) {
|
|
|
+ return 'r';
|
|
|
+ }
|
|
|
+ if (above1 && above2 && above5) {
|
|
|
+ return 't';
|
|
|
+ }
|
|
|
+ if (!above3 && !above4 && !above5) {
|
|
|
+ return 'b';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ else {
|
|
|
+ // Parent's height > width
|
|
|
+ // 1 2
|
|
|
+ // \ /
|
|
|
+ // +----+
|
|
|
+ // |\ t/|
|
|
|
+ // | \/ |
|
|
|
+ // |l | |
|
|
|
+ // | | |
|
|
|
+ // | /\r|
|
|
|
+ // |/b \|
|
|
|
+ // +----+
|
|
|
+ // / | \
|
|
|
+ // 3 5 4
|
|
|
+
|
|
|
+ const leftOf5 = c.x < parentGeometry.width / 2;
|
|
|
+
|
|
|
+ if (above1 && above2) {
|
|
|
+ return 't';
|
|
|
+ }
|
|
|
+ if (!above3 && !above4) {
|
|
|
+ return 'b';
|
|
|
+ }
|
|
|
+ if (above3 && !above1 && leftOf5) {
|
|
|
+ return 'l';
|
|
|
+ }
|
|
|
+ if (above4 && !above2 && !leftOf5) {
|
|
|
+ return 'r';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update port shape (so it points in the right direction), move port (and port label) to border of activity.
|
|
|
+ function snapPortToBorder(cell, cellGeometry, parentGeometry, border) {
|
|
|
+ const style = parseStyle(model.getStyle(cell));
|
|
|
+ const type = cell.getAttribute(TYPE_ATTR);
|
|
|
+ const inOrOut = isInport(type) ? "in" : "out";
|
|
|
+ style.set('shape', encodedPortShapes[inOrOut][border]);
|
|
|
+
|
|
|
+ switch (border) {
|
|
|
+ case 't':
|
|
|
+ cellGeometry.width = 35;
|
|
|
+ cellGeometry.height = 20;
|
|
|
+ cellGeometry.y = leftOrTopBorder(cellGeometry.height);
|
|
|
+
|
|
|
+ style.set('align', 'right');
|
|
|
+ style.set('verticalAlign', 'bottom');
|
|
|
+
|
|
|
+ cellGeometry.offset = new mxPoint(
|
|
|
+ -getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'),
|
|
|
+ -getFromConfig('portLabelOffsetEdgeDirection'));
|
|
|
+ break;
|
|
|
+ case 'r':
|
|
|
+ cellGeometry.width = 20;
|
|
|
+ cellGeometry.height = 35;
|
|
|
+ cellGeometry.x = rightOrBottomBorder(cellGeometry.width, parentGeometry.width);
|
|
|
+
|
|
|
+ style.set('align', 'left');
|
|
|
+ style.set('verticalAlign', 'bottom');
|
|
|
+
|
|
|
+ cellGeometry.offset = new mxPoint(
|
|
|
+ getFromConfig('portLabelOffsetEdgeDirection'),
|
|
|
+ -getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'));
|
|
|
+ break;
|
|
|
+ case 'l':
|
|
|
+ cellGeometry.width = 20;
|
|
|
+ cellGeometry.height = 35;
|
|
|
+ cellGeometry.x = leftOrTopBorder(cellGeometry.width);
|
|
|
+
|
|
|
+ style.set('align', 'right');
|
|
|
+ style.set('verticalAlign', 'top');
|
|
|
+
|
|
|
+ cellGeometry.offset = new mxPoint(
|
|
|
+ -getFromConfig('portLabelOffsetEdgeDirection'),
|
|
|
+ getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'));
|
|
|
+ break;
|
|
|
+ case 'b':
|
|
|
+ cellGeometry.width = 35;
|
|
|
+ cellGeometry.height = 20;
|
|
|
+ cellGeometry.y = rightOrBottomBorder(cellGeometry.height, parentGeometry.height);
|
|
|
+
|
|
|
+ style.set('align', 'left');
|
|
|
+ style.set('verticalAlign', 'top');
|
|
|
+
|
|
|
+ cellGeometry.offset = new mxPoint(
|
|
|
+ getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'),
|
|
|
+ getFromConfig('portLabelOffsetEdgeDirection'));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ model.setStyle(cell, unparseStyle(style));
|
|
|
+ }
|
|
|
+
|
|
|
+ function snapToBorderIfCellIsPortAndParentIsActivity(cell, geometry) {
|
|
|
+ const type = cell.getAttribute(TYPE_ATTR);
|
|
|
+ if (isPort(type)) {
|
|
|
+ // Port was moved
|
|
|
+ if (cell.parent) {
|
|
|
+ const parentType = cell.parent.getAttribute(TYPE_ATTR);
|
|
|
+ if (isActivityNode(parentType) || isTransformation(parentType)) {
|
|
|
+ // Snap port to activity border
|
|
|
+ const portSnappingAlgorithm = getFromConfig('newPortSnappingAlgorithm') ? closestBorder2 : closestBorder;
|
|
|
+ // border will be one of: 'l', 'r', 't', 'b'
|
|
|
+ const border = portSnappingAlgorithm(geometry, cell.parent.geometry);
|
|
|
+ snapPortToBorder(cell, geometry, cell.parent.geometry, border);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ui.editor.graph.addListener(mxEvent.MOVE_CELLS, (_, eventObj) => {
|
|
|
+ // High-level event: Happens when the user releases dragged shape(s)
|
|
|
+ for (const cell of eventObj.properties.cells) {
|
|
|
+ snapToBorderIfCellIsPortAndParentIsActivity(cell, cell.geometry);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ ui.editor.graph.addListener(mxEvent.RESIZE_CELLS, (_, eventObj) => {
|
|
|
+ // High-level event: Happens when the user resized cell(s)
|
|
|
+ for (let i=0; i<eventObj.properties.cells.length; i++) {
|
|
|
+ const cell = eventObj.properties.cells[i];
|
|
|
+ const type = cell.getAttribute(TYPE_ATTR);
|
|
|
+ if (isActivityNode(type) || isTransformation(type)) {
|
|
|
+ // Activity was resized...
|
|
|
+
|
|
|
+ // In drawio, 'arcSize' of a rounded rectangle is relative to size of shape.
|
|
|
+ // Retain apparent arc size of activity:
|
|
|
+ const style = parseStyle(model.getStyle(cell));
|
|
|
+ const newArcSize = 1000 / Math.sqrt(cell.geometry.width * cell.geometry.height);
|
|
|
+ style.set("arcSize", newArcSize);
|
|
|
+ model.setStyle(cell, unparseStyle(style));
|
|
|
+
|
|
|
+ if (cell.children) {
|
|
|
+ // Need this for moving contained ports:
|
|
|
+ const prevGeometry = eventObj.properties.previous[i];
|
|
|
+ const scaleW = cell.geometry.width / prevGeometry.width;
|
|
|
+ const scaleH = cell.geometry.height / prevGeometry.height;
|
|
|
+
|
|
|
+ for (const child of cell.children) {
|
|
|
+ const childType = child.getAttribute(TYPE_ATTR);
|
|
|
+ // Keep activity icon in place
|
|
|
+ if (childType === "activityIcon") {
|
|
|
+ newIconGeometry = child.geometry.clone();
|
|
|
+ newIconGeometry.x = cell.geometry.width - 28;
|
|
|
+ newIconGeometry.y = 4;
|
|
|
+ model.setGeometry(child, newIconGeometry);
|
|
|
+ }
|
|
|
+ else if (isPort(childType)) {
|
|
|
+ // Move contained ports
|
|
|
+ const border = closestBorder2(child.geometry, prevGeometry); // < what was the border in the old geometry?
|
|
|
+ const newGeometry = child.geometry.clone();
|
|
|
+ snapPortToBorder(child, newGeometry, cell.geometry, border);
|
|
|
+ if (getFromConfig('movePortsOnResizeActivity')) {
|
|
|
+ // Scale position
|
|
|
+ switch (border) {
|
|
|
+ case 't':
|
|
|
+ newGeometry.x = child.geometry.x * scaleW;
|
|
|
+ break;
|
|
|
+ case 'l':
|
|
|
+ newGeometry.y = child.geometry.y * scaleH;
|
|
|
+ break;
|
|
|
+ case 'b':
|
|
|
+ newGeometry.x = child.geometry.x * scaleW;
|
|
|
+ break;
|
|
|
+ case 'r':
|
|
|
+ newGeometry.y = child.geometry.y * scaleH;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ model.setGeometry(child, newGeometry);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // Hardcoded primitive shape libraries.
|
|
|
+ // To alter, make changes to the library in the drawio webapp, then 'save' (this downloads the library as XML), and overwrite the file(s) in drawiolibs.
|
|
|
+ Promise.all([
|
|
|
+ fetch("myPlugins/shape_libs/common.xml"),
|
|
|
+ fetch("myPlugins/shape_libs/ftg.xml"),
|
|
|
+ fetch("myPlugins/shape_libs/pm.xml"),
|
|
|
+ fetch("myPlugins/shape_libs/pt.xml"),
|
|
|
+ fetch("myPlugins/shape_libs/ss.xml"),
|
|
|
+ ])
|
|
|
+ .then(all => Promise.all(all.map(response => response.text())))
|
|
|
+ .then(([common, ftg, pm, pt, ss]) => {
|
|
|
+ ui.loadLibrary(new LocalLibrary(ui, ss, "FTG+PM - S/S"));
|
|
|
+ ui.loadLibrary(new LocalLibrary(ui, pt, "FTG+PM - PT"));
|
|
|
+ ui.loadLibrary(new LocalLibrary(ui, pm, "FTG+PM - PM"));
|
|
|
+ ui.loadLibrary(new LocalLibrary(ui, ftg, "FTG+PM - FTG"));
|
|
|
+ ui.loadLibrary(new LocalLibrary(ui, common, "FTG+PM - Common"));
|
|
|
+ });
|
|
|
+
|
|
|
+ // For debugging only
|
|
|
+ window.ui = ui;
|
|
|
+});
|