Commit dedf696b authored by Patrik Meijer's avatar Patrik Meijer
Browse files

Add support for HyperEdges

parent efa1eda1
...@@ -2439,9 +2439,9 @@ ...@@ -2439,9 +2439,9 @@
"dev": true "dev": true
}, },
"cytoscape": { "cytoscape": {
"version": "3.2.12", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.2.12.tgz", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.4.1.tgz",
"integrity": "sha512-epagPckuFpLO2hiKgKMXmCFBFM2K7XbJhYDQj8fH3riiEH+yFXpLYA30jV18TEbVcsq2c58ETOGnHE2YzSBr/Q==", "integrity": "sha512-oHbpo01yd4SB3TjOc/EU4C66TmauROo2+4tKtpLyFnk+/mu5R6OIlARt6OFSWrgoa1AB2f4wrcU0UnBRqhGNNw==",
"requires": { "requires": {
"heap": "^0.2.6", "heap": "^0.2.6",
"lodash.debounce": "^4.0.8" "lodash.debounce": "^4.0.8"
...@@ -2460,6 +2460,11 @@ ...@@ -2460,6 +2460,11 @@
"dagre": "^0.8.2" "dagre": "^0.8.2"
} }
}, },
"cytoscape-edge-connections": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/cytoscape-edge-connections/-/cytoscape-edge-connections-0.3.3.tgz",
"integrity": "sha512-e6W3nFsyxS9XvpFx5OcLHiT3LlF1dQcUdfxVStYdXFhaFVas9u7xndhiPwAE7GXyjIyh+3urVwvFO8XFakB9Qw=="
},
"dagre": { "dagre": {
"version": "0.8.4", "version": "0.8.4",
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.4.tgz", "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.4.tgz",
...@@ -4356,7 +4361,7 @@ ...@@ -4356,7 +4361,7 @@
}, },
"http-errors": { "http-errors": {
"version": "1.6.3", "version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"dev": true, "dev": true,
"requires": { "requires": {
...@@ -4385,7 +4390,7 @@ ...@@ -4385,7 +4390,7 @@
}, },
"http-proxy-middleware": { "http-proxy-middleware": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", "resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
"integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==",
"dev": true, "dev": true,
"requires": { "requires": {
......
...@@ -27,7 +27,7 @@ const CONSTANTS = { ...@@ -27,7 +27,7 @@ const CONSTANTS = {
const DEFAULT_STYLES = [ const DEFAULT_STYLES = [
{ {
selector: 'node[hasChildren]', selector: 'node.gme-node[hasChildren]',
style: { style: {
content: 'data(label)', content: 'data(label)',
// http://js.cytoscape.org/#style/background-image // http://js.cytoscape.org/#style/background-image
...@@ -36,7 +36,7 @@ const DEFAULT_STYLES = [ ...@@ -36,7 +36,7 @@ const DEFAULT_STYLES = [
}, },
}, },
{ {
selector: 'node[^hasChildren]', selector: 'node.gme-node[^hasChildren]',
style: { style: {
content: 'data(label)', content: 'data(label)',
// http://js.cytoscape.org/#style/background-image // http://js.cytoscape.org/#style/background-image
...@@ -44,20 +44,31 @@ const DEFAULT_STYLES = [ ...@@ -44,20 +44,31 @@ const DEFAULT_STYLES = [
'background-height': '80%', 'background-height': '80%',
}, },
}, },
{
selector: 'node.aux-node',
css: {
width: 6,
height: 6,
},
},
{ {
selector: 'edge.pointer', selector: 'edge.pointer',
style: { style: {
content: 'data(label)', content: 'data(label)',
'line-color': 'rgb(0,0,255)', 'line-color': 'rgb(0,0,255)',
'curve-style': 'bezier',
'target-arrow-color': 'rgb(0,0,255)', 'target-arrow-color': 'rgb(0,0,255)',
'target-arrow-shape': 'open', 'target-arrow-shape': 'vee',
}, },
}, },
{ {
selector: 'edge.set-member', selector: 'edge.set-member',
style: { style: {
content: 'data(label)', content: 'data(label)',
'curve-style': 'bezier',
'line-color': 'rgb(255,0,255)', 'line-color': 'rgb(255,0,255)',
'target-arrow-color': 'rgb(255,0,255)',
'target-arrow-shape': 'vee',
}, },
}, },
{ {
...@@ -65,6 +76,7 @@ const DEFAULT_STYLES = [ ...@@ -65,6 +76,7 @@ const DEFAULT_STYLES = [
style: { style: {
width: 1, width: 1,
'line-color': 'rgb(255,0,0)', 'line-color': 'rgb(255,0,0)',
'curve-style': 'bezier',
'target-arrow-fill': 'hollow', 'target-arrow-fill': 'hollow',
'target-arrow-color': 'rgb(255,0,0)', 'target-arrow-color': 'rgb(255,0,0)',
'target-arrow-shape': 'triangle', 'target-arrow-shape': 'triangle',
...@@ -245,8 +257,13 @@ export default class GraphEditor extends Component { ...@@ -245,8 +257,13 @@ export default class GraphEditor extends Component {
edges: [], edges: [],
}, },
style: [], style: [],
hyperEdges: [],
}; };
// This will be put in either elements.edges (regular ones) or in hyperEdges.
const edges = [];
const hyperTargets = {};
const nodeMap = {}; const nodeMap = {};
const nodeIdsWithChildren = {}; const nodeIdsWithChildren = {};
...@@ -263,6 +280,28 @@ export default class GraphEditor extends Component { ...@@ -263,6 +280,28 @@ export default class GraphEditor extends Component {
|| parentNode.registries.position)); || parentNode.registries.position));
}; };
const isRenderedAsGmeConnection = (id) => {
return nodes[id] &&
typeof nodes[id].pointers.src === 'string' &&
typeof nodes[id].pointers.dst === 'string' &&
targetsExist(nodes[id].pointers.src, nodes[id].pointers.dst);
}
const targetsExist = (src, dst) => {
return nodes[src] && nodes[dst] && src !== activeNode && dst !== activeNode;
};
const hasEdgeTargets = (src, dst) => {
function isEdge(target) {
return nodes[target].pointers &&
typeof nodes[target].pointers.src === 'string' &&
typeof nodes[target].pointers.dst === 'string' &&
targetsExist(nodes[target].pointers.src, nodes[target].pointers.dst);
}
return isEdge(src) || isEdge(dst);
};
Object.keys(nodes) Object.keys(nodes)
.forEach((id) => { .forEach((id) => {
if (id === activeNode) { if (id === activeNode) {
...@@ -270,21 +309,30 @@ export default class GraphEditor extends Component { ...@@ -270,21 +309,30 @@ export default class GraphEditor extends Component {
} }
const childData = nodes[id]; const childData = nodes[id];
const isGmeConnection = isRenderedAsGmeConnection(id);
if (activeFilters[`nodes$${childData.metaType}`]) { if (activeFilters[`nodes$${childData.metaType}`]) {
return; return;
} }
if (typeof childData.pointers.src === 'string' && typeof childData.pointers.dst === 'string') { if (isGmeConnection) {
result.elements.edges.push({ const edgeData = {
data: { data: {
id, id,
label: childData.attributes.name,
source: childData.pointers.src, source: childData.pointers.src,
target: childData.pointers.dst, target: childData.pointers.dst,
label: childData.attributes.name,
}, },
classes: `${activeSelection.includes(id) ? 'in-active-selection ' : ''}gme-connection`, classes: `${activeSelection.includes(id) ? 'in-active-selection ' : ''}gme-connection`,
}); };
if (hasEdgeTargets(childData.pointers.src, childData.pointers.dst)) {
result.hyperEdges.push(edgeData);
hyperTargets[childData.pointers.src] = true;
hyperTargets[childData.pointers.dst] = true;
} else {
edges.push(edgeData);
}
} else { } else {
const cytoData = { const cytoData = {
data: { data: {
...@@ -298,13 +346,9 @@ export default class GraphEditor extends Component { ...@@ -298,13 +346,9 @@ export default class GraphEditor extends Component {
}, },
position: getPosition(id), position: getPosition(id),
grabbable: !readOnly, grabbable: !readOnly,
classes: `${activeSelection.includes(id) ? 'in-active-selection' : ''}`, classes: `gme-node${activeSelection.includes(id) ? ' in-active-selection' : ''}`,
}; };
if (createPointer && createPointer.target === id) {
cytoData.classes += ' valid-pointer-target';
}
result.elements.nodes.push(cytoData); result.elements.nodes.push(cytoData);
// Keep track of the containers s.t. they cannot be grabbed. // Keep track of the containers s.t. they cannot be grabbed.
...@@ -313,65 +357,87 @@ export default class GraphEditor extends Component { ...@@ -313,65 +357,87 @@ export default class GraphEditor extends Component {
nodeIdsWithChildren[childData.parent] = true; nodeIdsWithChildren[childData.parent] = true;
} }
Object.keys(childData.sets) // Use the images defined for the node.
.forEach((setName) => { if (childData.registries.SVGIcon && childData.registries.SVGIcon.indexOf('<') === -1) {
if (!childData.sets[setName] || activeFilters[`sets$${setName}`]) { result.style.push({
return; selector: `node[id = "${id}"]`,
} style: {
'background-image': `url(/assets/DecoratorSVG/${childData.registries.SVGIcon})`,
childData.sets[setName].forEach((setMemberData) => { },
const edgeId = `${id}$${setName}$${setMemberData.id}`;
const edgeData = {
data: {
id: edgeId,
source: id,
target: setMemberData.id,
label: setName,
memberAttrs: setMemberData.memberAttrs,
},
classes: `set-member ${activeSelection.includes(edgeId)
? 'in-active-selection' : ''}`,
};
if (setMemberData.label !== null) {
edgeData.data.label = setMemberData.label;
}
result.elements.edges.push(edgeData);
});
}); });
}
}
Object.keys(childData.pointers) if (createPointer && createPointer.target === id) {
.forEach((pName) => { cytoData.classes += ' valid-pointer-target';
if (!childData.pointers[pName] || activeFilters[`pointers$${pName}`]) { }
Object.keys(childData.sets)
.forEach((setName) => {
if (!childData.sets[setName] || activeFilters[`sets$${setName}`]) {
return;
}
childData.sets[setName].forEach((setMemberData) => {
if (!targetsExist(id, setMemberData.id)) {
return; return;
} }
const edgeId = `${id}$${pName}$${childData.pointers[pName]}`; const edgeId = `${id}$${setName}$${setMemberData.id}`;
const edgeData = { const edgeData = {
data: { data: {
id: edgeId, id: edgeId,
source: id, source: id,
target: childData.pointers[pName], target: setMemberData.id,
label: pName, label: setName,
memberAttrs: setMemberData.memberAttrs,
}, },
classes: `${pName === 'base' ? 'base-pointer' : 'pointer'}\ classes: `set-member ${activeSelection.includes(edgeId)
${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`, ? 'in-active-selection' : ''}`,
}; };
result.elements.edges.push(edgeData); if (setMemberData.label !== null) {
edgeData.data.label = setMemberData.label;
}
if (hasEdgeTargets(id, setMemberData.id)) {
result.hyperEdges.push(edgeData);
hyperTargets[id] = true;
hyperTargets[setMemberData.id] = true;
} else {
edges.push(edgeData);
}
}); });
});
// Use the images defined for the node. Object.keys(childData.pointers)
if (childData.registries.SVGIcon && childData.registries.SVGIcon.indexOf('<') === -1) { .forEach((pName) => {
result.style.push({ const targetId = childData.pointers[pName];
selector: `node[id = "${id}"]`, if (!targetId || activeFilters[`pointers$${pName}`] || !targetsExist(id, targetId) ||
style: { (isGmeConnection && (pName === 'src' || pName === 'dst'))) {
'background-image': `url(/assets/DecoratorSVG/${childData.registries.SVGIcon})`, return;
}
const edgeId = `${id}$${pName}$${targetId}`;
const edgeData = {
data: {
id: edgeId,
source: id,
target: targetId,
label: pName,
}, },
}); classes: `${pName === 'base' ? 'base-pointer' : 'pointer'}\
} ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
} };
if (hasEdgeTargets(id, targetId)) {
result.hyperEdges.push(edgeData);
hyperTargets[id] = true;
hyperTargets[targetId] = true;
} else {
edges.push(edgeData);
}
});
}); });
Object.keys(nodeIdsWithChildren) Object.keys(nodeIdsWithChildren)
...@@ -379,6 +445,15 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`, ...@@ -379,6 +445,15 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
nodeMap[id].data.hasChildren = true; nodeMap[id].data.hasChildren = true;
}); });
// Resolve edges that need to be hyperEdges
edges.forEach((edgeData) => {
if (hyperTargets[edgeData.data.id]) {
result.hyperEdges.push(edgeData);
} else {
result.elements.edges.push(edgeData);
}
})
return result; return result;
} }
...@@ -450,7 +525,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`, ...@@ -450,7 +525,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
this.cy.on('free', ({target}) => { this.cy.on('free', ({target}) => {
const {setActiveSelection} = this.props; const {setActiveSelection} = this.props;
if (typeof target.id === 'function') { if (typeof target.id === 'function' && target.hasClass('aux-node') === false) {
// console.log('free', cyNode.id(), JSON.stringify(this.reposition)); // console.log('free', cyNode.id(), JSON.stringify(this.reposition));
this.storePosition(target.id()); this.storePosition(target.id());
setTimeout(() => { setTimeout(() => {
...@@ -471,7 +546,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`, ...@@ -471,7 +546,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
if (createPointer && createPointer.target === e.target.id()) { if (createPointer && createPointer.target === e.target.id()) {
this.setState({createPointer: null}); this.setState({createPointer: null});
gmeClient.setPointer(createPointer.nodeId, createPointer.ptrName, createPointer.target); gmeClient.setPointer(createPointer.nodeId, createPointer.ptrName, createPointer.target);
} else { } else if (e.target.hasClass('aux-node') === false) {
// console.log('vclick', e.target.id(), JSON.stringify(this.reposition)); // console.log('vclick', e.target.id(), JSON.stringify(this.reposition));
this.setState({ this.setState({
showNodeMenu: { showNodeMenu: {
...@@ -484,11 +559,11 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`, ...@@ -484,11 +559,11 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
}, },
createPointer: null, createPointer: null,
}); });
}
setTimeout(() => { setTimeout(() => {
setActiveSelection([e.target.id()]); setActiveSelection([e.target.id()]);
}); });
}
} else { } else {
setActiveSelection([]); setActiveSelection([]);
} }
...@@ -608,6 +683,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`, ...@@ -608,6 +683,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
width={width} width={width}
height={height} height={height}
elements={cytoData.elements} elements={cytoData.elements}
hyperEdges={cytoData.hyperEdges}
cyRef={(cy) => { cyRef={(cy) => {
if (!this.cy) { if (!this.cy) {
this.cy = cy; this.cy = cy;
...@@ -629,7 +705,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`, ...@@ -629,7 +705,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
onClose={() => { onClose={() => {
this.setState({showNodeMenu: null}); this.setState({showNodeMenu: null});
}} }}
createPointer = {this.startNewPointer} createPointer={this.startNewPointer}
setActiveNode={setActiveNode} setActiveNode={setActiveNode}
/>) : null} />) : null}
......
...@@ -8,19 +8,20 @@ import React, {Component} from 'react'; ...@@ -8,19 +8,20 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import cytoscape from 'cytoscape'; import cytoscape from 'cytoscape';
import edgeConnections from 'cytoscape-edge-connections';
import coseBilkent from 'cytoscape-cose-bilkent'; import coseBilkent from 'cytoscape-cose-bilkent';
import dagre from 'cytoscape-dagre'; import dagre from 'cytoscape-dagre';
cytoscape.use(edgeConnections);
cytoscape.use(coseBilkent); cytoscape.use(coseBilkent);
// cytoscape.use(cycola);
cytoscape.use(dagre); cytoscape.use(dagre);
export default class ReactCytoscape extends Component { export default class ReactCytoscape extends Component {
static propTypes = { static propTypes = {
cyRef: PropTypes.func.isRequired, cyRef: PropTypes.func.isRequired,
elements: PropTypes.object.isRequired, elements: PropTypes.object.isRequired,
hyperEdges: PropTypes.arrayOf(PropTypes.object).isRequired,
width: PropTypes.number.isRequired, width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired, height: PropTypes.number.isRequired,
containerID: PropTypes.string, containerID: PropTypes.string,
...@@ -34,37 +35,44 @@ export default class ReactCytoscape extends Component { ...@@ -34,37 +35,44 @@ export default class ReactCytoscape extends Component {
layout: {name: 'cola'}, layout: {name: 'cola'},
cytoscapeOptions: {}, cytoscapeOptions: {},
style: [ style: [
{ // {
selector: 'node', // selector: 'node.gme-node',
css: { // css: {
content: function elemRender(ele) { // content: function elemRender(ele) {
return ele.data('label') || ele.data('id'); // return ele.data('label') || ele.data('id');
}, // },
'text-valign': 'center', // 'text-valign': 'center',
'text-halign': 'center', // 'text-halign': 'center',
}, // },
}, // },
{ // {