dtdesign.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. Draw.loadPlugin(function(ui) {
  2. const WEE = "wee.rys.app";
  3. const BACKEND = "dtb.rys.app";
  4. const EXPECTED_BACKEND_VERSION = 6; // expected backend version
  5. const SPARQL_SERVER = "fuseki.rys.app"
  6. const SPARQL_ENDPOINT = "/SystemDesignOntology2Layers/"
  7. const dropVocabularyPrefix = str => str.substring(41);
  8. const dropDescriptionPrefix = str => str.substring(30);
  9. const dropArtifactPrefix = str => str.substring(41);
  10. const addFormalismsPrefix = str => "http://ua.be/sdo2l/vocabulary/formalisms/" + str;
  11. const addArtifactPrefix = str => "http://ua.be/sdo2l/description/artifacts/" + str;
  12. const QUERIES = {
  13. // Query that navigates the given link from the given source element, and returns the IRI and most-concrete-type of the target element.
  14. // If `reverse` is true, then incoming links are returned instead.
  15. getOutgoingLink: (iri, link_type, reverse=false) => `\
  16. PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
  17. PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
  18. PREFIX pt: <http://ua.be/sdo2l/vocabulary/formalisms/processtraces#>
  19. SELECT DISTINCT ?element ?type
  20. WHERE {
  21. ${reverse ? `?element <${link_type}> <${iri}>` : `<${iri}> <${link_type}> ?element`}.
  22. ?element a ?type .
  23. {
  24. ?type rdfs:subClassOf object_diagram:Object .
  25. } UNION {
  26. ?type rdfs:subClassOf pt:Event .
  27. } UNION {
  28. ?type rdfs:subClassOf pt:Artifact .
  29. }
  30. FILTER(NOT EXISTS {
  31. ?more_concrete_type rdfs:subClassOf ?type .
  32. ?element a ?more_concrete_type .
  33. FILTER(?more_concrete_type != ?type)
  34. })
  35. FILTER(NOT EXISTS {
  36. ?more_concrete_link_type rdfs:subPropertyOf <${link_type}> .
  37. ${reverse ? `?element ?more_concrete_link_type <${iri}>` : `<${iri}> ?more_concrete_link_type ?element`} .
  38. FILTER(?more_concrete_link_type != <${link_type}>)
  39. })
  40. }`,
  41. // Query that gets the IRI and most-concrete-type of a given drawio cell.
  42. getCellStuff: (cellId) => `\
  43. PREFIX drawio: <http://ua.be/sdo2l/vocabulary/formalisms/drawio#>
  44. PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
  45. PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
  46. SELECT DISTINCT ?element ?type
  47. WHERE {
  48. ?element drawio:hasDrawioId "${cellId}" .
  49. ?element a ?type .
  50. ?type rdfs:subClassOf object_diagram:Object .
  51. NOT EXISTS {
  52. ?more_concrete_type rdfs:subClassOf ?type .
  53. ?element a ?more_concrete_type .
  54. FILTER(?more_concrete_type != ?type)
  55. }
  56. }`,
  57. // Query that, for a given cell IRI, gets the name of the diagram containing the cell, and the ID of the cell.
  58. getDiagramAndCellId: cellIri => `\
  59. PREFIX drawio: <http://ua.be/sdo2l/vocabulary/formalisms/drawio#>
  60. PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
  61. SELECT DISTINCT ?diagramName ?cellId
  62. WHERE {
  63. <${cellIri}> drawio:hasDrawioId ?cellId .
  64. <${cellIri}> object_diagram:inModel ?model .
  65. ?model object_diagram:hasName ?diagramName .
  66. }`,
  67. // Query that gets ALL subclass relations.
  68. getSubClassRelations: `
  69. PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
  70. SELECT DISTINCT ?subclass ?superclass
  71. WHERE {
  72. ?subclass rdfs:subClassOf ?superclass
  73. }`,
  74. getAllGraphs: `
  75. SELECT ?g (COUNT(*) as ?count) {GRAPH ?g {?s ?p ?o}} GROUP BY ?g
  76. `,
  77. getArtifactFilename: artifactIri => `\
  78. PREFIX pt: <http://ua.be/sdo2l/vocabulary/formalisms/processtraces#>
  79. PREFIX pm: <http://ua.be/sdo2l/vocabulary/formalisms/pm#>
  80. PREFIX base: <http://ua.be/sdo2l/vocabulary/base/base#>
  81. SELECT DISTINCT ?filename ?formalismName
  82. WHERE {
  83. <${artifactIri}> pt:hasLocation ?filename .
  84. <${artifactIri}> pt:relatesTo ?pmArtifact .
  85. ?pmArtifact pm:hasType ?ftgFormalism .
  86. ?ftgFormalism base:hasGUID ?formalismName .
  87. }`,
  88. getModels: `\
  89. PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
  90. PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
  91. PREFIX cs_as: <http://ua.be/sdo2l/vocabulary/formalisms/cs_as#>
  92. SELECT DISTINCT ?model ?type
  93. WHERE {
  94. ?model a object_diagram:Model .
  95. ?model a ?type .
  96. ?type rdfs:subClassOf object_diagram:Model .
  97. FILTER(NOT EXISTS {
  98. ?model a cs_as:CorrespondenceModel .
  99. })
  100. FILTER(NOT EXISTS {
  101. ?more_concrete_type rdfs:subClassOf ?type .
  102. ?model a ?more_concrete_type .
  103. FILTER(?more_concrete_type != ?type)
  104. })
  105. }
  106. `,
  107. getProperty: (iri, propertyIri) => `\
  108. SELECT DISTINCT ?value
  109. WHERE {
  110. <${iri}> <${propertyIri}> ?value .
  111. }
  112. `,
  113. insertTraceLink: (fromIri, toIri) => `\
  114. PREFIX trace: <http://ua.be/sdo2l/vocabulary/formalisms/traceability_model#>
  115. INSERT DATA {
  116. GRAPH <http://ua.be/sdo2l/description/traces> {
  117. <${fromIri}> trace:traceLinkTo <${toIri}> .
  118. <${fromIri}> trace:traceLinkFrom <${toIri}> .
  119. }
  120. }
  121. `,
  122. };
  123. // When rebuilding OML, all graphs will be deleted from Fuseki, except for the following (which are not sourced from OML, but inserted directly as RDF triples into Fuseki)
  124. GRAPHS_NOT_TO_DELETE = [
  125. "http://ua.be/sdo2l/description/traces",
  126. ];
  127. const typeToDescription = new Map([
  128. ["ftg#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `ftg "${modelName}"`)],
  129. ["xopp#Text", async (element, type, getProperty) => getProperty("xopp#hasText").then(text => `text "${text}"`)],
  130. ["xopp#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `xournal++ model "${modelName}"`)],
  131. ["drawio#Cell", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `cell "${id}"`)],
  132. ["drawio#Vertex", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `vertex "${id}"`)],
  133. ["drawio#Edge", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `edge "${id}"`)],
  134. ["drawio#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `drawio model "${modelName}"`)],
  135. ["pm#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `process model "${modelName}"`)],
  136. ["pm#Activity", async (element, type, getProperty) => element],
  137. ["pm#Artifact", async (element, type, getProperty) => element],
  138. ["pm#CtrlInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl inport "${portname}"`)],
  139. ["pm#CtrlOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl outport "${portname}"`)],
  140. ["pm#DataInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data inport "${portname}"`)],
  141. ["pm#DataOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data outport "${portname}"`)],
  142. ])
  143. // (Hardcoded) link types exposed to the user.
  144. const typeToLinkType = new Map([
  145. ["object_diagram#Object", [
  146. {
  147. relation: "cs_as#parsedAs",
  148. description: `Is parsed as`,
  149. },
  150. {
  151. relation: "cs_as#renderedAs",
  152. description: `Is rendered as`,
  153. },
  154. // Outcommented, because it's not an interesting relation:
  155. {
  156. relation: "object_diagram#inModel",
  157. description: `Is part of`,
  158. },
  159. {
  160. relation: "traceability_model#traceLinkTo",
  161. description: `Trace-link (outgoing)`,
  162. },
  163. {
  164. relation: "traceability_model#traceLinkFrom",
  165. description: `Trace-link (incoming)`,
  166. },
  167. ]],
  168. ["drawio#Model", [
  169. {
  170. relation: "drawio#hasRootCell",
  171. description: `Has root`,
  172. },
  173. ]],
  174. // Outcommented, blows up:
  175. // ["drawio#Cell", [
  176. // {
  177. // relation: "drawio#hasChild",
  178. // description: `Has child`,
  179. // },
  180. // ]],
  181. ["pm#Activity", [
  182. {
  183. relation: "pm#isTransformation",
  184. description: `Is typed by`,
  185. },
  186. {
  187. relation: "pm#hasPort",
  188. description: `Has`,
  189. },
  190. ]],
  191. ["pm#element", [
  192. {
  193. relation: "processtraces#relatesTo",
  194. description: `Enacted by`,
  195. reverse: true,
  196. },
  197. ]],
  198. ["processtraces#element", [
  199. {
  200. relation: "processtraces#relatesTo",
  201. description: `Enactment of`,
  202. },
  203. // pt-element is not a sub-type of 'Object', so we must repeat this:
  204. {
  205. relation: "cs_as#renderedAs",
  206. description: `Is rendered as`,
  207. },
  208. ]],
  209. ["pm#Artifact", [
  210. {
  211. relation: "pm#hasType",
  212. description: `Is typed by`,
  213. },
  214. ]],
  215. ["ftg#Transformation", [
  216. {
  217. relation: "pm#occursAsActivity",
  218. description: `Occurs as`,
  219. }
  220. ]],
  221. ["ftg#Formalism", [
  222. {
  223. relation: "pm#occursAsArtifact",
  224. description: `Occurs as`,
  225. },
  226. ]],
  227. ["xopp#Model", [
  228. {
  229. relation: "xopp#hasPage",
  230. description: `Has page`,
  231. },
  232. ]],
  233. ["xopp#Page", [
  234. {
  235. relation: "xopp#hasLayer",
  236. description: `Has layer`,
  237. },
  238. ]],
  239. ["xopp#Layer", [
  240. {
  241. relation: "xopp#hasElement",
  242. description: `Has element`,
  243. },
  244. ]],
  245. ]);
  246. // mapping from class-IRI to array of superclass-IRIs
  247. const superclasses = new Map(); // map is populated as soon as the plugin is loaded
  248. function getQueries(type) {
  249. const result = [].concat(
  250. typeToLinkType.get(dropVocabularyPrefix(type)) || [],
  251. ... (superclasses.get(type) || []).map(supertype => typeToLinkType.get(dropVocabularyPrefix(supertype)) || []));
  252. console.log("getQueries,type=",type,"superclasses=",superclasses.get(type),"result=",result);
  253. return result;
  254. }
  255. function querySPARQL(query) {
  256. const body = new URLSearchParams();
  257. body.append("query", query);
  258. return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, {
  259. headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}),
  260. method: "POST",
  261. body,
  262. })
  263. .then(res => res.json())
  264. .then(json => {
  265. console.log("Query:\n"+query+"\Result:",json.results.bindings);
  266. return json.results.bindings;
  267. })
  268. }
  269. function updateSPARQL(update) {
  270. const body = new URLSearchParams();
  271. body.append("update", update);
  272. return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, {
  273. headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}),
  274. method: "POST",
  275. body,
  276. })
  277. .then(res => {
  278. console.log("Update:\n"+update+"\Result:",res);
  279. })
  280. }
  281. const defaultSettings = {
  282. dialogPosX: 100,
  283. dialogPosY: 100,
  284. dialogWidth: 240,
  285. dialogHeight: 400,
  286. };
  287. fetch(BACKEND+"/version")
  288. .then(response => response.json())
  289. .catch(() => 0) // parsing failed - probably backend doesn't even have a version
  290. .then(version => {
  291. if (version !== EXPECTED_BACKEND_VERSION) {
  292. alert("Incorrect DTDesign Python backend version.\nExpected: " + EXPECTED_BACKEND_VERSION + ", got: " + version + ".\nRefusing to load plugin. Please upgrade :)");
  293. return;
  294. }
  295. else {
  296. console.log("Backend version is ", version);
  297. console.log("All good.")
  298. }
  299. // Loads all 'rdfs:subClassOf' relations and stores them in a mapping.
  300. function loadSuperclasses() {
  301. return querySPARQL(QUERIES.getSubClassRelations)
  302. .then(results => {
  303. for (const {subclass, superclass} of results) {
  304. const superclasslist = superclasses.get(subclass.value) || (() => {
  305. const list = [];
  306. superclasses.set(subclass.value, list);
  307. return list;
  308. })();
  309. superclasslist.push(superclass.value);
  310. }
  311. });
  312. }
  313. loadSuperclasses();
  314. // Override context menu when right-clicking somewhere in the diagram:
  315. const oldFactoryMethod = ui.editor.graph.popupMenuHandler.factoryMethod;
  316. ui.editor.graph.popupMenuHandler.factoryMethod = function(menu, cell, evt) {
  317. const modelverseMenu = menu.addItem("Knowledge Graph...", null, null);
  318. const createLinkMenu = menu.addItem("Create traceability link to...", null, null);
  319. menu.addSeparator();
  320. oldFactoryMethod.apply(this, arguments);
  321. const entry = (element, type) => element+':'+type;
  322. function addMenuItem(element, type, maxRecursion, parentMenuItem, description, alreadyVisited, onClick) {
  323. if (alreadyVisited.has(entry(element, type))) {
  324. return; // don't go in circles
  325. }
  326. const shortType = dropVocabularyPrefix(type);
  327. const itemDescriptionGen = typeToDescription.get(shortType) || (async () => shortType);
  328. itemDescriptionGen(dropArtifactPrefix(element), shortType, property => {
  329. return querySPARQL(QUERIES.getProperty(element, addFormalismsPrefix(property)))
  330. .then(results => {
  331. if (results.length > 0) return results[0].value.value;
  332. else return "";
  333. });
  334. })
  335. .then(itemDescription => {
  336. const menuItemText = description + ' ' + itemDescription;
  337. let createdMenuItem;
  338. // If the menu item is a (subtype of a) drawio cell, then clicking on it will take us to that cell in drawio.
  339. if (onClick) {
  340. createdMenuItem = menu.addItem(menuItemText, null, () => onClick(element), parentMenuItem);
  341. }
  342. else if ((superclasses.get(type) || []).some(t => t.endsWith("drawio#Cell"))) {
  343. createdMenuItem = menu.addItem(menuItemText, null, () => goto(element), parentMenuItem);
  344. }
  345. else {
  346. createdMenuItem = menu.addItem(menuItemText, null, null, parentMenuItem);
  347. }
  348. if ((superclasses.get(type) || []).some(t => t.endsWith("processtraces#Artifact"))) {
  349. querySPARQL(QUERIES.getArtifactFilename(element))
  350. .then(results => {
  351. for (const {filename, formalismName} of results) {
  352. menu.addItem(`File "${filename.value}"`, null, () => (createDownloadHandler(formalismName.value))(filename.value), createdMenuItem);
  353. }
  354. })
  355. }
  356. alreadyVisited.add(entry(element, type));
  357. if (maxRecursion <= 0) {
  358. return;
  359. }
  360. addSubmenuItems(element, type, createdMenuItem, maxRecursion, alreadyVisited, onClick);
  361. })
  362. }
  363. function addSubmenuItems(element, type, parentMenuItem, maxRecursion, alreadyVisited, onClick) {
  364. const queries = getQueries(type);
  365. for (const {relation, description, reverse} of queries) {
  366. querySPARQL(QUERIES.getOutgoingLink(element, "http://ua.be/sdo2l/vocabulary/formalisms/"+relation, reverse))
  367. .then(results => {
  368. for (const {element, type} of results) {
  369. addMenuItem(element.value, type.value, maxRecursion-1, parentMenuItem, description, alreadyVisited, onClick);
  370. }
  371. })
  372. }
  373. }
  374. // Populate ModelVerse submenu asynchronously:
  375. if (cell && cell.vertex) {
  376. querySPARQL(QUERIES.getCellStuff(cell.id))
  377. .then(results => {
  378. for (const {element, type} of results) {
  379. const alreadyVisited = new Set([[element.value, type.value]]);
  380. addSubmenuItems(element.value, type.value, modelverseMenu, 3, alreadyVisited);
  381. querySPARQL(QUERIES.getModels)
  382. .then(results => {
  383. const alreadyVisited2 = new Set([[element.value, type.value]]);
  384. for (const {model, type} of results) {
  385. addMenuItem(model.value, type.value, 3, createLinkMenu, "Model",
  386. alreadyVisited2,
  387. targetIri => updateSPARQL(QUERIES.insertTraceLink(element.value, targetIri)));
  388. }
  389. });
  390. }
  391. })
  392. }
  393. }
  394. function getSetting(prop) {
  395. const result = localStorage.getItem("dtdesign-"+prop);
  396. if (result !== null) {
  397. return JSON.parse(result);
  398. }
  399. return defaultSettings[prop];
  400. }
  401. function setSetting(prop, value) {
  402. localStorage.setItem("dtdesign-"+prop, JSON.stringify(value));
  403. }
  404. const errWndDiv = document.createElement('div');
  405. // wndDiv.style.color = "red";
  406. errWndDiv.style.overflow = "auto";
  407. errWndDiv.style.height = "100%";
  408. errWndDiv.style.padding = '12px 14px 8px 14px';
  409. const errWnd = new mxWindow("Error Details",
  410. errWndDiv, 300, 300, 300, 360, true, true);
  411. errWnd.destroyOnClose = false;
  412. errWnd.setMaximizable(false);
  413. errWnd.setResizable(true);
  414. errWnd.setClosable(true);
  415. function getErrDetailsAnchor() {
  416. const anchor = document.createElement('a');
  417. anchor.innerText = "Details";
  418. anchor.href = "#";
  419. anchor.onclick = function() {
  420. errWnd.show();
  421. };
  422. return anchor;
  423. }
  424. let rebuildOMLPromise = Promise.resolve();
  425. function rejectIfStatusAbove400(httpResponse) {
  426. if (httpResponse.status >= 400) {
  427. return httpResponse.text().then(text => {
  428. return Promise.reject(text);
  429. });
  430. }
  431. return Promise.resolve();
  432. }
  433. function rebuildOMLForced() {
  434. return querySPARQL(QUERIES.getAllGraphs)
  435. .then(results => {
  436. const graphsToDelete = results
  437. .map(({g,count}) => g.value)
  438. .filter(graphIri => !GRAPHS_NOT_TO_DELETE.includes(graphIri));
  439. // Delete all graphs first
  440. return Promise.all(graphsToDelete.map(graphToDelete =>
  441. fetch(SPARQL_SERVER+SPARQL_ENDPOINT+"?graph="+graphToDelete, {
  442. method: "DELETE",
  443. })
  444. .then(rejectIfStatusAbove400)
  445. ))
  446. .then(() => {
  447. // Build OML and load the resulting .owl files:
  448. return fetch(BACKEND+"/owl_load", {
  449. method: "PUT",
  450. })
  451. .then(rejectIfStatusAbove400)
  452. });
  453. });
  454. }
  455. function rebuildOMLIncremental() {
  456. // Backend takes care of all the heavy work:
  457. return fetch(BACKEND+"/owl_reload_incremental", { method: "PUT", })
  458. .then(rejectIfStatusAbove400);
  459. }
  460. function showRebuildOMLResult(rebuildAsyncCallback) {
  461. // only perform one rebuild at a time -> queue rebuilds.
  462. rebuildOMLPromise = rebuildOMLPromise.then(() => {
  463. omlStatusDiv.style.color = null;
  464. omlStatusDiv.innerText = "Rebuilding OML...";
  465. return rebuildAsyncCallback()
  466. .then(() => {
  467. omlStatusDiv.innerText = "✓ Built OML";
  468. omlStatusDiv.style.color = "green";
  469. })
  470. .catch(errText => {
  471. errWndDiv.innerText = errText;
  472. omlStatusDiv.innerHTML = "✗ Failed to build OML ";
  473. omlStatusDiv.style.color = "red";
  474. omlStatusDiv.appendChild(getErrDetailsAnchor());
  475. });
  476. });
  477. }
  478. function uploadResponseHandler(statusDiv, refreshListCallback) {
  479. statusDiv.innerHTML = "Saving ...";
  480. statusDiv.style.color = null;
  481. omlStatusDiv.innerHTML = "";
  482. return res => {
  483. if (res.status >= 200 && res.status < 300) {
  484. errWndDiv.innerText = "No error.";
  485. res.json().then(parsedAs => {
  486. statusDiv.innerHTML = "✓ Generated OML (" + parsedAs.join(", ") + ")";
  487. statusDiv.style.color = "green";
  488. });
  489. refreshListCallback();
  490. showRebuildOMLResult(rebuildOMLIncremental);
  491. }
  492. else {
  493. statusDiv.innerHTML = "✗ Failed to save/parse. ";
  494. statusDiv.appendChild(getErrDetailsAnchor());
  495. res.text().then(text => {
  496. errWndDiv.innerText = text;
  497. });
  498. statusDiv.style.color = "red";
  499. }
  500. };
  501. }
  502. let highlight;
  503. // Given a cell IRI, open the diagram that contains the cell
  504. function goto(cellIri) {
  505. querySPARQL(QUERIES.getDiagramAndCellId(cellIri))
  506. .then(([{diagramName, cellId}]) => {
  507. // drop the "_drawio" at the end:
  508. const actualDiagramName = diagramName.value.substring(0, diagramName.value.length-7);
  509. loadPage(actualDiagramName)
  510. .then(() => {
  511. if (highlight) {
  512. highlight.destroy();
  513. }
  514. const [cell] = ui.editor.graph.getCellsById([cellId.value]);
  515. highlight = new mxCellHighlight(ui.editor.graph, "#eb34e8", 6);
  516. highlight.highlight(ui.editor.graph.view.getState(cell));
  517. })
  518. })
  519. }
  520. const wndDiv = document.createElement('div');
  521. wndDiv.style.userSelect = 'none';
  522. wndDiv.style.height = '100%';
  523. wndDiv.style.color = "rgb(112, 112, 112)";
  524. wndDiv.style.overflow = 'auto';
  525. wndDiv.classList.add("geFormatContainer");
  526. const omlDiv = document.createElement('div');
  527. omlDiv.classList.add('geFormatSection')
  528. omlDiv.style.padding = '12px 14px 8px 14px';
  529. wndDiv.appendChild(omlDiv);
  530. const omlLabel = document.createElement('div');
  531. omlLabel.innerText = "OML Status";
  532. omlLabel.style.fontWeight = 'bold';
  533. omlDiv.appendChild(omlLabel);
  534. const omlStatusDiv = document.createElement('div');
  535. omlStatusDiv.innerText = "Ready";
  536. omlDiv.appendChild(omlStatusDiv);
  537. const omlIncButton = mxUtils.button("Incremental rebuild (faster)", function() {
  538. showRebuildOMLResult(rebuildOMLIncremental);
  539. });
  540. omlDiv.appendChild(omlIncButton);
  541. const omlForceButton = mxUtils.button("Forced rebuild (slow)", function() {
  542. showRebuildOMLResult(rebuildOMLForced);
  543. });
  544. omlDiv.appendChild(omlForceButton);
  545. function createSavePageDiv(refreshModelsCallback) {
  546. const savePageDiv = document.createElement('div');
  547. const saveButton = mxUtils.button("Save Current Page", function() {
  548. const responseHandler = uploadResponseHandler(saveStatusDiv, refreshModelsCallback);
  549. const headers = new Headers({
  550. "Content-Type": "application/xml",
  551. });
  552. const serializer = new XMLSerializer();
  553. const xmlnode = ui.getXmlFileData(
  554. null, // ignore selection
  555. true, // only current page
  556. true); // uncompressed
  557. const diagram = xmlnode.children[0]
  558. const diagramName = diagram.getAttribute("name");
  559. const xml = serializer.serializeToString(diagram);
  560. return fetch(BACKEND + "/files/drawio/"+diagramName, {
  561. method: "PUT",
  562. headers,
  563. body: xml,
  564. })
  565. .then(responseHandler);
  566. });
  567. saveButton.style.width = "100%";
  568. saveButton.style.marginTop = "4px";
  569. saveButton.style.marginBottom = "2px";
  570. savePageDiv.appendChild(saveButton);
  571. const saveStatusDiv = document.createElement('div');
  572. savePageDiv.appendChild(saveStatusDiv);
  573. return savePageDiv;
  574. }
  575. function createUploadDiv(modelType, mimeType) {
  576. return function(refreshModelsCallback) {
  577. const uploadDiv = document.createElement('div');
  578. const uploadLabel = document.createElement('label');
  579. uploadLabel.innerText = "Upload "+modelType+":";
  580. uploadLabel.for = "uploadButton";
  581. uploadDiv.appendChild(uploadLabel);
  582. const uploadButton = document.createElement('input');
  583. uploadButton.type = "file";
  584. uploadButton.id = "uploadButton";
  585. uploadButton.style.width = "100%";
  586. uploadButton.style.marginTop = "4px";
  587. uploadButton.style.marginBottom = "2px";
  588. uploadButton.accept = mimeType;
  589. // uploadButton.multiple = true;
  590. uploadButton.onchange = e => {
  591. const responseHandler = uploadResponseHandler(statusDiv, refreshModelsCallback);
  592. Array.from(e.target.files).map(file => {
  593. fetch(BACKEND+"/files/"+modelType+"/"+file.name, {
  594. method: "PUT",
  595. body: file,
  596. })
  597. .then(responseHandler);
  598. });
  599. };
  600. uploadDiv.appendChild(uploadButton);
  601. const statusDiv = document.createElement('div');
  602. uploadDiv.appendChild(statusDiv);
  603. return uploadDiv;
  604. };
  605. }
  606. function createDownloadHandler(modelType) {
  607. return modelName => {
  608. const a = document.createElement('a');
  609. document.body.appendChild(a);
  610. a.setAttribute('href', BACKEND + "/files/" + modelType + "/" + modelName);
  611. a.setAttribute('download', modelName);
  612. a.setAttribute('target', "_blank"); // for browsers that don't support the 'download' attribute, tell them to open the link in a new tab
  613. a.click();
  614. document.body.removeChild(a);
  615. };
  616. }
  617. function createModelList(modelType, createUploadDiv, downloadButtonLabel, onDownloadClick, extraStuff) {
  618. const containerDiv = document.createElement('div');
  619. containerDiv.classList.add('geFormatSection')
  620. containerDiv.style.padding = '12px 14px 8px 14px';
  621. wndDiv.appendChild(containerDiv);
  622. const labelDiv = document.createElement('div');
  623. labelDiv.innerText = "Models — " + modelType;
  624. labelDiv.style.fontWeight = 'bold';
  625. containerDiv.appendChild(labelDiv);
  626. const modelListDiv = document.createElement('div');
  627. containerDiv.appendChild(modelListDiv);
  628. // Refreshes the list of models shown in the ModelVerse window
  629. function refreshList() {
  630. fetch(BACKEND + "/files/" + modelType, {
  631. method: "GET",
  632. })
  633. .then(res => res.json())
  634. .then(models => {
  635. if (models.length > 0) {
  636. modelListDiv.replaceChildren(...models.sort().map(modelName => {
  637. const div = document.createElement('div');
  638. div.style.padding = '3px 0px';
  639. const loadButton = mxUtils.button(downloadButtonLabel, () => onDownloadClick(modelName));
  640. loadButton.style.marginLeft = "12px";
  641. div.appendChild(document.createTextNode(modelName));
  642. div.appendChild(loadButton);
  643. const extraStuffWrapper = document.createElement('div');
  644. div.appendChild(extraStuffWrapper);
  645. function refreshExtraStuff() {
  646. extraStuff(modelName)
  647. .then(extraDiv => {
  648. if (extraDiv) {
  649. extraStuffWrapper.replaceChildren(extraDiv);
  650. extraDiv.appendChild(mxUtils.button("⟳", refreshExtraStuff));
  651. }
  652. });
  653. }
  654. refreshExtraStuff();
  655. return div;
  656. }));
  657. }
  658. else {
  659. const div = document.createElement('div');
  660. div.style.padding = '3px 0px';
  661. div.appendChild(document.createTextNode("No "+modelType+" models."));
  662. modelListDiv.replaceChildren(div);
  663. }
  664. });
  665. }
  666. refreshList();
  667. containerDiv.appendChild(createUploadDiv(refreshList));
  668. return refreshList;
  669. }
  670. function createPTrenderCallback(startTraceIri) {
  671. return () => {
  672. return fetch(BACKEND+"/render_pt/"+encodeURIComponent(startTraceIri))
  673. .then(treatResponseAsPageXml)
  674. .then(() => {
  675. refreshDrawioModelList();
  676. showRebuildOMLResult(rebuildOMLIncremental);
  677. });
  678. }
  679. }
  680. const extraPMstuff = modelName => {
  681. if (modelName.endsWith(":pm")) {
  682. const pmModel = addArtifactPrefix(modelName.substr(0, modelName.length-3)+"_pm#model");
  683. const urlEncoded = encodeURIComponent(pmModel);
  684. const enactButton = mxUtils.button("Start New...", ()=>{
  685. // open WEE in new tab:
  686. window.open(WEE+"/gettraces?iri="+urlEncoded, "_blank");
  687. });
  688. const shortTraceName = traceIri => dropArtifactPrefix(traceIri).split('#').findLast(str => str.length > 0);
  689. return Promise.all([
  690. fetch(WEE+"/traces/active/"+urlEncoded)
  691. .then(response => response.json())
  692. .then(enactments => {
  693. return enactments.map((enactment, i) => mxUtils.button(`Open "${shortTraceName(enactment.iri)}" (ongoing)`, createPTrenderCallback(enactment.iri)))
  694. }),
  695. fetch(WEE+"/traces/finished/"+urlEncoded)
  696. .then(response => response.json())
  697. .then(enactments => {
  698. return enactments.map((enactment, i) => mxUtils.button(`Open "${shortTraceName(enactment.iri)}" (finished)`, createPTrenderCallback(enactment.iri)))
  699. }),
  700. ])
  701. .then(([ongoing, finished]) => {
  702. const pmDiv = document.createElement('div');
  703. pmDiv.style.marginLeft = '8px';
  704. pmDiv.style.borderWidth = '1px';
  705. pmDiv.style.borderStyle = 'solid';
  706. pmDiv.appendChild(document.createTextNode("PM Enactment: "));
  707. pmDiv.appendChild(enactButton);
  708. if (ongoing.length > 0) {
  709. ongoing.forEach(o => pmDiv.appendChild(o));
  710. }
  711. if (finished.length > 0) {
  712. finished.forEach(f => pmDiv.appendChild(f));
  713. }
  714. return pmDiv;
  715. });
  716. }
  717. return Promise.resolve(null);
  718. };
  719. const noExtraStuff = () => Promise.resolve(null);
  720. const refreshDrawioModelList = createModelList("drawio", createSavePageDiv, "Open", modelName => loadPage(modelName), extraPMstuff);
  721. createModelList("xopp", createUploadDiv("xopp", "application/x-xopp,.xopp"), "Download", createDownloadHandler("xopp"), noExtraStuff);
  722. createModelList("csv", createUploadDiv("csv", "text/csv,.csv"), "Download", createDownloadHandler("csv"), noExtraStuff);
  723. createModelList("file", createUploadDiv("file", ""), "Download", createDownloadHandler("file"), noExtraStuff);
  724. // Load a model and add it as a new page to the editor
  725. function treatResponseAsPageXml(response) {
  726. return response.text()
  727. .then(xmltext => {
  728. // console.log(xmltext);
  729. const parser = new DOMParser();
  730. const doc = parser.parseFromString(xmltext, "application/xml");
  731. const node = doc.documentElement;
  732. const page = new DiagramPage(node);
  733. // if page with same name already exists, erase it:
  734. ui.pages = ui.pages.filter(pg => pg.node.getAttribute("name") != page.node.getAttribute("name"));
  735. ui.pages.push(page);
  736. ui.currentPage = page;
  737. ui.editor.setGraphXml(node);
  738. });
  739. }
  740. function loadPage(pageName) {
  741. return fetch(BACKEND + "/files/drawio/" + pageName)
  742. .then(treatResponseAsPageXml);
  743. }
  744. // make sure the plugin popup window is always within the bounds of the browser window:
  745. const dialogPosX = x => Math.max(0, Math.min(x, window.innerWidth - getSetting("dialogWidth")));
  746. const dialogPosY = y => Math.max(0, Math.min(y, window.innerHeight - getSetting("dialogHeight")));
  747. const wnd = new mxWindow("ModelVerse", wndDiv,
  748. dialogPosX(getSetting("dialogPosX")),
  749. dialogPosY(getSetting("dialogPosY")),
  750. getSetting("dialogWidth"),
  751. getSetting("dialogHeight"),
  752. true, true);
  753. wnd.destroyOnClose = false;
  754. wnd.setMaximizable(false);
  755. wnd.setResizable(true);
  756. wnd.setClosable(false);
  757. // remember window geometry in localstorage:
  758. wnd.addListener('resize', function(wnd) {
  759. // couldn't find a better way to get the new window size but the following:
  760. const parsepx = px => parseInt(px.substring(0, px.length-2));
  761. setSetting("dialogWidth", parsepx(wnd.div.style.width));
  762. setSetting("dialogHeight", parsepx(wnd.div.style.height));
  763. })
  764. wnd.addListener('move', function() {
  765. setSetting("dialogPosX", wnd.getX());
  766. setSetting("dialogPosY", wnd.getY());
  767. })
  768. wnd.show();
  769. window.onresize = event => {
  770. // make sure popup window remains within browser window bounds:
  771. const newX = dialogPosX(getSetting("dialogPosX"));
  772. const newY = dialogPosY(getSetting("dialogPosY"));
  773. wnd.div.style.left = newX + "px";
  774. wnd.div.style.top = newY + "px";
  775. // setSetting("dialogPosX", newX);
  776. // setSetting("dialogPosY", newY);
  777. }
  778. }) // end of promise that checks the backend version.
  779. })