diff --git a/js/apps/system/_admin/aardvark/APP/GruntFile.js b/js/apps/system/_admin/aardvark/APP/GruntFile.js index bddd6d6638..1183c8f079 100644 --- a/js/apps/system/_admin/aardvark/APP/GruntFile.js +++ b/js/apps/system/_admin/aardvark/APP/GruntFile.js @@ -61,8 +61,10 @@ "frontend/js/lib/sigma.min.js", "frontend/js/lib/sigma.plugins.animate.js", "frontend/js/lib/sigma.plugins.dragNodes.js", - "frontend/js/lib/sigma.layout.noverlap.js", "frontend/js/lib/sigma.plugins.fullScreen.js", + "frontend/js/lib/sigma.plugins.filter.js", + "frontend/js/lib/sigma.plugins.lasso.js", + "frontend/js/lib/sigma.layout.noverlap.js", "frontend/js/lib/sigma.layout.fruchtermanReingold.js", "frontend/js/lib/sigma.exporters.svg.js", "frontend/js/lib/sigma.canvas.edges.labels.curve.js", diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/collections/arangoDocument.js b/js/apps/system/_admin/aardvark/APP/frontend/js/collections/arangoDocument.js index 3828c0e6b8..2acf877c8b 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/collections/arangoDocument.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/collections/arangoDocument.js @@ -92,7 +92,7 @@ window.ArangoDocument = Backbone.Collection.extend({ } }, error: function (data) { - callback(true, data._id); + callback(true, data._id, data.responseJSON); } }); }, diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.plugins.filter.js b/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.plugins.filter.js new file mode 100644 index 0000000000..c3f4c4ee4a --- /dev/null +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.plugins.filter.js @@ -0,0 +1,504 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize package: + sigma.utils.pkg('sigma.plugins'); + + // Add custom graph methods: + /** + * This methods returns an array of nodes that are adjacent to a node. + * + * @param {string} id The node id. + * @return {array} The array of adjacent nodes. + */ + if (!sigma.classes.graph.hasMethod('adjacentNodes')) + sigma.classes.graph.addMethod('adjacentNodes', function(id) { + if (typeof id !== 'string') + throw 'adjacentNodes: the node id must be a string.'; + + var target, + nodes = []; + for(target in this.allNeighborsIndex[id]) { + nodes.push(this.nodesIndex[target]); + } + return nodes; + }); + + /** + * This methods returns an array of edges that are adjacent to a node. + * + * @param {string} id The node id. + * @return {array} The array of adjacent edges. + */ + if (!sigma.classes.graph.hasMethod('adjacentEdges')) + sigma.classes.graph.addMethod('adjacentEdges', function(id) { + if (typeof id !== 'string') + throw 'adjacentEdges: the node id must be a string.'; + + var a = this.allNeighborsIndex[id], + eid, + target, + edges = []; + for(target in a) { + for(eid in a[target]) { + edges.push(a[target][eid]); + } + } + return edges; + }); + + /** + * Sigma Filter + * ============================= + * + * @author Sébastien Heymann (Linkurious) + * @version 0.1 + */ + + var _g = undefined, + _s = undefined, + _chain = [], // chain of wrapped filters + _keysIndex = Object.create(null), + Processors = {}; // available predicate processors + + + /** + * Library of processors + * ------------------ + */ + + /** + * + * @param {function} fn The predicate. + */ + Processors.nodes = function nodes(fn) { + var n = _g.nodes(), + ln = n.length, + e = _g.edges(), + le = e.length; + + // hide node, or keep former value + while(ln--) + n[ln].hidden = !fn.call(_g, n[ln]) || n[ln].hidden; + + while(le--) + if (_g.nodes(e[le].source).hidden || _g.nodes(e[le].target).hidden) + e[le].hidden = true; + }; + + /** + * + * @param {function} fn The predicate. + */ + Processors.edges = function edges(fn) { + var e = _g.edges(), + le = e.length; + + // hide edge, or keep former value + while(le--) + e[le].hidden = !fn.call(_g, e[le]) || e[le].hidden; + }; + + /** + * + * @param {string} id The center node. + */ + Processors.neighbors = function neighbors(id) { + var n = _g.nodes(), + ln = n.length, + e = _g.edges(), + le = e.length, + neighbors = _g.adjacentNodes(id), + nn = neighbors.length, + no = {}; + + while(nn--) + no[neighbors[nn].id] = true; + + while(ln--) + if (n[ln].id !== id && !(n[ln].id in no)) + n[ln].hidden = true; + + while(le--) + if (_g.nodes(e[le].source).hidden || _g.nodes(e[le].target).hidden) + e[le].hidden = true; + }; + + + /** + * This function adds a filter to the chain of filters. + * + * @param {function} fn The filter (i.e. predicate processor). + * @param {function} p The predicate. + * @param {?string} key The key to identify the filter. + */ + function register(fn, p, key) { + if (key != undefined && typeof key !== 'string') + throw 'The filter key "'+ key.toString() +'" must be a string.'; + + if (key != undefined && !key.length) + throw 'The filter key must be a non-empty string.'; + + if (typeof fn !== 'function') + throw 'The predicate of key "'+ key +'" must be a function.'; + + if ('undo' === key) + throw '"undo" is a reserved key.'; + + if (_keysIndex[key]) + throw 'The filter "' + key + '" already exists.'; + + if (key) + _keysIndex[key] = true; + + _chain.push({ + 'key': key, + 'processor': fn, + 'predicate': p + }); + }; + + /** + * This function removes a set of filters from the chain. + * + * @param {object} o The filter keys. + */ + function unregister (o) { + _chain = _chain.filter(function(a) { + return !(a.key in o); + }); + + for(var key in o) + delete _keysIndex[key]; + }; + + + + + /** + * Filter Object + * ------------------ + * @param {sigma} s The related sigma instance. + */ + function Filter(s) { + _s = s; + _g = s.graph; + }; + + + /** + * This method is used to filter the nodes. The method must be called with + * the predicate, which is a function that takes a node as argument and + * returns a boolean. It may take an identifier as argument to undo the + * filter later. The method wraps the predicate into an anonymous function + * that looks through each node in the graph. When executed, the anonymous + * function hides the nodes that fail a truth test (predicate). The method + * adds the anonymous function to the chain of filters. The filter is not + * executed until the apply() method is called. + * + * > var filter = new sigma.plugins.filter(s); + * > filter.nodesBy(function(n) { + * > return this.degree(n.id) > 0; + * > }, 'degreeNotNull'); + * + * @param {function} fn The filter predicate. + * @param {?string} key The key to identify the filter. + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.nodesBy = function(fn, key) { + // Wrap the predicate to be applied on the graph and add it to the chain. + register(Processors.nodes, fn, key); + + return this; + }; + + /** + * This method is used to filter the edges. The method must be called with + * the predicate, which is a function that takes a node as argument and + * returns a boolean. It may take an identifier as argument to undo the + * filter later. The method wraps the predicate into an anonymous function + * that looks through each edge in the graph. When executed, the anonymous + * function hides the edges that fail a truth test (predicate). The method + * adds the anonymous function to the chain of filters. The filter is not + * executed until the apply() method is called. + * + * > var filter = new sigma.plugins.filter(s); + * > filter.edgesBy(function(e) { + * > return e.size > 1; + * > }, 'edgeSize'); + * + * @param {function} fn The filter predicate. + * @param {?string} key The key to identify the filter. + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.edgesBy = function(fn, key) { + // Wrap the predicate to be applied on the graph and add it to the chain. + register(Processors.edges, fn, key); + + return this; + }; + + /** + * This method is used to filter the nodes which are not direct connections + * of a given node. The method must be called with the node identifier. It + * may take an identifier as argument to undo the filter later. The filter + * is not executed until the apply() method is called. + * + * > var filter = new sigma.plugins.filter(s); + * > filter.neighborsOf('n0'); + * + * @param {string} id The node id. + * @param {?string} key The key to identify the filter. + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.neighborsOf = function(id, key) { + if (typeof id !== 'string') + throw 'The node id "'+ id.toString() +'" must be a string.'; + if (!id.length) + throw 'The node id must be a non-empty string.'; + + // Wrap the predicate to be applied on the graph and add it to the chain. + register(Processors.neighbors, id, key); + + return this; + }; + + /** + * This method is used to execute the chain of filters and to refresh the + * display. + * + * > var filter = new sigma.plugins.filter(s); + * > filter + * > .nodesBy(function(n) { + * > return this.degree(n.id) > 0; + * > }, 'degreeNotNull') + * > .apply(); + * + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.apply = function() { + for (var i = 0, len = _chain.length; i < len; ++i) { + _chain[i].processor(_chain[i].predicate); + }; + + if (_chain[0] && 'undo' === _chain[0].key) { + _chain.shift(); + } + + _s.refresh(); + + return this; + }; + + /** + * This method undoes one or several filters, depending on how it is called. + * + * To undo all filters, call "undo" without argument. To undo a specific + * filter, call it with the key of the filter. To undo multiple filters, call + * it with an array of keys or multiple arguments, and it will undo each + * filter, in the same order. The undo is not executed until the apply() + * method is called. For instance: + * + * > var filter = new sigma.plugins.filter(s); + * > filter + * > .nodesBy(function(n) { + * > return this.degree(n.id) > 0; + * > }, 'degreeNotNull'); + * > .edgesBy(function(e) { + * > return e.size > 1; + * > }, 'edgeSize') + * > .undo(); + * + * Other examples: + * > filter.undo(); + * > filter.undo('myfilter'); + * > filter.undo(['myfilter1', 'myfilter2']); + * > filter.undo('myfilter1', 'myfilter2'); + * + * @param {?(string|array|*string))} v Eventually one key, an array of keys. + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.undo = function(v) { + var q = Object.create(null), + la = arguments.length; + + // find removable filters + if (la === 1) { + if (Object.prototype.toString.call(v) === '[object Array]') + for (var i = 0, len = v.length; i < len; i++) + q[v[i]] = true; + + else // 1 filter key + q[v] = true; + + } else if (la > 1) { + for (var i = 0; i < la; i++) + q[arguments[i]] = true; + } + else + this.clear(); + + unregister(q); + + function processor() { + var n = _g.nodes(), + ln = n.length, + e = _g.edges(), + le = e.length; + + while(ln--) + n[ln].hidden = false; + + while(le--) + e[le].hidden = false; + }; + + _chain.unshift({ + 'key': 'undo', + 'processor': processor + }); + + return this; + }; + + // fast deep copy function + function deepCopy(o) { + var copy = Object.create(null); + for (var i in o) { + if (typeof o[i] === "object" && o[i] !== null) { + copy[i] = deepCopy(o[i]); + } + else if (typeof o[i] === "function" && o[i] !== null) { + // clone function: + eval(" copy[i] = " + o[i].toString()); + //copy[i] = o[i].bind(_g); + } + + else + copy[i] = o[i]; + } + return copy; + }; + + function cloneChain(chain) { + // Clone the array of filters: + var copy = chain.slice(0); + for (var i = 0, len = copy.length; i < len; i++) { + copy[i] = deepCopy(copy[i]); + if (typeof copy[i].processor === "function") + copy[i].processor = 'filter.processors.' + copy[i].processor.name; + }; + return copy; + } + + /** + * This method is used to empty the chain of filters. + * Prefer the undo() method to reset filters. + * + * > var filter = new sigma.plugins.filter(s); + * > filter.clear(); + * + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.clear = function() { + _chain.length = 0; // clear the array + _keysIndex = Object.create(null); + return this; + }; + + /** + * This method clones the filter chain and return the copy. + * + * > var filter = new sigma.plugins.filter(s); + * > var chain = filter.export(); + * + * @return {object} The cloned chain of filters. + */ + Filter.prototype.export = function() { + var c = cloneChain(_chain); + return c; + }; + + /** + * This method sets the chain of filters with the specified chain. + * + * > var filter = new sigma.plugins.filter(s); + * > var chain = [ + * > { + * > key: 'my-filter', + * > predicate: function(n) {...}, + * > processor: 'filter.processors.nodes' + * > }, ... + * > ]; + * > filter.import(chain); + * + * @param {array} chain The chain of filters. + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.import = function(chain) { + if (chain === undefined) + throw 'Wrong arguments.'; + + if (Object.prototype.toString.call(chain) !== '[object Array]') + throw 'The chain" must be an array.'; + + var copy = cloneChain(chain); + + for (var i = 0, len = copy.length; i < len; i++) { + if (copy[i].predicate === undefined || copy[i].processor === undefined) + throw 'Wrong arguments.'; + + if (copy[i].key != undefined && typeof copy[i].key !== 'string') + throw 'The filter key "'+ copy[i].key.toString() +'" must be a string.'; + + if (typeof copy[i].predicate !== 'function') + throw 'The predicate of key "'+ copy[i].key +'" must be a function.'; + + if (typeof copy[i].processor !== 'string') + throw 'The processor of key "'+ copy[i].key +'" must be a string.'; + + // Replace the processor name by the corresponding function: + switch(copy[i].processor) { + case 'filter.processors.nodes': + copy[i].processor = Processors.nodes; + break; + case 'filter.processors.edges': + copy[i].processor = Processors.edges; + break; + case 'filter.processors.neighbors': + copy[i].processor = Processors.neighbors; + break; + default: + throw 'Unknown processor ' + copy[i].processor; + } + }; + + _chain = copy; + + return this; + }; + + + /** + * Interface + * ------------------ + * + * > var filter = new sigma.plugins.filter(s); + */ + var filter = null; + + /** + * @param {sigma} s The related sigma instance. + */ + sigma.plugins.filter = function(s) { + // Create filter if undefined + if (!filter) { + filter = new Filter(s); + } + return filter; + }; + +}).call(this); diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.plugins.lasso.js b/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.plugins.lasso.js new file mode 100644 index 0000000000..78ed386875 --- /dev/null +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.plugins.lasso.js @@ -0,0 +1,337 @@ +/** + * Sigma Lasso + * ============================= + * + * @author Florent Schildknecht (Florent Schildknecht) + * @version 0.0.2 + */ +;(function (undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize package: + sigma.utils.pkg('sigma.plugins'); + + var _body = undefined, + _instances = {}; + + /** + * Lasso Object + * ------------------ + * @param {sigma} sigmaInstance The related sigma instance. + * @param {renderer} renderer The sigma instance renderer. + * @param {sigma.classes.configurable} settings A settings class + */ + function Lasso (sigmaInstance, renderer, settings) { + // Lasso is also an event dispatcher + sigma.classes.dispatcher.extend(this); + + // A quick hardcoded rule to prevent people from using this plugin with the + // WebGL renderer (which is impossible at the moment): + if ( + sigma.renderers.webgl && + renderer instanceof sigma.renderers.webgl + ) + throw new Error( + 'The sigma.plugins.lasso is not compatible with the WebGL renderer' + ); + + this.sigmaInstance = sigmaInstance; + this.renderer = renderer; + this.drawingCanvas = undefined; + this.drawingContext = undefined; + this.drewPoints = []; + this.selectedNodes = []; + this.isActive = false; + this.isDrawing = false; + + _body = document.body; + + // Extends default settings + this.settings = new sigma.classes.configurable({ + 'strokeStyle': 'black', + 'lineWidth': 2, + 'fillWhileDrawing': false, + 'fillStyle': 'rgba(200, 200, 200, 0.25)', + 'cursor': 'crosshair' + }, settings || {}); + }; + + /** + * This method is used to destroy the lasso. + * + * > var lasso = new sigma.plugins.lasso(sigmaInstance); + * > lasso.clear(); + * + * @return {sigma.plugins.lasso} Returns the instance. + */ + Lasso.prototype.clear = function () { + this.deactivate(); + + this.sigmaInstance = undefined; + this.renderer = undefined; + + return this; + }; + + // Lasso.prototype.getSigmaInstance = function () { + // return this.sigmaInstance; + // } + + /** + * This method is used to activate the lasso mode. + * + * > var lasso = new sigma.plugins.lasso(sigmaInstance); + * > lasso.activate(); + * + * @return {sigma.plugins.lasso} Returns the instance. + */ + Lasso.prototype.activate = function () { + if (this.sigmaInstance && !this.isActive) { + this.isActive = true; + + // Add a new background layout canvas to draw the path on + if (!this.renderer.domElements['lasso']) { + this.renderer.initDOM('canvas', 'lasso'); + this.drawingCanvas = this.renderer.domElements['lasso']; + + this.drawingCanvas.width = this.renderer.container.offsetWidth; + this.drawingCanvas.height = this.renderer.container.offsetHeight; + this.renderer.container.appendChild(this.drawingCanvas); + this.drawingContext = this.drawingCanvas.getContext('2d'); + this.drawingCanvas.style.cursor = this.settings('cursor'); + } + + _bindAll.apply(this); + } + + return this; + }; + + /** + * This method is used to deactivate the lasso mode. + * + * > var lasso = new sigma.plugins.lasso(sigmaInstance); + * > lasso.deactivate(); + * + * @return {sigma.plugins.lasso} Returns the instance. + */ + Lasso.prototype.deactivate = function () { + if (this.sigmaInstance && this.isActive) { + this.isActive = false; + this.isDrawing = false; + + _unbindAll.apply(this); + + if (this.renderer.domElements['lasso']) { + this.renderer.container.removeChild(this.renderer.domElements['lasso']); + delete this.renderer.domElements['lasso']; + this.drawingCanvas.style.cursor = ''; + this.drawingCanvas = undefined; + this.drawingContext = undefined; + this.drewPoints = []; + } + } + + return this; + }; + + /** + * This method is used to bind all events of the lasso mode. + * + * > var lasso = new sigma.plugins.lasso(sigmaInstance); + * > lasso.activate(); + * + * @return {sigma.plugins.lasso} Returns the instance. + */ + var _bindAll = function () { + // Mouse events + this.drawingCanvas.addEventListener('mousedown', onDrawingStart.bind(this)); + _body.addEventListener('mousemove', onDrawing.bind(this)); + _body.addEventListener('mouseup', onDrawingEnd.bind(this)); + // Touch events + this.drawingCanvas.addEventListener('touchstart', onDrawingStart.bind(this)); + _body.addEventListener('touchmove', onDrawing.bind(this)); + _body.addEventListener('touchcancel', onDrawingEnd.bind(this)); + _body.addEventListener('touchleave', onDrawingEnd.bind(this)); + _body.addEventListener('touchend', onDrawingEnd.bind(this)); + }; + + /** + * This method is used to unbind all events of the lasso mode. + * + * > var lasso = new sigma.plugins.lasso(sigmaInstance); + * > lasso.activate(); + * + * @return {sigma.plugins.lasso} Returns the instance. + */ + var _unbindAll = function () { + // Mouse events + this.drawingCanvas.removeEventListener('mousedown', onDrawingStart.bind(this)); + _body.removeEventListener('mousemove', onDrawing.bind(this)); + _body.removeEventListener('mouseup', onDrawingEnd.bind(this)); + // Touch events + this.drawingCanvas.removeEventListener('touchstart', onDrawingStart.bind(this)); + this.drawingCanvas.removeEventListener('touchmove', onDrawing.bind(this)); + _body.removeEventListener('touchcancel', onDrawingEnd.bind(this)); + _body.removeEventListener('touchleave', onDrawingEnd.bind(this)); + _body.removeEventListener('touchend', onDrawingEnd.bind(this)); + }; + + /** + * This method is used to retrieve the previously selected nodes + * + * > var lasso = new sigma.plugins.lasso(sigmaInstance); + * > lasso.getSelectedNodes(); + * + * @return {array} Returns an array of nodes. + */ + Lasso.prototype.getSelectedNodes = function () { + return this.selectedNodes; + }; + + function onDrawingStart (event) { + var drawingRectangle = this.drawingCanvas.getBoundingClientRect(); + + if (this.isActive) { + this.isDrawing = true; + this.drewPoints = []; + this.selectedNodes = []; + + this.sigmaInstance.refresh(); + + this.drewPoints.push({ + x: event.clientX - drawingRectangle.left, + y: event.clientY - drawingRectangle.top + }); + + this.drawingCanvas.style.cursor = this.settings('cursor'); + + event.stopPropagation(); + } + } + + function onDrawing (event) { + if (this.isActive && this.isDrawing) { + var x = 0, + y = 0, + drawingRectangle = this.drawingCanvas.getBoundingClientRect(); + switch (event.type) { + case 'touchmove': + x = event.touches[0].clientX; + y = event.touches[0].clientY; + break; + default: + x = event.clientX; + y = event.clientY; + break; + } + this.drewPoints.push({ + x: x - drawingRectangle.left, + y: y - drawingRectangle.top + }); + + // Drawing styles + this.drawingContext.lineWidth = this.settings('lineWidth'); + this.drawingContext.strokeStyle = this.settings('strokeStyle'); + this.drawingContext.fillStyle = this.settings('fillStyle'); + this.drawingContext.lineJoin = 'round'; + this.drawingContext.lineCap = 'round'; + + // Clear the canvas + this.drawingContext.clearRect(0, 0, this.drawingContext.canvas.width, this.drawingContext.canvas.height); + + // Redraw the complete path for a smoother effect + // Even smoother with quadratic curves + var sourcePoint = this.drewPoints[0], + destinationPoint = this.drewPoints[1], + pointsLength = this.drewPoints.length, + getMiddlePointCoordinates = function (firstPoint, secondPoint) { + return { + x: firstPoint.x + (secondPoint.x - firstPoint.x) / 2, + y: firstPoint.y + (secondPoint.y - firstPoint.y) / 2 + }; + }; + + this.drawingContext.beginPath(); + this.drawingContext.moveTo(sourcePoint.x, sourcePoint.y); + + for (var i = 1; i < pointsLength; i++) { + var middlePoint = getMiddlePointCoordinates(sourcePoint, destinationPoint); + // this.drawingContext.lineTo(this.drewPoints[i].x, this.drewPoints[i].y); + this.drawingContext.quadraticCurveTo(sourcePoint.x, sourcePoint.y, middlePoint.x, middlePoint.y); + sourcePoint = this.drewPoints[i]; + destinationPoint = this.drewPoints[i+1]; + } + + this.drawingContext.lineTo(sourcePoint.x, sourcePoint.y); + this.drawingContext.stroke(); + + if (this.settings('fillWhileDrawing')) { + this.drawingContext.fill(); + } + + event.stopPropagation(); + } + } + + function onDrawingEnd (event) { + if (this.isActive && this.isDrawing) { + this.isDrawing = false; + + // Select the nodes inside the path + var nodes = this.renderer.nodesOnScreen, + nodesLength = nodes.length, + i = 0, + prefix = this.renderer.options.prefix || ''; + + // Loop on all nodes and check if they are in the path + while (nodesLength--) { + var node = nodes[nodesLength], + x = node[prefix + 'x'], + y = node[prefix + 'y']; + + if (this.drawingContext.isPointInPath(x, y) && !node.hidden) { + this.selectedNodes.push(node); + } + } + + // Dispatch event with selected nodes + this.dispatchEvent('selectedNodes', this.selectedNodes); + + // Clear the drawing canvas + this.drawingContext.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height); + + this.drawingCanvas.style.cursor = this.settings('cursor'); + + event.stopPropagation(); + } + } + + /** + * @param {sigma} sigmaInstance The related sigma instance. + * @param {renderer} renderer The sigma instance renderer. + * @param {sigma.classes.configurable} settings A settings class + * + * @return {sigma.plugins.lasso} Returns the instance + */ + sigma.plugins.lasso = function (sigmaInstance, renderer, settings) { + // Create lasso if undefined + if (!_instances[sigmaInstance.id]) { + _instances[sigmaInstance.id] = new Lasso(sigmaInstance, renderer, settings); + } + + // Listen for sigmaInstance kill event, and remove the lasso isntance + sigmaInstance.bind('kill', function () { + if (_instances[sigmaInstance.id] instanceof Lasso) { + _instances[sigmaInstance.id].clear(); + delete _instances[sigmaInstance.id]; + } + }); + + return _instances[sigmaInstance.id]; + }; + +}).call(this); diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js b/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js index e72d20bba7..2d3ef56fca 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js @@ -53,6 +53,10 @@ if (callback) { callback.apply(this, args); } + + if (this.graphViewer2) { + this.graphViewer2.graphSettingsView.hide(); + } }, checkUser: function () { diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/templates/graphSettingsView.ejs b/js/apps/system/_admin/aardvark/APP/frontend/js/templates/graphSettingsView.ejs index 389fe5f9ac..9f039fac47 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/templates/graphSettingsView.ejs +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/templates/graphSettingsView.ejs @@ -11,23 +11,58 @@
+
+ +
+ + <% _.each(general, function(val, key) { %> + <% if (val.type === 'divider') { %> +
<%=val.name%>
+ <% } else { %> +
<%=val.name%>
+
+ + <% if (val.type === 'select') { %> + + <% } %> + + <% if (val.type === 'string') { %> + + <% } %> + + <% if (val.type === 'number') { %> + + <% } %> + + <% if (val.type === 'range') { %> + + <% } %> + + <% if (val.type === 'color') { %> + + <% } %> + +
+ <% } %> + + <% }); %> +
+ +
-
-
-
- Graph specific -
-
-
-
<% _.each(specific, function(val, key) { %> <% if (val.type === 'divider') { %> -
<%=val.name%>
-
+
<%=val.name%>
<% } else { %>
<%=val.name%>
@@ -53,6 +88,10 @@ <% } %> + <% if (val.type === 'range') { %> + + <% } %> + <% if (val.type === 'select') { %> - <% _.each(val, function(option, optKey) { %> - <% if (option.name) { %> - - <% } %> - <% }); %> - - <% } %> - - <% if (val.type === 'string') { %> - - <% } %> - - <% if (val.type === 'number') { %> - - <% } %> - <% } %> - - <% }); %> -
- -
- - +
diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphSettingsView.js b/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphSettingsView.js index a56780c071..018584025a 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphSettingsView.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphSettingsView.js @@ -67,7 +67,13 @@ 'nodeLabel': { type: 'string', name: 'Label', - desc: 'Default node color. RGB or HEX value.', + desc: 'Node label. Please choose a valid and available node attribute.', + default: '_key' + }, + 'nodeLabelThreshold': { + type: 'range', + name: 'Node label threshold', + desc: 'The minimum size a node must have on screen to see its label displayed. This does not affect hovering behavior.', default: '_key' }, 'nodeColor': { @@ -90,6 +96,12 @@ name: 'Label', desc: 'Default edge label.' }, + 'edgeLabelThreshold': { + type: 'range', + name: 'Edge label threshold', + desc: 'The minimum size an edge must have on screen to see its label displayed. This does not affect hovering behavior.', + default: '_key' + }, 'edgeColor': { type: 'color', name: 'Color', @@ -112,7 +124,8 @@ curve: { name: 'Curve', val: 'curve' - }, + } + /* arrow: { name: 'Arrow', val: 'arrow' @@ -121,6 +134,7 @@ name: 'Curved Arrow', val: 'curvedArrow' } + */ } }, @@ -137,13 +151,14 @@ 'click #restoreGraphSettings': 'restoreGraphSettings', 'keyup #graphSettingsView input': 'checkEnterKey', 'keyup #graphSettingsView select': 'checkEnterKey', + 'change input[type="range"]': 'saveGraphSettings', + 'change input[type="color"]': 'checkColor', + 'change select': 'saveGraphSettings', 'focus #graphSettingsView input': 'lastFocus', 'focus #graphSettingsView select': 'lastFocus' }, lastFocus: function (e) { - console.log(e.currentTarget.id); - console.log(e.currentTarget); this.lastFocussed = e.currentTarget.id; }, @@ -167,17 +182,25 @@ }); }, - saveGraphSettings: function () { + checkColor: function () { + this.saveGraphSettings(true); + }, + + saveGraphSettings: function (color, nodeStart) { var self = this; + console.log('CLICK'); var combinedName = window.App.currentDB.toJSON().name + '_' + this.name; var config = {}; + config[combinedName] = { layout: $('#g_layout').val(), renderer: $('#g_renderer').val(), depth: $('#g_depth').val(), nodeColor: $('#g_nodeColor').val(), + nodeLabelThreshold: $('#g_nodeLabelThreshold').val(), edgeColor: $('#g_edgeColor').val(), + edgeLabelThreshold: $('#g_edgeLabelThreshold').val(), nodeLabel: $('#g_nodeLabel').val(), edgeLabel: $('#g_edgeLabel').val(), edgeType: $('#g_edgeType').val(), @@ -186,9 +209,17 @@ nodeStart: $('#g_nodeStart').val() }; + if (nodeStart) { + config[combinedName].nodeStart = nodeStart; + } + var callback = function () { if (window.App.graphViewer2) { - window.App.graphViewer2.render(self.lastFocussed); + if (color) { + window.App.graphViewer2.updateColors(); + } else { + window.App.graphViewer2.render(self.lastFocussed); + } } else { arangoHelper.arangoNotification('Graph ' + this.name, 'Configuration saved.'); } diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphViewer2.js b/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphViewer2.js index 073eb19643..03b2d66aec 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphViewer2.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphViewer2.js @@ -34,7 +34,8 @@ events: { 'click #downloadPNG': 'downloadSVG', 'click #reloadGraph': 'reloadGraph', - 'click #settingsMenu': 'toggleSettings' + 'click #settingsMenu': 'toggleSettings', + 'click #noGraphToggle': 'toggleSettings' }, cursorX: 0, @@ -62,6 +63,18 @@ } return neighbors; }); + + sigma.classes.graph.addMethod('getNodeEdges', function (nodeId) { + var edges = this.edges(); + var edgesToReturn = []; + + _.each(edges, function (edge) { + if (edge.source === nodeId || edge.target === nodeId) { + edgesToReturn.push(edge.id); + } + }); + return edgesToReturn; + }); } catch (ignore) {} }, @@ -220,19 +233,96 @@ this.cursorY = e.y; }, + deleteNode: function () { + var self = this; + var documentKey = $('#delete-node-attr-id').text(); + var collectionId = documentKey.split('/')[0]; + var documentId = documentKey.split('/')[1]; + + if ($('#delete-node-edges-attr').val() === 'yes') { + $.ajax({ + cache: false, + type: 'DELETE', + contentType: 'application/json', + url: arangoHelper.databaseUrl( + '/_api/gharial/' + encodeURIComponent(self.name) + '/vertex/' + encodeURIComponent(documentKey.split('/')[0]) + '/' + encodeURIComponent(documentKey.split('/')[1]) + ), + success: function (data) { + self.currentGraph.graph.dropNode(documentKey); + self.currentGraph.refresh(); + }, + error: function () { + arangoHelper.arangoError('Graph', 'Could not delete node.'); + } + }); + } else { + var callback = function (error) { + if (!error) { + self.currentGraph.graph.dropNode(documentKey); + + // rerender graph + self.currentGraph.refresh(); + } else { + arangoHelper.arangoError('Graph', 'Could not delete node.'); + } + }; + + this.documentStore.deleteDocument(collectionId, documentId, callback); + } + window.modalView.hide(); + }, + + deleteNodeModal: function (nodeId) { + var buttons = []; var tableContent = []; + + tableContent.push( + window.modalView.createReadOnlyEntry('delete-node-attr-id', 'Really delete node', nodeId) + ); + + tableContent.push( + window.modalView.createSelectEntry( + 'delete-node-edges-attr', + 'Also delete edges?', + undefined, + undefined, + [ + { + value: 'yes', + label: 'Yes' + }, + { + value: 'no', + label: 'No' + } + ] + ) + ); + + buttons.push( + window.modalView.createDeleteButton('Delete', this.deleteNode.bind(this)) + ); + + window.modalView.show( + 'modalTable.ejs', + 'Delete node', + buttons, + tableContent + ); + }, + addNode: function () { var self = this; var collectionId = $('.modal-body #new-node-collection-attr').val(); var key = $('.modal-body #new-node-key-attr').last().val(); - var callback = function (error, id) { + var callback = function (error, id, msg) { if (error) { - arangoHelper.arangoError('Error', 'Could not create node'); + arangoHelper.arangoError('Could not create node', msg.errorMessage); } else { self.currentGraph.graph.addNode({ id: id, - label: self.graphConfig.nodeLabel, + label: self.graphConfig.nodeLabel || '', size: self.graphConfig.nodeSize || Math.random(), color: self.graphConfig.nodeColor || '#2ecc71', x: self.cursorX, @@ -412,6 +502,26 @@ } }, + updateColors: function () { + var combinedName = window.App.currentDB.toJSON().name + '_' + this.name; + var self = this; + + this.userConfig.fetch({ + success: function (data) { + self.graphConfig = data.toJSON().graphs[combinedName]; + self.currentGraph.graph.nodes().forEach(function (n) { + n.color = self.graphConfig.nodeColor; + }); + + self.currentGraph.graph.edges().forEach(function (e) { + e.color = self.graphConfig.edgeColor; + }); + + self.currentGraph.refresh(); + } + }); + }, + // right click background context menu createContextMenu: function (e) { var self = this; @@ -498,7 +608,7 @@ wheel.multiSelect = true; wheel.clickModeRotate = false; wheel.slicePathFunction = slicePath().DonutSlice; - wheel.createWheel([icon.edit, icon.trash, icon.arrowleft2, icon.connect]); + wheel.createWheel([icon.edit, icon.trash, icon.play, icon.connect]); wheel.navItems[0].selected = false; wheel.navItems[0].hovered = false; @@ -513,7 +623,7 @@ // function 1: delete wheel.navItems[1].navigateFunction = function (e) { self.clearOldContextMenu(); - self.deleteNode(nodeId); + self.deleteNodeModal(nodeId); }; // function 2: mark as start node @@ -599,6 +709,11 @@ }); }, + setStartNode: function (id) { + this.graphConfig.nodeStart = id; + this.graphSettingsView.saveGraphSettings(null, id); + }, + editNode: function (id) { var callback = function () {}; @@ -627,6 +742,44 @@ return array; }, + initializeGraph: function (sigmaInstance, graph) { + var self = this; + // sigmaInstance.graph.read(graph); + sigmaInstance.refresh(); + + this.Sigma.plugins.Lasso = sigma.plugins.lasso; + + var lasso = new this.Sigma.plugins.Lasso(sigmaInstance, sigmaInstance.renderers[0], { + 'strokeStyle': 'black', + 'lineWidth': 1, + 'fillWhileDrawing': true, + 'fillStyle': 'rgba(41, 41, 41, 0.2)', + 'cursor': 'crosshair' + }); + + // Listen for selectedNodes event + lasso.bind('selectedNodes', function (event) { + // Do something with the selected nodes + var nodes = event.data; + + console.log('nodes', nodes); + + // For instance, reset all node size as their initial size + sigmaInstance.graph.nodes().forEach(function (node) { + node.color = self.graphConfig.nodeColor; + }); + + // Then increase the size of selected nodes... + nodes.forEach(function (node) { + node.color = 'red'; + }); + + sigmaInstance.refresh(); + }); + + return lasso; + }, + renderGraph: function (graph, toFocus) { var self = this; @@ -634,16 +787,22 @@ if (graph.edges.length === 0) { var string = 'No edges found for starting point: ' + self.graphSettings.startVertex._id + ''; - arangoHelper.arangoError('Graph', string); $('#calculatingGraph').html( '
Stopped.

' + string + - '. Please choose a different start node or try to reload the graph. ' + + '. Please choose a different start node or try to reload the graph. ' + '
' ); return; + } else { + $('#content').append( + '
' + + '' + graph.nodes.length + ' nodes' + + '' + graph.edges.length + ' edges' + + '
' + ); } + this.Sigma = sigma; // defaults @@ -674,15 +833,15 @@ defaultEdgeHoverColor: '#000', defaultEdgeType: 'line', edgeHoverSizeRatio: 2, - edgeHoverExtremities: true + edgeHoverExtremities: true, + // lasso settings + autoRescale: true, + mouseEnabled: true, + touchEnabled: true, + nodesPowRatio: 1, + edgesPowRatio: 1 }; - if (this.graphConfig) { - if (this.graphConfig.edgeType) { - settings.defaultEdgeType = this.graphConfig.edgeType; - } - } - // adjust display settings for big graphs if (graph.nodes.length > 500) { // show node label if size is 15 @@ -690,6 +849,20 @@ settings.hideEdgesOnMove = true; } + if (this.graphConfig) { + if (this.graphConfig.edgeType) { + settings.defaultEdgeType = this.graphConfig.edgeType; + } + + if (this.graphConfig.nodeLabelThreshold) { + settings.labelThreshold = this.graphConfig.nodeLabelThreshold; + } + + if (this.graphConfig.edgeLabelThreshold) { + settings.edgeLabelThreshold = this.graphConfig.edgeLabelThreshold; + } + } + // adjust display settings for webgl renderer if (renderer === 'webgl') { settings.enableEdgeHovering = false; @@ -751,6 +924,54 @@ self.clearMouseCanvas(); }); + s.bind('overNode', function (e) { + $('.nodeInfoDiv').remove(); + if (self.contextState.createEdge === false) { + var callback = function (error, data) { + if (!error) { + var obj = {}; + var counter = 0; + var more = false; + + _.each(data, function (val, key) { + if (counter < 15) { + if (typeof val === 'string') { + if (val.length > 10) { + obj[key] = val.substr(0, 10) + ' ...'; + } else { + obj[key] = val; + } + } + } else { + more = true; + } + counter++; + }); + + var string = '
' + + '
' + JSON.stringify(obj, null, 2);
+
+                if (more) {
+                  string = string.substr(0, string.length - 2);
+                  string += ' \n\n  ... \n\n } 
'; + } else { + string += '
'; + } + + $('#content').append(string); + } + }; + + self.documentStore.getDocument(e.data.node.id.split('/')[0], e.data.node.id.split('/')[1], callback); + } + }); + + s.bind('outNode', function (e) { + if (self.contextState.createEdge === false) { + $('.nodeInfoDiv').remove(); + } + }); + s.bind('clickNode', function (e) { if (self.contextState.createEdge === true) { // create the edge @@ -831,6 +1052,7 @@ window.setTimeout(function () { s.stopForceAtlas2(); dragListener = sigma.plugins.dragNodes(s, s.renderers[0]); + console.log(dragListener); }, duration); } else if (algorithm === 'fruchtermann') { // Start the Fruchterman-Reingold algorithm: @@ -839,7 +1061,6 @@ } else { dragListener = sigma.plugins.dragNodes(s, s.renderers[0]); } - console.log(dragListener); // add listener to keep track of cursor position var c = document.getElementsByClassName('sigma-mouse')[0]; @@ -849,6 +1070,28 @@ if (toFocus) { $('#' + toFocus).focus(); } + + // init graph lasso + self.graphLasso = self.initializeGraph(s, graph); + self.graphLasso.activate(); + self.graphLasso.deactivate(); + + // add lasso event + // Toggle lasso activation on Alt + l + document.addEventListener('keyup', function (event) { + switch (event.keyCode) { + case 76: + if (event.altKey) { + if (self.graphLasso.isActive) { + self.graphLasso.deactivate(); + } else { + self.graphLasso.activate(); + } + } + break; + } + }); + // clear up info div $('#calculatingGraph').remove(); } diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/views/queryView.js b/js/apps/system/_admin/aardvark/APP/frontend/js/views/queryView.js index 1564237da9..4ef4beb745 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/views/queryView.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/views/queryView.js @@ -1473,15 +1473,6 @@ ); } } - if (data.extra.stats.scannedFull > 0) { - appendSpan( - 'full collection scan', 'fa-exclamation-circle warning', 'additional' - ); - } else { - appendSpan( - 'no full collection scan', 'fa-check-circle positive', 'additional' - ); - } } } diff --git a/js/apps/system/_admin/aardvark/APP/frontend/scss/_graphViewer2.scss b/js/apps/system/_admin/aardvark/APP/frontend/scss/_graphViewer2.scss index c06540e5f0..b460f924ee 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/scss/_graphViewer2.scss +++ b/js/apps/system/_admin/aardvark/APP/frontend/scss/_graphViewer2.scss @@ -1,5 +1,7 @@ .graphContent { + margin-top: 3px; + #graph-container { background-color: $c-white; z-index: 5; @@ -40,6 +42,22 @@ } } +.nodeInfoDiv { + left: 25px; + overflow: hidden; + position: absolute; + top: 130px; + + pre { + background-color: rgba(64, 74, 83, .9); + border-radius: 2px; + color: $c-white; + max-height: 400px; + max-width: 330px; + overflow: hidden; + } +} + .nodeContextMenu { position: fixed; @@ -67,6 +85,11 @@ border: 0; border-radius: 0; height: 100%; + overflow-y: scroll; + + button { + margin-bottom: 125px; + } } #graphSettingsContent { @@ -79,13 +102,29 @@ width: 400px; .pure-g { + + font-size: 10pt; + + input, + select { + color: $c-black; + } + .left { color: $c-white; + height: 40px; } .pure-u-2-3 { text-align: right; } + + .heading { + border-bottom: 1px solid $c-white; + height: 10px; + margin-bottom: 15px; + } + } .pure-table { diff --git a/js/apps/system/_admin/aardvark/APP/frontend/scss/_range.scss b/js/apps/system/_admin/aardvark/APP/frontend/scss/_range.scss new file mode 100644 index 0000000000..67742f7939 --- /dev/null +++ b/js/apps/system/_admin/aardvark/APP/frontend/scss/_range.scss @@ -0,0 +1,89 @@ +input[type=range] { + -webkit-appearance: none; + border: 1px solid $c-white; + border-radius: 3px; + margin-top: 10px; + width: 218px; +} + +input[type=range]::-webkit-slider-runnable-track { + background: $c-tab-bottom-border; + border: 0; + border-radius: 3px; + height: 5px; + width: 218px; +} + +input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + background: $c-positive; + border: 0; + border-radius: 50%; + height: 16px; + margin-top: -5px; + width: 16px; +} + +input[type=range]:focus { + outline: none; +} + +input[type=range]:focus::-webkit-slider-runnable-track { + background: $c-accordion-heading; +} + +input[type=range]::-moz-range-track { + background: $c-e1grey; + border: 0; + border-radius: 3px; + height: 5px; + width: 218px; +} + +input[type=range]::-moz-range-thumb { + background: $c-positive; + border: 0; + border-radius: 50%; + height: 16px; + width: 16px; +} + +input[type=range]:-moz-focusring { + outline: 1px solid $c-white; + outline-offset: -1px; +} + +input[type=range]::-ms-track { + background: transparent; + border-color: transparent; + border-width: 6px 0; + color: transparent; + height: 5px; + width: 218px; +} + +input[type=range]::-ms-fill-lower { + background: $c-darker-grey; + border-radius: 10px; +} + +input[type=range]::-ms-fill-upper { + background: $c-tab-bottom-border; + border-radius: 10px; +} + +input[type=range]::-ms-thumb { + background: $c-positive; + border: 0; + border-radius: 50%; + height: 16px; + width: 16px; +} + +input[type=range]:focus::-ms-fill-lower { + background: $c-dark-grey; +} + +input[type=range]:focus::-ms-fill-upper { + background: $c-accordion-heading; +} diff --git a/js/apps/system/_admin/aardvark/APP/frontend/scss/_state.scss b/js/apps/system/_admin/aardvark/APP/frontend/scss/_state.scss new file mode 100644 index 0000000000..cae2e78711 --- /dev/null +++ b/js/apps/system/_admin/aardvark/APP/frontend/scss/_state.scss @@ -0,0 +1,8 @@ +.arangoState { + background-color: $c-bluegrey-dark; + border-radius: 3px; + color: $c-white; + font-size: 10pt; + font-weight: 100; + padding: 5px 8px; +} diff --git a/js/apps/system/_admin/aardvark/APP/frontend/scss/style.scss b/js/apps/system/_admin/aardvark/APP/frontend/scss/style.scss index c7b70d939c..ce62020c64 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/scss/style.scss +++ b/js/apps/system/_admin/aardvark/APP/frontend/scss/style.scss @@ -86,6 +86,11 @@ // screen shards @import 'shards'; +// input type range +@import 'range'; +// state +@import 'state'; + //arangoTable Template @import 'arangoTable'; //arangoTabbar Template