ftgpm.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  1. Draw.loadPlugin(function(ui) {
  2. const model = ui.editor.graph.model;
  3. const defaultConfig = {
  4. // 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.
  5. movePortsOnResizeActivity: false,
  6. // The new algorithm probably gives better results under all circumstances.
  7. newPortSnappingAlgorithm: true,
  8. // Edge styles.
  9. edgeStyleCtrlFlow: "edgeStyle=orthogonalEdgeStyle;endArrow=classic;rounded=0;html=1;strokeWidth=2;fontSize=14;strokeColor=#004C99;jumpStyle=gap;",
  10. edgeStyleDataFlow: "edgeStyle=orthogonalEdgeStyle;endArrow=open;rounded=0;html=1;strokeWidth=1;fontSize=14;fontColor=#000000;fillColor=#d5e8d4;strokeColor=#6D9656;endFill=0;jumpStyle=gap;",
  11. edgeStyleTypedBy: "edgeStyle=0;endArrow=blockThin;html=1;strokeWidth=1;fontSize=14;fontColor=#000000;dashed=1;endFill=1;endSize=6;strokeColor=#999999;",
  12. edgeStyleParentVersion: "edgeStyle=orthogonalEdgeStyle;endArrow=classic;rounded=0;html=1;fontSize=11;fontColor=#000000;fillColor=#fff2cc;strokeColor=#d6b656;rounded=0;jumpStyle=gap;",
  13. edgeStyleSSLink: "edgeStyle=0;endArrow=blockThin;html=1;fontSize=11;strokeColor=#694B2E;rounded=0;dashed=1;dashPattern=1 4;endFill=1;",
  14. edgeStyleComment: "edgeStyle=0;endArrow=none;dashed=1;html=1;",
  15. portLabelOffsetEdgeDirection: 22,
  16. portLabelOffsetPerpendicularToEdgeDirection: 24,
  17. };
  18. let currentConfig;
  19. const existingConfig = window.localStorage.getItem('ftgpmConfig');
  20. if (existingConfig === null) { // unlike Object and Map, localStorage returns 'null' when item doesn't exist...
  21. currentConfig = {};
  22. } else {
  23. console.log("Have existing ftgpm config...", existingConfig)
  24. currentConfig = JSON.parse(existingConfig);
  25. }
  26. // Configuration dialog window:
  27. function createFtgpmConfigWindow() { // only called once - this is only a function in order not to pollute namespace
  28. // Drawio has no decent abstraction for UI elements or even displaying a dialog window.
  29. // The following is based on code from src/main/webapp/js/diagramly/Dialogs.js
  30. const wndDiv = document.createElement('div');
  31. wndDiv.style.userSelect = 'none';
  32. wndDiv.style.overflow = 'hidden';
  33. wndDiv.style.padding = '10px';
  34. wndDiv.style.height = '100%';
  35. const wnd = new mxWindow("FTG+PM Plugin Configuration",
  36. wndDiv, 100, 100, 640, 570, true, true);
  37. wnd.destroyOnClose = false;
  38. wnd.setMaximizable(false);
  39. wnd.setResizable(false);
  40. wnd.setClosable(true);
  41. wnd.addListener('show', mxUtils.bind(this, function() {
  42. statusText.innerHTML = "";
  43. }));
  44. function createConfig(labelText, json, readonly) {
  45. const columnDiv = document.createElement('div');
  46. columnDiv.style.display = 'inline-block';
  47. const label = document.createElement('label');
  48. const textarea = document.createElement('textarea');
  49. label.setAttribute('for', textarea);
  50. label.innerHTML = labelText;
  51. textarea.setAttribute('wrap', 'off');
  52. textarea.setAttribute('spellcheck', 'false');
  53. textarea.setAttribute('autocorrect', 'off');
  54. textarea.setAttribute('autocomplete', 'off');
  55. textarea.setAttribute('autocapitalize', 'off');
  56. textarea.setAttribute('wrap', 'hard');
  57. if (readonly) {
  58. textarea.setAttribute('readonly', 'true');
  59. }
  60. textarea.value = JSON.stringify(json, null, 2);
  61. textarea.style.overflow = 'auto';
  62. textarea.style.resize = 'none';
  63. textarea.style.width = '300px';
  64. textarea.style.height = '360px';
  65. textarea.style.marginBottom = '16px';
  66. columnDiv.appendChild(label);
  67. mxUtils.br(columnDiv);
  68. columnDiv.appendChild(textarea);
  69. return [columnDiv, textarea];
  70. }
  71. const [defaultColumn] = createConfig("Default config (hardcoded & read-only):", defaultConfig, true);
  72. const [customColumn, customTextArea] = createConfig("Custom config (acts as an 'overlay' on top of default config):", currentConfig, false);
  73. wndDiv.appendChild(defaultColumn);
  74. wndDiv.appendChild(customColumn);
  75. mxUtils.br(wndDiv);
  76. const buttonsDiv = document.createElement('div');
  77. buttonsDiv.style.textAlign = 'right';
  78. const statusText = document.createElement('div');
  79. statusText.style.display = 'inline-block';
  80. // attempts to save user configuration and displays status text as a side effect
  81. // returns true if all went well
  82. function saveConfigurationAndDisplayStatus() {
  83. let parsed;
  84. try {
  85. parsed = JSON.parse(customTextArea.value); // may throw
  86. } catch (parseErr) {
  87. statusText.innerHTML = "Parse error: " + parseErr.toString();
  88. statusText.style.color = 'red';
  89. return false;
  90. }
  91. if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
  92. statusText.innerHTML = "JSON value is not an object";
  93. statusText.style.color = 'red';
  94. return false;
  95. }
  96. currentConfig = parsed;
  97. window.localStorage.setItem('ftgpmConfig', JSON.stringify(parsed));
  98. customTextArea.value = JSON.stringify(parsed, null, 2); // prettify
  99. statusText.innerHTML = "Configuration successfully set and saved (in window.localStorage)";
  100. statusText.style.color = 'green';
  101. return true;
  102. }
  103. const applyButton = mxUtils.button("Apply", saveConfigurationAndDisplayStatus);
  104. applyButton.className = 'geBtn gePrimaryBtn';
  105. applyButton.style.float = 'none'; // override geBtn style
  106. const resetButton = mxUtils.button("Reset", function() {
  107. customTextArea.value = JSON.stringify(currentConfig, null, 2);
  108. });
  109. resetButton.className = 'geBtn';
  110. resetButton.style.float = 'none'; // override geBtn style
  111. buttonsDiv.appendChild(statusText);
  112. buttonsDiv.appendChild(applyButton);
  113. buttonsDiv.appendChild(resetButton);
  114. // hacks buttons:
  115. const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
  116. if (saveConfigurationAndDisplayStatus()) {
  117. model.beginUpdate();
  118. for (const [cellId, cell] of Object.entries(model.cells)) {
  119. applyCellStyleFromConfig(cell);
  120. }
  121. model.endUpdate();
  122. }
  123. });
  124. const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
  125. if (saveConfigurationAndDisplayStatus()) {
  126. model.beginUpdate();
  127. for (const [cellId, cell] of Object.entries(model.cells)) {
  128. if (cell.geometry) {
  129. // we'll force a geometry update:
  130. const newGeometry = cell.geometry.clone();
  131. snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
  132. model.setGeometry(cell, newGeometry);
  133. }
  134. }
  135. model.endUpdate();
  136. }
  137. })
  138. forceEdgeStyleButton.className = 'geBtn';
  139. forceEdgeStyleButton.style.float = 'none'; // override geBtn style
  140. forcePortSnapButton.className = 'geBtn';
  141. forcePortSnapButton.style.float = 'none'; // override geBtn style
  142. hacksDiv = document.createElement('div');
  143. hacksDiv.appendChild(document.createTextNode("Hacks: (use undo if you fuck things up)"));
  144. mxUtils.br(hacksDiv);
  145. hacksDiv.appendChild(forceEdgeStyleButton);
  146. mxUtils.br(hacksDiv);
  147. hacksDiv.appendChild(forcePortSnapButton);
  148. wndDiv.appendChild(buttonsDiv);
  149. mxUtils.br(wndDiv);
  150. wndDiv.appendChild(hacksDiv);
  151. return wnd;
  152. }
  153. const ftgpmConfigWindow = createFtgpmConfigWindow();
  154. function getFromConfig(parameter) {
  155. if (currentConfig.hasOwnProperty(parameter)) {
  156. return currentConfig[parameter];
  157. } else {
  158. return defaultConfig[parameter];
  159. }
  160. }
  161. // // Force style window:
  162. // const forceStyleDiv = document.createElement('div');
  163. // const forceStyleWindow = new mxWindow("FTG+PM: Force Style Rules",
  164. // forceStyleDiv, 200, 200, 400, 90, true, true);
  165. // forceStyleWindow.destroyOnClose = false;
  166. // forceStyleWindow.setMaximizable(false);
  167. // forceStyleWindow.setResizable(false);
  168. // forceStyleWindow.setClosable(true);
  169. // const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
  170. // for (const [cellId, cell] of Object.entries(model.cells)) {
  171. // applyCellStyleFromConfig(cell);
  172. // }
  173. // });
  174. // const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
  175. // for (const [cellId, cell] of Object.entries(model.cells)) {
  176. // if (cell.geometry) {
  177. // // we'll force a geometry update:
  178. // const newGeometry = cell.geometry.clone();
  179. // snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
  180. // model.setGeometry(cell, newGeometry);
  181. // }
  182. // }
  183. // })
  184. // forceEdgeStyleButton.className = 'geBtn';
  185. // forcePortSnapButton.className = 'geBtn';
  186. // forceStyleDiv.appendChild(forceEdgeStyleButton);
  187. // mxUtils.br(forceStyleDiv);
  188. // forceStyleDiv.appendChild(forcePortSnapButton);
  189. window.onkeyup = function(e) {
  190. // Shortcut to show FTG+PM config:
  191. if (e.ctrlKey && e.key === '.') {
  192. ftgpmConfigWindow.show();
  193. }
  194. // // Shortcut to apply style from config to all edges:
  195. // if (e.ctrlKey && e.key === 'Enter') {
  196. // forceStyleWindow.show();
  197. // }
  198. }
  199. // For auto-updating port orientation
  200. // Note: this is only the 'shape', not the full style
  201. const encodedPortShapes = {
  202. in: {
  203. l: `stencil(pZNdD4IgFIZ/DbcNYa3rZvU/yE7JRHBAX/++Y0c3zXC2btzO+4wHfFEm81CqBpjgJZM7JoRc4wPHO42C06hCA0Wk7Ka8VkcDREL0roK7PsXOoG0JXseWyj3jW9Y6OJN54axFiXY2jMiAo0xpi2v5g2Td9s9uWnWna3CHGiJ4yjNKmTgsFmf/iGWOSep1ZH5URXXx7mpP01VJdnYevoAeN6rtdwJ6XLsbJFuba6cXGG0Hgg9Dtun7+UUydshFhsK4ADNXiXmiCWxQG0NfY+rKJh2/U/oD3sEL)`,
  204. r: `stencil(pZNdD4IgFIZ/DbcNYa3rZvU/yE7JQnBAVv++o0eXH9ls3bid9xkP8qJMpiFXJTDBcyZ3TAi5xgeOdxoFp1GFErJIWaW8VkcDREL07gp3fYqtQdscvI41lXvGt6x2cCbTzFmLEu1sGJAeR5nSFtfyB8na7Z/ttGrfrsQdCojgKU8oZeKwWJz8I5YpJnPHkelRZdeLdzd7mq6aZWfn4QPocKnqfiegw4Wr4H24cW3f6ukMRtueYShINl0/vzhGErlIkRkX4MtdYj5TBVaojaHPce7OJiU3Kf0CTfAC)`,
  205. t: `stencil(nZPRDoIgGIWfhtuGsNZ1s3oP0r9kIjggrbfv1183S23VDds5Z3zAAZhMQ6FqYIIXTB6YEILjgLIlKbckVaghi+Q1ymt1NkBJiN6V0Oo8DgRtC/A6dqk8Mr5nHZIzmWbOWoRoZ8NLMskRprTFufxOML4ZNvAYNKkaV6gggic3IZeJ09/g5BewTNFZO45Mzyorr97dbD6ftZpdnIeFYIxr1fU7C8a4cg1MDrfU2XI7I8BoOwHIxdZ/ISS7t4K/g2TGBfhwleivNIENamPoNa5d2azj3qUf0BtP)`,
  206. b: `stencil(nZNRD4IgFIV/Da8NYa3nZvU/EG/JQnBAWv++q1c3m9pWL27nnPHJPQCTeaxUA0zwiskTE0Jw/KDsSMo9SRUb0Im8VgWjCguUxBT8HTpTppFgXAXBpD6VZ8aPrEdyJnPtnUOI8S5+JLMcYco4XMufBOO7cQOvUZNq8A81JAjkZuQycfkbnP0Cljk6W+PIvFD6fgv+4crlqs3s6gOsBFPcqL7fRTDFtW9hNtzHaJNcr2ciWONmhOywXvwvDLn/Yxva+ghfzhL9jSqwQmMtXcetM1uUPLj0BAbjDQ==)`,
  207. }
  208. };
  209. encodedPortShapes.out = {
  210. r: encodedPortShapes.in.l,
  211. l: encodedPortShapes.in.r,
  212. t: encodedPortShapes.in.b,
  213. b: encodedPortShapes.in.t,
  214. }
  215. // Styles for our different edge types
  216. // Mapping from 'pmRole' attribute to key in config.
  217. const edgeStyles = {
  218. ctrl_flow: 'edgeStyleCtrlFlow',
  219. data_flow: 'edgeStyleDataFlow',
  220. typed_by: 'edgeStyleTypedBy',
  221. parent_version: 'edgeStyleParentVersion',
  222. comment_edge: 'edgeStyleComment',
  223. };
  224. function applyCellStyleFromConfig(cell) {
  225. const ftgpmType = cell.getAttribute(TYPE_ATTR);
  226. if (ftgpmType) {
  227. if (edgeStyles.hasOwnProperty(ftgpmType)) {
  228. setEdgeStyle(cell, ftgpmType);
  229. }
  230. // TODO: do the same on nodes.
  231. }
  232. }
  233. const TYPE_ATTR = "pmRole"; // cell attribute holding the ftgpm type
  234. function parseStyle(str) {
  235. const pairs = str.split(';').filter(s => s !== '');
  236. const map = new Map();
  237. pairs.map(pair => {
  238. const [key,value] = pair.split('=');
  239. map.set(key, value);
  240. });
  241. return map;
  242. }
  243. function unparseStyle(map) {
  244. let str = "";
  245. for (const [key, value] of map.entries()) {
  246. if (value === undefined) {
  247. str += key + ';';
  248. } else {
  249. str += key + '=' + value + ';';
  250. }
  251. }
  252. return str;
  253. }
  254. // copy selected keys (=array) from fromMap to toMap
  255. function mapAssign(toMap, fromMap, keys) {
  256. for (const key of keys) {
  257. const value = fromMap.get(key);
  258. if (value !== undefined) {
  259. toMap.set(key, value);
  260. }
  261. }
  262. }
  263. function reverseEdge(edge, sourceCell, targetCell) {
  264. // Reverse source and target
  265. ui.editor.graph.model.setTerminals(edge, targetCell, sourceCell);
  266. // Reverse 'waypoints'
  267. if (edge.geometry.points) {
  268. const newGeometry = edge.geometry.clone();
  269. newGeometry.points.reverse();
  270. ui.editor.graph.model.setGeometry(edge, newGeometry);
  271. }
  272. // Reverse entry and exit points
  273. const oldStyle = parseStyle(model.getStyle(edge));
  274. const newStyle = new Map(oldStyle);
  275. // entry becomes exit
  276. newStyle.set('entryX', oldStyle.get('exitX'));
  277. newStyle.set('entryY', oldStyle.get('exitY'));
  278. // exit becomes entry
  279. newStyle.set('exitX', oldStyle.get('entryX'));
  280. newStyle.set('exitY', oldStyle.get('entryY'));
  281. // entryD becomes exitD
  282. newStyle.set('entryDx', oldStyle.get('exitDx'));
  283. newStyle.set('entryDy', oldStyle.get('exitDy'));
  284. // exitD becomes entryD
  285. newStyle.set('exitDx', oldStyle.get('entryDx'));
  286. newStyle.set('exitDy', oldStyle.get('entryDy'));
  287. model.setStyle(edge, unparseStyle(newStyle));
  288. }
  289. // Simply sets the data attribute TYPE_ATTR to ftgpm_type.
  290. function setFtgpmType(cell, ftgpm_type) {
  291. // Set type attribute
  292. let value = model.getValue(cell);
  293. if (!value) {
  294. // Workaround: 'value' must be an XML element
  295. value = mxUtils.createXmlDocument().createElement('object');
  296. value.setAttribute('label', '');
  297. }
  298. value.setAttribute(TYPE_ATTR, ftgpm_type);
  299. model.setValue(cell, value);
  300. }
  301. function setEdgeStyle(edge, style_type) {
  302. // Update style
  303. // Workaround: don't overwrite connection points
  304. const oldstyle = parseStyle(model.getStyle(edge));
  305. const newstyle = new Map(parseStyle(getFromConfig(edgeStyles[style_type])));
  306. mapAssign(newstyle, oldstyle, [
  307. // retain these properties from oldstyle:
  308. "entryX", "entryY", "entryDx", "entryDy",
  309. "exitX", "exitY", "exitDx", "exitDy"
  310. ]);
  311. model.setStyle(edge, unparseStyle(newstyle));
  312. }
  313. function isArtifact(type) {
  314. return type === "artifact";
  315. }
  316. function isFormalism(type) {
  317. return type === "formalism";
  318. }
  319. function isTransformation(type) {
  320. return type === "auto_transformation" || type === "transformation" || type === "comp_transformation";
  321. }
  322. function isActivityNode(type) {
  323. return type === "autom_activity" || type === "activity" || type === "comp_activity";
  324. }
  325. function isControlFlowPort(type) {
  326. return type === "ctrl_in" || type === "ctrl_out";
  327. }
  328. function isDataPort(type) {
  329. return type === "data_in" || type === "data_out";
  330. }
  331. function isInport(type) {
  332. return type === "data_in" || type === "ctrl_in";
  333. }
  334. function isOutport(type) {
  335. return type === "data_out" || type === "ctrl_out";
  336. }
  337. function isPort(type) {
  338. return ["data_in", "data_out", "ctrl_in", "ctrl_out"].includes(type);
  339. }
  340. function isControlFlowNode(type) {
  341. return isControlFlowPort(type) || type === "initial" || type === "final" || type === "fork_join";
  342. }
  343. function isDataFlowNode(type) {
  344. return isDataPort(type) || isArtifact(type) || isFormalism(type);
  345. }
  346. function isTraceEvent(type) {
  347. return type === "traceevent_begin" || type === "traceevent_end";
  348. }
  349. function isArtifactVersion(type) {
  350. return type === "artifact_version";
  351. }
  352. function getFlowType(type) {
  353. if (isDataFlowNode(type)) {
  354. return "data";
  355. }
  356. if (isControlFlowNode(type)) {
  357. return "ctrl";
  358. }
  359. // throw new Error("unknown flow type");
  360. console.log("unknown flow type");
  361. }
  362. // [ condition, flow-type ]
  363. const edgeTypeRules = [
  364. // PM control flow
  365. [ (src, tgt) => isControlFlowNode(src) && isControlFlowNode(tgt), "ctrl_flow" ],
  366. // PM data flow
  367. [ (src, tgt) => isDataFlowNode(src) && isDataFlowNode(tgt), "data_flow" ],
  368. // FTG data flow
  369. [ (src, tgt) => isTransformation(src) && isFormalism(tgt), "data_flow" ],
  370. [ (src, tgt) => isFormalism(src) && isTransformation(tgt), "data_flow" ],
  371. [ (src,tgt) => isActivityNode(src) && isTransformation(tgt), "typed_by" ],
  372. [ (src,tgt) => isArtifact(src) && isFormalism(tgt), "typed_by" ],
  373. [ (src,tgt) => isTraceEvent(src) && isArtifactVersion(tgt), "data_flow"],
  374. [ (src,tgt) => isArtifactVersion(src) && isTraceEvent(tgt), "data_flow"],
  375. [ (src,tgt) => isTraceEvent(src) && isTraceEvent(tgt), "ctrl_flow"],
  376. [ (src,tgt) => src === "traceevent_begin" && tgt === "ctrl_in", "typed_by"],
  377. [ (src,tgt) => src === "traceevent_end" && tgt === "ctrl_out", "typed_by"],
  378. [ (src,tgt) => isArtifactVersion(src) && isArtifact(tgt), "typed_by"],
  379. [ (src,tgt) => isArtifactVersion(src) && isArtifactVersion(tgt), "parent_version"],
  380. [ (src,tgt) => isArtifactVersion(src) && tgt === "storage", "ss_link"],
  381. [ (src,tgt) => isArtifactVersion(src) && tgt === "real_object", "ss_link"],
  382. [ (src,tgt) => src === "traceevent_begin" && tgt === "service", "ss_link"],
  383. [ (src,tgt) => src === "comment" || tgt === "comment", "comment_edge"],
  384. ];
  385. // Very specific: This function is used to determine the correct direction of an arrow automatically
  386. // An activity or artifact at the highest level (= a direct child of a layer) has level 0
  387. // Every time something is nested in an activity, the level increases by 1.
  388. // Ports are special: they can only be a child of an activity, but they have the same level as the activity.
  389. function getNestedLevel(cell) {
  390. function getLvlRecursive(cell) {
  391. if (!cell.parent) {
  392. throw Error("getNestedLevel called with parentless cell (i.e. cell is root, or is deleted)");
  393. }
  394. if (model.isLayer(cell)) {
  395. return 0;
  396. // throw Error("getNestedLevel called with layer cell");
  397. }
  398. if (cell.parent === model.isLayer(cell.parent)) {
  399. return 0;
  400. }
  401. else if (isActivityNode(cell.parent.getAttribute(TYPE_ATTR))) {
  402. return getLvlRecursive(cell.parent) + 1;
  403. }
  404. else {
  405. return getLvlRecursive(cell.parent);
  406. }
  407. }
  408. const baseLevel = getLvlRecursive(cell);
  409. if (isPort(cell.getAttribute(TYPE_ATTR))) {
  410. return baseLevel - 1;
  411. } else {
  412. return baseLevel;
  413. }
  414. }
  415. function isDirectionless(type) {
  416. return isArtifact(type) || type === "fork_join";
  417. }
  418. ui.editor.graph.addListener(mxEvent.CELL_CONNECTED, (_, eventObj) => {
  419. // Happens whenever an edge is (dis)connected.
  420. const edge = eventObj.properties.edge;
  421. const sourceCell = edge.source;
  422. const targetCell = edge.getTerminal();
  423. // This will change the edge style WITHIN the transaction of the edit operation.
  424. // The terminal-change and style-change will be one edit operation from point of view of undo manager.
  425. const sourceType = sourceCell ? sourceCell.getAttribute(TYPE_ATTR) : null;
  426. const targetType = targetCell ? targetCell.getAttribute(TYPE_ATTR) : null;
  427. // Update style if necessary
  428. for (const [cond, linkType] of edgeTypeRules) {
  429. if (cond(sourceType, targetType)) {
  430. setFtgpmType(edge, linkType);
  431. setEdgeStyle(edge, linkType);
  432. }
  433. }
  434. if (sourceCell && targetCell) {
  435. // Auto-correct edge direction in certain cases.
  436. // We call 'reverseEdge' if we have an illegal situation, but reversing the arrow gives a legal situation.
  437. const sourceLvl = getNestedLevel(sourceCell);
  438. const targetLvl = getNestedLevel(targetCell);
  439. // We have 2 flow types: ctrl and data. They remain segregated.
  440. if (getFlowType(sourceType) === getFlowType(targetType)) {
  441. if (sourceLvl === targetLvl) {
  442. // Equally nested ...
  443. if (isInport(sourceType) && isOutport(targetType)
  444. || isInport(sourceType) && isDirectionless(targetType)
  445. || isDirectionless(sourceType) && isOutport(targetType)) {
  446. reverseEdge(edge, sourceCell, targetCell);
  447. }
  448. }
  449. else if (sourceLvl === targetLvl + 1) {
  450. // Source of arrow is nested deeper ...
  451. if (isInport(sourceType) && isInport(targetType)
  452. || isDirectionless(sourceType) && isInport(targetType)) {
  453. // Should flow from shallowly nested input port to deeply nested input port:
  454. reverseEdge(edge, sourceCell, targetCell);
  455. }
  456. }
  457. else if (sourceLvl + 1 === targetLvl) {
  458. // Target of arrow is nested deeper ...
  459. if (isOutport(sourceType) && isOutport(targetType)
  460. || isOutport(sourceType) && isDirectionless(targetType)) {
  461. // Should flow from deeply nested output port to shallowly nested output port
  462. reverseEdge(edge, sourceCell, targetCell);
  463. }
  464. }
  465. }
  466. }
  467. });
  468. function outsideOffset(wOrH) {
  469. return 0;
  470. }
  471. function centerOffset(wOrH) {
  472. return -wOrH / 2;
  473. }
  474. function insideOffset(wOrH) {
  475. return -wOrH;
  476. }
  477. // Change this to position ports outside, inside or centered at the edge.
  478. const borderOffset = centerOffset;
  479. function rightOrBottomBorder(wOrH, parentWorH) {
  480. return parentWorH + borderOffset(wOrH);
  481. }
  482. function leftOrTopBorder(wOrH) {
  483. return -wOrH - borderOffset(wOrH);
  484. }
  485. function cellCenter(cellGeometry) {
  486. // Coordinates are relative to topleft corner of parent shape
  487. const {x,y, width, height} = cellGeometry;
  488. // Center of cell
  489. return {
  490. x: x + width/2,
  491. y: y + height/2,
  492. };
  493. }
  494. // Deprecated snapping algorithm
  495. function closestBorder(cellGeometry, parentGeometry) {
  496. // Cell center is relative to parent: (0,0) is topleft
  497. const c = cellCenter(cellGeometry);
  498. // 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.
  499. //
  500. // 2
  501. // /
  502. // +----+
  503. // |\ t/|
  504. // |l\/ |
  505. // | /\r|
  506. // |/b \|
  507. // +----+
  508. // \
  509. // 1
  510. const slope = parentGeometry.height / parentGeometry.width;
  511. const above1 = c.y < slope * c.x;
  512. const above2 = c.y < parentGeometry.height - slope * c.x;
  513. if (above1)
  514. if (above2)
  515. return 't';
  516. else
  517. return 'r';
  518. else
  519. if (above2)
  520. return 'l';
  521. else
  522. return 'b';
  523. }
  524. // Improved snapping algorithm
  525. function closestBorder2(cellGeometry, parentGeometry) {
  526. // Cell center is relative to parent: (0,0) is topleft
  527. const c = cellCenter(cellGeometry);
  528. const slope = 1; // fixed slope, 45 degrees
  529. const above1 = c.y < slope * c.x;
  530. const above3 = c.y < (-slope) * c.x + parentGeometry.height ;
  531. const above4 = c.y < slope * (c.x - parentGeometry.width) + parentGeometry.height;
  532. const above2 = c.y < (-slope) * (c.x - parentGeometry.width);
  533. if (parentGeometry.width >= parentGeometry.height) {
  534. // Parent's width > height
  535. //
  536. // 1 2
  537. // \ /
  538. // +------+ --> x
  539. // |\ t /|
  540. // |l\__/ |__ 5
  541. // | / \r|
  542. // |/ b \|
  543. // +------+
  544. // / \
  545. // 3 4
  546. // |
  547. // V
  548. // y
  549. const above5 = c.y < parentGeometry.height / 2;
  550. if (!above1 && above3) {
  551. return 'l';
  552. }
  553. if (!above2 && above4) {
  554. return 'r';
  555. }
  556. if (above1 && above2 && above5) {
  557. return 't';
  558. }
  559. if (!above3 && !above4 && !above5) {
  560. return 'b';
  561. }
  562. }
  563. else {
  564. // Parent's height > width
  565. // 1 2
  566. // \ /
  567. // +----+
  568. // |\ t/|
  569. // | \/ |
  570. // |l | |
  571. // | | |
  572. // | /\r|
  573. // |/b \|
  574. // +----+
  575. // / | \
  576. // 3 5 4
  577. const leftOf5 = c.x < parentGeometry.width / 2;
  578. if (above1 && above2) {
  579. return 't';
  580. }
  581. if (!above3 && !above4) {
  582. return 'b';
  583. }
  584. if (above3 && !above1 && leftOf5) {
  585. return 'l';
  586. }
  587. if (above4 && !above2 && !leftOf5) {
  588. return 'r';
  589. }
  590. }
  591. }
  592. // Update port shape (so it points in the right direction), move port (and port label) to border of activity.
  593. function snapPortToBorder(cell, cellGeometry, parentGeometry, border) {
  594. const style = parseStyle(model.getStyle(cell));
  595. const type = cell.getAttribute(TYPE_ATTR);
  596. const inOrOut = isInport(type) ? "in" : "out";
  597. style.set('shape', encodedPortShapes[inOrOut][border]);
  598. switch (border) {
  599. case 't':
  600. cellGeometry.width = 35;
  601. cellGeometry.height = 20;
  602. cellGeometry.y = leftOrTopBorder(cellGeometry.height);
  603. style.set('align', 'right');
  604. style.set('verticalAlign', 'bottom');
  605. cellGeometry.offset = new mxPoint(
  606. -getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'),
  607. -getFromConfig('portLabelOffsetEdgeDirection'));
  608. break;
  609. case 'r':
  610. cellGeometry.width = 20;
  611. cellGeometry.height = 35;
  612. cellGeometry.x = rightOrBottomBorder(cellGeometry.width, parentGeometry.width);
  613. style.set('align', 'left');
  614. style.set('verticalAlign', 'bottom');
  615. cellGeometry.offset = new mxPoint(
  616. getFromConfig('portLabelOffsetEdgeDirection'),
  617. -getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'));
  618. break;
  619. case 'l':
  620. cellGeometry.width = 20;
  621. cellGeometry.height = 35;
  622. cellGeometry.x = leftOrTopBorder(cellGeometry.width);
  623. style.set('align', 'right');
  624. style.set('verticalAlign', 'top');
  625. cellGeometry.offset = new mxPoint(
  626. -getFromConfig('portLabelOffsetEdgeDirection'),
  627. getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'));
  628. break;
  629. case 'b':
  630. cellGeometry.width = 35;
  631. cellGeometry.height = 20;
  632. cellGeometry.y = rightOrBottomBorder(cellGeometry.height, parentGeometry.height);
  633. style.set('align', 'left');
  634. style.set('verticalAlign', 'top');
  635. cellGeometry.offset = new mxPoint(
  636. getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'),
  637. getFromConfig('portLabelOffsetEdgeDirection'));
  638. break;
  639. }
  640. model.setStyle(cell, unparseStyle(style));
  641. }
  642. function snapToBorderIfCellIsPortAndParentIsActivity(cell, geometry) {
  643. const type = cell.getAttribute(TYPE_ATTR);
  644. if (isPort(type)) {
  645. // Port was moved
  646. if (cell.parent) {
  647. const parentType = cell.parent.getAttribute(TYPE_ATTR);
  648. if (isActivityNode(parentType) || isTransformation(parentType)) {
  649. // Snap port to activity border
  650. const portSnappingAlgorithm = getFromConfig('newPortSnappingAlgorithm') ? closestBorder2 : closestBorder;
  651. // border will be one of: 'l', 'r', 't', 'b'
  652. const border = portSnappingAlgorithm(geometry, cell.parent.geometry);
  653. snapPortToBorder(cell, geometry, cell.parent.geometry, border);
  654. }
  655. }
  656. }
  657. }
  658. ui.editor.graph.addListener(mxEvent.MOVE_CELLS, (_, eventObj) => {
  659. // High-level event: Happens when the user releases dragged shape(s)
  660. for (const cell of eventObj.properties.cells) {
  661. snapToBorderIfCellIsPortAndParentIsActivity(cell, cell.geometry);
  662. }
  663. });
  664. ui.editor.graph.addListener(mxEvent.RESIZE_CELLS, (_, eventObj) => {
  665. // High-level event: Happens when the user resized cell(s)
  666. for (let i=0; i<eventObj.properties.cells.length; i++) {
  667. const cell = eventObj.properties.cells[i];
  668. const type = cell.getAttribute(TYPE_ATTR);
  669. if (isActivityNode(type) || isTransformation(type)) {
  670. // Activity was resized...
  671. // In drawio, 'arcSize' of a rounded rectangle is relative to size of shape.
  672. // Retain apparent arc size of activity:
  673. const style = parseStyle(model.getStyle(cell));
  674. const newArcSize = 1000 / Math.sqrt(cell.geometry.width * cell.geometry.height);
  675. style.set("arcSize", newArcSize);
  676. model.setStyle(cell, unparseStyle(style));
  677. if (cell.children) {
  678. // Need this for moving contained ports:
  679. const prevGeometry = eventObj.properties.previous[i];
  680. const scaleW = cell.geometry.width / prevGeometry.width;
  681. const scaleH = cell.geometry.height / prevGeometry.height;
  682. for (const child of cell.children) {
  683. const childType = child.getAttribute(TYPE_ATTR);
  684. // Keep activity icon in place
  685. if (childType === "activityIcon") {
  686. newIconGeometry = child.geometry.clone();
  687. newIconGeometry.x = cell.geometry.width - 28;
  688. newIconGeometry.y = 4;
  689. model.setGeometry(child, newIconGeometry);
  690. }
  691. else if (isPort(childType)) {
  692. // Move contained ports
  693. const border = closestBorder2(child.geometry, prevGeometry); // < what was the border in the old geometry?
  694. const newGeometry = child.geometry.clone();
  695. snapPortToBorder(child, newGeometry, cell.geometry, border);
  696. if (getFromConfig('movePortsOnResizeActivity')) {
  697. // Scale position
  698. switch (border) {
  699. case 't':
  700. newGeometry.x = child.geometry.x * scaleW;
  701. break;
  702. case 'l':
  703. newGeometry.y = child.geometry.y * scaleH;
  704. break;
  705. case 'b':
  706. newGeometry.x = child.geometry.x * scaleW;
  707. break;
  708. case 'r':
  709. newGeometry.y = child.geometry.y * scaleH;
  710. break;
  711. }
  712. }
  713. model.setGeometry(child, newGeometry);
  714. }
  715. }
  716. }
  717. }
  718. }
  719. });
  720. // Hardcoded primitive shape libraries.
  721. // 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.
  722. Promise.all([
  723. fetch("myPlugins/shape_libs/common.xml"),
  724. fetch("myPlugins/shape_libs/ftg.xml"),
  725. fetch("myPlugins/shape_libs/pm.xml"),
  726. fetch("myPlugins/shape_libs/pt.xml"),
  727. fetch("myPlugins/shape_libs/ss.xml"),
  728. ])
  729. .then(all => Promise.all(all.map(response => response.text())))
  730. .then(([common, ftg, pm, pt, ss]) => {
  731. ui.loadLibrary(new LocalLibrary(ui, ss, "FTG+PM - S/S"));
  732. ui.loadLibrary(new LocalLibrary(ui, pt, "FTG+PM - PT"));
  733. ui.loadLibrary(new LocalLibrary(ui, pm, "FTG+PM - PM"));
  734. ui.loadLibrary(new LocalLibrary(ui, ftg, "FTG+PM - FTG"));
  735. ui.loadLibrary(new LocalLibrary(ui, common, "FTG+PM - Common"));
  736. });
  737. // For debugging only
  738. window.ui = ui;
  739. });