;(function(undefined) { 'use strict'; /** * Sigma Quadtree Module for edges * =============================== * * Author: Sébastien Heymann, * from the quad of Guillaume Plique (Yomguithereal) * Version: 0.2 */ /** * Quad Geometric Operations * ------------------------- * * A useful batch of geometric operations used by the quadtree. */ var _geom = { /** * Transforms a graph node with x, y and size into an * axis-aligned square. * * @param {object} A graph node with at least a point (x, y) and a size. * @return {object} A square: two points (x1, y1), (x2, y2) and height. */ pointToSquare: function(n) { return { x1: n.x - n.size, y1: n.y - n.size, x2: n.x + n.size, y2: n.y - n.size, height: n.size * 2 }; }, /** * Transforms a graph edge with x1, y1, x2, y2 and size into an * axis-aligned square. * * @param {object} A graph edge with at least two points * (x1, y1), (x2, y2) and a size. * @return {object} A square: two points (x1, y1), (x2, y2) and height. */ lineToSquare: function(e) { if (e.y1 < e.y2) { // (e.x1, e.y1) on top if (e.x1 < e.x2) { // (e.x1, e.y1) on left return { x1: e.x1 - e.size, y1: e.y1 - e.size, x2: e.x2 + e.size, y2: e.y1 - e.size, height: e.y2 - e.y1 + e.size * 2 }; } // (e.x1, e.y1) on right return { x1: e.x2 - e.size, y1: e.y1 - e.size, x2: e.x1 + e.size, y2: e.y1 - e.size, height: e.y2 - e.y1 + e.size * 2 }; } // (e.x2, e.y2) on top if (e.x1 < e.x2) { // (e.x1, e.y1) on left return { x1: e.x1 - e.size, y1: e.y2 - e.size, x2: e.x2 + e.size, y2: e.y2 - e.size, height: e.y1 - e.y2 + e.size * 2 }; } // (e.x2, e.y2) on right return { x1: e.x2 - e.size, y1: e.y2 - e.size, x2: e.x1 + e.size, y2: e.y2 - e.size, height: e.y1 - e.y2 + e.size * 2 }; }, /** * Transforms a graph edge of type 'curve' with x1, y1, x2, y2, * control point and size into an axis-aligned square. * * @param {object} e A graph edge with at least two points * (x1, y1), (x2, y2) and a size. * @param {object} cp A control point (x,y). * @return {object} A square: two points (x1, y1), (x2, y2) and height. */ quadraticCurveToSquare: function(e, cp) { var pt = sigma.utils.getPointOnQuadraticCurve( 0.5, e.x1, e.y1, e.x2, e.y2, cp.x, cp.y ); // Bounding box of the two points and the point at the middle of the // curve: var minX = Math.min(e.x1, e.x2, pt.x), maxX = Math.max(e.x1, e.x2, pt.x), minY = Math.min(e.y1, e.y2, pt.y), maxY = Math.max(e.y1, e.y2, pt.y); return { x1: minX - e.size, y1: minY - e.size, x2: maxX + e.size, y2: minY - e.size, height: maxY - minY + e.size * 2 }; }, /** * Transforms a graph self loop into an axis-aligned square. * * @param {object} n A graph node with a point (x, y) and a size. * @return {object} A square: two points (x1, y1), (x2, y2) and height. */ selfLoopToSquare: function(n) { // Fitting to the curve is too costly, we compute a larger bounding box // using the control points: var cp = sigma.utils.getSelfLoopControlPoints(n.x, n.y, n.size); // Bounding box of the point and the two control points: var minX = Math.min(n.x, cp.x1, cp.x2), maxX = Math.max(n.x, cp.x1, cp.x2), minY = Math.min(n.y, cp.y1, cp.y2), maxY = Math.max(n.y, cp.y1, cp.y2); return { x1: minX - n.size, y1: minY - n.size, x2: maxX + n.size, y2: minY - n.size, height: maxY - minY + n.size * 2 }; }, /** * Checks whether a rectangle is axis-aligned. * * @param {object} A rectangle defined by two points * (x1, y1) and (x2, y2). * @return {boolean} True if the rectangle is axis-aligned. */ isAxisAligned: function(r) { return r.x1 === r.x2 || r.y1 === r.y2; }, /** * Compute top points of an axis-aligned rectangle. This is useful in * cases when the rectangle has been rotated (left, right or bottom up) and * later operations need to know the top points. * * @param {object} An axis-aligned rectangle defined by two points * (x1, y1), (x2, y2) and height. * @return {object} A rectangle: two points (x1, y1), (x2, y2) and height. */ axisAlignedTopPoints: function(r) { // Basic if (r.y1 === r.y2 && r.x1 < r.x2) return r; // Rotated to right if (r.x1 === r.x2 && r.y2 > r.y1) return { x1: r.x1 - r.height, y1: r.y1, x2: r.x1, y2: r.y1, height: r.height }; // Rotated to left if (r.x1 === r.x2 && r.y2 < r.y1) return { x1: r.x1, y1: r.y2, x2: r.x2 + r.height, y2: r.y2, height: r.height }; // Bottom's up return { x1: r.x2, y1: r.y1 - r.height, x2: r.x1, y2: r.y1 - r.height, height: r.height }; }, /** * Get coordinates of a rectangle's lower left corner from its top points. * * @param {object} A rectangle defined by two points (x1, y1) and (x2, y2). * @return {object} Coordinates of the corner (x, y). */ lowerLeftCoor: function(r) { var width = ( Math.sqrt( Math.pow(r.x2 - r.x1, 2) + Math.pow(r.y2 - r.y1, 2) ) ); return { x: r.x1 - (r.y2 - r.y1) * r.height / width, y: r.y1 + (r.x2 - r.x1) * r.height / width }; }, /** * Get coordinates of a rectangle's lower right corner from its top points * and its lower left corner. * * @param {object} A rectangle defined by two points (x1, y1) and (x2, y2). * @param {object} A corner's coordinates (x, y). * @return {object} Coordinates of the corner (x, y). */ lowerRightCoor: function(r, llc) { return { x: llc.x - r.x1 + r.x2, y: llc.y - r.y1 + r.y2 }; }, /** * Get the coordinates of all the corners of a rectangle from its top point. * * @param {object} A rectangle defined by two points (x1, y1) and (x2, y2). * @return {array} An array of the four corners' coordinates (x, y). */ rectangleCorners: function(r) { var llc = this.lowerLeftCoor(r), lrc = this.lowerRightCoor(r, llc); return [ {x: r.x1, y: r.y1}, {x: r.x2, y: r.y2}, {x: llc.x, y: llc.y}, {x: lrc.x, y: lrc.y} ]; }, /** * Split a square defined by its boundaries into four. * * @param {object} Boundaries of the square (x, y, width, height). * @return {array} An array containing the four new squares, themselves * defined by an array of their four corners (x, y). */ splitSquare: function(b) { return [ [ {x: b.x, y: b.y}, {x: b.x + b.width / 2, y: b.y}, {x: b.x, y: b.y + b.height / 2}, {x: b.x + b.width / 2, y: b.y + b.height / 2} ], [ {x: b.x + b.width / 2, y: b.y}, {x: b.x + b.width, y: b.y}, {x: b.x + b.width / 2, y: b.y + b.height / 2}, {x: b.x + b.width, y: b.y + b.height / 2} ], [ {x: b.x, y: b.y + b.height / 2}, {x: b.x + b.width / 2, y: b.y + b.height / 2}, {x: b.x, y: b.y + b.height}, {x: b.x + b.width / 2, y: b.y + b.height} ], [ {x: b.x + b.width / 2, y: b.y + b.height / 2}, {x: b.x + b.width, y: b.y + b.height / 2}, {x: b.x + b.width / 2, y: b.y + b.height}, {x: b.x + b.width, y: b.y + b.height} ] ]; }, /** * Compute the four axis between corners of rectangle A and corners of * rectangle B. This is needed later to check an eventual collision. * * @param {array} An array of rectangle A's four corners (x, y). * @param {array} An array of rectangle B's four corners (x, y). * @return {array} An array of four axis defined by their coordinates (x,y). */ axis: function(c1, c2) { return [ {x: c1[1].x - c1[0].x, y: c1[1].y - c1[0].y}, {x: c1[1].x - c1[3].x, y: c1[1].y - c1[3].y}, {x: c2[0].x - c2[2].x, y: c2[0].y - c2[2].y}, {x: c2[0].x - c2[1].x, y: c2[0].y - c2[1].y} ]; }, /** * Project a rectangle's corner on an axis. * * @param {object} Coordinates of a corner (x, y). * @param {object} Coordinates of an axis (x, y). * @return {object} The projection defined by coordinates (x, y). */ projection: function(c, a) { var l = ( (c.x * a.x + c.y * a.y) / (Math.pow(a.x, 2) + Math.pow(a.y, 2)) ); return { x: l * a.x, y: l * a.y }; }, /** * Check whether two rectangles collide on one particular axis. * * @param {object} An axis' coordinates (x, y). * @param {array} Rectangle A's corners. * @param {array} Rectangle B's corners. * @return {boolean} True if the rectangles collide on the axis. */ axisCollision: function(a, c1, c2) { var sc1 = [], sc2 = []; for (var ci = 0; ci < 4; ci++) { var p1 = this.projection(c1[ci], a), p2 = this.projection(c2[ci], a); sc1.push(p1.x * a.x + p1.y * a.y); sc2.push(p2.x * a.x + p2.y * a.y); } var maxc1 = Math.max.apply(Math, sc1), maxc2 = Math.max.apply(Math, sc2), minc1 = Math.min.apply(Math, sc1), minc2 = Math.min.apply(Math, sc2); return (minc2 <= maxc1 && maxc2 >= minc1); }, /** * Check whether two rectangles collide on each one of their four axis. If * all axis collide, then the two rectangles do collide on the plane. * * @param {array} Rectangle A's corners. * @param {array} Rectangle B's corners. * @return {boolean} True if the rectangles collide. */ collision: function(c1, c2) { var axis = this.axis(c1, c2), col = true; for (var i = 0; i < 4; i++) col = col && this.axisCollision(axis[i], c1, c2); return col; } }; /** * Quad Functions * ------------ * * The Quadtree functions themselves. * For each of those functions, we consider that in a splitted quad, the * index of each node is the following: * 0: top left * 1: top right * 2: bottom left * 3: bottom right * * Moreover, the hereafter quad's philosophy is to consider that if an element * collides with more than one nodes, this element belongs to each of the * nodes it collides with where other would let it lie on a higher node. */ /** * Get the index of the node containing the point in the quad * * @param {object} point A point defined by coordinates (x, y). * @param {object} quadBounds Boundaries of the quad (x, y, width, heigth). * @return {integer} The index of the node containing the point. */ function _quadIndex(point, quadBounds) { var xmp = quadBounds.x + quadBounds.width / 2, ymp = quadBounds.y + quadBounds.height / 2, top = (point.y < ymp), left = (point.x < xmp); if (top) { if (left) return 0; else return 1; } else { if (left) return 2; else return 3; } } /** * Get a list of indexes of nodes containing an axis-aligned rectangle * * @param {object} rectangle A rectangle defined by two points (x1, y1), * (x2, y2) and height. * @param {array} quadCorners An array of the quad nodes' corners. * @return {array} An array of indexes containing one to * four integers. */ function _quadIndexes(rectangle, quadCorners) { var indexes = []; // Iterating through quads for (var i = 0; i < 4; i++) if ((rectangle.x2 >= quadCorners[i][0].x) && (rectangle.x1 <= quadCorners[i][1].x) && (rectangle.y1 + rectangle.height >= quadCorners[i][0].y) && (rectangle.y1 <= quadCorners[i][2].y)) indexes.push(i); return indexes; } /** * Get a list of indexes of nodes containing a non-axis-aligned rectangle * * @param {array} corners An array containing each corner of the * rectangle defined by its coordinates (x, y). * @param {array} quadCorners An array of the quad nodes' corners. * @return {array} An array of indexes containing one to * four integers. */ function _quadCollision(corners, quadCorners) { var indexes = []; // Iterating through quads for (var i = 0; i < 4; i++) if (_geom.collision(corners, quadCorners[i])) indexes.push(i); return indexes; } /** * Subdivide a quad by creating a node at a precise index. The function does * not generate all four nodes not to potentially create unused nodes. * * @param {integer} index The index of the node to create. * @param {object} quad The quad object to subdivide. * @return {object} A new quad representing the node created. */ function _quadSubdivide(index, quad) { var next = quad.level + 1, subw = Math.round(quad.bounds.width / 2), subh = Math.round(quad.bounds.height / 2), qx = Math.round(quad.bounds.x), qy = Math.round(quad.bounds.y), x, y; switch (index) { case 0: x = qx; y = qy; break; case 1: x = qx + subw; y = qy; break; case 2: x = qx; y = qy + subh; break; case 3: x = qx + subw; y = qy + subh; break; } return _quadTree( {x: x, y: y, width: subw, height: subh}, next, quad.maxElements, quad.maxLevel ); } /** * Recursively insert an element into the quadtree. Only points * with size, i.e. axis-aligned squares, may be inserted with this * method. * * @param {object} el The element to insert in the quadtree. * @param {object} sizedPoint A sized point defined by two top points * (x1, y1), (x2, y2) and height. * @param {object} quad The quad in which to insert the element. * @return {undefined} The function does not return anything. */ function _quadInsert(el, sizedPoint, quad) { if (quad.level < quad.maxLevel) { // Searching appropriate quads var indexes = _quadIndexes(sizedPoint, quad.corners); // Iterating for (var i = 0, l = indexes.length; i < l; i++) { // Subdividing if necessary if (quad.nodes[indexes[i]] === undefined) quad.nodes[indexes[i]] = _quadSubdivide(indexes[i], quad); // Recursion _quadInsert(el, sizedPoint, quad.nodes[indexes[i]]); } } else { // Pushing the element in a leaf node quad.elements.push(el); } } /** * Recursively retrieve every elements held by the node containing the * searched point. * * @param {object} point The searched point (x, y). * @param {object} quad The searched quad. * @return {array} An array of elements contained in the relevant * node. */ function _quadRetrievePoint(point, quad) { if (quad.level < quad.maxLevel) { var index = _quadIndex(point, quad.bounds); // If node does not exist we return an empty list if (quad.nodes[index] !== undefined) { return _quadRetrievePoint(point, quad.nodes[index]); } else { return []; } } else { return quad.elements; } } /** * Recursively retrieve every elements contained within an rectangular area * that may or may not be axis-aligned. * * @param {object|array} rectData The searched area defined either by * an array of four corners (x, y) in * the case of a non-axis-aligned * rectangle or an object with two top * points (x1, y1), (x2, y2) and height. * @param {object} quad The searched quad. * @param {function} collisionFunc The collision function used to search * for node indexes. * @param {array?} els The retrieved elements. * @return {array} An array of elements contained in the * area. */ function _quadRetrieveArea(rectData, quad, collisionFunc, els) { els = els || {}; if (quad.level < quad.maxLevel) { var indexes = collisionFunc(rectData, quad.corners); for (var i = 0, l = indexes.length; i < l; i++) if (quad.nodes[indexes[i]] !== undefined) _quadRetrieveArea( rectData, quad.nodes[indexes[i]], collisionFunc, els ); } else for (var j = 0, m = quad.elements.length; j < m; j++) if (els[quad.elements[j].id] === undefined) els[quad.elements[j].id] = quad.elements[j]; return els; } /** * Creates the quadtree object itself. * * @param {object} bounds The boundaries of the quad defined by an * origin (x, y), width and heigth. * @param {integer} level The level of the quad in the tree. * @param {integer} maxElements The max number of element in a leaf node. * @param {integer} maxLevel The max recursion level of the tree. * @return {object} The quadtree object. */ function _quadTree(bounds, level, maxElements, maxLevel) { return { level: level || 0, bounds: bounds, corners: _geom.splitSquare(bounds), maxElements: maxElements || 40, maxLevel: maxLevel || 8, elements: [], nodes: [] }; } /** * Sigma Quad Constructor * ---------------------- * * The edgequad API as exposed to sigma. */ /** * The edgequad core that will become the sigma interface with the quadtree. * * property {object} _tree Property holding the quadtree object. * property {object} _geom Exposition of the _geom namespace for testing. * property {object} _cache Cache for the area method. * property {boolean} _enabled Can index and retreive elements. */ var edgequad = function() { this._geom = _geom; this._tree = null; this._cache = { query: false, result: false }; this._enabled = true; }; /** * Index a graph by inserting its edges into the quadtree. * * @param {object} graph A graph instance. * @param {object} params An object of parameters with at least the quad * bounds. * @return {object} The quadtree object. * * Parameters: * ---------- * bounds: {object} boundaries of the quad defined by its origin (x, y) * width and heigth. * prefix: {string?} a prefix for edge geometric attributes. * maxElements: {integer?} the max number of elements in a leaf node. * maxLevel: {integer?} the max recursion level of the tree. */ edgequad.prototype.index = function(graph, params) { if (!this._enabled) return this._tree; // Enforcing presence of boundaries if (!params.bounds) throw 'sigma.classes.edgequad.index: bounds information not given.'; // Prefix var prefix = params.prefix || '', cp, source, target, n, e; // Building the tree this._tree = _quadTree( params.bounds, 0, params.maxElements, params.maxLevel ); var edges = graph.edges(); // Inserting graph edges into the tree for (var i = 0, l = edges.length; i < l; i++) { source = graph.nodes(edges[i].source); target = graph.nodes(edges[i].target); e = { x1: source[prefix + 'x'], y1: source[prefix + 'y'], x2: target[prefix + 'x'], y2: target[prefix + 'y'], size: edges[i][prefix + 'size'] || 0 }; // Inserting edge if (edges[i].type === 'curve' || edges[i].type === 'curvedArrow') { if (source.id === target.id) { n = { x: source[prefix + 'x'], y: source[prefix + 'y'], size: source[prefix + 'size'] || 0 }; _quadInsert( edges[i], _geom.selfLoopToSquare(n), this._tree); } else { cp = sigma.utils.getQuadraticControlPoint(e.x1, e.y1, e.x2, e.y2); _quadInsert( edges[i], _geom.quadraticCurveToSquare(e, cp), this._tree); } } else { _quadInsert( edges[i], _geom.lineToSquare(e), this._tree); } } // Reset cache: this._cache = { query: false, result: false }; // remove? return this._tree; }; /** * Retrieve every graph edges held by the quadtree node containing the * searched point. * * @param {number} x of the point. * @param {number} y of the point. * @return {array} An array of edges retrieved. */ edgequad.prototype.point = function(x, y) { if (!this._enabled) return []; return this._tree ? _quadRetrievePoint({x: x, y: y}, this._tree) || [] : []; }; /** * Retrieve every graph edges within a rectangular area. The methods keep the * last area queried in cache for optimization reason and will act differently * for the same reason if the area is axis-aligned or not. * * @param {object} A rectangle defined by two top points (x1, y1), (x2, y2) * and height. * @return {array} An array of edges retrieved. */ edgequad.prototype.area = function(rect) { if (!this._enabled) return []; var serialized = JSON.stringify(rect), collisionFunc, rectData; // Returning cache? if (this._cache.query === serialized) return this._cache.result; // Axis aligned ? if (_geom.isAxisAligned(rect)) { collisionFunc = _quadIndexes; rectData = _geom.axisAlignedTopPoints(rect); } else { collisionFunc = _quadCollision; rectData = _geom.rectangleCorners(rect); } // Retrieving edges var edges = this._tree ? _quadRetrieveArea( rectData, this._tree, collisionFunc ) : []; // Object to array var edgesArray = []; for (var i in edges) edgesArray.push(edges[i]); // Caching this._cache.query = serialized; this._cache.result = edgesArray; return edgesArray; }; /** * EXPORT: * ******* */ if (typeof this.sigma !== 'undefined') { this.sigma.classes = this.sigma.classes || {}; this.sigma.classes.edgequad = edgequad; } else if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) exports = module.exports = edgequad; exports.edgequad = edgequad; } else this.edgequad = edgequad; }).call(this);