|
@@ -0,0 +1,850 @@
|
|
|
+Draw.loadPlugin(function(ui) {
|
|
|
+
|
|
|
+const WEE = "wee.rys.app";
|
|
|
+
|
|
|
+const BACKEND = "dtb.rys.app";
|
|
|
+const EXPECTED_BACKEND_VERSION = 6; // expected backend version
|
|
|
+
|
|
|
+const SPARQL_SERVER = "fuseki.rys.app"
|
|
|
+const SPARQL_ENDPOINT = "/SystemDesignOntology2Layers/"
|
|
|
+
|
|
|
+const dropVocabularyPrefix = str => str.substring(41);
|
|
|
+const dropDescriptionPrefix = str => str.substring(30);
|
|
|
+const dropArtifactPrefix = str => str.substring(41);
|
|
|
+const addFormalismsPrefix = str => "http://ua.be/sdo2l/vocabulary/formalisms/" + str;
|
|
|
+const addArtifactPrefix = str => "http://ua.be/sdo2l/description/artifacts/" + str;
|
|
|
+
|
|
|
+const QUERIES = {
|
|
|
+ // Query that navigates the given link from the given source element, and returns the IRI and most-concrete-type of the target element.
|
|
|
+ // If `reverse` is true, then incoming links are returned instead.
|
|
|
+ getOutgoingLink: (iri, link_type, reverse=false) => `\
|
|
|
+PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
|
|
|
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
|
+PREFIX pt: <http://ua.be/sdo2l/vocabulary/formalisms/processtraces#>
|
|
|
+
|
|
|
+SELECT DISTINCT ?element ?type
|
|
|
+WHERE {
|
|
|
+ ${reverse ? `?element <${link_type}> <${iri}>` : `<${iri}> <${link_type}> ?element`}.
|
|
|
+ ?element a ?type .
|
|
|
+ {
|
|
|
+ ?type rdfs:subClassOf object_diagram:Object .
|
|
|
+ } UNION {
|
|
|
+ ?type rdfs:subClassOf pt:Event .
|
|
|
+ } UNION {
|
|
|
+ ?type rdfs:subClassOf pt:Artifact .
|
|
|
+ }
|
|
|
+ FILTER(NOT EXISTS {
|
|
|
+ ?more_concrete_type rdfs:subClassOf ?type .
|
|
|
+ ?element a ?more_concrete_type .
|
|
|
+ FILTER(?more_concrete_type != ?type)
|
|
|
+ })
|
|
|
+ FILTER(NOT EXISTS {
|
|
|
+ ?more_concrete_link_type rdfs:subPropertyOf <${link_type}> .
|
|
|
+ ${reverse ? `?element ?more_concrete_link_type <${iri}>` : `<${iri}> ?more_concrete_link_type ?element`} .
|
|
|
+ FILTER(?more_concrete_link_type != <${link_type}>)
|
|
|
+ })
|
|
|
+}`,
|
|
|
+
|
|
|
+ // Query that gets the IRI and most-concrete-type of a given drawio cell.
|
|
|
+ getCellStuff: (cellId) => `\
|
|
|
+PREFIX drawio: <http://ua.be/sdo2l/vocabulary/formalisms/drawio#>
|
|
|
+PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
|
|
|
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
|
+
|
|
|
+SELECT DISTINCT ?element ?type
|
|
|
+WHERE {
|
|
|
+ ?element drawio:hasDrawioId "${cellId}" .
|
|
|
+ ?element a ?type .
|
|
|
+ ?type rdfs:subClassOf object_diagram:Object .
|
|
|
+ NOT EXISTS {
|
|
|
+ ?more_concrete_type rdfs:subClassOf ?type .
|
|
|
+ ?element a ?more_concrete_type .
|
|
|
+ FILTER(?more_concrete_type != ?type)
|
|
|
+ }
|
|
|
+}`,
|
|
|
+
|
|
|
+ // Query that, for a given cell IRI, gets the name of the diagram containing the cell, and the ID of the cell.
|
|
|
+ getDiagramAndCellId: cellIri => `\
|
|
|
+PREFIX drawio: <http://ua.be/sdo2l/vocabulary/formalisms/drawio#>
|
|
|
+PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
|
|
|
+
|
|
|
+SELECT DISTINCT ?diagramName ?cellId
|
|
|
+WHERE {
|
|
|
+ <${cellIri}> drawio:hasDrawioId ?cellId .
|
|
|
+ <${cellIri}> object_diagram:inModel ?model .
|
|
|
+ ?model object_diagram:hasName ?diagramName .
|
|
|
+}`,
|
|
|
+
|
|
|
+ // Query that gets ALL subclass relations.
|
|
|
+ getSubClassRelations: `
|
|
|
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
|
+
|
|
|
+SELECT DISTINCT ?subclass ?superclass
|
|
|
+WHERE {
|
|
|
+ ?subclass rdfs:subClassOf ?superclass
|
|
|
+}`,
|
|
|
+
|
|
|
+ getAllGraphs: `
|
|
|
+SELECT ?g (COUNT(*) as ?count) {GRAPH ?g {?s ?p ?o}} GROUP BY ?g
|
|
|
+`,
|
|
|
+
|
|
|
+ getArtifactFilename: artifactIri => `\
|
|
|
+PREFIX pt: <http://ua.be/sdo2l/vocabulary/formalisms/processtraces#>
|
|
|
+PREFIX pm: <http://ua.be/sdo2l/vocabulary/formalisms/pm#>
|
|
|
+PREFIX base: <http://ua.be/sdo2l/vocabulary/base/base#>
|
|
|
+
|
|
|
+SELECT DISTINCT ?filename ?formalismName
|
|
|
+WHERE {
|
|
|
+ <${artifactIri}> pt:hasLocation ?filename .
|
|
|
+ <${artifactIri}> pt:relatesTo ?pmArtifact .
|
|
|
+ ?pmArtifact pm:hasType ?ftgFormalism .
|
|
|
+ ?ftgFormalism base:hasGUID ?formalismName .
|
|
|
+}`,
|
|
|
+
|
|
|
+ getModels: `\
|
|
|
+PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
|
|
|
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
|
+PREFIX cs_as: <http://ua.be/sdo2l/vocabulary/formalisms/cs_as#>
|
|
|
+
|
|
|
+SELECT DISTINCT ?model ?type
|
|
|
+WHERE {
|
|
|
+ ?model a object_diagram:Model .
|
|
|
+ ?model a ?type .
|
|
|
+ ?type rdfs:subClassOf object_diagram:Model .
|
|
|
+ FILTER(NOT EXISTS {
|
|
|
+ ?model a cs_as:CorrespondenceModel .
|
|
|
+ })
|
|
|
+ FILTER(NOT EXISTS {
|
|
|
+ ?more_concrete_type rdfs:subClassOf ?type .
|
|
|
+ ?model a ?more_concrete_type .
|
|
|
+ FILTER(?more_concrete_type != ?type)
|
|
|
+ })
|
|
|
+}
|
|
|
+`,
|
|
|
+
|
|
|
+ getProperty: (iri, propertyIri) => `\
|
|
|
+SELECT DISTINCT ?value
|
|
|
+WHERE {
|
|
|
+ <${iri}> <${propertyIri}> ?value .
|
|
|
+}
|
|
|
+`,
|
|
|
+
|
|
|
+ insertTraceLink: (fromIri, toIri) => `\
|
|
|
+PREFIX trace: <http://ua.be/sdo2l/vocabulary/formalisms/traceability_model#>
|
|
|
+INSERT DATA {
|
|
|
+ GRAPH <http://ua.be/sdo2l/description/traces> {
|
|
|
+ <${fromIri}> trace:traceLinkTo <${toIri}> .
|
|
|
+ <${fromIri}> trace:traceLinkFrom <${toIri}> .
|
|
|
+ }
|
|
|
+}
|
|
|
+`,
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// 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)
|
|
|
+GRAPHS_NOT_TO_DELETE = [
|
|
|
+ "http://ua.be/sdo2l/description/traces",
|
|
|
+];
|
|
|
+
|
|
|
+const typeToDescription = new Map([
|
|
|
+ ["ftg#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `ftg "${modelName}"`)],
|
|
|
+ ["xopp#Text", async (element, type, getProperty) => getProperty("xopp#hasText").then(text => `text "${text}"`)],
|
|
|
+ ["xopp#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `xournal++ model "${modelName}"`)],
|
|
|
+ ["drawio#Cell", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `cell "${id}"`)],
|
|
|
+ ["drawio#Vertex", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `vertex "${id}"`)],
|
|
|
+ ["drawio#Edge", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `edge "${id}"`)],
|
|
|
+ ["drawio#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `drawio model "${modelName}"`)],
|
|
|
+ ["pm#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `process model "${modelName}"`)],
|
|
|
+ ["pm#Activity", async (element, type, getProperty) => element],
|
|
|
+ ["pm#Artifact", async (element, type, getProperty) => element],
|
|
|
+ ["pm#CtrlInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl inport "${portname}"`)],
|
|
|
+ ["pm#CtrlOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl outport "${portname}"`)],
|
|
|
+ ["pm#DataInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data inport "${portname}"`)],
|
|
|
+ ["pm#DataOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data outport "${portname}"`)],
|
|
|
+])
|
|
|
+
|
|
|
+// (Hardcoded) link types exposed to the user.
|
|
|
+const typeToLinkType = new Map([
|
|
|
+ ["object_diagram#Object", [
|
|
|
+ {
|
|
|
+ relation: "cs_as#parsedAs",
|
|
|
+ description: `Is parsed as`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ relation: "cs_as#renderedAs",
|
|
|
+ description: `Is rendered as`,
|
|
|
+ },
|
|
|
+ // Outcommented, because it's not an interesting relation:
|
|
|
+ {
|
|
|
+ relation: "object_diagram#inModel",
|
|
|
+ description: `Is part of`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ relation: "traceability_model#traceLinkTo",
|
|
|
+ description: `Trace-link (outgoing)`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ relation: "traceability_model#traceLinkFrom",
|
|
|
+ description: `Trace-link (incoming)`,
|
|
|
+ },
|
|
|
+ ]],
|
|
|
+ ["drawio#Model", [
|
|
|
+ {
|
|
|
+ relation: "drawio#hasRootCell",
|
|
|
+ description: `Has root`,
|
|
|
+ },
|
|
|
+ ]],
|
|
|
+ // Outcommented, blows up:
|
|
|
+ // ["drawio#Cell", [
|
|
|
+ // {
|
|
|
+ // relation: "drawio#hasChild",
|
|
|
+ // description: `Has child`,
|
|
|
+ // },
|
|
|
+ // ]],
|
|
|
+ ["pm#Activity", [
|
|
|
+ {
|
|
|
+ relation: "pm#isTransformation",
|
|
|
+ description: `Is typed by`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ relation: "pm#hasPort",
|
|
|
+ description: `Has`,
|
|
|
+ },
|
|
|
+ ]],
|
|
|
+ ["pm#element", [
|
|
|
+ {
|
|
|
+ relation: "processtraces#relatesTo",
|
|
|
+ description: `Enacted by`,
|
|
|
+ reverse: true,
|
|
|
+ },
|
|
|
+ ]],
|
|
|
+ ["processtraces#element", [
|
|
|
+ {
|
|
|
+ relation: "processtraces#relatesTo",
|
|
|
+ description: `Enactment of`,
|
|
|
+ },
|
|
|
+ // pt-element is not a sub-type of 'Object', so we must repeat this:
|
|
|
+ {
|
|
|
+ relation: "cs_as#renderedAs",
|
|
|
+ description: `Is rendered as`,
|
|
|
+ },
|
|
|
+ ]],
|
|
|
+ ["pm#Artifact", [
|
|
|
+ {
|
|
|
+ relation: "pm#hasType",
|
|
|
+ description: `Is typed by`,
|
|
|
+ },
|
|
|
+ ]],
|
|
|
+ ["ftg#Transformation", [
|
|
|
+ {
|
|
|
+ relation: "pm#occursAsActivity",
|
|
|
+ description: `Occurs as`,
|
|
|
+ }
|
|
|
+ ]],
|
|
|
+ ["ftg#Formalism", [
|
|
|
+ {
|
|
|
+ relation: "pm#occursAsArtifact",
|
|
|
+ description: `Occurs as`,
|
|
|
+ },
|
|
|
+ ]],
|
|
|
+ ["xopp#Model", [
|
|
|
+ {
|
|
|
+ relation: "xopp#hasPage",
|
|
|
+ description: `Has page`,
|
|
|
+ },
|
|
|
+ ]],
|
|
|
+ ["xopp#Page", [
|
|
|
+ {
|
|
|
+ relation: "xopp#hasLayer",
|
|
|
+ description: `Has layer`,
|
|
|
+ },
|
|
|
+ ]],
|
|
|
+ ["xopp#Layer", [
|
|
|
+ {
|
|
|
+ relation: "xopp#hasElement",
|
|
|
+ description: `Has element`,
|
|
|
+ },
|
|
|
+ ]],
|
|
|
+]);
|
|
|
+
|
|
|
+// mapping from class-IRI to array of superclass-IRIs
|
|
|
+const superclasses = new Map(); // map is populated as soon as the plugin is loaded
|
|
|
+
|
|
|
+function getQueries(type) {
|
|
|
+ const result = [].concat(
|
|
|
+ typeToLinkType.get(dropVocabularyPrefix(type)) || [],
|
|
|
+ ... (superclasses.get(type) || []).map(supertype => typeToLinkType.get(dropVocabularyPrefix(supertype)) || []));
|
|
|
+ console.log("getQueries,type=",type,"superclasses=",superclasses.get(type),"result=",result);
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+function querySPARQL(query) {
|
|
|
+ const body = new URLSearchParams();
|
|
|
+ body.append("query", query);
|
|
|
+ return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, {
|
|
|
+ headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}),
|
|
|
+ method: "POST",
|
|
|
+ body,
|
|
|
+ })
|
|
|
+ .then(res => res.json())
|
|
|
+ .then(json => {
|
|
|
+ console.log("Query:\n"+query+"\Result:",json.results.bindings);
|
|
|
+ return json.results.bindings;
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function updateSPARQL(update) {
|
|
|
+ const body = new URLSearchParams();
|
|
|
+ body.append("update", update);
|
|
|
+ return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, {
|
|
|
+ headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}),
|
|
|
+ method: "POST",
|
|
|
+ body,
|
|
|
+ })
|
|
|
+ .then(res => {
|
|
|
+ console.log("Update:\n"+update+"\Result:",res);
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+const defaultSettings = {
|
|
|
+ dialogPosX: 100,
|
|
|
+ dialogPosY: 100,
|
|
|
+ dialogWidth: 240,
|
|
|
+ dialogHeight: 400,
|
|
|
+};
|
|
|
+
|
|
|
+fetch(BACKEND+"/version")
|
|
|
+.then(response => response.json())
|
|
|
+.catch(() => 0) // parsing failed - probably backend doesn't even have a version
|
|
|
+.then(version => {
|
|
|
+
|
|
|
+ if (version !== EXPECTED_BACKEND_VERSION) {
|
|
|
+ alert("Incorrect DTDesign Python backend version.\nExpected: " + EXPECTED_BACKEND_VERSION + ", got: " + version + ".\nRefusing to load plugin. Please upgrade :)");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ console.log("Backend version is ", version);
|
|
|
+ console.log("All good.")
|
|
|
+ }
|
|
|
+
|
|
|
+ // Loads all 'rdfs:subClassOf' relations and stores them in a mapping.
|
|
|
+ function loadSuperclasses() {
|
|
|
+ return querySPARQL(QUERIES.getSubClassRelations)
|
|
|
+ .then(results => {
|
|
|
+ for (const {subclass, superclass} of results) {
|
|
|
+ const superclasslist = superclasses.get(subclass.value) || (() => {
|
|
|
+ const list = [];
|
|
|
+ superclasses.set(subclass.value, list);
|
|
|
+ return list;
|
|
|
+ })();
|
|
|
+ superclasslist.push(superclass.value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ loadSuperclasses();
|
|
|
+
|
|
|
+ // Override context menu when right-clicking somewhere in the diagram:
|
|
|
+ const oldFactoryMethod = ui.editor.graph.popupMenuHandler.factoryMethod;
|
|
|
+ ui.editor.graph.popupMenuHandler.factoryMethod = function(menu, cell, evt) {
|
|
|
+ const modelverseMenu = menu.addItem("Knowledge Graph...", null, null);
|
|
|
+ const createLinkMenu = menu.addItem("Create traceability link to...", null, null);
|
|
|
+ menu.addSeparator();
|
|
|
+ oldFactoryMethod.apply(this, arguments);
|
|
|
+
|
|
|
+ const entry = (element, type) => element+':'+type;
|
|
|
+
|
|
|
+ function addMenuItem(element, type, maxRecursion, parentMenuItem, description, alreadyVisited, onClick) {
|
|
|
+ if (alreadyVisited.has(entry(element, type))) {
|
|
|
+ return; // don't go in circles
|
|
|
+ }
|
|
|
+ const shortType = dropVocabularyPrefix(type);
|
|
|
+ const itemDescriptionGen = typeToDescription.get(shortType) || (async () => shortType);
|
|
|
+ itemDescriptionGen(dropArtifactPrefix(element), shortType, property => {
|
|
|
+ return querySPARQL(QUERIES.getProperty(element, addFormalismsPrefix(property)))
|
|
|
+ .then(results => {
|
|
|
+ if (results.length > 0) return results[0].value.value;
|
|
|
+ else return "";
|
|
|
+ });
|
|
|
+ })
|
|
|
+ .then(itemDescription => {
|
|
|
+ const menuItemText = description + ' ' + itemDescription;
|
|
|
+ let createdMenuItem;
|
|
|
+ // If the menu item is a (subtype of a) drawio cell, then clicking on it will take us to that cell in drawio.
|
|
|
+ if (onClick) {
|
|
|
+ createdMenuItem = menu.addItem(menuItemText, null, () => onClick(element), parentMenuItem);
|
|
|
+ }
|
|
|
+ else if ((superclasses.get(type) || []).some(t => t.endsWith("drawio#Cell"))) {
|
|
|
+ createdMenuItem = menu.addItem(menuItemText, null, () => goto(element), parentMenuItem);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ createdMenuItem = menu.addItem(menuItemText, null, null, parentMenuItem);
|
|
|
+ }
|
|
|
+ if ((superclasses.get(type) || []).some(t => t.endsWith("processtraces#Artifact"))) {
|
|
|
+ querySPARQL(QUERIES.getArtifactFilename(element))
|
|
|
+ .then(results => {
|
|
|
+ for (const {filename, formalismName} of results) {
|
|
|
+ menu.addItem(`File "${filename.value}"`, null, () => (createDownloadHandler(formalismName.value))(filename.value), createdMenuItem);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ alreadyVisited.add(entry(element, type));
|
|
|
+ if (maxRecursion <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ addSubmenuItems(element, type, createdMenuItem, maxRecursion, alreadyVisited, onClick);
|
|
|
+ })
|
|
|
+ }
|
|
|
+ function addSubmenuItems(element, type, parentMenuItem, maxRecursion, alreadyVisited, onClick) {
|
|
|
+ const queries = getQueries(type);
|
|
|
+ for (const {relation, description, reverse} of queries) {
|
|
|
+ querySPARQL(QUERIES.getOutgoingLink(element, "http://ua.be/sdo2l/vocabulary/formalisms/"+relation, reverse))
|
|
|
+ .then(results => {
|
|
|
+ for (const {element, type} of results) {
|
|
|
+ addMenuItem(element.value, type.value, maxRecursion-1, parentMenuItem, description, alreadyVisited, onClick);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Populate ModelVerse submenu asynchronously:
|
|
|
+ if (cell && cell.vertex) {
|
|
|
+ querySPARQL(QUERIES.getCellStuff(cell.id))
|
|
|
+ .then(results => {
|
|
|
+ for (const {element, type} of results) {
|
|
|
+ const alreadyVisited = new Set([[element.value, type.value]]);
|
|
|
+ addSubmenuItems(element.value, type.value, modelverseMenu, 3, alreadyVisited);
|
|
|
+
|
|
|
+ querySPARQL(QUERIES.getModels)
|
|
|
+ .then(results => {
|
|
|
+ const alreadyVisited2 = new Set([[element.value, type.value]]);
|
|
|
+ for (const {model, type} of results) {
|
|
|
+ addMenuItem(model.value, type.value, 3, createLinkMenu, "Model",
|
|
|
+ alreadyVisited2,
|
|
|
+ targetIri => updateSPARQL(QUERIES.insertTraceLink(element.value, targetIri)));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ function getSetting(prop) {
|
|
|
+ const result = localStorage.getItem("dtdesign-"+prop);
|
|
|
+ if (result !== null) {
|
|
|
+ return JSON.parse(result);
|
|
|
+ }
|
|
|
+ return defaultSettings[prop];
|
|
|
+ }
|
|
|
+ function setSetting(prop, value) {
|
|
|
+ localStorage.setItem("dtdesign-"+prop, JSON.stringify(value));
|
|
|
+ }
|
|
|
+
|
|
|
+ const errWndDiv = document.createElement('div');
|
|
|
+ // wndDiv.style.color = "red";
|
|
|
+ errWndDiv.style.overflow = "auto";
|
|
|
+ errWndDiv.style.height = "100%";
|
|
|
+ errWndDiv.style.padding = '12px 14px 8px 14px';
|
|
|
+ const errWnd = new mxWindow("Error Details",
|
|
|
+ errWndDiv, 300, 300, 300, 360, true, true);
|
|
|
+ errWnd.destroyOnClose = false;
|
|
|
+ errWnd.setMaximizable(false);
|
|
|
+ errWnd.setResizable(true);
|
|
|
+ errWnd.setClosable(true);
|
|
|
+
|
|
|
+ function getErrDetailsAnchor() {
|
|
|
+ const anchor = document.createElement('a');
|
|
|
+ anchor.innerText = "Details";
|
|
|
+ anchor.href = "#";
|
|
|
+ anchor.onclick = function() {
|
|
|
+ errWnd.show();
|
|
|
+ };
|
|
|
+ return anchor;
|
|
|
+ }
|
|
|
+
|
|
|
+ let rebuildOMLPromise = Promise.resolve();
|
|
|
+
|
|
|
+ function rejectIfStatusAbove400(httpResponse) {
|
|
|
+ if (httpResponse.status >= 400) {
|
|
|
+ return httpResponse.text().then(text => {
|
|
|
+ return Promise.reject(text);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return Promise.resolve();
|
|
|
+ }
|
|
|
+
|
|
|
+ function rebuildOMLForced() {
|
|
|
+ return querySPARQL(QUERIES.getAllGraphs)
|
|
|
+ .then(results => {
|
|
|
+ const graphsToDelete = results
|
|
|
+ .map(({g,count}) => g.value)
|
|
|
+ .filter(graphIri => !GRAPHS_NOT_TO_DELETE.includes(graphIri));
|
|
|
+ // Delete all graphs first
|
|
|
+ return Promise.all(graphsToDelete.map(graphToDelete =>
|
|
|
+ fetch(SPARQL_SERVER+SPARQL_ENDPOINT+"?graph="+graphToDelete, {
|
|
|
+ method: "DELETE",
|
|
|
+ })
|
|
|
+ .then(rejectIfStatusAbove400)
|
|
|
+ ))
|
|
|
+ .then(() => {
|
|
|
+ // Build OML and load the resulting .owl files:
|
|
|
+ return fetch(BACKEND+"/owl_load", {
|
|
|
+ method: "PUT",
|
|
|
+ })
|
|
|
+ .then(rejectIfStatusAbove400)
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function rebuildOMLIncremental() {
|
|
|
+ // Backend takes care of all the heavy work:
|
|
|
+ return fetch(BACKEND+"/owl_reload_incremental", { method: "PUT", })
|
|
|
+ .then(rejectIfStatusAbove400);
|
|
|
+ }
|
|
|
+
|
|
|
+ function showRebuildOMLResult(rebuildAsyncCallback) {
|
|
|
+ // only perform one rebuild at a time -> queue rebuilds.
|
|
|
+ rebuildOMLPromise = rebuildOMLPromise.then(() => {
|
|
|
+ omlStatusDiv.style.color = null;
|
|
|
+ omlStatusDiv.innerText = "Rebuilding OML...";
|
|
|
+ return rebuildAsyncCallback()
|
|
|
+ .then(() => {
|
|
|
+ omlStatusDiv.innerText = "✓ Built OML";
|
|
|
+ omlStatusDiv.style.color = "green";
|
|
|
+ })
|
|
|
+ .catch(errText => {
|
|
|
+ errWndDiv.innerText = errText;
|
|
|
+ omlStatusDiv.innerHTML = "✗ Failed to build OML ";
|
|
|
+ omlStatusDiv.style.color = "red";
|
|
|
+ omlStatusDiv.appendChild(getErrDetailsAnchor());
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function uploadResponseHandler(statusDiv, refreshListCallback) {
|
|
|
+ statusDiv.innerHTML = "Saving ...";
|
|
|
+ statusDiv.style.color = null;
|
|
|
+ omlStatusDiv.innerHTML = "";
|
|
|
+
|
|
|
+ return res => {
|
|
|
+ if (res.status >= 200 && res.status < 300) {
|
|
|
+ errWndDiv.innerText = "No error.";
|
|
|
+ res.json().then(parsedAs => {
|
|
|
+ statusDiv.innerHTML = "✓ Generated OML (" + parsedAs.join(", ") + ")";
|
|
|
+ statusDiv.style.color = "green";
|
|
|
+ });
|
|
|
+ refreshListCallback();
|
|
|
+ showRebuildOMLResult(rebuildOMLIncremental);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ statusDiv.innerHTML = "✗ Failed to save/parse. ";
|
|
|
+ statusDiv.appendChild(getErrDetailsAnchor());
|
|
|
+ res.text().then(text => {
|
|
|
+ errWndDiv.innerText = text;
|
|
|
+ });
|
|
|
+ statusDiv.style.color = "red";
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ let highlight;
|
|
|
+
|
|
|
+ // Given a cell IRI, open the diagram that contains the cell
|
|
|
+ function goto(cellIri) {
|
|
|
+ querySPARQL(QUERIES.getDiagramAndCellId(cellIri))
|
|
|
+ .then(([{diagramName, cellId}]) => {
|
|
|
+ // drop the "_drawio" at the end:
|
|
|
+ const actualDiagramName = diagramName.value.substring(0, diagramName.value.length-7);
|
|
|
+ loadPage(actualDiagramName)
|
|
|
+ .then(() => {
|
|
|
+ if (highlight) {
|
|
|
+ highlight.destroy();
|
|
|
+ }
|
|
|
+ const [cell] = ui.editor.graph.getCellsById([cellId.value]);
|
|
|
+ highlight = new mxCellHighlight(ui.editor.graph, "#eb34e8", 6);
|
|
|
+ highlight.highlight(ui.editor.graph.view.getState(cell));
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const wndDiv = document.createElement('div');
|
|
|
+ wndDiv.style.userSelect = 'none';
|
|
|
+ wndDiv.style.height = '100%';
|
|
|
+ wndDiv.style.color = "rgb(112, 112, 112)";
|
|
|
+ wndDiv.style.overflow = 'auto';
|
|
|
+ wndDiv.classList.add("geFormatContainer");
|
|
|
+
|
|
|
+ const omlDiv = document.createElement('div');
|
|
|
+ omlDiv.classList.add('geFormatSection')
|
|
|
+ omlDiv.style.padding = '12px 14px 8px 14px';
|
|
|
+ wndDiv.appendChild(omlDiv);
|
|
|
+
|
|
|
+ const omlLabel = document.createElement('div');
|
|
|
+ omlLabel.innerText = "OML Status";
|
|
|
+ omlLabel.style.fontWeight = 'bold';
|
|
|
+ omlDiv.appendChild(omlLabel);
|
|
|
+
|
|
|
+ const omlStatusDiv = document.createElement('div');
|
|
|
+ omlStatusDiv.innerText = "Ready";
|
|
|
+ omlDiv.appendChild(omlStatusDiv);
|
|
|
+
|
|
|
+ const omlIncButton = mxUtils.button("Incremental rebuild (faster)", function() {
|
|
|
+ showRebuildOMLResult(rebuildOMLIncremental);
|
|
|
+ });
|
|
|
+ omlDiv.appendChild(omlIncButton);
|
|
|
+ const omlForceButton = mxUtils.button("Forced rebuild (slow)", function() {
|
|
|
+ showRebuildOMLResult(rebuildOMLForced);
|
|
|
+ });
|
|
|
+ omlDiv.appendChild(omlForceButton);
|
|
|
+
|
|
|
+ function createSavePageDiv(refreshModelsCallback) {
|
|
|
+ const savePageDiv = document.createElement('div');
|
|
|
+ const saveButton = mxUtils.button("Save Current Page", function() {
|
|
|
+ const responseHandler = uploadResponseHandler(saveStatusDiv, refreshModelsCallback);
|
|
|
+ const headers = new Headers({
|
|
|
+ "Content-Type": "application/xml",
|
|
|
+ });
|
|
|
+ const serializer = new XMLSerializer();
|
|
|
+ const xmlnode = ui.getXmlFileData(
|
|
|
+ null, // ignore selection
|
|
|
+ true, // only current page
|
|
|
+ true); // uncompressed
|
|
|
+ const diagram = xmlnode.children[0]
|
|
|
+ const diagramName = diagram.getAttribute("name");
|
|
|
+ const xml = serializer.serializeToString(diagram);
|
|
|
+ return fetch(BACKEND + "/files/drawio/"+diagramName, {
|
|
|
+ method: "PUT",
|
|
|
+ headers,
|
|
|
+ body: xml,
|
|
|
+ })
|
|
|
+ .then(responseHandler);
|
|
|
+ });
|
|
|
+ saveButton.style.width = "100%";
|
|
|
+ saveButton.style.marginTop = "4px";
|
|
|
+ saveButton.style.marginBottom = "2px";
|
|
|
+ savePageDiv.appendChild(saveButton);
|
|
|
+ const saveStatusDiv = document.createElement('div');
|
|
|
+ savePageDiv.appendChild(saveStatusDiv);
|
|
|
+ return savePageDiv;
|
|
|
+ }
|
|
|
+ function createUploadDiv(modelType, mimeType) {
|
|
|
+ return function(refreshModelsCallback) {
|
|
|
+ const uploadDiv = document.createElement('div');
|
|
|
+ const uploadLabel = document.createElement('label');
|
|
|
+ uploadLabel.innerText = "Upload "+modelType+":";
|
|
|
+ uploadLabel.for = "uploadButton";
|
|
|
+ uploadDiv.appendChild(uploadLabel);
|
|
|
+ const uploadButton = document.createElement('input');
|
|
|
+ uploadButton.type = "file";
|
|
|
+ uploadButton.id = "uploadButton";
|
|
|
+ uploadButton.style.width = "100%";
|
|
|
+ uploadButton.style.marginTop = "4px";
|
|
|
+ uploadButton.style.marginBottom = "2px";
|
|
|
+ uploadButton.accept = mimeType;
|
|
|
+ // uploadButton.multiple = true;
|
|
|
+ uploadButton.onchange = e => {
|
|
|
+ const responseHandler = uploadResponseHandler(statusDiv, refreshModelsCallback);
|
|
|
+ Array.from(e.target.files).map(file => {
|
|
|
+ fetch(BACKEND+"/files/"+modelType+"/"+file.name, {
|
|
|
+ method: "PUT",
|
|
|
+ body: file,
|
|
|
+ })
|
|
|
+ .then(responseHandler);
|
|
|
+ });
|
|
|
+ };
|
|
|
+ uploadDiv.appendChild(uploadButton);
|
|
|
+ const statusDiv = document.createElement('div');
|
|
|
+ uploadDiv.appendChild(statusDiv);
|
|
|
+ return uploadDiv;
|
|
|
+ };
|
|
|
+ }
|
|
|
+ function createDownloadHandler(modelType) {
|
|
|
+ return modelName => {
|
|
|
+ const a = document.createElement('a');
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.setAttribute('href', BACKEND + "/files/" + modelType + "/" + modelName);
|
|
|
+ a.setAttribute('download', modelName);
|
|
|
+ a.setAttribute('target', "_blank"); // for browsers that don't support the 'download' attribute, tell them to open the link in a new tab
|
|
|
+ a.click();
|
|
|
+ document.body.removeChild(a);
|
|
|
+ };
|
|
|
+ }
|
|
|
+ function createModelList(modelType, createUploadDiv, downloadButtonLabel, onDownloadClick, extraStuff) {
|
|
|
+ const containerDiv = document.createElement('div');
|
|
|
+ containerDiv.classList.add('geFormatSection')
|
|
|
+ containerDiv.style.padding = '12px 14px 8px 14px';
|
|
|
+ wndDiv.appendChild(containerDiv);
|
|
|
+ const labelDiv = document.createElement('div');
|
|
|
+ labelDiv.innerText = "Models — " + modelType;
|
|
|
+ labelDiv.style.fontWeight = 'bold';
|
|
|
+ containerDiv.appendChild(labelDiv);
|
|
|
+ const modelListDiv = document.createElement('div');
|
|
|
+ containerDiv.appendChild(modelListDiv);
|
|
|
+
|
|
|
+ // Refreshes the list of models shown in the ModelVerse window
|
|
|
+ function refreshList() {
|
|
|
+ fetch(BACKEND + "/files/" + modelType, {
|
|
|
+ method: "GET",
|
|
|
+ })
|
|
|
+ .then(res => res.json())
|
|
|
+ .then(models => {
|
|
|
+ if (models.length > 0) {
|
|
|
+ modelListDiv.replaceChildren(...models.sort().map(modelName => {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.style.padding = '3px 0px';
|
|
|
+ const loadButton = mxUtils.button(downloadButtonLabel, () => onDownloadClick(modelName));
|
|
|
+ loadButton.style.marginLeft = "12px";
|
|
|
+ div.appendChild(document.createTextNode(modelName));
|
|
|
+ div.appendChild(loadButton);
|
|
|
+ const extraStuffWrapper = document.createElement('div');
|
|
|
+ div.appendChild(extraStuffWrapper);
|
|
|
+ function refreshExtraStuff() {
|
|
|
+ extraStuff(modelName)
|
|
|
+ .then(extraDiv => {
|
|
|
+ if (extraDiv) {
|
|
|
+ extraStuffWrapper.replaceChildren(extraDiv);
|
|
|
+ extraDiv.appendChild(mxUtils.button("⟳", refreshExtraStuff));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ refreshExtraStuff();
|
|
|
+ return div;
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.style.padding = '3px 0px';
|
|
|
+ div.appendChild(document.createTextNode("No "+modelType+" models."));
|
|
|
+ modelListDiv.replaceChildren(div);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ refreshList();
|
|
|
+
|
|
|
+ containerDiv.appendChild(createUploadDiv(refreshList));
|
|
|
+
|
|
|
+ return refreshList;
|
|
|
+ }
|
|
|
+
|
|
|
+ function createPTrenderCallback(startTraceIri) {
|
|
|
+ return () => {
|
|
|
+ return fetch(BACKEND+"/render_pt/"+encodeURIComponent(startTraceIri))
|
|
|
+ .then(treatResponseAsPageXml)
|
|
|
+ .then(() => {
|
|
|
+ refreshDrawioModelList();
|
|
|
+ showRebuildOMLResult(rebuildOMLIncremental);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const extraPMstuff = modelName => {
|
|
|
+ if (modelName.endsWith(":pm")) {
|
|
|
+ const pmModel = addArtifactPrefix(modelName.substr(0, modelName.length-3)+"_pm#model");
|
|
|
+ const urlEncoded = encodeURIComponent(pmModel);
|
|
|
+ const enactButton = mxUtils.button("Start New...", ()=>{
|
|
|
+ // open WEE in new tab:
|
|
|
+ window.open(WEE+"/gettraces?iri="+urlEncoded, "_blank");
|
|
|
+ });
|
|
|
+ const shortTraceName = traceIri => dropArtifactPrefix(traceIri).split('#').findLast(str => str.length > 0);
|
|
|
+ return Promise.all([
|
|
|
+ fetch(WEE+"/traces/active/"+urlEncoded)
|
|
|
+ .then(response => response.json())
|
|
|
+ .then(enactments => {
|
|
|
+ return enactments.map((enactment, i) => mxUtils.button(`Open "${shortTraceName(enactment.iri)}" (ongoing)`, createPTrenderCallback(enactment.iri)))
|
|
|
+ }),
|
|
|
+ fetch(WEE+"/traces/finished/"+urlEncoded)
|
|
|
+ .then(response => response.json())
|
|
|
+ .then(enactments => {
|
|
|
+ return enactments.map((enactment, i) => mxUtils.button(`Open "${shortTraceName(enactment.iri)}" (finished)`, createPTrenderCallback(enactment.iri)))
|
|
|
+ }),
|
|
|
+ ])
|
|
|
+ .then(([ongoing, finished]) => {
|
|
|
+ const pmDiv = document.createElement('div');
|
|
|
+ pmDiv.style.marginLeft = '8px';
|
|
|
+ pmDiv.style.borderWidth = '1px';
|
|
|
+ pmDiv.style.borderStyle = 'solid';
|
|
|
+ pmDiv.appendChild(document.createTextNode("PM Enactment: "));
|
|
|
+ pmDiv.appendChild(enactButton);
|
|
|
+ if (ongoing.length > 0) {
|
|
|
+ ongoing.forEach(o => pmDiv.appendChild(o));
|
|
|
+ }
|
|
|
+ if (finished.length > 0) {
|
|
|
+ finished.forEach(f => pmDiv.appendChild(f));
|
|
|
+ }
|
|
|
+ return pmDiv;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return Promise.resolve(null);
|
|
|
+ };
|
|
|
+ const noExtraStuff = () => Promise.resolve(null);
|
|
|
+
|
|
|
+ const refreshDrawioModelList = createModelList("drawio", createSavePageDiv, "Open", modelName => loadPage(modelName), extraPMstuff);
|
|
|
+ createModelList("xopp", createUploadDiv("xopp", "application/x-xopp,.xopp"), "Download", createDownloadHandler("xopp"), noExtraStuff);
|
|
|
+ createModelList("csv", createUploadDiv("csv", "text/csv,.csv"), "Download", createDownloadHandler("csv"), noExtraStuff);
|
|
|
+ createModelList("file", createUploadDiv("file", ""), "Download", createDownloadHandler("file"), noExtraStuff);
|
|
|
+
|
|
|
+ // Load a model and add it as a new page to the editor
|
|
|
+ function treatResponseAsPageXml(response) {
|
|
|
+ return response.text()
|
|
|
+ .then(xmltext => {
|
|
|
+ // console.log(xmltext);
|
|
|
+ const parser = new DOMParser();
|
|
|
+ const doc = parser.parseFromString(xmltext, "application/xml");
|
|
|
+ const node = doc.documentElement;
|
|
|
+ const page = new DiagramPage(node);
|
|
|
+ // if page with same name already exists, erase it:
|
|
|
+ ui.pages = ui.pages.filter(pg => pg.node.getAttribute("name") != page.node.getAttribute("name"));
|
|
|
+ ui.pages.push(page);
|
|
|
+ ui.currentPage = page;
|
|
|
+ ui.editor.setGraphXml(node);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function loadPage(pageName) {
|
|
|
+ return fetch(BACKEND + "/files/drawio/" + pageName)
|
|
|
+ .then(treatResponseAsPageXml);
|
|
|
+ }
|
|
|
+
|
|
|
+ // make sure the plugin popup window is always within the bounds of the browser window:
|
|
|
+ const dialogPosX = x => Math.max(0, Math.min(x, window.innerWidth - getSetting("dialogWidth")));
|
|
|
+ const dialogPosY = y => Math.max(0, Math.min(y, window.innerHeight - getSetting("dialogHeight")));
|
|
|
+
|
|
|
+ const wnd = new mxWindow("ModelVerse", wndDiv,
|
|
|
+ dialogPosX(getSetting("dialogPosX")),
|
|
|
+ dialogPosY(getSetting("dialogPosY")),
|
|
|
+ getSetting("dialogWidth"),
|
|
|
+ getSetting("dialogHeight"),
|
|
|
+ true, true);
|
|
|
+ wnd.destroyOnClose = false;
|
|
|
+ wnd.setMaximizable(false);
|
|
|
+ wnd.setResizable(true);
|
|
|
+ wnd.setClosable(false);
|
|
|
+ // remember window geometry in localstorage:
|
|
|
+ wnd.addListener('resize', function(wnd) {
|
|
|
+ // couldn't find a better way to get the new window size but the following:
|
|
|
+ const parsepx = px => parseInt(px.substring(0, px.length-2));
|
|
|
+ setSetting("dialogWidth", parsepx(wnd.div.style.width));
|
|
|
+ setSetting("dialogHeight", parsepx(wnd.div.style.height));
|
|
|
+ })
|
|
|
+ wnd.addListener('move', function() {
|
|
|
+ setSetting("dialogPosX", wnd.getX());
|
|
|
+ setSetting("dialogPosY", wnd.getY());
|
|
|
+ })
|
|
|
+
|
|
|
+ wnd.show();
|
|
|
+
|
|
|
+ window.onresize = event => {
|
|
|
+ // make sure popup window remains within browser window bounds:
|
|
|
+ const newX = dialogPosX(getSetting("dialogPosX"));
|
|
|
+ const newY = dialogPosY(getSetting("dialogPosY"));
|
|
|
+ wnd.div.style.left = newX + "px";
|
|
|
+ wnd.div.style.top = newY + "px";
|
|
|
+ // setSetting("dialogPosX", newX);
|
|
|
+ // setSetting("dialogPosY", newY);
|
|
|
+ }
|
|
|
+
|
|
|
+}) // end of promise that checks the backend version.
|
|
|
+})
|