Graph database Analysis of the Steam Network
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

832 lines
24 KiB

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