|
|
- ;(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);
|