From c68799c80da44ca2d03a8f7a4d6b2fb69cb4b295 Mon Sep 17 00:00:00 2001 From: stephane <stephane.dervaux@inrae.fr> Date: Tue, 27 Aug 2024 10:31:46 +0200 Subject: [PATCH 1/6] first try with v2 (upgrade d3js >= 4) --- .../dataView/ItineraryOverviewController.java | 2 +- .../po2vocabmanager/graph/graph-creator2.css | 192 ++++++ .../po2vocabmanager/graph/graph-creator2.js | 648 ++++++++++++++++++ .../fr/inra/po2vocabmanager/graph/graph2.html | 32 + 4 files changed, 873 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.css create mode 100644 src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js create mode 100644 src/main/resources/fr/inra/po2vocabmanager/graph/graph2.html diff --git a/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java b/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java index 33d496e2..92dd67e9 100644 --- a/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java +++ b/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java @@ -311,7 +311,7 @@ public class ItineraryOverviewController { Platform.runLater(() -> { - webEngine.load(MainApp.class.getResource("graph/graph.html").toExternalForm()); + webEngine.load(MainApp.class.getResource("graph/graph2.html").toExternalForm()); }); } diff --git a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.css b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.css new file mode 100644 index 00000000..3e29acbe --- /dev/null +++ b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.css @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2023. INRAE + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the “Softwareâ€), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS ISâ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ + +body{ + margin: 0; + padding: 0; + overflow:hidden; +} + +p{ + text-align: center; + overflow: overlay; + position: relative; +} + +body{ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: rgb(248, 248, 248) +} + +#toolbox{ + position: absolute; + bottom: 0; + left: 0; + margin-bottom: 0.5em; + margin-left: 1em; + border: 2px solid #EEEEEE; + border-radius: 5px; + padding: 1em; + z-index: 5; +} + +#toolbox input{ + width: 30px; + opacity: 0.4; +} +#toolbox input:hover{ + opacity: 1; + cursor: pointer; +} + +#hidden-file-upload{ + display: none; +} + +#download-input{ + margin: 0 0.5em; +} + +.conceptG text{ + pointer-events: none; +} + +marker{ + fill: #333; +} + +g.conceptG[nodeType=cluster] circle { + fill: rgba(255, 255, 255, 0); + stroke-width: 0px; +} +g.conceptG[nodeType=hat][obs=no] circle{ + fill: #fff; + stroke: url(#hat); + stroke-width: 6px; +} +g.conceptG[nodeType=hat][obs=yes] circle{ + fill: #fff; + stroke: url(#hatobs); + stroke-width: 6px; +} + +g.conceptG[nodeType=step][obs=no] circle{ + fill: #fff; + stroke: #000; + stroke-width: 6px; +} +g.conceptG[nodeType=step][obs=yes] circle{ + fill: #fff; + stroke: url(#stepobs); + stroke-width: 6px; +} + +g.conceptG[nodeType=composition] circle { + fill: #fff; + stroke: #ff4747; + stroke-width: 6px +} + +g.conceptG:hover[nodeType][obs] circle{ + fill: #53aab1; +} + +g.selected[nodeType][obs] circle{ + fill: #a0a0a0; +} +g.selected:hover circle{ + fill: #ff6464; +} + + + +path.link[linkType=hc]{ + fill: none; + stroke: #ffb941; + stroke-width: 6px; + cursor: default; +} +path.link[linkType=ch]{ + fill: none; + stroke: #ffb941; + stroke-width: 6px; + cursor: default; +} + +path.link[linkType=sh]{ + fill: none; + stroke: #ffb941; + stroke-width: 6px; + cursor: default; +} + +path.link[linkType=ss]{ + fill: none; + stroke: #000000; + stroke-width: 6px; + cursor: default; +} +path.link[linkType=hs]{ + fill: none; + stroke: #5593ff; + /*stroke: url(#linksc);*/ + stroke-width: 6px; + cursor: default; +} +path.link[linkType=sc]{ + fill: none; + stroke: #ff6464; + /*stroke: url(#linksc);*/ + stroke-width: 6px; + cursor: default; +} +path.link[linkType=cs]{ + fill: none; + stroke: #ff6464; + /*stroke: url(#linkcs);*/ + stroke-width: 6px; + cursor: default; +} + +path.link[linkType=drag]{ + fill: none; + stroke: #000000; + stroke-width: 6px; + cursor: default; +} + +path.link:hover{ + stroke: #53aab1; +} + +g.connect-node circle{ + fill: #BEFFFF; +} + +path.link.hidden{ + stroke-width: 0; +} + +path.link.selected { + stroke: #a0a0a0; +} diff --git a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js new file mode 100644 index 00000000..3cbe8b32 --- /dev/null +++ b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js @@ -0,0 +1,648 @@ +/* + * Copyright (c) 2023. INRAE + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the “Softwareâ€), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS ISâ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ + +// Copied from https://bl.ocks.org/cjrd/6863459 + + // define graphcreator object + var GraphCreator = function(svg) { + var thisGraph = this; + + thisGraph.nodes = []; + thisGraph.edges = []; + + thisGraph.state = { + selectedNode: null, + selectedEdge: null, + mouseDownNode: null, + mouseOverNode: null, + mouseDownLink: null, + justDragged: false, + justScaleTransGraph: false, + lastKeyDown: -1, + shiftNodeDrag: false, + selectedText: null + }; + + // define arrow markers for graph links + var defs = svg.append('svg:defs'); + defs.append('svg:marker') + .attr('id', 'end-arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', "32") + .attr('markerWidth', 3.5) + .attr('markerHeight', 3.5) + .attr('orient', 'auto') + .append('svg:path') + .attr('d', 'M0,-5L10,0L0,5'); + + // define arrow markers for leading arrow + defs.append('svg:marker') + .attr('id', 'mark-end-arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 7) + .attr('markerWidth', 3.5) + .attr('markerHeight', 3.5) + .attr('orient', 'auto') + .append('svg:path') + .attr('d', 'M0,-5L10,0L0,5'); + + var grad = defs.append('svg:linearGradient') + .attr('id', 'hat') + grad.append("svg:stop") + .attr("offset","0%") + .attr("stop-color","#000000"); + grad.append("svg:stop") + .attr("offset","100%") + .attr("stop-color","#5593ff"); + + + var grad = defs.append('svg:linearGradient') + .attr('id', 'hatobs') + grad.append("svg:stop") + .attr("offset","0%") + .attr("stop-color","#000000"); + grad.append("svg:stop") + .attr("offset","30%") + .attr("stop-color","#5593ff"); + grad.append("svg:stop") + .attr("offset","100%") + .attr("stop-color","#51d13a"); + + var grad = defs.append('svg:linearGradient') + .attr('id', 'stepobs') + grad.append("svg:stop") + .attr("offset","0%") + .attr("stop-color","#000000"); + grad.append("svg:stop") + .attr("offset","100%") + .attr("stop-color","#51d13a"); + + var gradsc = defs.append('svg:linearGradient') + .attr('id', 'linksc') + gradsc.append("svg:stop") + .attr("offset","0%") + .attr("stop-color","#000000"); + gradsc.append("svg:stop") + .attr("offset","100%") + .attr("stop-color","#ff6464"); + + var gradcs = defs.append('svg:linearGradient') + .attr('id', 'linkcs') + gradcs.append("svg:stop") + .attr("offset","0%") + .attr("stop-color","#ff6464"); + gradcs.append("svg:stop") + .attr("offset","100%") + .attr("stop-color","#000000"); + + thisGraph.svg = svg; + thisGraph.svgG = svg.append("g") + .classed(thisGraph.consts.graphClass, true); + var svgG = thisGraph.svgG; + svgG.attr("width", width) + .attr("height", height); + // displayed when dragging between nodes + thisGraph.dragLine = svgG.append('svg:path') + .attr('class', 'link') + .attr('linkType', 'drag') + .style('marker-end', 'url(#mark-end-arrow)'); + + // svg nodes and edges + thisGraph.paths = svgG.append("g").selectAll("g"); + thisGraph.circles = svgG.append("g").selectAll("g"); + + thisGraph.drag = d3.drag() + .subject(function(d) { + return { + x: d.x, + y: d.y + }; + }) + .on("drag", function(args) { + thisGraph.state.justDragged = true; + thisGraph.dragmove.call(thisGraph, args); + }) + .on("end", function(d) { + thisGraph.MouseUp.call(thisGraph, d3.select(this), d); + }); + + // listen for key events + d3.select(window).on("keydown", function() { + thisGraph.svgKeyDown.call(thisGraph); + }) + .on("keyup", function() { + thisGraph.svgKeyUp.call(thisGraph); + }); + svg.on("mousedown", function(d) { + thisGraph.svgMouseDown.call(thisGraph, d); + }); + svg.on("mouseup", function(d) { + console.log("mouse up on svg"); + thisGraph.svgMouseUp.call(thisGraph, d); + }); + + + // listen for resize + window.onresize = function() { + thisGraph.updateWindow(svg); + }; + }; + + GraphCreator.prototype.consts = { + selectedClass: "selected", + connectClass: "connect-node", + circleGClass: "conceptG", + graphClass: "graph", + activeEditId: "active-editing", + BACKSPACE_KEY: 8, + DELETE_KEY: 46, + ENTER_KEY: 13, + nodeRadius: 50 + }; + + /* PROTOTYPE FUNCTIONS */ + + GraphCreator.prototype.dragmove = function(d) { + var thisGraph = this; + if (thisGraph.state.shiftNodeDrag) { + thisGraph.dragLine.attr('d', 'M' + d.x + ',' + d.y + 'L' + d3.mouse(thisGraph.svgG.node())[0] + ',' + d3.mouse(this.svgG.node())[1]); + } else { + d.x += d3.event.dx; + d.y += d3.event.dy; + thisGraph.updateGraph(); + } + }; + + /* select all text in element: taken from http://stackoverflow.com/questions/6139107/programatically-select-text-in-a-contenteditable-html-element */ + GraphCreator.prototype.selectElementContents = function(el) { + var range = document.createRange(); + range.selectNodeContents(el); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + }; + + /* insert svg line breaks: taken from http://stackoverflow.com/questions/13241475/how-do-i-include-newlines-in-labels-in-d3-charts */ + GraphCreator.prototype.insertTitleLinebreaks = function(gEl, title) { + var newtext = title.replace(/([^\n]{1,11})\s/g, '$1!:!'); + var words = newtext.split(/!:!/g), + nwords = words.length; + var el = gEl.append("text") + .attr("text-anchor", "middle") + .attr("dy", "-" + (nwords - 1) * 7.5); + + for (var i = 0; i < words.length; i++) { + var tspan = el.append('tspan').text(words[i]); + if (i > 0) + tspan.attr('x', 0).attr('dy', '15'); + } + }; + + // remove edges associated with a node + GraphCreator.prototype.spliceLinksForNode = function(node) { + var thisGraph = this, + toSplice = thisGraph.edges.filter(function(l) { + return (l.source === node || l.target === node); + }); + toSplice.map(function(l) { + // remove edge only if target && source = step. + if(l.source.type === "step" && l.target === "step") { + thisGraph.javaFXDelEdge(l); + thisGraph.edges.splice(thisGraph.edges.indexOf(l), 1); + } + }); + }; + + GraphCreator.prototype.replaceSelectEdge = function(d3Path, edgeData) { + var thisGraph = this; + d3Path.classed(thisGraph.consts.selectedClass, true); + if (thisGraph.state.selectedEdge) { + thisGraph.removeSelectFromEdge(); + } + thisGraph.state.selectedEdge = edgeData; + }; + + GraphCreator.prototype.replaceSelectNode = function(d3Node, nodeData) { + var thisGraph = this; + d3Node.classed(this.consts.selectedClass, true); + if (thisGraph.state.selectedNode) { + thisGraph.removeSelectFromNode(); + } + thisGraph.state.selectedNode = nodeData; + thisGraph.javaFXHighLightNode(nodeData); + }; + + GraphCreator.prototype.removeSelectFromNode = function() { + var thisGraph = this; + thisGraph.circles.filter(function(cd) { + return cd.id === thisGraph.state.selectedNode.id; + }).classed(thisGraph.consts.selectedClass, false); + thisGraph.state.selectedNode = null; + }; + + GraphCreator.prototype.removeSelectFromEdge = function() { + var thisGraph = this; + thisGraph.paths.filter(function(cd) { + return cd === thisGraph.state.selectedEdge; + }).classed(thisGraph.consts.selectedClass, false); + thisGraph.state.selectedEdge = null; + }; + + GraphCreator.prototype.pathMouseDown = function(d3path, d) { + var thisGraph = this, + state = thisGraph.state; + d3.event.stopPropagation(); + state.mouseDownLink = d; + + if (state.selectedNode) { + thisGraph.removeSelectFromNode(); + } + + var prevEdge = state.selectedEdge; + if (!prevEdge || prevEdge !== d) { + thisGraph.replaceSelectEdge(d3path, d); + } else { + thisGraph.removeSelectFromEdge(); + } + }; + + // mousedown on node + GraphCreator.prototype.circleMouseDown = function(d3node, d) { + var thisGraph = this, + state = thisGraph.state; + d3.event.stopPropagation(); + state.mouseDownNode = d; + if (d3.event.shiftKey) { + state.shiftNodeDrag = d3.event.shiftKey; + // reposition dragged directed edge + thisGraph.dragLine.classed('hidden', false) + .attr('d', 'M' + d.x + ',' + d.y + 'L' + d.x + ',' + d.y); + return; + } + }; + + /* place editable text on node in place of svg text */ + + // mouseup on nodes + GraphCreator.prototype.MouseUp = function(d3node, d) { + var thisGraph = this, + state = thisGraph.state, + consts = thisGraph.consts; + // reset the states + state.shiftNodeDrag = false; + state.mouseDownLink = null; + + d3node.classed(consts.connectClass, false); + + var mouseOverNode = state.mouseOverNode; + + thisGraph.dragLine.classed("hidden", true); + + if (!mouseOverNode) return; + + + if (mouseOverNode !== d && ((mouseOverNode.type === "step" || d.type === "step") && (mouseOverNode.type !== "hat" && d.type !== "hat"))) { + // we're in a different node: create new edge for mousedown edge and add to graph + var newEdge = { + source: d, + target: mouseOverNode + }; + var filtRes = thisGraph.paths.filter(function(d) { + if (mouseOverNode.source === newEdge.target && mouseOverNode.target === newEdge.source && thisGraph.javaFXEditable()) { + // inversion de sens de l'arc. + thisGraph.javaFXDelEdge(mouseOverNode); + thisGraph.edges.splice(thisGraph.edges.indexOf(mouseOverNode), 1); + } + return mouseOverNode.source === newEdge.source && mouseOverNode.target === newEdge.target; + }); + if (!filtRes[0].length && thisGraph.javaFXEditable()) { + thisGraph.edges.push(newEdge); + thisGraph.updateGraph(false); + thisGraph.javaFXAddEdge(newEdge.source, newEdge.target); + } + } else { + // we're in the same node + if (state.justDragged) { + // dragged, not clicked + state.justDragged = false; + } else { + // clicked, not dragged + if (d3.event.shiftKey) { + } else { + if (state.selectedEdge) { + thisGraph.removeSelectFromEdge(); + } + var prevNode = state.selectedNode; + + if (!prevNode || prevNode.id !== d.id) { + thisGraph.replaceSelectNode(d3node, mouseOverNode); + } else { + thisGraph.removeSelectFromNode(); + } + } + } + } + state.mouseDownNode = null; + return; + + }; // end of circles mouseup + + // mousedown on main svg + GraphCreator.prototype.svgMouseDown = function() { + this.state.graphMouseDown = true; + }; + + // mouseup on main svg + GraphCreator.prototype.svgMouseUp = function() { + var thisGraph = this, + state = thisGraph.state; + if (state.justScaleTransGraph) { + // dragged not clicked + state.justScaleTransGraph = false; + } else if (state.graphMouseDown && d3.event.shiftKey) { + + } else if (state.shiftNodeDrag) { + // dragged from node + state.shiftNodeDrag = false; + thisGraph.dragLine.classed("hidden", true); + } + state.graphMouseDown = false; + }; + + // keydown on main svg + GraphCreator.prototype.svgKeyDown = function() { + + var thisGraph = this, + state = thisGraph.state, + consts = thisGraph.consts; + // make sure repeated key presses don't register for each keydown + if (state.lastKeyDown !== -1) return; + + state.lastKeyDown = d3.event.keyCode; + var selectedNode = state.selectedNode, + selectedEdge = state.selectedEdge; + + switch (d3.event.keyCode) { + case consts.BACKSPACE_KEY: + case consts.DELETE_KEY: + + d3.event.preventDefault(); + if (selectedNode && selectedNode.type === "composition" && thisGraph.javaFXEditable()) { + alert("Composition can't be removed here"); + thisGraph.svgKeyUp(); // force keyup because event is blocked by alert ! + } else if(selectedNode && selectedNode.type === "hat" && thisGraph.javaFXEditable()) { + alert("Box step can't be removed here"); + thisGraph.svgKeyUp(); // force keyup because event is blocked by alert ! + } else if (selectedNode && selectedNode.type === "step" && thisGraph.javaFXEditable()) { + thisGraph.javaFXDelNode(selectedNode); + thisGraph.nodes.splice(thisGraph.nodes.indexOf(selectedNode), 1); + thisGraph.spliceLinksForNode(selectedNode); + state.selectedNode = null; + thisGraph.updateGraph(false); + } else if(selectedEdge && selectedEdge.source.type === "hat" && selectedEdge.target.type === "step") { + alert("Substep link can't be removed here"); + thisGraph.svgKeyUp(); + } else if (selectedEdge && thisGraph.javaFXEditable()) { + thisGraph.javaFXDelEdge(selectedEdge); + thisGraph.edges.splice(thisGraph.edges.indexOf(selectedEdge), 1); + state.selectedEdge = null; + thisGraph.updateGraph(false); + } + break; + } + }; + + GraphCreator.prototype.svgKeyUp = function() { + this.state.lastKeyDown = -1; + }; + + // call to propagate changes to graph + GraphCreator.prototype.updateGraph = function() { + + var thisGraph = this, + consts = thisGraph.consts, + state = thisGraph.state; + + + thisGraph.paths = thisGraph.paths.data(thisGraph.edges, d => String(d.source.id) + "+" + String(d.target.id)).join( + enter => enter.append("path") + .style('marker-end', 'url(#end-arrow)') + .classed("link", true) + .attr("linkType", function(d) { + return d.source.type[0]+d.target.type[0]; + }) + .attr("d", function(d) { + return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; + }) + .on("mousedown", function(d) { + thisGraph.pathMouseDown.call(thisGraph, d3.select(this), d); + }), + update => update.style('marker-end', 'url(#end-arrow)') + .classed(consts.selectedClass, function(d) { + return d === state.selectedEdge; + }) + .attr("d", function(d) { + return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; + }), + exit => exit.remove() + ); + + // update nodes data + thisGraph.circles = thisGraph.circles.data(thisGraph.nodes, d => d.id).join( + enter => { + let gg = enter.append("g") + .classed(consts.circleGClass, true) + .attr("transform", d => "translate(" + d.x + "," + d.y + ")") + .attr("nodeType", d => d.type) + .attr("obs", d => d.obs) + .on("mouseover", function(d) { + state.mouseOverNode = d; + if (state.shiftNodeDrag) { + d3.select(this).classed(consts.connectClass, true); + } + }) + .on("mouseout", function(d) { + state.mouseOverNode = null; + d3.select(this).classed(consts.connectClass, false); + }) + .on("mousedown", function(d) { + thisGraph.circleMouseDown.call(thisGraph, d3.select(this), d); + }) + .call(thisGraph.drag) + + gg.append("circle") + .attr("r", String(consts.nodeRadius)); + gg.each(function(d) { + thisGraph.insertTitleLinebreaks(d3.select(this), d.title); + }); + return gg; + }, + update => update.attr("transform", d => "translate(" + d.x + "," + d.y + ")"), + exit => exit.remove() + ); + + var bbox = thisGraph.svgG.node().getBBox(); + thisGraph.svg.attr("viewBox", [bbox.x-3, bbox.y-3, bbox.width+6, bbox.height+6]); + }; + + GraphCreator.prototype.updateWindow = function(svg) { + var x = window.innerWidth ; + var y = window.innerHeight; + svg.attr("width", x).attr("height", y); + }; + + + + // initial node data + + +GraphCreator.prototype.addNodeNoUpdate = function(nodeName, nodeId, nodeType,obsNb, posX, posY) { + var thisGraph = this; + var txt = nodeName; + // if (obsNb > 0) { + // txt = txt + "\n "+obsNb + " obs"; + // } + var obs = 'no'; + if(obsNb > 0) { + obs = 'yes'; + } + var d = { + id: nodeId, + title: txt, + obs: obs, + x: posX, + y: posY, + type: nodeType + }; + thisGraph.nodes.push(d); +}; + +GraphCreator.prototype.clear = function() { + var thisGraph = this; + thisGraph.nodes.splice(0,thisGraph.nodes.length) + thisGraph.edges.splice(0,thisGraph.edges.length) + thisGraph.updateGraph(); +}; + +GraphCreator.prototype.addNode = function(nodeName, nodeId, nodeType,obsNb, posX, posY) { + var thisGraph = this; + var txt = nodeName; + // if (obsNb > 0) { + // txt = txt + "\n "+obsNb + " obs"; + // } + var obs = 'no'; + if(obsNb > 0) { + obs = 'yes'; + } + var d = { + id: nodeId, + title: txt, + obs: obs, + x: posX, + y: posY, + type: nodeType + }; + thisGraph.nodes.push(d); + thisGraph.updateGraph(); + return "node added " + nodeId; + }; + +GraphCreator.prototype.addEdgeNoUpdate = function(nodeSrc, nodeTgt) { + var thisGraph = this; + var d = { + source: thisGraph.nodes[nodeSrc], + target: thisGraph.nodes[nodeTgt] + }; + thisGraph.edges.push(d); +}; + +GraphCreator.prototype.addEdge = function(nodeSrc, nodeTgt) { + var thisGraph = this; + var d = { + source: thisGraph.nodes[nodeSrc], + target: thisGraph.nodes[nodeTgt] + }; + thisGraph.edges.push(d); + thisGraph.updateGraph(false); + }; + +GraphCreator.prototype.javaFXEditable = function() { + var edit = javaConnector.checkEdition(); + if(!edit) { + alert("Not in edition mode"); + graph.svgKeyUp(); // force keyup because event is blocked by alert ! + } + return edit; + } + +GraphCreator.prototype.javaFXAddEdge = function(source, target) { + javaConnector.addEdge(JSON.stringify(source), JSON.stringify(target)); + } + +GraphCreator.prototype.javaFXDelEdge = function(edge) { + javaConnector.delEdge(JSON.stringify(edge.source), JSON.stringify(edge.target)); + } + +GraphCreator.prototype.javaFXDelNode = function(node){ + javaConnector.delNode(JSON.stringify(node)); + } + +GraphCreator.prototype.javaFXHighLightNode = function(node) { + javaConnector.highlightStep(JSON.stringify(node)); + } + +var width = window.innerWidth, + height = window.innerHeight; + + +var svg = d3.select("#graph-div").append("svg") + .attr("width", width) + .attr("height", height) + .attr("viewBox", [0, 0, width, height]); + +var graph = new GraphCreator(svg); +graph.addNodeNoUpdate('kneading (fabrication pate)',444,'step', 1,1066.9,244.0); +graph.addNodeNoUpdate('assembling (préparation pizza)',445,'step',1, 477.92999999999995,532.0); +graph.addNodeNoUpdate('cooking (cuisson pizza)',447,'step', 1,477.92999999999995,820.0); +graph.addNodeNoUpdate('pizza cuite',454,'composition',0, 477.92999999999995,964.0); +graph.addNodeNoUpdate('pizza crue',453,'composition',0, 600.93,676.0); +graph.addNodeNoUpdate('pate 1',449,'composition', 0,1066.9,388.0); +graph.addNodeNoUpdate('pate 2',450,'composition', 0,1265.9,388.0); +graph.addNodeNoUpdate('mixing (fabrication sauce)',446,'step',0, 12.930000000000007,244.0); +graph.addNodeNoUpdate('sauce tomate',452,'composition',0, 140.93,388.0); +graph.addNodeNoUpdate('mix pate',448,'composition', 0,1066.9,100.0); +graph.addNodeNoUpdate('mix sauce',451,'composition',0, 12.930000000000007,100.0); +graph.addNodeNoUpdate('basilic',455,'composition',0, 367.93,388.0); +graph.addNodeNoUpdate('mozzarella',456,'composition', 0,587.93,388.0); +graph.addEdgeNoUpdate(0,1); +graph.addEdgeNoUpdate(0,5); +graph.addEdgeNoUpdate(0,6); +graph.addEdgeNoUpdate(1,2); +graph.addEdgeNoUpdate(1,4); +graph.addEdgeNoUpdate(2,3); +graph.addEdgeNoUpdate(4,2); +graph.addEdgeNoUpdate(5,1); +graph.addEdgeNoUpdate(7,1); +graph.addEdgeNoUpdate(7,8); +graph.addEdgeNoUpdate(8,1); +graph.addEdgeNoUpdate(9,0); +graph.addEdgeNoUpdate(10,7); +graph.addEdgeNoUpdate(11,1); +graph.addEdgeNoUpdate(12,1); + +graph.updateGraph(); \ No newline at end of file diff --git a/src/main/resources/fr/inra/po2vocabmanager/graph/graph2.html b/src/main/resources/fr/inra/po2vocabmanager/graph/graph2.html new file mode 100644 index 00000000..7ad6be4a --- /dev/null +++ b/src/main/resources/fr/inra/po2vocabmanager/graph/graph2.html @@ -0,0 +1,32 @@ +<!-- + ~ Copyright (c) 2023. INRAE + ~ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + ~ documentation files (the “Softwareâ€), to deal in the Software without restriction, including without limitation + ~ the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + ~ and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + ~ + ~ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + ~ + ~ THE SOFTWARE IS PROVIDED “AS ISâ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + ~ BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + ~ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + ~ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ~ + ~ SPDX-License-Identifier: MIT + --> + +<html> + <head> + <link href="graph-creator2.css" rel="stylesheet" /> + + <script charset="utf-8" src="http://d3js.org/d3.v5.min.js"></script> + + </head> + <body> + <div id="graph-div"> + + </div> + <script src="graph-creator2.js"></script> + </body> +</html> -- GitLab From 8f6d044827cf0776409c788a7eda9849926a95fa Mon Sep 17 00:00:00 2001 From: stephane <stephane.dervaux@inrae.fr> Date: Tue, 27 Aug 2024 11:35:41 +0200 Subject: [PATCH 2/6] bug fix on upgrade d3js version --- .../po2vocabmanager/graph/graph-creator2.js | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js index 3cbe8b32..997162a9 100644 --- a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js +++ b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js @@ -323,14 +323,14 @@ target: mouseOverNode }; var filtRes = thisGraph.paths.filter(function(d) { - if (mouseOverNode.source === newEdge.target && mouseOverNode.target === newEdge.source && thisGraph.javaFXEditable()) { + if (d.source === newEdge.target && d.target === newEdge.source && thisGraph.javaFXEditable()) { // inversion de sens de l'arc. - thisGraph.javaFXDelEdge(mouseOverNode); - thisGraph.edges.splice(thisGraph.edges.indexOf(mouseOverNode), 1); + thisGraph.javaFXDelEdge(d); + thisGraph.edges.splice(thisGraph.edges.indexOf(d), 1); } - return mouseOverNode.source === newEdge.source && mouseOverNode.target === newEdge.target; + return d.source === newEdge.source && d.target === newEdge.target; }); - if (!filtRes[0].length && thisGraph.javaFXEditable()) { + if (!filtRes.size() && thisGraph.javaFXEditable()) { thisGraph.edges.push(newEdge); thisGraph.updateGraph(false); thisGraph.javaFXAddEdge(newEdge.source, newEdge.target); @@ -349,7 +349,7 @@ } var prevNode = state.selectedNode; - if (!prevNode || prevNode.id !== d.id) { + if (!prevNode || prevNode.id !== mouseOverNode.id) { thisGraph.replaceSelectNode(d3node, mouseOverNode); } else { thisGraph.removeSelectFromNode(); @@ -360,7 +360,7 @@ state.mouseDownNode = null; return; - }; // end of circles mouseup + }; // end of mouseup // mousedown on main svg GraphCreator.prototype.svgMouseDown = function() { @@ -616,33 +616,33 @@ var svg = d3.select("#graph-div").append("svg") .attr("viewBox", [0, 0, width, height]); var graph = new GraphCreator(svg); -graph.addNodeNoUpdate('kneading (fabrication pate)',444,'step', 1,1066.9,244.0); -graph.addNodeNoUpdate('assembling (préparation pizza)',445,'step',1, 477.92999999999995,532.0); -graph.addNodeNoUpdate('cooking (cuisson pizza)',447,'step', 1,477.92999999999995,820.0); -graph.addNodeNoUpdate('pizza cuite',454,'composition',0, 477.92999999999995,964.0); -graph.addNodeNoUpdate('pizza crue',453,'composition',0, 600.93,676.0); -graph.addNodeNoUpdate('pate 1',449,'composition', 0,1066.9,388.0); -graph.addNodeNoUpdate('pate 2',450,'composition', 0,1265.9,388.0); -graph.addNodeNoUpdate('mixing (fabrication sauce)',446,'step',0, 12.930000000000007,244.0); -graph.addNodeNoUpdate('sauce tomate',452,'composition',0, 140.93,388.0); -graph.addNodeNoUpdate('mix pate',448,'composition', 0,1066.9,100.0); -graph.addNodeNoUpdate('mix sauce',451,'composition',0, 12.930000000000007,100.0); -graph.addNodeNoUpdate('basilic',455,'composition',0, 367.93,388.0); -graph.addNodeNoUpdate('mozzarella',456,'composition', 0,587.93,388.0); -graph.addEdgeNoUpdate(0,1); -graph.addEdgeNoUpdate(0,5); -graph.addEdgeNoUpdate(0,6); -graph.addEdgeNoUpdate(1,2); -graph.addEdgeNoUpdate(1,4); -graph.addEdgeNoUpdate(2,3); -graph.addEdgeNoUpdate(4,2); -graph.addEdgeNoUpdate(5,1); -graph.addEdgeNoUpdate(7,1); -graph.addEdgeNoUpdate(7,8); -graph.addEdgeNoUpdate(8,1); -graph.addEdgeNoUpdate(9,0); -graph.addEdgeNoUpdate(10,7); -graph.addEdgeNoUpdate(11,1); -graph.addEdgeNoUpdate(12,1); - -graph.updateGraph(); \ No newline at end of file +// graph.addNodeNoUpdate('kneading (fabrication pate)',444,'step', 1,1066.9,244.0); +// graph.addNodeNoUpdate('assembling (préparation pizza)',445,'step',1, 477.92999999999995,532.0); +// graph.addNodeNoUpdate('cooking (cuisson pizza)',447,'step', 1,477.92999999999995,820.0); +// graph.addNodeNoUpdate('pizza cuite',454,'composition',0, 477.92999999999995,964.0); +// graph.addNodeNoUpdate('pizza crue',453,'composition',0, 600.93,676.0); +// graph.addNodeNoUpdate('pate 1',449,'composition', 0,1066.9,388.0); +// graph.addNodeNoUpdate('pate 2',450,'composition', 0,1265.9,388.0); +// graph.addNodeNoUpdate('mixing (fabrication sauce)',446,'step',0, 12.930000000000007,244.0); +// graph.addNodeNoUpdate('sauce tomate',452,'composition',0, 140.93,388.0); +// graph.addNodeNoUpdate('mix pate',448,'composition', 0,1066.9,100.0); +// graph.addNodeNoUpdate('mix sauce',451,'composition',0, 12.930000000000007,100.0); +// graph.addNodeNoUpdate('basilic',455,'composition',0, 367.93,388.0); +// graph.addNodeNoUpdate('mozzarella',456,'composition', 0,587.93,388.0); +// graph.addEdgeNoUpdate(0,1); +// graph.addEdgeNoUpdate(0,5); +// graph.addEdgeNoUpdate(0,6); +// graph.addEdgeNoUpdate(1,2); +// graph.addEdgeNoUpdate(1,4); +// graph.addEdgeNoUpdate(2,3); +// graph.addEdgeNoUpdate(4,2); +// graph.addEdgeNoUpdate(5,1); +// graph.addEdgeNoUpdate(7,1); +// graph.addEdgeNoUpdate(7,8); +// graph.addEdgeNoUpdate(8,1); +// graph.addEdgeNoUpdate(9,0); +// graph.addEdgeNoUpdate(10,7); +// graph.addEdgeNoUpdate(11,1); +// graph.addEdgeNoUpdate(12,1); +// +// graph.updateGraph(); \ No newline at end of file -- GitLab From 152bb5f0837e5dcd0dd336d73e5ff61bdae2a9ce Mon Sep 17 00:00:00 2001 From: stephane <stephane.dervaux@inrae.fr> Date: Thu, 29 Aug 2024 11:21:36 +0200 Subject: [PATCH 3/6] allow multiple node with the same name --- .../po2vocabmanager/utils/JavaConnector.java | 42 ++++++++--------- .../dataView/ItineraryOverviewController.java | 45 ++++++++++--------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/main/java/fr/inra/po2vocabmanager/utils/JavaConnector.java b/src/main/java/fr/inra/po2vocabmanager/utils/JavaConnector.java index b7072356..6e2bfb04 100644 --- a/src/main/java/fr/inra/po2vocabmanager/utils/JavaConnector.java +++ b/src/main/java/fr/inra/po2vocabmanager/utils/JavaConnector.java @@ -29,10 +29,10 @@ import java.util.HashMap; public class JavaConnector { ItineraryFile itiFile; - HashMap<Integer, StepFile> listStep; - HashMap<Integer, CompositionFile> listComposition; + HashMap<String, StepFile> listStep; + HashMap<String, CompositionFile> listComposition; - public JavaConnector(ItineraryFile file, HashMap<Integer, StepFile> listStep, HashMap<Integer, CompositionFile> listComposition) { + public JavaConnector(ItineraryFile file, HashMap<String, StepFile> listStep, HashMap<String, CompositionFile> listComposition) { this.itiFile = file; this.listStep = listStep; this.listComposition = listComposition; @@ -53,20 +53,20 @@ public class JavaConnector { JSONObject oSource = new JSONObject(source); JSONObject oTarget = new JSONObject(target); - if(listStep.containsKey(oSource.getInt("id"))) { // stepFile -> - if(listStep.containsKey(oTarget.getInt("id"))) { // -> stepFile => enchainement step - itiFile.addLinkItinerary(listStep.get(oSource.getInt("id")), listStep.get(oTarget.getInt("id"))); + if(listStep.containsKey(oSource.getString("id"))) { // stepFile -> + if(listStep.containsKey(oTarget.getString("id"))) { // -> stepFile => enchainement step + itiFile.addLinkItinerary(listStep.get(oSource.getString("id")), listStep.get(oTarget.getString("id"))); } else { - if(listComposition.containsKey(oTarget.getInt("id"))) { // -> compoFile => compo output - listStep.get(oSource.getInt("id")).addCompositionFile(listComposition.get(oTarget.getInt("id")), false); + if(listComposition.containsKey(oTarget.getString("id"))) { // -> compoFile => compo output + listStep.get(oSource.getString("id")).addCompositionFile(listComposition.get(oTarget.getString("id")), false); } else { // error } } } else { - if(listComposition.containsKey(oSource.getInt("id"))) { // compoFile -> - if(listStep.containsKey(oTarget.getInt("id"))) { // -> stepFile => compo input - listStep.get(oTarget.getInt("id")).addCompositionFile(listComposition.get(oSource.getInt("id")), true); + if(listComposition.containsKey(oSource.getString("id"))) { // compoFile -> + if(listStep.containsKey(oTarget.getString("id"))) { // -> stepFile => compo input + listStep.get(oTarget.getString("id")).addCompositionFile(listComposition.get(oSource.getString("id")), true); } else { // error. Cas impossible } @@ -80,20 +80,20 @@ public class JavaConnector { } JSONObject oSource = new JSONObject(source); JSONObject oTarget = new JSONObject(target); - if(listStep.containsKey(oSource.getInt("id"))) { // stepFile -> - if(listStep.containsKey(oTarget.getInt("id"))) { // -> stepFile => enchainement step - itiFile.removeLinkItinerary(listStep.get(oSource.getInt("id")),listStep.get(oTarget.getInt("id")) ); + if(listStep.containsKey(oSource.getString("id"))) { // stepFile -> + if(listStep.containsKey(oTarget.getString("id"))) { // -> stepFile => enchainement step + itiFile.removeLinkItinerary(listStep.get(oSource.getString("id")),listStep.get(oTarget.getString("id")) ); } else { - if(listComposition.containsKey(oTarget.getInt("id"))) { // -> compoFile - listStep.get(oSource.getInt("id")).removeCompositionFile(listComposition.get(oTarget.getInt("id"))); + if(listComposition.containsKey(oTarget.getString("id"))) { // -> compoFile + listStep.get(oSource.getString("id")).removeCompositionFile(listComposition.get(oTarget.getString("id"))); } else { // error } } } else { - if(listComposition.containsKey(oSource.getInt("id"))) { // compoFile -> - if(listStep.containsKey(oTarget.getInt("id"))) { // -> stepFile => compo input - listStep.get(oTarget.getInt("id")).removeCompositionFile(listComposition.get(oSource.getInt("id"))); + if(listComposition.containsKey(oSource.getString("id"))) { // compoFile -> + if(listStep.containsKey(oTarget.getString("id"))) { // -> stepFile => compo input + listStep.get(oTarget.getString("id")).removeCompositionFile(listComposition.get(oSource.getString("id"))); } else { // error. Cas impossible } @@ -106,7 +106,7 @@ public class JavaConnector { return; } JSONObject oStep = new JSONObject(node); - StepFile s = listStep.get(oStep.getInt("id")); + StepFile s = listStep.get(oStep.getString("id")); if(s != null) { itiFile.removeStepFromItinerary(s); MainApp.getDataControler().buildItineraryTree(itiFile); @@ -116,7 +116,7 @@ public class JavaConnector { public void highlightStep(String node) { JSONObject oStep = new JSONObject(node); - StepFile s = listStep.get(oStep.getInt("id")); + StepFile s = listStep.get(oStep.getString("id")); if(s != null) { MainApp.getDataControler().highlightStep(s, itiFile); } diff --git a/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java b/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java index 92dd67e9..4932cb77 100644 --- a/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java +++ b/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java @@ -86,8 +86,8 @@ public class ItineraryOverviewController { BooleanProperty showAllCompo = new SimpleBooleanProperty(true); BooleanProperty showISCompo = new SimpleBooleanProperty(false); BooleanProperty showNoneCompo = new SimpleBooleanProperty(false); - private HashMap<Integer, StepFile> listIdStep; - private HashMap<Integer, CompositionFile> listIdComposition; + private HashMap<String, StepFile> listIdStep; + private HashMap<String, CompositionFile> listIdComposition; private JavaConnector jc; private ListView<TextField> listProduct; private WebView browser; @@ -315,7 +315,7 @@ public class ItineraryOverviewController { }); } - private void generateGraph2(WebView webView, ItineraryFile itiFile, String title, HashMap<Integer, StepFile> listIdStep, HashMap<Integer, CompositionFile> listIdComposition) { + private void generateGraph2(WebView webView, ItineraryFile itiFile, String title, HashMap<String, StepFile> listIdStep, HashMap<String, CompositionFile> listIdComposition) { ObservableList<Pair<Pair<ComplexField, ObjectProperty<StepFile>>, Pair<ComplexField, ObjectProperty<StepFile>>>> itinerary = itiFile.getItinerary(); @@ -333,17 +333,19 @@ public class ItineraryOverviewController { HashMap<CompositionFile, MutableNode> listCompo = new HashMap<>(); HashMap<StepFile, MutableGraph> listSubGraph = new HashMap<>(); - final Integer[] nbNode = {444}; +// final Integer[] nbNode = {444}; itiFile.getListStep().filtered(s -> s.getFather() != null).forEach(s -> { StepFile father = s.getFather(); if(!listStepNode.containsKey(father)) { - MutableNode ms = Factory.mutNode(father.getOntoType() + " (" + father.getId() + ")"); - ms.add("id", nbNode[0]); + MutableNode ms = Factory.mutNode(father.getUuid()); + ms.add("id", father.getUuid()); + ms.add(guru.nidi.graphviz.attribute.Label.of(father.getId())); ms.add("observation", father.getObservationFiles().size()); ms.add("hat", "yes"); - listIdStep.put(nbNode[0], father); + ms.add("type", "step"); + listIdStep.put(father.getUuid(), father); - MutableGraph cluster = Factory.mutGraph(nbNode[0].toString()); + MutableGraph cluster = Factory.mutGraph(father.getUuid()); cluster.setDirected(true); cluster.graphAttrs().add(Rank.dir(LEFT_TO_RIGHT)); cluster.setCluster(false); @@ -351,18 +353,18 @@ public class ItineraryOverviewController { cluster.addTo(g); listSubGraph.put(father, cluster); listStepNode.put(father, ms); - nbNode[0]++; } }); for(StepFile s : itiFile.getListStep()) { if(!listIdStep.containsValue(s)) { - MutableNode ms = Factory.mutNode(s.getOntoType() + " (" + s.getId() + ")"); - ms.add("id", nbNode[0]); + MutableNode ms = Factory.mutNode(s.getUuid()); + ms.add("id", s.getUuid()); + ms.add(guru.nidi.graphviz.attribute.Label.of(s.getId())); ms.add("observation", s.getObservationFiles().size()); ms.add("hat", "no"); - listIdStep.put(nbNode[0],s); - nbNode[0]++; + ms.add("type", "step"); + listIdStep.put(s.getUuid(),s); listStepNode.put( s, ms); @@ -414,12 +416,12 @@ public class ItineraryOverviewController { MutableNode pc = null; pc = listCompo.get(en.getKey()); if (pc == null) { - pc = Factory.mutNode( en.getKey().getFileName()); + pc = Factory.mutNode( en.getKey().getUuid()); pc.add(guru.nidi.graphviz.attribute.Label.of(en.getKey().getCompositionID().getValue().get())); pc.add(Color.named("red")); - pc.add("id", nbNode[0]); - listIdComposition.put(nbNode[0], en.getKey()); - nbNode[0]++; + pc.add("id", en.getKey().getUuid()); + pc.add("type", "composition"); + listIdComposition.put(en.getKey().getUuid(), en.getKey()); g.add(pc); listCompo.put(en.getKey(), pc); } @@ -459,15 +461,14 @@ public class ItineraryOverviewController { Double posX = Double.valueOf(pos[0]) - 100.0; Double posY = maxHeaght - (Double.valueOf(pos[1]) - 100.0); - if (label.isEmpty()) { + if (node.getString("type").equalsIgnoreCase("step")) { Platform.runLater(() -> { String type = node.getString("hat").equalsIgnoreCase("yes") ? "hat" : "step"; - webEngine.executeScript("graph.addNodeNoUpdate('" + node.getString("name").replaceAll("'", "\\\\'") + "'," + node.getInt("id") + ",'" + type + "', " + node.getInt("observation") + "," + posX + "," + posY + ");"); + webEngine.executeScript("graph.addNodeNoUpdate('" + label.replaceAll("'", "\\\\'") + "','" + node.getString("id") + "','" + type + "', " + node.getInt("observation") + "," + posX + "," + posY + ");"); }); } else { Platform.runLater(() -> { - - webEngine.executeScript("graph.addNodeNoUpdate('" + label.replaceAll("'", "\\\\'") + "'," + node.getInt("id") + ",'composition', 0 ," + posX + "," + posY + ");"); + webEngine.executeScript("graph.addNodeNoUpdate('" + label.replaceAll("'", "\\\\'") + "','" + node.getString("id") + "','composition', 0 ," + posX + "," + posY + ");"); }); } } else { @@ -476,7 +477,7 @@ public class ItineraryOverviewController { Double posY = 0.0; Platform.runLater(() -> { - webEngine.executeScript("graph.addNodeNoUpdate(''," + -1 + ",'" + "cluster" + "', " + "0" + "," + posX + "," + posY + ");"); + webEngine.executeScript("graph.addNodeNoUpdate('','" + -1 + "','" + "cluster" + "', " + "0" + "," + posX + "," + posY + ");"); }); } } -- GitLab From f1d89166c516a7ad270c22a6ca13db95ab26f6fa Mon Sep 17 00:00:00 2001 From: stephane <stephane.dervaux@inrae.fr> Date: Tue, 3 Sep 2024 10:55:36 +0200 Subject: [PATCH 4/6] prevent bug on highligh node when selecting node in graph --- .../view/dataView/DataOverviewController.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/inra/po2vocabmanager/view/dataView/DataOverviewController.java b/src/main/java/fr/inra/po2vocabmanager/view/dataView/DataOverviewController.java index 645a2f30..9e07c2b7 100644 --- a/src/main/java/fr/inra/po2vocabmanager/view/dataView/DataOverviewController.java +++ b/src/main/java/fr/inra/po2vocabmanager/view/dataView/DataOverviewController.java @@ -802,10 +802,14 @@ public class DataOverviewController { Platform.runLater(() -> { Timeline timeline = new Timeline( new KeyFrame(Duration.seconds(0.2), evt -> { - treeItem.getGraphic().getParent().setVisible(false); + if(treeItem.getGraphic().getParent() != null) { + treeItem.getGraphic().getParent().setVisible(false); + } }), new KeyFrame(Duration.seconds( 0.4), evt -> { - treeItem.getGraphic().getParent().setVisible(true); + if(treeItem.getGraphic().getParent() != null) { + treeItem.getGraphic().getParent().setVisible(true); + } })); timeline.setCycleCount(10); timeline.play(); -- GitLab From 5b023a2aa86551f7749478a541f6b1d1ece51d38 Mon Sep 17 00:00:00 2001 From: stephane <stephane.dervaux@inrae.fr> Date: Tue, 3 Sep 2024 10:57:37 +0200 Subject: [PATCH 5/6] rebuild data node itinerary without remove it. --- .../po2vocabmanager/utils/JavaConnector.java | 6 ++++- .../view/dataView/DataOverviewController.java | 22 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/fr/inra/po2vocabmanager/utils/JavaConnector.java b/src/main/java/fr/inra/po2vocabmanager/utils/JavaConnector.java index 6e2bfb04..c42f32ba 100644 --- a/src/main/java/fr/inra/po2vocabmanager/utils/JavaConnector.java +++ b/src/main/java/fr/inra/po2vocabmanager/utils/JavaConnector.java @@ -19,6 +19,7 @@ package fr.inra.po2vocabmanager.utils; import fr.inra.po2vocabmanager.MainApp; +import fr.inra.po2vocabmanager.model.DataNode; import fr.inrae.po2engine.model.dataModel.CompositionFile; import fr.inrae.po2engine.model.dataModel.ItineraryFile; import fr.inrae.po2engine.model.dataModel.StepFile; @@ -110,7 +111,10 @@ public class JavaConnector { if(s != null) { itiFile.removeStepFromItinerary(s); MainApp.getDataControler().buildItineraryTree(itiFile); - MainApp.getDataControler().selectNode(itiFile); + DataNode itineraryNode = MainApp.getDataControler().getDataNode(itiFile); + if(itineraryNode != null) { + MainApp.getDataControler().getItineraryControler().showNodeDetails(itineraryNode); + } } } diff --git a/src/main/java/fr/inra/po2vocabmanager/view/dataView/DataOverviewController.java b/src/main/java/fr/inra/po2vocabmanager/view/dataView/DataOverviewController.java index 9e07c2b7..8f86f5dc 100644 --- a/src/main/java/fr/inra/po2vocabmanager/view/dataView/DataOverviewController.java +++ b/src/main/java/fr/inra/po2vocabmanager/view/dataView/DataOverviewController.java @@ -331,19 +331,27 @@ public class DataOverviewController { } + public ItineraryOverviewController getItineraryControler() { + return this.itineraryControler; + } + public void buildItineraryTree(ItineraryFile itiF) { DataNode processNode = getDataNode(itiF.getProcessFile()); - DataNode oldIti = getDataNode(itiF); - if(oldIti != null) { - MainApp.getDataControler().deleteDataNode(oldIti); + DataNode itineraryNode = getDataNode(itiF); + if(itineraryNode != null) { + List<DataNode> tempList = FXCollections.observableArrayList(itineraryNode.getSubNodeNoSort()); + for(DataNode allNode : tempList) { + MainApp.getDataControler().deleteDataNode(allNode); + } + } else { + itineraryNode = new DataNode(DataNodeType.ITINERARY); + addNode(itiF, itineraryNode, processNode); } - DataNode dataNodeIti = new DataNode(DataNodeType.ITINERARY); - addNode(itiF, dataNodeIti, processNode); + // node observation. for(ObservationFile obf : itiF.getListObservation()) { DataNode dataNodeObs = new DataNode(DataNodeType.OBSERVATION); -// dataNodeObs.setItineraryFile(itiF); - addNode(obf, dataNodeObs ,dataNodeIti); + addNode(obf, dataNodeObs ,itineraryNode); } // node step (and step obs) for(StepFile stepF : itiF.getListStep()) { -- GitLab From 62e65b53f8d489099f03e72070e7813099282f7c Mon Sep 17 00:00:00 2001 From: stephane <stephane.dervaux@inrae.fr> Date: Tue, 3 Sep 2024 11:08:24 +0200 Subject: [PATCH 6/6] upgrade graph to d3js version v5 add a minimap Remove subgraph management for hat node because link in subgraph were duplicated --- .../dataView/ItineraryOverviewController.java | 32 +- .../po2vocabmanager/graph/graph-creator.css | 26 + .../po2vocabmanager/graph/graph-creator.js | 553 +++++---------- .../po2vocabmanager/graph/graph-creator2.css | 192 ------ .../po2vocabmanager/graph/graph-creator2.js | 648 ------------------ .../fr/inra/po2vocabmanager/graph/graph.html | 38 +- .../fr/inra/po2vocabmanager/graph/graph2.html | 32 - 7 files changed, 231 insertions(+), 1290 deletions(-) delete mode 100644 src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.css delete mode 100644 src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js delete mode 100644 src/main/resources/fr/inra/po2vocabmanager/graph/graph2.html diff --git a/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java b/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java index 4932cb77..a1e9ed97 100644 --- a/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java +++ b/src/main/java/fr/inra/po2vocabmanager/view/dataView/ItineraryOverviewController.java @@ -30,7 +30,6 @@ import fr.inrae.po2engine.model.dataModel.StepFile; import fr.inrae.po2engine.utils.ProgressPO2; import fr.inrae.po2engine.utils.Tools; import guru.nidi.graphviz.attribute.Color; -import guru.nidi.graphviz.attribute.Rank; import guru.nidi.graphviz.engine.*; import guru.nidi.graphviz.model.Factory; import guru.nidi.graphviz.model.MutableGraph; @@ -70,8 +69,6 @@ import java.io.IOException; import java.util.*; import java.util.stream.Collectors; -import static guru.nidi.graphviz.attribute.Rank.RankDir.LEFT_TO_RIGHT; - public class ItineraryOverviewController { @FXML @@ -311,13 +308,12 @@ public class ItineraryOverviewController { Platform.runLater(() -> { - webEngine.load(MainApp.class.getResource("graph/graph2.html").toExternalForm()); + webEngine.load(MainApp.class.getResource("graph/graph.html").toExternalForm()); }); } private void generateGraph2(WebView webView, ItineraryFile itiFile, String title, HashMap<String, StepFile> listIdStep, HashMap<String, CompositionFile> listIdComposition) { - ObservableList<Pair<Pair<ComplexField, ObjectProperty<StepFile>>, Pair<ComplexField, ObjectProperty<StepFile>>>> itinerary = itiFile.getItinerary(); WebEngine webEngine = webView.getEngine(); @@ -331,9 +327,7 @@ public class ItineraryOverviewController { HashMap<StepFile, MutableNode> listStepNode = new HashMap<>(); HashMap<CompositionFile, MutableNode> listCompo = new HashMap<>(); - HashMap<StepFile, MutableGraph> listSubGraph = new HashMap<>(); -// final Integer[] nbNode = {444}; itiFile.getListStep().filtered(s -> s.getFather() != null).forEach(s -> { StepFile father = s.getFather(); if(!listStepNode.containsKey(father)) { @@ -345,13 +339,7 @@ public class ItineraryOverviewController { ms.add("type", "step"); listIdStep.put(father.getUuid(), father); - MutableGraph cluster = Factory.mutGraph(father.getUuid()); - cluster.setDirected(true); - cluster.graphAttrs().add(Rank.dir(LEFT_TO_RIGHT)); - cluster.setCluster(false); - cluster.add(ms); - cluster.addTo(g); - listSubGraph.put(father, cluster); + g.add(ms); listStepNode.put(father, ms); } }); @@ -365,14 +353,8 @@ public class ItineraryOverviewController { ms.add("hat", "no"); ms.add("type", "step"); listIdStep.put(s.getUuid(),s); - listStepNode.put( s, ms); - - if(s.getFather() != null) { - listSubGraph.get(s.getFather()).add(ms); - } else { - g.add(ms); - } + g.add(ms); } } @@ -439,6 +421,11 @@ public class ItineraryOverviewController { Graphviz.useEngine(listEngine); Graphviz viz = Graphviz.fromGraph(g); String json = viz.render(Format.JSON).toString(); +// try { +// viz.render(Format.PNG).toFile(new File("resources/graph.png")); +// } catch (IOException e) { +// throw new RuntimeException(e); +// } JSONObject jsonGraph = new JSONObject(json); JSONArray listGraphNode = jsonGraph.optJSONArray("objects"); @@ -493,7 +480,8 @@ public class ItineraryOverviewController { } } Platform.runLater(() -> { - webEngine.executeScript("graph.updateGraph(true)"); + webEngine.executeScript("graph.updateGraph()"); + webEngine.executeScript("graph.fitWindow()"); }); } diff --git a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator.css b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator.css index 1ec8edb7..c7c1a2ec 100644 --- a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator.css +++ b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator.css @@ -38,6 +38,32 @@ body{ background-color: rgb(248, 248, 248) } +#viewport { + border: 1px solid black; + + /*margin-top: 5px;*/ + /*margin-left: 5px;*/ + /*margin-bottom: 5px;*/ + /*margin-right: 5px;*/ +} + +#minimap { + position: absolute; + right: 10px; + bottom: 10px; + + border: 1px solid black; + background-color: white; +} + +html, body { + height: 100%; + width: 100%; + + padding: 0; + margin: 0; +} + #toolbox{ position: absolute; bottom: 0; diff --git a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator.js b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator.js index af469b86..b155d110 100644 --- a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator.js +++ b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator.js @@ -19,17 +19,17 @@ // Copied from https://bl.ocks.org/cjrd/6863459 // define graphcreator object - var GraphCreator = function(svg, nodes, edges) { + var GraphCreator = function(svg) { var thisGraph = this; - thisGraph.idct = 0; - thisGraph.nodes = nodes || []; - thisGraph.edges = edges || []; + thisGraph.nodes = []; + thisGraph.edges = []; thisGraph.state = { selectedNode: null, selectedEdge: null, mouseDownNode: null, + mouseOverNode: null, mouseDownLink: null, justDragged: false, justScaleTransGraph: false, @@ -124,10 +124,10 @@ // svg nodes and edges thisGraph.paths = svgG.append("g").selectAll("g"); - thisGraph.circles = svgG.append("g").selectAll("g"); + thisGraph.circles = svgG.append("g").attr("id","nodes").selectAll("g"); - thisGraph.drag = d3.behavior.drag() - .origin(function(d) { + thisGraph.drag = d3.drag() + .subject(function(d) { return { x: d.x, y: d.y @@ -137,8 +137,8 @@ thisGraph.state.justDragged = true; thisGraph.dragmove.call(thisGraph, args); }) - .on("dragend", function() { - // todo check if edge-mode is selected + .on("end", function(d) { + thisGraph.MouseUp.call(thisGraph, d3.select(this), d); }); // listen for key events @@ -152,98 +152,41 @@ thisGraph.svgMouseDown.call(thisGraph, d); }); svg.on("mouseup", function(d) { + console.log("mouse up on svg"); thisGraph.svgMouseUp.call(thisGraph, d); }); - // listen for dragging - thisGraph.dragSvg = d3.behavior.zoom() - .on("zoom", function() { - if (d3.event.sourceEvent.shiftKey) { - // TODO the internal d3 state is still changing - return false; - } else { - thisGraph.zoomed.call(thisGraph); - } - return true; - }) - .on("zoomstart", function() { - var ael = d3.select("#" + thisGraph.consts.activeEditId).node(); - if (ael) { - ael.blur(); - } - if (!d3.event.sourceEvent.shiftKey) d3.select('body').style("cursor", "move"); - }) - .on("zoomend", function() { - d3.select('body').style("cursor", "auto"); - }); - - svg.call(thisGraph.dragSvg).on("dblclick.zoom", null); - - // listen for resize - window.onresize = function() { - thisGraph.updateWindow(svg); - }; - // handle download data - d3.select("#download-input").on("click", function() { - var svgString = getSVGString(graph.svg.node()); - svgString2Image( svgString, width, height, 'png', save ); - function save( dataBlob, filesize ){ - saveAs( dataBlob, 'D3 vis exported to PNG.png' ); // FileSaver.js function - } - }); + let transform = d3.zoomIdentity.translate(0, 0).scale(1); + thisGraph.zoom = d3.zoom() + // .scaleExtent([1, 100]) + .on('zoom', zoomed); - // handle uploaded data - d3.select("#upload-input").on("click", function() { - document.getElementById("hidden-file-upload").click(); - }); - d3.select("#hidden-file-upload").on("change", function() { - if (window.File && window.FileReader && window.FileList && window.Blob) { - var uploadFile = this.files[0]; - var filereader = new window.FileReader(); - - filereader.onload = function() { - var txtRes = filereader.result; - // TODO better error handling - try { - var jsonObj = JSON.parse(txtRes); - thisGraph.deleteGraph(true); - thisGraph.nodes = jsonObj.nodes; - thisGraph.setIdCt(jsonObj.nodes.length + 1); - var newEdges = jsonObj.edges; - newEdges.forEach(function(e, i) { - newEdges[i] = { - source: thisGraph.nodes.filter(function(n) { - return n.id == e.source; - })[0], - target: thisGraph.nodes.filter(function(n) { - return n.id == e.target; - })[0] - }; - }); - thisGraph.edges = newEdges; - thisGraph.updateGraph(false); - } catch (err) { - window.alert("Error parsing uploaded file\nerror message: " + err.message); - return; - } - }; - filereader.readAsText(uploadFile); + thisGraph.svg.call(thisGraph.zoom).call(thisGraph.zoom.transform, transform); + function zoomed() { + let transform = d3.event.transform; + let modifiedTransform = d3.zoomIdentity.scale( 1/transform.k ).translate( -transform.x - 50, -transform.y + 50); - } else { - alert("Your browser won't let you save this graph -- try upgrading your browser to IE 10+ or Chrome or Firefox."); - } + let mapMainContainer = thisGraph.svgG + .attr('transform', transform); - }); + console.log(mapMainContainer.node().getBBox().width) + d3.select('#minimapRect') + .attr('width', mapMainContainer.node().getBBox().width ) + .attr('height', mapMainContainer.node().getBBox().height) + .attr('stroke', 'red') + .attr('stroke-width', 10/modifiedTransform.k ) + .attr('stroke-dasharray', 10/modifiedTransform.k ) + .attr('fill', 'none') + .attr('transform', modifiedTransform) - // handle delete graph - d3.select("#delete-graph").on("click", function() { - thisGraph.deleteGraph(false); - }); - }; + thisGraph.updateGraph(); + } - GraphCreator.prototype.setIdCt = function(idct) { - this.idct = idct; + // listen for resize + window.onresize = function() { + thisGraph.updateWindow(svg); + }; }; GraphCreator.prototype.consts = { @@ -267,20 +210,7 @@ } else { d.x += d3.event.dx; d.y += d3.event.dy; - thisGraph.updateGraph(false); - } - }; - - GraphCreator.prototype.deleteGraph = function(skipPrompt) { - var thisGraph = this, - doDelete = true; - if (!skipPrompt) { - doDelete = window.confirm("Press OK to delete this graph"); - } - if (doDelete) { - thisGraph.nodes = []; - thisGraph.edges = []; - thisGraph.updateGraph(false); + thisGraph.updateGraph(); } }; @@ -393,65 +323,30 @@ }; /* place editable text on node in place of svg text */ - GraphCreator.prototype.changeTextOfNode = function(d3node, d) { - var thisGraph = this, - consts = thisGraph.consts, - htmlEl = d3node.node(); - d3node.selectAll("text").remove(); - var nodeBCR = htmlEl.getBoundingClientRect(), - curScale = nodeBCR.width / consts.nodeRadius, - placePad = 5 * curScale, - useHW = curScale > 1 ? nodeBCR.width * 0.71 : consts.nodeRadius * 1.42; - // replace with editableconent text - var d3txt = thisGraph.svg.selectAll("foreignObject") - .data([d]) - .enter() - .append("foreignObject") - .attr("x", nodeBCR.left + placePad) - .attr("y", nodeBCR.top + placePad) - .attr("height", 2 * useHW) - .attr("width", useHW) - .append("xhtml:p") - .attr("id", consts.activeEditId) - .attr("contentEditable", "true") - .text(d.title) - .on("mousedown", function(d) { - d3.event.stopPropagation(); - }) - .on("keydown", function(d) { - d3.event.stopPropagation(); - if (d3.event.keyCode == consts.ENTER_KEY && !d3.event.shiftKey) { - this.blur(); - } - }) - .on("blur", function(d) { - d.title = this.textContent; - thisGraph.insertTitleLinebreaks(d3node, d.title); - d3.select(this.parentElement).remove(); - }); - return d3txt; - }; // mouseup on nodes - GraphCreator.prototype.circleMouseUp = function(d3node, d) { + GraphCreator.prototype.MouseUp = function(d3node, d) { var thisGraph = this, state = thisGraph.state, consts = thisGraph.consts; // reset the states state.shiftNodeDrag = false; - d3node.classed(consts.connectClass, false); + state.mouseDownLink = null; - var mouseDownNode = state.mouseDownNode; + d3node.classed(consts.connectClass, false); - if (!mouseDownNode) return; + var mouseOverNode = state.mouseOverNode; thisGraph.dragLine.classed("hidden", true); - if (mouseDownNode !== d && ((mouseDownNode.type === "step" || d.type === "step") && (mouseDownNode.type !== "hat" && d.type !== "hat"))) { + if (!mouseOverNode) return; + + + if (mouseOverNode !== d && ((mouseOverNode.type === "step" || d.type === "step") && (mouseOverNode.type !== "hat" && d.type !== "hat"))) { // we're in a different node: create new edge for mousedown edge and add to graph var newEdge = { - source: mouseDownNode, - target: d + source: d, + target: mouseOverNode }; var filtRes = thisGraph.paths.filter(function(d) { if (d.source === newEdge.target && d.target === newEdge.source && thisGraph.javaFXEditable()) { @@ -461,7 +356,7 @@ } return d.source === newEdge.source && d.target === newEdge.target; }); - if (!filtRes[0].length && thisGraph.javaFXEditable()) { + if (!filtRes.size() && thisGraph.javaFXEditable()) { thisGraph.edges.push(newEdge); thisGraph.updateGraph(false); thisGraph.javaFXAddEdge(newEdge.source, newEdge.target); @@ -474,19 +369,14 @@ } else { // clicked, not dragged if (d3.event.shiftKey) { - // shift-clicked node: edit text content - // var d3txt = thisGraph.changeTextOfNode(d3node, d); - // var txtNode = d3txt.node(); - // thisGraph.selectElementContents(txtNode); - // txtNode.focus(); } else { if (state.selectedEdge) { thisGraph.removeSelectFromEdge(); } var prevNode = state.selectedNode; - if (!prevNode || prevNode.id !== d.id) { - thisGraph.replaceSelectNode(d3node, d); + if (!prevNode || prevNode.id !== mouseOverNode.id) { + thisGraph.replaceSelectNode(d3node, mouseOverNode); } else { thisGraph.removeSelectFromNode(); } @@ -496,7 +386,7 @@ state.mouseDownNode = null; return; - }; // end of circles mouseup + }; // end of mouseup // mousedown on main svg GraphCreator.prototype.svgMouseDown = function() { @@ -511,23 +401,7 @@ // dragged not clicked state.justScaleTransGraph = false; } else if (state.graphMouseDown && d3.event.shiftKey) { - // clicked not dragged from svg - //var xycoords = d3.mouse(thisGraph.svgG.node()), - // d = { - // id: thisGraph.idct++, - // title: "new concept", - // x: xycoords[0], - // y: xycoords[1] - // }; - //thisGraph.nodes.push(d); - //thisGraph.updateGraph(); - // make title of text immediently editable - // var d3txt = thisGraph.changeTextOfNode(thisGraph.circles.filter(function(dval) { - // return dval.id === d.id; - // }), d), - // txtNode = d3txt.node(); - // thisGraph.selectElementContents(txtNode); - // txtNode.focus(); + } else if (state.shiftNodeDrag) { // dragged from node state.shiftNodeDrag = false; @@ -584,178 +458,110 @@ }; // call to propagate changes to graph - GraphCreator.prototype.updateGraph = function(reinit) { - + GraphCreator.prototype.updateGraph = function() { var thisGraph = this, consts = thisGraph.consts, state = thisGraph.state; - thisGraph.paths = thisGraph.paths.data(thisGraph.edges, function(d) { - return String(d.source.id) + "+" + String(d.target.id); - }); - var paths = thisGraph.paths; - // update existing paths - paths.style('marker-end', 'url(#end-arrow)') - .classed(consts.selectedClass, function(d) { - return d === state.selectedEdge; - }) - .attr("d", function(d) { - return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; - }); - - // add new paths - paths.enter() - .append("path") - .style('marker-end', 'url(#end-arrow)') - .classed("link", true) - .attr("linkType", function(d) { - return d.source.type[0]+d.target.type[0]; - }) - .attr("d", function(d) { - return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; - }) - .on("mousedown", function(d) { - thisGraph.pathMouseDown.call(thisGraph, d3.select(this), d); - }) - .on("mouseup", function(d) { - state.mouseDownLink = null; - }); - - // remove old links - paths.exit().remove(); - - // update existing nodes - thisGraph.circles = thisGraph.circles.data(thisGraph.nodes, function(d) { - return d.id; - }); - thisGraph.circles.attr("transform", function(d) { - return "translate(" + d.x + "," + d.y + ")"; - }); - - // add new nodes - var newGs = thisGraph.circles.enter() - .append("g"); - - newGs.classed(consts.circleGClass, true) - .attr("transform", function(d) { - return "translate(" + d.x + "," + d.y + ")"; - }) - .attr("nodeType", function(d) { - return d.type; - }) - .attr("obs", function(d) { - return d.obs; - }) - .on("mouseover", function(d) { - if (state.shiftNodeDrag) { - d3.select(this).classed(consts.connectClass, true); - } - }) - .on("mouseout", function(d) { - d3.select(this).classed(consts.connectClass, false); - }) - .on("mousedown", function(d) { - thisGraph.circleMouseDown.call(thisGraph, d3.select(this), d); - }) - .on("mouseup", function(d) { - thisGraph.circleMouseUp.call(thisGraph, d3.select(this), d); - }) - .call(thisGraph.drag); - - newGs.append("circle") - .attr("r", String(consts.nodeRadius)); - - newGs.each(function(d) { - thisGraph.insertTitleLinebreaks(d3.select(this), d.title); - }); - // remove old nodes - thisGraph.circles.exit().remove(); - if(reinit) { - thisGraph.reinit(); + thisGraph.paths = thisGraph.paths.data(thisGraph.edges, d => String(d.source.id) + "+" + String(d.target.id)).join( + enter => enter.append("path") + .style('marker-end', 'url(#end-arrow)') + .classed("link", true) + .attr("linkType", function(d) { + return d.source.type[0]+d.target.type[0]; + }) + .attr("d", function(d) { + return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; + }) + .on("mousedown", function(d) { + thisGraph.pathMouseDown.call(thisGraph, d3.select(this), d); + }), + update => update.style('marker-end', 'url(#end-arrow)') + .classed(consts.selectedClass, function(d) { + return d === state.selectedEdge; + }) + .attr("d", function(d) { + return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; + }), + exit => exit.remove() + ); + + // update nodes data + thisGraph.circles = thisGraph.circles.data(thisGraph.nodes, d => d.id).join( + enter => { + let gg = enter.append("g") + .classed(consts.circleGClass, true) + .attr("transform", d => "translate(" + d.x + "," + d.y + ")") + .attr("nodeType", d => d.type) + .attr("obs", d => d.obs) + .on("mouseover", function(d) { + state.mouseOverNode = d; + if (state.shiftNodeDrag) { + d3.select(this).classed(consts.connectClass, true); + } + }) + .on("mouseout", function(d) { + state.mouseOverNode = null; + d3.select(this).classed(consts.connectClass, false); + }) + .on("mousedown", function(d) { + thisGraph.circleMouseDown.call(thisGraph, d3.select(this), d); + }) + .call(thisGraph.drag) + + gg.append("circle") + .attr("r", String(consts.nodeRadius)); + gg.each(function(d) { + thisGraph.insertTitleLinebreaks(d3.select(this), d.title); + }); + return gg; + }, + update => update.attr("transform", d => "translate(" + d.x + "," + d.y + ")"), + exit => exit.remove() + ); + + // mini map management + var oldMiniMap = document.querySelector('#minimap>svg'); + var cloneRect; + if (oldMiniMap != null) { // remove the old minimap if exist + cloneRect = d3.select("#minimap>svg>g>rect").node().cloneNode(true); + document.querySelector('#minimap').removeChild(oldMiniMap); } - }; + // creating a new minimap + var newMiniMap = thisGraph.svg.node().cloneNode(true); + + document.querySelector('#minimap').append(newMiniMap); + let minimap = d3.select('#minimap').select('svg') + .attr('width', window.innerWidth * 0.25).attr("height", window.innerHeight * 0.25); + if (cloneRect) { + document.querySelector("#minimap>svg>g").append(cloneRect) + } else { + minimap.select("g").append('rect') + .attr('id', 'minimapRect'); + } + // fiting in the box - GraphCreator.prototype.zoomed = function() { - this.state.justScaleTransGraph = true; - d3.select("." + this.consts.graphClass) - .attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")"); + var bboxMiniMap = minimap.node().getBBox(); + console.log("new bbox " + bboxMiniMap.width) + minimap.attr("viewBox", [bboxMiniMap.x-3, bboxMiniMap.y-3, bboxMiniMap.width+6, bboxMiniMap.height+6]); }; - GraphCreator.prototype.reinit = function() { - this.state.justScaleTransGraph = true; - - var curSc = this.dragSvg.scale(); - // alert(this.svgG + " - " + this.svgG[0] + " - " + this.svgG[0][0]); - var curWidth = this.svgG[0][0].getBoundingClientRect().width / curSc ; - var curHeight = this.svgG[0][0].getBoundingClientRect().height / curSc; - var availWidth = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth; - var availHeight = window.innerHeight || docEl.clientHeight || bodyEl.clientHeight; - - var scW = availWidth / curWidth; - var scH = availHeight / curHeight; - - var newScale = Math.min(scW, scH); - - this.dragSvg.scale(newScale); - - - var trW = (availWidth / 2) - ((curWidth * newScale) / 2); - trW += 40.0 * newScale ; - var trH = (availHeight / 2) - ((curHeight * newScale) / 2); - trH -= 25.0 * newScale; - - this.dragSvg.translate([trW,trH]); - d3.select("." + this.consts.graphClass) - .attr("transform", "translate("+trW+","+trH+") scale("+newScale+")"); + GraphCreator.prototype.fitWindow = function() { + var bbox = this.svgG.node().getBBox(); + this.svg.attr("viewBox", [bbox.x-3, bbox.y-3, bbox.width+6, bbox.height+6]); + this.updateGraph(); }; GraphCreator.prototype.updateWindow = function(svg) { - var docEl = document.documentElement, - bodyEl = document.getElementsByTagName('body')[0]; - var x = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth; - var y = window.innerHeight || docEl.clientHeight || bodyEl.clientHeight; + console.log("update"); + var x = window.innerWidth ; + var y = window.innerHeight; svg.attr("width", x).attr("height", y); + this.updateGraph(); }; - /**** MAIN ****/ - // warn the user when leaving - window.onbeforeunload = function() { - return "Make sure to save your graph locally before leaving :-)"; - }; - - var docEl = document.documentElement, - bodyEl = document.getElementsByTagName('body')[0]; - - var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth, - height = window.innerHeight || docEl.clientHeight || bodyEl.clientHeight; - - var xLoc = width / 2 - 25, - yLoc = 100; - - // initial node data - var nodes = [ -// { -// title: "new concept", -// type: "step", -// id: 0, -// x: 100, -// y: 100 -// }, { -// title: "new concept", -// type: "step", -// id: 1, -// x: 2500, -// y: 200 -// } - ]; - var edges = [ -// { -// source: nodes[1], -// target: nodes[0] -// } - ]; GraphCreator.prototype.addNodeNoUpdate = function(nodeName, nodeId, nodeType,obsNb, posX, posY) { var thisGraph = this; @@ -782,10 +588,10 @@ GraphCreator.prototype.clear = function() { var thisGraph = this; thisGraph.nodes.splice(0,thisGraph.nodes.length) thisGraph.edges.splice(0,thisGraph.edges.length) - thisGraph.updateGraph(true); + thisGraph.updateGraph(); }; - GraphCreator.prototype.addNode = function(nodeName, nodeId, nodeType,obsNb, posX, posY) { +GraphCreator.prototype.addNode = function(nodeName, nodeId, nodeType,obsNb, posX, posY) { var thisGraph = this; var txt = nodeName; // if (obsNb > 0) { @@ -804,7 +610,7 @@ GraphCreator.prototype.clear = function() { type: nodeType }; thisGraph.nodes.push(d); - thisGraph.updateGraph(false); + thisGraph.updateGraph(); return "node added " + nodeId; }; @@ -817,7 +623,7 @@ GraphCreator.prototype.addEdgeNoUpdate = function(nodeSrc, nodeTgt) { thisGraph.edges.push(d); }; - GraphCreator.prototype.addEdge = function(nodeSrc, nodeTgt) { +GraphCreator.prototype.addEdge = function(nodeSrc, nodeTgt) { var thisGraph = this; var d = { source: thisGraph.nodes[nodeSrc], @@ -827,7 +633,7 @@ GraphCreator.prototype.addEdgeNoUpdate = function(nodeSrc, nodeTgt) { thisGraph.updateGraph(false); }; - GraphCreator.prototype.javaFXEditable = function() { +GraphCreator.prototype.javaFXEditable = function() { var edit = javaConnector.checkEdition(); if(!edit) { alert("Not in edition mode"); @@ -836,58 +642,63 @@ GraphCreator.prototype.addEdgeNoUpdate = function(nodeSrc, nodeTgt) { return edit; } - - GraphCreator.prototype.javaFXAddEdge = function(source, target) { +GraphCreator.prototype.javaFXAddEdge = function(source, target) { javaConnector.addEdge(JSON.stringify(source), JSON.stringify(target)); } - GraphCreator.prototype.javaFXDelEdge = function(edge) { +GraphCreator.prototype.javaFXDelEdge = function(edge) { javaConnector.delEdge(JSON.stringify(edge.source), JSON.stringify(edge.target)); } - GraphCreator.prototype.javaFXDelNode = function(node){ +GraphCreator.prototype.javaFXDelNode = function(node){ javaConnector.delNode(JSON.stringify(node)); } - GraphCreator.prototype.javaFXHighLightNode = function(node) { +GraphCreator.prototype.javaFXHighLightNode = function(node) { javaConnector.highlightStep(JSON.stringify(node)); } - /** MAIN SVG **/ - var svg = d3.select("body").append("svg") +var width = window.innerWidth, + height = window.innerHeight; + + +var viewport = d3.select("#viewport").attr("width", width).attr("height", height); + +var svg = d3.select("#graph-div").append("svg") .attr("width", width) .attr("height", height); - var graph = new GraphCreator(svg, nodes, edges); - - -// graph.addNodeNoUpdate('kneading (fabrication pate)',444,'step', 1,1066.9,244.0); -// graph.addNodeNoUpdate('assembling (préparation pizza)',445,'step',1, 477.92999999999995,532.0); -// graph.addNodeNoUpdate('cooking (cuisson pizza)',447,'step', 1,477.92999999999995,820.0); -// graph.addNodeNoUpdate('pizza cuite',454,'composition',0, 477.92999999999995,964.0); -// graph.addNodeNoUpdate('pizza crue',453,'composition',0, 600.93,676.0); -// graph.addNodeNoUpdate('pate 1',449,'composition', 0,1066.9,388.0); -// graph.addNodeNoUpdate('pate 2',450,'composition', 0,1265.9,388.0); -// graph.addNodeNoUpdate('mixing (fabrication sauce)',446,'step',0, 12.930000000000007,244.0); -// graph.addNodeNoUpdate('sauce tomate',452,'composition',0, 140.93,388.0); -// graph.addNodeNoUpdate('mix pate',448,'composition', 0,1066.9,100.0); -// graph.addNodeNoUpdate('mix sauce',451,'composition',0, 12.930000000000007,100.0); -// graph.addNodeNoUpdate('basilic',455,'composition',0, 367.93,388.0); -// graph.addNodeNoUpdate('mozzarella',456,'composition', 0,587.93,388.0); + // .attr("viewBox", [0, 0, width, height]); + +var graph = new GraphCreator(svg); +// graph.addNodeNoUpdate('PHBV Characterization','96b0f6b3-a06a-400b-bb75-dbab9d83fa28','step', 0,620.84,532.0); +// graph.addNodeNoUpdate('Extrusion PHBV-B00(10%)','9b5bf2b9-9908-4c39-a781-0e4227868fc0','step', 2,390.84,820.0); +// graph.addNodeNoUpdate('Pelletizing+Drying PHBV-B00(10%)','6f9de507-84a6-43df-9281-50dde279317e','step', 2,390.84,1108.0); +// graph.addNodeNoUpdate('Thermopressin PHBV-B00(10%)','a122f3de-d326-46ef-8976-ab02c3167b51','step', 6,390.84,1396.0); +// graph.addNodeNoUpdate('PHBV-B00(10%) film','eada0aed-c2c4-4d10-adea-469212a3b109','composition', 0 ,390.84,1540.0); +// graph.addNodeNoUpdate('PHBV-B00(10%) dried','5fce487c-fd69-49fc-8ebd-426257dff10f','composition', 0 ,545.84,1200.0); +// graph.addNodeNoUpdate('PHBV-B00(10%)','6a1772e9-9ad7-4db0-a767-480f66ccb2ec','composition', 0 ,531.84,964.0); +// graph.addNodeNoUpdate('PHBV','316db778-19c5-4cf7-b095-9874d57a92d9','composition', 0 ,737.84,676.0); +// graph.addNodeNoUpdate('B00 Characterization','3d2e525f-41a7-43ce-859a-da5755bc0b61','step', 2,9.840000000000003,676.0); +// graph.addNodeNoUpdate('Pelletizing + Drying PHBV-0.5wt%BN 2','3496953d-6f93-45d7-bbdb-e911d37d0b7c','step', 0,620.84,244.0); +// graph.addNodeNoUpdate('PHBV 2 dried','909ddee4-8972-4896-9cb2-68515e3b50e2','composition', 0 ,620.84,388.0); +// graph.addNodeNoUpdate('B00','2356e14d-b789-4bc3-b21b-4b8edcc659d3','composition', 0 ,278.84,676.0); +// graph.addNodeNoUpdate('PHBV 2','29c3c2e6-8be9-4afc-a1cc-6636911b30c0','composition', 0 ,620.84,100.0); // graph.addEdgeNoUpdate(0,1); -// graph.addEdgeNoUpdate(0,5); -// graph.addEdgeNoUpdate(0,6); +// graph.addEdgeNoUpdate(0,7); // graph.addEdgeNoUpdate(1,2); -// graph.addEdgeNoUpdate(1,4); +// graph.addEdgeNoUpdate(1,6); // graph.addEdgeNoUpdate(2,3); -// graph.addEdgeNoUpdate(4,2); -// graph.addEdgeNoUpdate(5,1); +// graph.addEdgeNoUpdate(2,5); +// graph.addEdgeNoUpdate(3,4); +// graph.addEdgeNoUpdate(5,3); +// graph.addEdgeNoUpdate(6,2); // graph.addEdgeNoUpdate(7,1); -// graph.addEdgeNoUpdate(7,8); // graph.addEdgeNoUpdate(8,1); -// graph.addEdgeNoUpdate(9,0); -// graph.addEdgeNoUpdate(10,7); +// graph.addEdgeNoUpdate(9,10); +// graph.addEdgeNoUpdate(10,0); // graph.addEdgeNoUpdate(11,1); -// graph.addEdgeNoUpdate(12,1); +// graph.addEdgeNoUpdate(12,9); +// // // graph.updateGraph(); -// graph.reinit(); \ No newline at end of file +// graph.fitWindow(); \ No newline at end of file diff --git a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.css b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.css deleted file mode 100644 index 3e29acbe..00000000 --- a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.css +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (c) 2023. INRAE - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the “Softwareâ€), to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS ISâ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING - * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * SPDX-License-Identifier: MIT - */ - -body{ - margin: 0; - padding: 0; - overflow:hidden; -} - -p{ - text-align: center; - overflow: overlay; - position: relative; -} - -body{ - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-color: rgb(248, 248, 248) -} - -#toolbox{ - position: absolute; - bottom: 0; - left: 0; - margin-bottom: 0.5em; - margin-left: 1em; - border: 2px solid #EEEEEE; - border-radius: 5px; - padding: 1em; - z-index: 5; -} - -#toolbox input{ - width: 30px; - opacity: 0.4; -} -#toolbox input:hover{ - opacity: 1; - cursor: pointer; -} - -#hidden-file-upload{ - display: none; -} - -#download-input{ - margin: 0 0.5em; -} - -.conceptG text{ - pointer-events: none; -} - -marker{ - fill: #333; -} - -g.conceptG[nodeType=cluster] circle { - fill: rgba(255, 255, 255, 0); - stroke-width: 0px; -} -g.conceptG[nodeType=hat][obs=no] circle{ - fill: #fff; - stroke: url(#hat); - stroke-width: 6px; -} -g.conceptG[nodeType=hat][obs=yes] circle{ - fill: #fff; - stroke: url(#hatobs); - stroke-width: 6px; -} - -g.conceptG[nodeType=step][obs=no] circle{ - fill: #fff; - stroke: #000; - stroke-width: 6px; -} -g.conceptG[nodeType=step][obs=yes] circle{ - fill: #fff; - stroke: url(#stepobs); - stroke-width: 6px; -} - -g.conceptG[nodeType=composition] circle { - fill: #fff; - stroke: #ff4747; - stroke-width: 6px -} - -g.conceptG:hover[nodeType][obs] circle{ - fill: #53aab1; -} - -g.selected[nodeType][obs] circle{ - fill: #a0a0a0; -} -g.selected:hover circle{ - fill: #ff6464; -} - - - -path.link[linkType=hc]{ - fill: none; - stroke: #ffb941; - stroke-width: 6px; - cursor: default; -} -path.link[linkType=ch]{ - fill: none; - stroke: #ffb941; - stroke-width: 6px; - cursor: default; -} - -path.link[linkType=sh]{ - fill: none; - stroke: #ffb941; - stroke-width: 6px; - cursor: default; -} - -path.link[linkType=ss]{ - fill: none; - stroke: #000000; - stroke-width: 6px; - cursor: default; -} -path.link[linkType=hs]{ - fill: none; - stroke: #5593ff; - /*stroke: url(#linksc);*/ - stroke-width: 6px; - cursor: default; -} -path.link[linkType=sc]{ - fill: none; - stroke: #ff6464; - /*stroke: url(#linksc);*/ - stroke-width: 6px; - cursor: default; -} -path.link[linkType=cs]{ - fill: none; - stroke: #ff6464; - /*stroke: url(#linkcs);*/ - stroke-width: 6px; - cursor: default; -} - -path.link[linkType=drag]{ - fill: none; - stroke: #000000; - stroke-width: 6px; - cursor: default; -} - -path.link:hover{ - stroke: #53aab1; -} - -g.connect-node circle{ - fill: #BEFFFF; -} - -path.link.hidden{ - stroke-width: 0; -} - -path.link.selected { - stroke: #a0a0a0; -} diff --git a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js b/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js deleted file mode 100644 index 997162a9..00000000 --- a/src/main/resources/fr/inra/po2vocabmanager/graph/graph-creator2.js +++ /dev/null @@ -1,648 +0,0 @@ -/* - * Copyright (c) 2023. INRAE - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the “Softwareâ€), to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS ISâ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING - * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * SPDX-License-Identifier: MIT - */ - -// Copied from https://bl.ocks.org/cjrd/6863459 - - // define graphcreator object - var GraphCreator = function(svg) { - var thisGraph = this; - - thisGraph.nodes = []; - thisGraph.edges = []; - - thisGraph.state = { - selectedNode: null, - selectedEdge: null, - mouseDownNode: null, - mouseOverNode: null, - mouseDownLink: null, - justDragged: false, - justScaleTransGraph: false, - lastKeyDown: -1, - shiftNodeDrag: false, - selectedText: null - }; - - // define arrow markers for graph links - var defs = svg.append('svg:defs'); - defs.append('svg:marker') - .attr('id', 'end-arrow') - .attr('viewBox', '0 -5 10 10') - .attr('refX', "32") - .attr('markerWidth', 3.5) - .attr('markerHeight', 3.5) - .attr('orient', 'auto') - .append('svg:path') - .attr('d', 'M0,-5L10,0L0,5'); - - // define arrow markers for leading arrow - defs.append('svg:marker') - .attr('id', 'mark-end-arrow') - .attr('viewBox', '0 -5 10 10') - .attr('refX', 7) - .attr('markerWidth', 3.5) - .attr('markerHeight', 3.5) - .attr('orient', 'auto') - .append('svg:path') - .attr('d', 'M0,-5L10,0L0,5'); - - var grad = defs.append('svg:linearGradient') - .attr('id', 'hat') - grad.append("svg:stop") - .attr("offset","0%") - .attr("stop-color","#000000"); - grad.append("svg:stop") - .attr("offset","100%") - .attr("stop-color","#5593ff"); - - - var grad = defs.append('svg:linearGradient') - .attr('id', 'hatobs') - grad.append("svg:stop") - .attr("offset","0%") - .attr("stop-color","#000000"); - grad.append("svg:stop") - .attr("offset","30%") - .attr("stop-color","#5593ff"); - grad.append("svg:stop") - .attr("offset","100%") - .attr("stop-color","#51d13a"); - - var grad = defs.append('svg:linearGradient') - .attr('id', 'stepobs') - grad.append("svg:stop") - .attr("offset","0%") - .attr("stop-color","#000000"); - grad.append("svg:stop") - .attr("offset","100%") - .attr("stop-color","#51d13a"); - - var gradsc = defs.append('svg:linearGradient') - .attr('id', 'linksc') - gradsc.append("svg:stop") - .attr("offset","0%") - .attr("stop-color","#000000"); - gradsc.append("svg:stop") - .attr("offset","100%") - .attr("stop-color","#ff6464"); - - var gradcs = defs.append('svg:linearGradient') - .attr('id', 'linkcs') - gradcs.append("svg:stop") - .attr("offset","0%") - .attr("stop-color","#ff6464"); - gradcs.append("svg:stop") - .attr("offset","100%") - .attr("stop-color","#000000"); - - thisGraph.svg = svg; - thisGraph.svgG = svg.append("g") - .classed(thisGraph.consts.graphClass, true); - var svgG = thisGraph.svgG; - svgG.attr("width", width) - .attr("height", height); - // displayed when dragging between nodes - thisGraph.dragLine = svgG.append('svg:path') - .attr('class', 'link') - .attr('linkType', 'drag') - .style('marker-end', 'url(#mark-end-arrow)'); - - // svg nodes and edges - thisGraph.paths = svgG.append("g").selectAll("g"); - thisGraph.circles = svgG.append("g").selectAll("g"); - - thisGraph.drag = d3.drag() - .subject(function(d) { - return { - x: d.x, - y: d.y - }; - }) - .on("drag", function(args) { - thisGraph.state.justDragged = true; - thisGraph.dragmove.call(thisGraph, args); - }) - .on("end", function(d) { - thisGraph.MouseUp.call(thisGraph, d3.select(this), d); - }); - - // listen for key events - d3.select(window).on("keydown", function() { - thisGraph.svgKeyDown.call(thisGraph); - }) - .on("keyup", function() { - thisGraph.svgKeyUp.call(thisGraph); - }); - svg.on("mousedown", function(d) { - thisGraph.svgMouseDown.call(thisGraph, d); - }); - svg.on("mouseup", function(d) { - console.log("mouse up on svg"); - thisGraph.svgMouseUp.call(thisGraph, d); - }); - - - // listen for resize - window.onresize = function() { - thisGraph.updateWindow(svg); - }; - }; - - GraphCreator.prototype.consts = { - selectedClass: "selected", - connectClass: "connect-node", - circleGClass: "conceptG", - graphClass: "graph", - activeEditId: "active-editing", - BACKSPACE_KEY: 8, - DELETE_KEY: 46, - ENTER_KEY: 13, - nodeRadius: 50 - }; - - /* PROTOTYPE FUNCTIONS */ - - GraphCreator.prototype.dragmove = function(d) { - var thisGraph = this; - if (thisGraph.state.shiftNodeDrag) { - thisGraph.dragLine.attr('d', 'M' + d.x + ',' + d.y + 'L' + d3.mouse(thisGraph.svgG.node())[0] + ',' + d3.mouse(this.svgG.node())[1]); - } else { - d.x += d3.event.dx; - d.y += d3.event.dy; - thisGraph.updateGraph(); - } - }; - - /* select all text in element: taken from http://stackoverflow.com/questions/6139107/programatically-select-text-in-a-contenteditable-html-element */ - GraphCreator.prototype.selectElementContents = function(el) { - var range = document.createRange(); - range.selectNodeContents(el); - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - }; - - /* insert svg line breaks: taken from http://stackoverflow.com/questions/13241475/how-do-i-include-newlines-in-labels-in-d3-charts */ - GraphCreator.prototype.insertTitleLinebreaks = function(gEl, title) { - var newtext = title.replace(/([^\n]{1,11})\s/g, '$1!:!'); - var words = newtext.split(/!:!/g), - nwords = words.length; - var el = gEl.append("text") - .attr("text-anchor", "middle") - .attr("dy", "-" + (nwords - 1) * 7.5); - - for (var i = 0; i < words.length; i++) { - var tspan = el.append('tspan').text(words[i]); - if (i > 0) - tspan.attr('x', 0).attr('dy', '15'); - } - }; - - // remove edges associated with a node - GraphCreator.prototype.spliceLinksForNode = function(node) { - var thisGraph = this, - toSplice = thisGraph.edges.filter(function(l) { - return (l.source === node || l.target === node); - }); - toSplice.map(function(l) { - // remove edge only if target && source = step. - if(l.source.type === "step" && l.target === "step") { - thisGraph.javaFXDelEdge(l); - thisGraph.edges.splice(thisGraph.edges.indexOf(l), 1); - } - }); - }; - - GraphCreator.prototype.replaceSelectEdge = function(d3Path, edgeData) { - var thisGraph = this; - d3Path.classed(thisGraph.consts.selectedClass, true); - if (thisGraph.state.selectedEdge) { - thisGraph.removeSelectFromEdge(); - } - thisGraph.state.selectedEdge = edgeData; - }; - - GraphCreator.prototype.replaceSelectNode = function(d3Node, nodeData) { - var thisGraph = this; - d3Node.classed(this.consts.selectedClass, true); - if (thisGraph.state.selectedNode) { - thisGraph.removeSelectFromNode(); - } - thisGraph.state.selectedNode = nodeData; - thisGraph.javaFXHighLightNode(nodeData); - }; - - GraphCreator.prototype.removeSelectFromNode = function() { - var thisGraph = this; - thisGraph.circles.filter(function(cd) { - return cd.id === thisGraph.state.selectedNode.id; - }).classed(thisGraph.consts.selectedClass, false); - thisGraph.state.selectedNode = null; - }; - - GraphCreator.prototype.removeSelectFromEdge = function() { - var thisGraph = this; - thisGraph.paths.filter(function(cd) { - return cd === thisGraph.state.selectedEdge; - }).classed(thisGraph.consts.selectedClass, false); - thisGraph.state.selectedEdge = null; - }; - - GraphCreator.prototype.pathMouseDown = function(d3path, d) { - var thisGraph = this, - state = thisGraph.state; - d3.event.stopPropagation(); - state.mouseDownLink = d; - - if (state.selectedNode) { - thisGraph.removeSelectFromNode(); - } - - var prevEdge = state.selectedEdge; - if (!prevEdge || prevEdge !== d) { - thisGraph.replaceSelectEdge(d3path, d); - } else { - thisGraph.removeSelectFromEdge(); - } - }; - - // mousedown on node - GraphCreator.prototype.circleMouseDown = function(d3node, d) { - var thisGraph = this, - state = thisGraph.state; - d3.event.stopPropagation(); - state.mouseDownNode = d; - if (d3.event.shiftKey) { - state.shiftNodeDrag = d3.event.shiftKey; - // reposition dragged directed edge - thisGraph.dragLine.classed('hidden', false) - .attr('d', 'M' + d.x + ',' + d.y + 'L' + d.x + ',' + d.y); - return; - } - }; - - /* place editable text on node in place of svg text */ - - // mouseup on nodes - GraphCreator.prototype.MouseUp = function(d3node, d) { - var thisGraph = this, - state = thisGraph.state, - consts = thisGraph.consts; - // reset the states - state.shiftNodeDrag = false; - state.mouseDownLink = null; - - d3node.classed(consts.connectClass, false); - - var mouseOverNode = state.mouseOverNode; - - thisGraph.dragLine.classed("hidden", true); - - if (!mouseOverNode) return; - - - if (mouseOverNode !== d && ((mouseOverNode.type === "step" || d.type === "step") && (mouseOverNode.type !== "hat" && d.type !== "hat"))) { - // we're in a different node: create new edge for mousedown edge and add to graph - var newEdge = { - source: d, - target: mouseOverNode - }; - var filtRes = thisGraph.paths.filter(function(d) { - if (d.source === newEdge.target && d.target === newEdge.source && thisGraph.javaFXEditable()) { - // inversion de sens de l'arc. - thisGraph.javaFXDelEdge(d); - thisGraph.edges.splice(thisGraph.edges.indexOf(d), 1); - } - return d.source === newEdge.source && d.target === newEdge.target; - }); - if (!filtRes.size() && thisGraph.javaFXEditable()) { - thisGraph.edges.push(newEdge); - thisGraph.updateGraph(false); - thisGraph.javaFXAddEdge(newEdge.source, newEdge.target); - } - } else { - // we're in the same node - if (state.justDragged) { - // dragged, not clicked - state.justDragged = false; - } else { - // clicked, not dragged - if (d3.event.shiftKey) { - } else { - if (state.selectedEdge) { - thisGraph.removeSelectFromEdge(); - } - var prevNode = state.selectedNode; - - if (!prevNode || prevNode.id !== mouseOverNode.id) { - thisGraph.replaceSelectNode(d3node, mouseOverNode); - } else { - thisGraph.removeSelectFromNode(); - } - } - } - } - state.mouseDownNode = null; - return; - - }; // end of mouseup - - // mousedown on main svg - GraphCreator.prototype.svgMouseDown = function() { - this.state.graphMouseDown = true; - }; - - // mouseup on main svg - GraphCreator.prototype.svgMouseUp = function() { - var thisGraph = this, - state = thisGraph.state; - if (state.justScaleTransGraph) { - // dragged not clicked - state.justScaleTransGraph = false; - } else if (state.graphMouseDown && d3.event.shiftKey) { - - } else if (state.shiftNodeDrag) { - // dragged from node - state.shiftNodeDrag = false; - thisGraph.dragLine.classed("hidden", true); - } - state.graphMouseDown = false; - }; - - // keydown on main svg - GraphCreator.prototype.svgKeyDown = function() { - - var thisGraph = this, - state = thisGraph.state, - consts = thisGraph.consts; - // make sure repeated key presses don't register for each keydown - if (state.lastKeyDown !== -1) return; - - state.lastKeyDown = d3.event.keyCode; - var selectedNode = state.selectedNode, - selectedEdge = state.selectedEdge; - - switch (d3.event.keyCode) { - case consts.BACKSPACE_KEY: - case consts.DELETE_KEY: - - d3.event.preventDefault(); - if (selectedNode && selectedNode.type === "composition" && thisGraph.javaFXEditable()) { - alert("Composition can't be removed here"); - thisGraph.svgKeyUp(); // force keyup because event is blocked by alert ! - } else if(selectedNode && selectedNode.type === "hat" && thisGraph.javaFXEditable()) { - alert("Box step can't be removed here"); - thisGraph.svgKeyUp(); // force keyup because event is blocked by alert ! - } else if (selectedNode && selectedNode.type === "step" && thisGraph.javaFXEditable()) { - thisGraph.javaFXDelNode(selectedNode); - thisGraph.nodes.splice(thisGraph.nodes.indexOf(selectedNode), 1); - thisGraph.spliceLinksForNode(selectedNode); - state.selectedNode = null; - thisGraph.updateGraph(false); - } else if(selectedEdge && selectedEdge.source.type === "hat" && selectedEdge.target.type === "step") { - alert("Substep link can't be removed here"); - thisGraph.svgKeyUp(); - } else if (selectedEdge && thisGraph.javaFXEditable()) { - thisGraph.javaFXDelEdge(selectedEdge); - thisGraph.edges.splice(thisGraph.edges.indexOf(selectedEdge), 1); - state.selectedEdge = null; - thisGraph.updateGraph(false); - } - break; - } - }; - - GraphCreator.prototype.svgKeyUp = function() { - this.state.lastKeyDown = -1; - }; - - // call to propagate changes to graph - GraphCreator.prototype.updateGraph = function() { - - var thisGraph = this, - consts = thisGraph.consts, - state = thisGraph.state; - - - thisGraph.paths = thisGraph.paths.data(thisGraph.edges, d => String(d.source.id) + "+" + String(d.target.id)).join( - enter => enter.append("path") - .style('marker-end', 'url(#end-arrow)') - .classed("link", true) - .attr("linkType", function(d) { - return d.source.type[0]+d.target.type[0]; - }) - .attr("d", function(d) { - return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; - }) - .on("mousedown", function(d) { - thisGraph.pathMouseDown.call(thisGraph, d3.select(this), d); - }), - update => update.style('marker-end', 'url(#end-arrow)') - .classed(consts.selectedClass, function(d) { - return d === state.selectedEdge; - }) - .attr("d", function(d) { - return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; - }), - exit => exit.remove() - ); - - // update nodes data - thisGraph.circles = thisGraph.circles.data(thisGraph.nodes, d => d.id).join( - enter => { - let gg = enter.append("g") - .classed(consts.circleGClass, true) - .attr("transform", d => "translate(" + d.x + "," + d.y + ")") - .attr("nodeType", d => d.type) - .attr("obs", d => d.obs) - .on("mouseover", function(d) { - state.mouseOverNode = d; - if (state.shiftNodeDrag) { - d3.select(this).classed(consts.connectClass, true); - } - }) - .on("mouseout", function(d) { - state.mouseOverNode = null; - d3.select(this).classed(consts.connectClass, false); - }) - .on("mousedown", function(d) { - thisGraph.circleMouseDown.call(thisGraph, d3.select(this), d); - }) - .call(thisGraph.drag) - - gg.append("circle") - .attr("r", String(consts.nodeRadius)); - gg.each(function(d) { - thisGraph.insertTitleLinebreaks(d3.select(this), d.title); - }); - return gg; - }, - update => update.attr("transform", d => "translate(" + d.x + "," + d.y + ")"), - exit => exit.remove() - ); - - var bbox = thisGraph.svgG.node().getBBox(); - thisGraph.svg.attr("viewBox", [bbox.x-3, bbox.y-3, bbox.width+6, bbox.height+6]); - }; - - GraphCreator.prototype.updateWindow = function(svg) { - var x = window.innerWidth ; - var y = window.innerHeight; - svg.attr("width", x).attr("height", y); - }; - - - - // initial node data - - -GraphCreator.prototype.addNodeNoUpdate = function(nodeName, nodeId, nodeType,obsNb, posX, posY) { - var thisGraph = this; - var txt = nodeName; - // if (obsNb > 0) { - // txt = txt + "\n "+obsNb + " obs"; - // } - var obs = 'no'; - if(obsNb > 0) { - obs = 'yes'; - } - var d = { - id: nodeId, - title: txt, - obs: obs, - x: posX, - y: posY, - type: nodeType - }; - thisGraph.nodes.push(d); -}; - -GraphCreator.prototype.clear = function() { - var thisGraph = this; - thisGraph.nodes.splice(0,thisGraph.nodes.length) - thisGraph.edges.splice(0,thisGraph.edges.length) - thisGraph.updateGraph(); -}; - -GraphCreator.prototype.addNode = function(nodeName, nodeId, nodeType,obsNb, posX, posY) { - var thisGraph = this; - var txt = nodeName; - // if (obsNb > 0) { - // txt = txt + "\n "+obsNb + " obs"; - // } - var obs = 'no'; - if(obsNb > 0) { - obs = 'yes'; - } - var d = { - id: nodeId, - title: txt, - obs: obs, - x: posX, - y: posY, - type: nodeType - }; - thisGraph.nodes.push(d); - thisGraph.updateGraph(); - return "node added " + nodeId; - }; - -GraphCreator.prototype.addEdgeNoUpdate = function(nodeSrc, nodeTgt) { - var thisGraph = this; - var d = { - source: thisGraph.nodes[nodeSrc], - target: thisGraph.nodes[nodeTgt] - }; - thisGraph.edges.push(d); -}; - -GraphCreator.prototype.addEdge = function(nodeSrc, nodeTgt) { - var thisGraph = this; - var d = { - source: thisGraph.nodes[nodeSrc], - target: thisGraph.nodes[nodeTgt] - }; - thisGraph.edges.push(d); - thisGraph.updateGraph(false); - }; - -GraphCreator.prototype.javaFXEditable = function() { - var edit = javaConnector.checkEdition(); - if(!edit) { - alert("Not in edition mode"); - graph.svgKeyUp(); // force keyup because event is blocked by alert ! - } - return edit; - } - -GraphCreator.prototype.javaFXAddEdge = function(source, target) { - javaConnector.addEdge(JSON.stringify(source), JSON.stringify(target)); - } - -GraphCreator.prototype.javaFXDelEdge = function(edge) { - javaConnector.delEdge(JSON.stringify(edge.source), JSON.stringify(edge.target)); - } - -GraphCreator.prototype.javaFXDelNode = function(node){ - javaConnector.delNode(JSON.stringify(node)); - } - -GraphCreator.prototype.javaFXHighLightNode = function(node) { - javaConnector.highlightStep(JSON.stringify(node)); - } - -var width = window.innerWidth, - height = window.innerHeight; - - -var svg = d3.select("#graph-div").append("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [0, 0, width, height]); - -var graph = new GraphCreator(svg); -// graph.addNodeNoUpdate('kneading (fabrication pate)',444,'step', 1,1066.9,244.0); -// graph.addNodeNoUpdate('assembling (préparation pizza)',445,'step',1, 477.92999999999995,532.0); -// graph.addNodeNoUpdate('cooking (cuisson pizza)',447,'step', 1,477.92999999999995,820.0); -// graph.addNodeNoUpdate('pizza cuite',454,'composition',0, 477.92999999999995,964.0); -// graph.addNodeNoUpdate('pizza crue',453,'composition',0, 600.93,676.0); -// graph.addNodeNoUpdate('pate 1',449,'composition', 0,1066.9,388.0); -// graph.addNodeNoUpdate('pate 2',450,'composition', 0,1265.9,388.0); -// graph.addNodeNoUpdate('mixing (fabrication sauce)',446,'step',0, 12.930000000000007,244.0); -// graph.addNodeNoUpdate('sauce tomate',452,'composition',0, 140.93,388.0); -// graph.addNodeNoUpdate('mix pate',448,'composition', 0,1066.9,100.0); -// graph.addNodeNoUpdate('mix sauce',451,'composition',0, 12.930000000000007,100.0); -// graph.addNodeNoUpdate('basilic',455,'composition',0, 367.93,388.0); -// graph.addNodeNoUpdate('mozzarella',456,'composition', 0,587.93,388.0); -// graph.addEdgeNoUpdate(0,1); -// graph.addEdgeNoUpdate(0,5); -// graph.addEdgeNoUpdate(0,6); -// graph.addEdgeNoUpdate(1,2); -// graph.addEdgeNoUpdate(1,4); -// graph.addEdgeNoUpdate(2,3); -// graph.addEdgeNoUpdate(4,2); -// graph.addEdgeNoUpdate(5,1); -// graph.addEdgeNoUpdate(7,1); -// graph.addEdgeNoUpdate(7,8); -// graph.addEdgeNoUpdate(8,1); -// graph.addEdgeNoUpdate(9,0); -// graph.addEdgeNoUpdate(10,7); -// graph.addEdgeNoUpdate(11,1); -// graph.addEdgeNoUpdate(12,1); -// -// graph.updateGraph(); \ No newline at end of file diff --git a/src/main/resources/fr/inra/po2vocabmanager/graph/graph.html b/src/main/resources/fr/inra/po2vocabmanager/graph/graph.html index f0be13cf..2cf6d77c 100644 --- a/src/main/resources/fr/inra/po2vocabmanager/graph/graph.html +++ b/src/main/resources/fr/inra/po2vocabmanager/graph/graph.html @@ -17,31 +17,19 @@ --> <html> + <head> + <link href="graph-creator.css" rel="stylesheet" /> -<head> - <link href="graph-creator.css" rel="stylesheet" /> -</head> - -<body> -<!--hello--> -<!--<script>document.write(window.location.href);</script>--> -<!-- <div id="toolbox">--> -<!-- <input type="file" id="hidden-file-upload">--> -<!-- <input id="upload-input" type="image" title="upload graph" src="upload-icon.png" alt="upload graph">--> -<!-- <input type="image" id="download-input" title="download graph" src="download-icon.png" alt="download graph">--> -<!-- <input type="image" id="delete-graph" title="delete graph" src="trash-icon.png" alt="delete graph">--> -<!-- </div>--> - - <script charset="utf-8" src="d3.v3.min.js"></script> - <!-- window.D3 --> -<!-- <script src="//cdn.jsdelivr.net/filesaver.js/0.1/FileSaver.min.js"></script>--> - <!-- window.saveAs window.Blob --> - <script src="graph-creator.js"></script> - <script src="saveSvgAsPng.js"></script> -<!--<script src="https://cdn.rawgit.com/eligrey/canvas-toBlob.js/f1a01896135ab378aa5c0118eadd81da55e698d8/canvas-toBlob.js"></script>--> - -<!--<script src="https://cdn.rawgit.com/eligrey/FileSaver.js/e9d941381475b5df8b7d7691013401e171014e89/FileSaver.min.js"></script>--> - -</body> + <script charset="utf-8" src="http://d3js.org/d3.v5.min.js"></script> + </head> + <body> + <div id="viewport"> + <div id="graph-div"> + </div> + <div id="minimap"> + </div> + </div> + <script src="graph-creator.js"></script> + </body> </html> diff --git a/src/main/resources/fr/inra/po2vocabmanager/graph/graph2.html b/src/main/resources/fr/inra/po2vocabmanager/graph/graph2.html deleted file mode 100644 index 7ad6be4a..00000000 --- a/src/main/resources/fr/inra/po2vocabmanager/graph/graph2.html +++ /dev/null @@ -1,32 +0,0 @@ -<!-- - ~ Copyright (c) 2023. INRAE - ~ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - ~ documentation files (the “Softwareâ€), to deal in the Software without restriction, including without limitation - ~ the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, - ~ and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - ~ - ~ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - ~ - ~ THE SOFTWARE IS PROVIDED “AS ISâ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING - ~ BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - ~ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - ~ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ~ - ~ SPDX-License-Identifier: MIT - --> - -<html> - <head> - <link href="graph-creator2.css" rel="stylesheet" /> - - <script charset="utf-8" src="http://d3js.org/d3.v5.min.js"></script> - - </head> - <body> - <div id="graph-div"> - - </div> - <script src="graph-creator2.js"></script> - </body> -</html> -- GitLab