26334 lines
735 KiB
JavaScript
26334 lines
735 KiB
JavaScript
/*!
|
|
* This file is part of Cytoscape.js 2.4.6.
|
|
*
|
|
* Cytoscape.js is free software: you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by the Free
|
|
* Software Foundation, either version 3 of the License, or (at your option) any
|
|
* later version.
|
|
*
|
|
* Cytoscape.js is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
|
* details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License along with
|
|
* Cytoscape.js. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
// this is put as a global var in the browser
|
|
// or it's just a global to this module if commonjs
|
|
|
|
var cytoscape;
|
|
|
|
(function(window){ 'use strict';
|
|
|
|
// the object iteself is a function that init's an instance of cytoscape
|
|
|
|
var $$ = cytoscape = function(){ // jshint ignore:line
|
|
return cytoscape.init.apply(cytoscape, arguments);
|
|
};
|
|
|
|
$$.version = '2.4.6';
|
|
|
|
// allow functional access to cytoscape.js
|
|
// e.g. var cyto = $.cytoscape({ selector: "#foo", ... });
|
|
// var nodes = cyto.nodes();
|
|
$$.init = function( options ){
|
|
|
|
// if no options specified, use default
|
|
if( options === undefined ){
|
|
options = {};
|
|
}
|
|
|
|
// create instance
|
|
if( $$.is.plainObject( options ) ){
|
|
return new $$.Core( options );
|
|
}
|
|
|
|
// allow for registration of extensions
|
|
// e.g. $.cytoscape('renderer', 'svg', SvgRenderer);
|
|
// e.g. $.cytoscape('renderer', 'svg', 'nodeshape', 'ellipse', SvgEllipseNodeShape);
|
|
// e.g. $.cytoscape('core', 'doSomething', function(){ /* doSomething code */ });
|
|
// e.g. $.cytoscape('collection', 'doSomething', function(){ /* doSomething code */ });
|
|
else if( $$.is.string( options ) ) {
|
|
return $$.extension.apply($$.extension, arguments);
|
|
}
|
|
};
|
|
|
|
// define the function namespace here, since it has members in many places
|
|
$$.fn = {};
|
|
|
|
if( typeof module !== 'undefined' && module.exports ){ // expose as a commonjs module
|
|
module.exports = cytoscape;
|
|
}
|
|
|
|
if( typeof define !== 'undefined' && define.amd ){ // expose as an amd/requirejs module
|
|
define('cytoscape', function(){
|
|
return cytoscape;
|
|
});
|
|
}
|
|
|
|
// make sure we always register in the window just in case (e.g. w/ derbyjs)
|
|
if( window ){
|
|
window.cytoscape = cytoscape;
|
|
}
|
|
|
|
})( typeof window === 'undefined' ? null : window );
|
|
|
|
// extra set to `this` is necessary for meteor
|
|
this.cytoscape = cytoscape;
|
|
|
|
// internal, minimal Promise impl s.t. apis can return promises in old envs
|
|
// based on thenable (http://github.com/rse/thenable)
|
|
|
|
// NB: you must use `new $$.Promise`, because you may have native promises that don't autonew for you
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
/* promise states [Promises/A+ 2.1] */
|
|
var STATE_PENDING = 0; /* [Promises/A+ 2.1.1] */
|
|
var STATE_FULFILLED = 1; /* [Promises/A+ 2.1.2] */
|
|
var STATE_REJECTED = 2; /* [Promises/A+ 2.1.3] */
|
|
|
|
/* promise object constructor */
|
|
var api = function (executor) {
|
|
/* optionally support non-constructor/plain-function call */
|
|
if (!(this instanceof api))
|
|
return new api(executor);
|
|
|
|
/* initialize object */
|
|
this.id = "Thenable/1.0.7";
|
|
this.state = STATE_PENDING; /* initial state */
|
|
this.fulfillValue = undefined; /* initial value */ /* [Promises/A+ 1.3, 2.1.2.2] */
|
|
this.rejectReason = undefined; /* initial reason */ /* [Promises/A+ 1.5, 2.1.3.2] */
|
|
this.onFulfilled = []; /* initial handlers */
|
|
this.onRejected = []; /* initial handlers */
|
|
|
|
/* provide optional information-hiding proxy */
|
|
this.proxy = {
|
|
then: this.then.bind(this)
|
|
};
|
|
|
|
/* support optional executor function */
|
|
if (typeof executor === "function")
|
|
executor.call(this, this.fulfill.bind(this), this.reject.bind(this));
|
|
};
|
|
|
|
/* promise API methods */
|
|
api.prototype = {
|
|
/* promise resolving methods */
|
|
fulfill: function (value) { return deliver(this, STATE_FULFILLED, "fulfillValue", value); },
|
|
reject: function (value) { return deliver(this, STATE_REJECTED, "rejectReason", value); },
|
|
|
|
/* "The then Method" [Promises/A+ 1.1, 1.2, 2.2] */
|
|
then: function (onFulfilled, onRejected) {
|
|
var curr = this;
|
|
var next = new api(); /* [Promises/A+ 2.2.7] */
|
|
curr.onFulfilled.push(
|
|
resolver(onFulfilled, next, "fulfill")); /* [Promises/A+ 2.2.2/2.2.6] */
|
|
curr.onRejected.push(
|
|
resolver(onRejected, next, "reject" )); /* [Promises/A+ 2.2.3/2.2.6] */
|
|
execute(curr);
|
|
return next.proxy; /* [Promises/A+ 2.2.7, 3.3] */
|
|
}
|
|
};
|
|
|
|
/* deliver an action */
|
|
var deliver = function (curr, state, name, value) {
|
|
if (curr.state === STATE_PENDING) {
|
|
curr.state = state; /* [Promises/A+ 2.1.2.1, 2.1.3.1] */
|
|
curr[name] = value; /* [Promises/A+ 2.1.2.2, 2.1.3.2] */
|
|
execute(curr);
|
|
}
|
|
return curr;
|
|
};
|
|
|
|
/* execute all handlers */
|
|
var execute = function (curr) {
|
|
if (curr.state === STATE_FULFILLED)
|
|
execute_handlers(curr, "onFulfilled", curr.fulfillValue);
|
|
else if (curr.state === STATE_REJECTED)
|
|
execute_handlers(curr, "onRejected", curr.rejectReason);
|
|
};
|
|
|
|
/* execute particular set of handlers */
|
|
var execute_handlers = function (curr, name, value) {
|
|
/* global process: true */
|
|
/* global setImmediate: true */
|
|
/* global setTimeout: true */
|
|
|
|
/* short-circuit processing */
|
|
if (curr[name].length === 0)
|
|
return;
|
|
|
|
/* iterate over all handlers, exactly once */
|
|
var handlers = curr[name];
|
|
curr[name] = []; /* [Promises/A+ 2.2.2.3, 2.2.3.3] */
|
|
var func = function () {
|
|
for (var i = 0; i < handlers.length; i++)
|
|
handlers[i](value); /* [Promises/A+ 2.2.5] */
|
|
};
|
|
|
|
/* execute procedure asynchronously */ /* [Promises/A+ 2.2.4, 3.1] */
|
|
if (typeof process === "object" && typeof process.nextTick === "function")
|
|
process.nextTick(func);
|
|
else if (typeof setImmediate === "function")
|
|
setImmediate(func);
|
|
else
|
|
setTimeout(func, 0);
|
|
};
|
|
|
|
/* generate a resolver function */
|
|
var resolver = function (cb, next, method) {
|
|
return function (value) {
|
|
if (typeof cb !== "function") /* [Promises/A+ 2.2.1, 2.2.7.3, 2.2.7.4] */
|
|
next[method].call(next, value); /* [Promises/A+ 2.2.7.3, 2.2.7.4] */
|
|
else {
|
|
var result;
|
|
try { result = cb(value); } /* [Promises/A+ 2.2.2.1, 2.2.3.1, 2.2.5, 3.2] */
|
|
catch (e) {
|
|
next.reject(e); /* [Promises/A+ 2.2.7.2] */
|
|
return;
|
|
}
|
|
resolve(next, result); /* [Promises/A+ 2.2.7.1] */
|
|
}
|
|
};
|
|
};
|
|
|
|
/* "Promise Resolution Procedure" */ /* [Promises/A+ 2.3] */
|
|
var resolve = function (promise, x) {
|
|
/* sanity check arguments */ /* [Promises/A+ 2.3.1] */
|
|
if (promise === x || promise.proxy === x) {
|
|
promise.reject(new TypeError("cannot resolve promise with itself"));
|
|
return;
|
|
}
|
|
|
|
/* surgically check for a "then" method
|
|
(mainly to just call the "getter" of "then" only once) */
|
|
var then;
|
|
if ((typeof x === "object" && x !== null) || typeof x === "function") {
|
|
try { then = x.then; } /* [Promises/A+ 2.3.3.1, 3.5] */
|
|
catch (e) {
|
|
promise.reject(e); /* [Promises/A+ 2.3.3.2] */
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* handle own Thenables [Promises/A+ 2.3.2]
|
|
and similar "thenables" [Promises/A+ 2.3.3] */
|
|
if (typeof then === "function") {
|
|
var resolved = false;
|
|
try {
|
|
/* call retrieved "then" method */ /* [Promises/A+ 2.3.3.3] */
|
|
then.call(x,
|
|
/* resolvePromise */ /* [Promises/A+ 2.3.3.3.1] */
|
|
function (y) {
|
|
if (resolved) return; resolved = true; /* [Promises/A+ 2.3.3.3.3] */
|
|
if (y === x) /* [Promises/A+ 3.6] */
|
|
promise.reject(new TypeError("circular thenable chain"));
|
|
else
|
|
resolve(promise, y);
|
|
},
|
|
|
|
/* rejectPromise */ /* [Promises/A+ 2.3.3.3.2] */
|
|
function (r) {
|
|
if (resolved) return; resolved = true; /* [Promises/A+ 2.3.3.3.3] */
|
|
promise.reject(r);
|
|
}
|
|
);
|
|
}
|
|
catch (e) {
|
|
if (!resolved) /* [Promises/A+ 2.3.3.3.3] */
|
|
promise.reject(e); /* [Promises/A+ 2.3.3.3.4] */
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* handle other values */
|
|
promise.fulfill(x); /* [Promises/A+ 2.3.4, 2.3.3.4] */
|
|
};
|
|
|
|
// use native promises where possible
|
|
$$.Promise = typeof Promise === 'undefined' ? api : Promise;
|
|
|
|
// so we always have Promise.all()
|
|
$$.Promise.all = $$.Promise.all || function( ps ){
|
|
return new $$.Promise(function( resolveAll, rejectAll ){
|
|
var vals = new Array( ps.length );
|
|
var doneCount = 0;
|
|
|
|
var fulfill = function( i, val ){
|
|
vals[i] = val;
|
|
doneCount++;
|
|
|
|
if( doneCount === ps.length ){
|
|
resolveAll( vals );
|
|
}
|
|
};
|
|
|
|
for( var i = 0; i < ps.length; i++ ){
|
|
(function( i ){
|
|
var p = ps[i];
|
|
var isPromise = p.then != null;
|
|
|
|
if( isPromise ){
|
|
p.then(function( val ){
|
|
fulfill( i, val );
|
|
}, function( err ){
|
|
rejectAll( err );
|
|
});
|
|
} else {
|
|
var val = p;
|
|
fulfill( i, val );
|
|
}
|
|
})( i );
|
|
}
|
|
|
|
});
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
// type testing utility functions
|
|
|
|
;(function($$, window){ 'use strict';
|
|
|
|
var typeofstr = typeof '';
|
|
var typeofobj = typeof {};
|
|
var typeoffn = typeof function(){};
|
|
|
|
$$.is = {
|
|
defined: function(obj){
|
|
return obj != null; // not undefined or null
|
|
},
|
|
|
|
string: function(obj){
|
|
return obj != null && typeof obj == typeofstr;
|
|
},
|
|
|
|
fn: function(obj){
|
|
return obj != null && typeof obj === typeoffn;
|
|
},
|
|
|
|
array: function(obj){
|
|
return Array.isArray ? Array.isArray(obj) : obj != null && obj instanceof Array;
|
|
},
|
|
|
|
plainObject: function(obj){
|
|
return obj != null && typeof obj === typeofobj && !$$.is.array(obj) && obj.constructor === Object;
|
|
},
|
|
|
|
object: function(obj){
|
|
return obj != null && typeof obj === typeofobj;
|
|
},
|
|
|
|
number: function(obj){
|
|
return obj != null && typeof obj === typeof 1 && !isNaN(obj);
|
|
},
|
|
|
|
integer: function( obj ){
|
|
return $$.is.number(obj) && Math.floor(obj) === obj;
|
|
},
|
|
|
|
color: function(obj){
|
|
return obj != null && typeof obj === typeof '' && $.Color(obj).toString() !== '';
|
|
},
|
|
|
|
bool: function(obj){
|
|
return obj != null && typeof obj === typeof true;
|
|
},
|
|
|
|
elementOrCollection: function(obj){
|
|
return $$.is.element(obj) || $$.is.collection(obj);
|
|
},
|
|
|
|
element: function(obj){
|
|
return obj instanceof $$.Element && obj._private.single;
|
|
},
|
|
|
|
collection: function(obj){
|
|
return obj instanceof $$.Collection && !obj._private.single;
|
|
},
|
|
|
|
core: function(obj){
|
|
return obj instanceof $$.Core;
|
|
},
|
|
|
|
style: function(obj){
|
|
return obj instanceof $$.Style;
|
|
},
|
|
|
|
stylesheet: function(obj){
|
|
return obj instanceof $$.Stylesheet;
|
|
},
|
|
|
|
event: function(obj){
|
|
return obj instanceof $$.Event;
|
|
},
|
|
|
|
thread: function(obj){
|
|
return obj instanceof $$.Thread;
|
|
},
|
|
|
|
fabric: function(obj){
|
|
return obj instanceof $$.Fabric;
|
|
},
|
|
|
|
emptyString: function(obj){
|
|
if( !obj ){ // null is empty
|
|
return true;
|
|
} else if( $$.is.string(obj) ){
|
|
if( obj === '' || obj.match(/^\s+$/) ){
|
|
return true; // empty string is empty
|
|
}
|
|
}
|
|
|
|
return false; // otherwise, we don't know what we've got
|
|
},
|
|
|
|
nonemptyString: function(obj){
|
|
if( obj && $$.is.string(obj) && obj !== '' && !obj.match(/^\s+$/) ){
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
domElement: function(obj){
|
|
if( typeof HTMLElement === 'undefined' ){
|
|
return false; // we're not in a browser so it doesn't matter
|
|
} else {
|
|
return obj instanceof HTMLElement;
|
|
}
|
|
},
|
|
|
|
boundingBox: function(obj){
|
|
return $$.is.plainObject(obj) &&
|
|
$$.is.number(obj.x1) && $$.is.number(obj.x2) &&
|
|
$$.is.number(obj.y1) && $$.is.number(obj.y2)
|
|
;
|
|
},
|
|
|
|
promise: function(obj){
|
|
return $$.is.object(obj) && $$.is.fn(obj.then);
|
|
},
|
|
|
|
touch: function(){
|
|
return window && ( ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch );
|
|
},
|
|
|
|
gecko: function(){
|
|
return typeof InstallTrigger !== 'undefined' || ('MozAppearance' in document.documentElement.style);
|
|
},
|
|
|
|
webkit: function(){
|
|
return typeof webkitURL !== 'undefined' || ('WebkitAppearance' in document.documentElement.style);
|
|
},
|
|
|
|
chromium: function(){
|
|
return typeof chrome !== 'undefined';
|
|
},
|
|
|
|
khtml: function(){
|
|
return navigator.vendor.match(/kde/i); // TODO probably a better way to detect this...
|
|
},
|
|
|
|
khtmlEtc: function(){
|
|
return $$.is.khtml() || $$.is.webkit() || $$.is.chromium();
|
|
},
|
|
|
|
trident: function(){
|
|
return typeof ActiveXObject !== 'undefined' || /*@cc_on!@*/false;
|
|
},
|
|
|
|
windows: function(){
|
|
return typeof navigator !== 'undefined' && navigator.appVersion.match(/Win/i);
|
|
},
|
|
|
|
mac: function(){
|
|
return typeof navigator !== 'undefined' && navigator.appVersion.match(/Mac/i);
|
|
},
|
|
|
|
linux: function(){
|
|
return typeof navigator !== 'undefined' && navigator.appVersion.match(/Linux/i);
|
|
},
|
|
|
|
unix: function(){
|
|
return typeof navigator !== 'undefined' && navigator.appVersion.match(/X11/i);
|
|
}
|
|
};
|
|
|
|
})( cytoscape, typeof window === 'undefined' ? null : window );
|
|
|
|
;(function($$, window){ 'use strict';
|
|
|
|
// utility functions only for internal use
|
|
|
|
$$.util = {
|
|
|
|
// the jquery extend() function
|
|
// NB: modified to use $$.is etc since we can't use jquery functions
|
|
extend: function() {
|
|
var options, name, src, copy, copyIsArray, clone,
|
|
target = arguments[0] || {},
|
|
i = 1,
|
|
length = arguments.length,
|
|
deep = false;
|
|
|
|
// Handle a deep copy situation
|
|
if ( typeof target === 'boolean' ) {
|
|
deep = target;
|
|
target = arguments[1] || {};
|
|
// skip the boolean and the target
|
|
i = 2;
|
|
}
|
|
|
|
// Handle case when target is a string or something (possible in deep copy)
|
|
if ( typeof target !== 'object' && !$$.is.fn(target) ) {
|
|
target = {};
|
|
}
|
|
|
|
// extend jQuery itself if only one argument is passed
|
|
if ( length === i ) {
|
|
target = this;
|
|
--i;
|
|
}
|
|
|
|
for ( ; i < length; i++ ) {
|
|
// Only deal with non-null/undefined values
|
|
if ( (options = arguments[ i ]) != null ) {
|
|
// Extend the base object
|
|
for ( name in options ) {
|
|
src = target[ name ];
|
|
copy = options[ name ];
|
|
|
|
// Prevent never-ending loop
|
|
if ( target === copy ) {
|
|
continue;
|
|
}
|
|
|
|
// Recurse if we're merging plain objects or arrays
|
|
if ( deep && copy && ( $$.is.plainObject(copy) || (copyIsArray = $$.is.array(copy)) ) ) {
|
|
if ( copyIsArray ) {
|
|
copyIsArray = false;
|
|
clone = src && $$.is.array(src) ? src : [];
|
|
|
|
} else {
|
|
clone = src && $$.is.plainObject(src) ? src : {};
|
|
}
|
|
|
|
// Never move original objects, clone them
|
|
target[ name ] = $$.util.extend( deep, clone, copy );
|
|
|
|
// Don't bring in undefined values
|
|
} else if ( copy !== undefined ) {
|
|
target[ name ] = copy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the modified object
|
|
return target;
|
|
},
|
|
|
|
// require that pulls in module from commonjs, amd, or window (falling back until found)
|
|
require: function( name, callback, options ){
|
|
var ret;
|
|
options = $$.util.extend({
|
|
msgIfNotFound: true
|
|
}, options);
|
|
|
|
var done = false;
|
|
var fulfil = function( ret ){
|
|
done = true;
|
|
callback( ret );
|
|
};
|
|
|
|
var checkWindow = function( next ){
|
|
if( window ){ // detected browser/window env
|
|
ret = window[ name ];
|
|
}
|
|
|
|
if( ret !== undefined ){ fulfil(ret); }
|
|
if( next ){ next(); }
|
|
};
|
|
var onCheckWindowDone = function(){
|
|
if( !done ){
|
|
checkCommonJs( onCheckCommonJsDone );
|
|
}
|
|
};
|
|
|
|
var checkCommonJs = function( next ){
|
|
if( typeof module !== 'undefined' && module.exports && require ){ // detected commonjs env
|
|
try {
|
|
ret = require( name ); // regular require
|
|
} catch( err ){}
|
|
}
|
|
|
|
if( ret !== undefined ){ fulfil(ret); }
|
|
if( next ){ next(); }
|
|
};
|
|
var onCheckCommonJsDone = function(){
|
|
if( !done ){
|
|
checkAmd( onCheckAmdDone );
|
|
}
|
|
};
|
|
|
|
var checkAmd = function( next ){
|
|
if( typeof define !== 'undefined' && define.amd && require ){ // detected amd env w/ defined module
|
|
require([ name ], function( nameImpl ){
|
|
ret = nameImpl;
|
|
|
|
if( ret !== undefined ){ fulfil(ret); }
|
|
if( next ){ next(); }
|
|
}, function( err ){
|
|
if( next ){ next(); }
|
|
});
|
|
}
|
|
};
|
|
var onCheckAmdDone = function(){
|
|
if( !done && options.msgIfNotFound ){
|
|
$$.util.error('Cytoscape.js tried to pull in dependency `' + name + '` but no module (i.e. CommonJS, AMD, or window) was found');
|
|
}
|
|
};
|
|
|
|
// kick off 1st check: window
|
|
checkWindow( onCheckWindowDone );
|
|
|
|
},
|
|
|
|
// multiple requires in one callback
|
|
requires: function( names, callback ){
|
|
var impls = [];
|
|
var gotImpl = [];
|
|
|
|
var checkDone = function(){
|
|
for( var i = 0; i < names.length; i++ ){ // check have all impls
|
|
if( !gotImpl[i] ){ return; }
|
|
}
|
|
|
|
// otherwise, all got all impls => done
|
|
callback.apply( callback, impls );
|
|
};
|
|
|
|
for( var i = 0; i < names.length; i++ ){ (function(){ // w/scope
|
|
var name = names[i];
|
|
var index = i;
|
|
|
|
$$.util.require(name, function(impl){
|
|
impls[index] = impl;
|
|
gotImpl[index] = true;
|
|
|
|
checkDone();
|
|
});
|
|
})(); }
|
|
},
|
|
|
|
// ported lodash throttle function
|
|
throttle: function(func, wait, options) {
|
|
var leading = true,
|
|
trailing = true;
|
|
|
|
if (options === false) {
|
|
leading = false;
|
|
} else if ($$.is.plainObject(options)) {
|
|
leading = 'leading' in options ? options.leading : leading;
|
|
trailing = 'trailing' in options ? options.trailing : trailing;
|
|
}
|
|
options = options || {};
|
|
options.leading = leading;
|
|
options.maxWait = wait;
|
|
options.trailing = trailing;
|
|
|
|
return $$.util.debounce(func, wait, options);
|
|
},
|
|
|
|
now: function(){
|
|
return +new Date();
|
|
},
|
|
|
|
// ported lodash debounce function
|
|
debounce: function(func, wait, options) {
|
|
var args,
|
|
maxTimeoutId,
|
|
result,
|
|
stamp,
|
|
thisArg,
|
|
timeoutId,
|
|
trailingCall,
|
|
lastCalled = 0,
|
|
maxWait = false,
|
|
trailing = true;
|
|
|
|
if (!$$.is.fn(func)) {
|
|
return;
|
|
}
|
|
wait = Math.max(0, wait) || 0;
|
|
if (options === true) {
|
|
var leading = true;
|
|
trailing = false;
|
|
} else if ($$.is.plainObject(options)) {
|
|
leading = options.leading;
|
|
maxWait = 'maxWait' in options && (Math.max(wait, options.maxWait) || 0);
|
|
trailing = 'trailing' in options ? options.trailing : trailing;
|
|
}
|
|
var delayed = function() {
|
|
var remaining = wait - ($$.util.now() - stamp);
|
|
if (remaining <= 0) {
|
|
if (maxTimeoutId) {
|
|
clearTimeout(maxTimeoutId);
|
|
}
|
|
var isCalled = trailingCall;
|
|
maxTimeoutId = timeoutId = trailingCall = undefined;
|
|
if (isCalled) {
|
|
lastCalled = $$.util.now();
|
|
result = func.apply(thisArg, args);
|
|
if (!timeoutId && !maxTimeoutId) {
|
|
args = thisArg = null;
|
|
}
|
|
}
|
|
} else {
|
|
timeoutId = setTimeout(delayed, remaining);
|
|
}
|
|
};
|
|
|
|
var maxDelayed = function() {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
maxTimeoutId = timeoutId = trailingCall = undefined;
|
|
if (trailing || (maxWait !== wait)) {
|
|
lastCalled = $$.util.now();
|
|
result = func.apply(thisArg, args);
|
|
if (!timeoutId && !maxTimeoutId) {
|
|
args = thisArg = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
return function() {
|
|
args = arguments;
|
|
stamp = $$.util.now();
|
|
thisArg = this;
|
|
trailingCall = trailing && (timeoutId || !leading);
|
|
|
|
if (maxWait === false) {
|
|
var leadingCall = leading && !timeoutId;
|
|
} else {
|
|
if (!maxTimeoutId && !leading) {
|
|
lastCalled = stamp;
|
|
}
|
|
var remaining = maxWait - (stamp - lastCalled),
|
|
isCalled = remaining <= 0;
|
|
|
|
if (isCalled) {
|
|
if (maxTimeoutId) {
|
|
maxTimeoutId = clearTimeout(maxTimeoutId);
|
|
}
|
|
lastCalled = stamp;
|
|
result = func.apply(thisArg, args);
|
|
}
|
|
else if (!maxTimeoutId) {
|
|
maxTimeoutId = setTimeout(maxDelayed, remaining);
|
|
}
|
|
}
|
|
if (isCalled && timeoutId) {
|
|
timeoutId = clearTimeout(timeoutId);
|
|
}
|
|
else if (!timeoutId && wait !== maxWait) {
|
|
timeoutId = setTimeout(delayed, wait);
|
|
}
|
|
if (leadingCall) {
|
|
isCalled = true;
|
|
result = func.apply(thisArg, args);
|
|
}
|
|
if (isCalled && !timeoutId && !maxTimeoutId) {
|
|
args = thisArg = null;
|
|
}
|
|
return result;
|
|
};
|
|
},
|
|
|
|
error: function( msg ){
|
|
if( console ){
|
|
if( console.error ){
|
|
console.error.apply( console, arguments );
|
|
} else if( console.log ){
|
|
console.log.apply( console, arguments );
|
|
} else {
|
|
throw msg;
|
|
}
|
|
} else {
|
|
throw msg;
|
|
}
|
|
},
|
|
|
|
clone: function( obj ){
|
|
var target = {};
|
|
for (var i in obj) {
|
|
//if( obj.hasOwnProperty(i) ){ // TODO is this hasOwnProperty() call necessary for our use?
|
|
target[i] = obj[i];
|
|
//}
|
|
}
|
|
return target;
|
|
},
|
|
|
|
// gets a shallow copy of the argument
|
|
copy: function( obj ){
|
|
if( obj == null ){
|
|
return obj;
|
|
} if( $$.is.array(obj) ){
|
|
return obj.slice();
|
|
} else if( $$.is.plainObject(obj) ){
|
|
return $$.util.clone( obj );
|
|
} else {
|
|
return obj;
|
|
}
|
|
},
|
|
|
|
// makes a full bb (x1, y1, x2, y2, w, h) from implicit params
|
|
makeBoundingBox: function( bb ){
|
|
if( bb.x1 != null && bb.y1 != null ){
|
|
if( bb.x2 != null && bb.y2 != null && bb.x2 >= bb.x1 && bb.y2 >= bb.y1 ){
|
|
return {
|
|
x1: bb.x1,
|
|
y1: bb.y1,
|
|
x2: bb.x2,
|
|
y2: bb.y2,
|
|
w: bb.x2 - bb.x1,
|
|
h: bb.y2 - bb.y1
|
|
};
|
|
} else if( bb.w != null && bb.h != null && bb.w >= 0 && bb.h >= 0 ){
|
|
return {
|
|
x1: bb.x1,
|
|
y1: bb.y1,
|
|
x2: bb.x1 + bb.w,
|
|
y2: bb.y1 + bb.h,
|
|
w: bb.w,
|
|
h: bb.h
|
|
};
|
|
}
|
|
}
|
|
},
|
|
|
|
// has anything been set in the map
|
|
mapEmpty: function( map ){
|
|
var empty = true;
|
|
|
|
if( map != null ){
|
|
for(var i in map){ // jshint ignore:line
|
|
empty = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return empty;
|
|
},
|
|
|
|
// pushes to the array at the end of a map (map may not be built)
|
|
pushMap: function( options ){
|
|
var array = $$.util.getMap(options);
|
|
|
|
if( array == null ){ // if empty, put initial array
|
|
$$.util.setMap( $.extend({}, options, {
|
|
value: [ options.value ]
|
|
}) );
|
|
} else {
|
|
array.push( options.value );
|
|
}
|
|
},
|
|
|
|
// sets the value in a map (map may not be built)
|
|
setMap: function( options ){
|
|
var obj = options.map;
|
|
var key;
|
|
var keys = options.keys;
|
|
var l = keys.length;
|
|
|
|
for(var i = 0; i < l; i++){
|
|
var key = keys[i];
|
|
|
|
if( $$.is.plainObject( key ) ){
|
|
$$.util.error('Tried to set map with object key');
|
|
}
|
|
|
|
if( i < keys.length - 1 ){
|
|
|
|
// extend the map if necessary
|
|
if( obj[key] == null ){
|
|
obj[key] = {};
|
|
}
|
|
|
|
obj = obj[key];
|
|
} else {
|
|
// set the value
|
|
obj[key] = options.value;
|
|
}
|
|
}
|
|
},
|
|
|
|
// gets the value in a map even if it's not built in places
|
|
getMap: function( options ){
|
|
var obj = options.map;
|
|
var keys = options.keys;
|
|
var l = keys.length;
|
|
|
|
for(var i = 0; i < l; i++){
|
|
var key = keys[i];
|
|
|
|
if( $$.is.plainObject( key ) ){
|
|
$$.util.error('Tried to get map with object key');
|
|
}
|
|
|
|
obj = obj[key];
|
|
|
|
if( obj == null ){
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
},
|
|
|
|
// deletes the entry in the map
|
|
deleteMap: function( options ){
|
|
var obj = options.map;
|
|
var keys = options.keys;
|
|
var l = keys.length;
|
|
var keepChildren = options.keepChildren;
|
|
|
|
for(var i = 0; i < l; i++){
|
|
var key = keys[i];
|
|
|
|
if( $$.is.plainObject( key ) ){
|
|
$$.util.error('Tried to delete map with object key');
|
|
}
|
|
|
|
var lastKey = i === options.keys.length - 1;
|
|
if( lastKey ){
|
|
|
|
if( keepChildren ){ // then only delete child fields not in keepChildren
|
|
for( var child in obj ){
|
|
if( !keepChildren[child] ){
|
|
obj[child] = undefined;
|
|
}
|
|
}
|
|
} else {
|
|
obj[key] = undefined;
|
|
}
|
|
|
|
} else {
|
|
obj = obj[key];
|
|
}
|
|
}
|
|
},
|
|
|
|
capitalize: function(str){
|
|
if( $$.is.emptyString(str) ){
|
|
return str;
|
|
}
|
|
|
|
return str.charAt(0).toUpperCase() + str.substring(1);
|
|
},
|
|
|
|
// strip spaces from beginning of string and end of string
|
|
trim: function( str ){
|
|
var first, last;
|
|
|
|
// find first non-space char
|
|
for( first = 0; first < str.length && str[first] === ' '; first++ ){}
|
|
|
|
// find last non-space char
|
|
for( last = str.length - 1; last > first && str[last] === ' '; last-- ){}
|
|
|
|
return str.substring(first, last + 1);
|
|
},
|
|
|
|
// get [r, g, b] from #abc or #aabbcc
|
|
hex2tuple: function( hex ){
|
|
if( !(hex.length === 4 || hex.length === 7) || hex[0] !== "#" ){ return; }
|
|
|
|
var shortHex = hex.length === 4;
|
|
var r, g, b;
|
|
var base = 16;
|
|
|
|
if( shortHex ){
|
|
r = parseInt( hex[1] + hex[1], base );
|
|
g = parseInt( hex[2] + hex[2], base );
|
|
b = parseInt( hex[3] + hex[3], base );
|
|
} else {
|
|
r = parseInt( hex[1] + hex[2], base );
|
|
g = parseInt( hex[3] + hex[4], base );
|
|
b = parseInt( hex[5] + hex[6], base );
|
|
}
|
|
|
|
return [r, g, b];
|
|
},
|
|
|
|
// get [r, g, b, a] from hsl(0, 0, 0) or hsla(0, 0, 0, 0)
|
|
hsl2tuple: function( hsl ){
|
|
var ret;
|
|
var h, s, l, a, r, g, b;
|
|
function hue2rgb(p, q, t){
|
|
if(t < 0) t += 1;
|
|
if(t > 1) t -= 1;
|
|
if(t < 1/6) return p + (q - p) * 6 * t;
|
|
if(t < 1/2) return q;
|
|
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
|
return p;
|
|
}
|
|
|
|
var m = new RegExp("^" + $$.util.regex.hsla + "$").exec(hsl);
|
|
if( m ){
|
|
|
|
// get hue
|
|
h = parseInt( m[1] );
|
|
if( h < 0 ){
|
|
h = ( 360 - (-1*h % 360) ) % 360;
|
|
} else if( h > 360 ){
|
|
h = h % 360;
|
|
}
|
|
h /= 360; // normalise on [0, 1]
|
|
|
|
s = parseFloat( m[2] );
|
|
if( s < 0 || s > 100 ){ return; } // saturation is [0, 100]
|
|
s = s/100; // normalise on [0, 1]
|
|
|
|
l = parseFloat( m[3] );
|
|
if( l < 0 || l > 100 ){ return; } // lightness is [0, 100]
|
|
l = l/100; // normalise on [0, 1]
|
|
|
|
a = m[4];
|
|
if( a !== undefined ){
|
|
a = parseFloat( a );
|
|
|
|
if( a < 0 || a > 1 ){ return; } // alpha is [0, 1]
|
|
}
|
|
|
|
// now, convert to rgb
|
|
// code from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
|
|
if( s === 0 ){
|
|
r = g = b = Math.round(l * 255); // achromatic
|
|
} else {
|
|
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
var p = 2 * l - q;
|
|
r = Math.round( 255 * hue2rgb(p, q, h + 1/3) );
|
|
g = Math.round( 255 * hue2rgb(p, q, h) );
|
|
b = Math.round( 255 * hue2rgb(p, q, h - 1/3) );
|
|
}
|
|
|
|
ret = [r, g, b, a];
|
|
}
|
|
|
|
return ret;
|
|
},
|
|
|
|
// get [r, g, b, a] from rgb(0, 0, 0) or rgba(0, 0, 0, 0)
|
|
rgb2tuple: function( rgb ){
|
|
var ret;
|
|
|
|
var m = new RegExp("^" + $$.util.regex.rgba + "$").exec(rgb);
|
|
if( m ){
|
|
ret = [];
|
|
|
|
var isPct = [];
|
|
for( var i = 1; i <= 3; i++ ){
|
|
var channel = m[i];
|
|
|
|
if( channel[ channel.length - 1 ] === "%" ){
|
|
isPct[i] = true;
|
|
}
|
|
channel = parseFloat( channel );
|
|
|
|
if( isPct[i] ){
|
|
channel = channel/100 * 255; // normalise to [0, 255]
|
|
}
|
|
|
|
if( channel < 0 || channel > 255 ){ return; } // invalid channel value
|
|
|
|
ret.push( Math.floor(channel) );
|
|
}
|
|
|
|
var atLeastOneIsPct = isPct[1] || isPct[2] || isPct[3];
|
|
var allArePct = isPct[1] && isPct[2] && isPct[3];
|
|
if( atLeastOneIsPct && !allArePct ){ return; } // must all be percent values if one is
|
|
|
|
var alpha = m[4];
|
|
if( alpha !== undefined ){
|
|
alpha = parseFloat( alpha );
|
|
|
|
if( alpha < 0 || alpha > 1 ){ return; } // invalid alpha value
|
|
|
|
ret.push( alpha );
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
},
|
|
|
|
colorname2tuple: function( color ){
|
|
return $$.util.colors[ color.toLowerCase() ];
|
|
},
|
|
|
|
color2tuple: function( color ){
|
|
return ( $$.is.array(color) ? color : null )
|
|
|| $$.util.colorname2tuple(color)
|
|
|| $$.util.hex2tuple(color)
|
|
|| $$.util.rgb2tuple(color)
|
|
|| $$.util.hsl2tuple(color);
|
|
},
|
|
|
|
tuple2hex: function( tuple ){
|
|
var r = tuple[0];
|
|
var g = tuple[1];
|
|
var b = tuple[2];
|
|
|
|
function ch2hex( ch ){
|
|
var hex = ch.toString(16);
|
|
|
|
if( hex.length === 1 ){
|
|
hex = '0' + hex;
|
|
}
|
|
|
|
return hex;
|
|
}
|
|
|
|
return '#' + ch2hex(r) + ch2hex(g) + ch2hex(b);
|
|
},
|
|
|
|
colors: {
|
|
// special colour names
|
|
transparent: [0,0,0,0], // NB alpha === 0
|
|
|
|
// regular colours
|
|
aliceblue: [240,248,255],
|
|
antiquewhite: [250,235,215],
|
|
aqua: [0,255,255],
|
|
aquamarine: [127,255,212],
|
|
azure: [240,255,255],
|
|
beige: [245,245,220],
|
|
bisque: [255,228,196],
|
|
black: [0,0,0],
|
|
blanchedalmond: [255,235,205],
|
|
blue: [0,0,255],
|
|
blueviolet: [138,43,226],
|
|
brown: [165,42,42],
|
|
burlywood: [222,184,135],
|
|
cadetblue: [95,158,160],
|
|
chartreuse: [127,255,0],
|
|
chocolate: [210,105,30],
|
|
coral: [255,127,80],
|
|
cornflowerblue: [100,149,237],
|
|
cornsilk: [255,248,220],
|
|
crimson: [220,20,60],
|
|
cyan: [0,255,255],
|
|
darkblue: [0,0,139],
|
|
darkcyan: [0,139,139],
|
|
darkgoldenrod: [184,134,11],
|
|
darkgray: [169,169,169],
|
|
darkgreen: [0,100,0],
|
|
darkgrey: [169,169,169],
|
|
darkkhaki: [189,183,107],
|
|
darkmagenta: [139,0,139],
|
|
darkolivegreen: [85,107,47],
|
|
darkorange: [255,140,0],
|
|
darkorchid: [153,50,204],
|
|
darkred: [139,0,0],
|
|
darksalmon: [233,150,122],
|
|
darkseagreen: [143,188,143],
|
|
darkslateblue: [72,61,139],
|
|
darkslategray: [47,79,79],
|
|
darkslategrey: [47,79,79],
|
|
darkturquoise: [0,206,209],
|
|
darkviolet: [148,0,211],
|
|
deeppink: [255,20,147],
|
|
deepskyblue: [0,191,255],
|
|
dimgray: [105,105,105],
|
|
dimgrey: [105,105,105],
|
|
dodgerblue: [30,144,255],
|
|
firebrick: [178,34,34],
|
|
floralwhite: [255,250,240],
|
|
forestgreen: [34,139,34],
|
|
fuchsia: [255,0,255],
|
|
gainsboro: [220,220,220],
|
|
ghostwhite: [248,248,255],
|
|
gold: [255,215,0],
|
|
goldenrod: [218,165,32],
|
|
gray: [128,128,128],
|
|
grey: [128,128,128],
|
|
green: [0,128,0],
|
|
greenyellow: [173,255,47],
|
|
honeydew: [240,255,240],
|
|
hotpink: [255,105,180],
|
|
indianred: [205,92,92],
|
|
indigo: [75,0,130],
|
|
ivory: [255,255,240],
|
|
khaki: [240,230,140],
|
|
lavender: [230,230,250],
|
|
lavenderblush: [255,240,245],
|
|
lawngreen: [124,252,0],
|
|
lemonchiffon: [255,250,205],
|
|
lightblue: [173,216,230],
|
|
lightcoral: [240,128,128],
|
|
lightcyan: [224,255,255],
|
|
lightgoldenrodyellow: [250,250,210],
|
|
lightgray: [211,211,211],
|
|
lightgreen: [144,238,144],
|
|
lightgrey: [211,211,211],
|
|
lightpink: [255,182,193],
|
|
lightsalmon: [255,160,122],
|
|
lightseagreen: [32,178,170],
|
|
lightskyblue: [135,206,250],
|
|
lightslategray: [119,136,153],
|
|
lightslategrey: [119,136,153],
|
|
lightsteelblue: [176,196,222],
|
|
lightyellow: [255,255,224],
|
|
lime: [0,255,0],
|
|
limegreen: [50,205,50],
|
|
linen: [250,240,230],
|
|
magenta: [255,0,255],
|
|
maroon: [128,0,0],
|
|
mediumaquamarine: [102,205,170],
|
|
mediumblue: [0,0,205],
|
|
mediumorchid: [186,85,211],
|
|
mediumpurple: [147,112,219],
|
|
mediumseagreen: [60,179,113],
|
|
mediumslateblue: [123,104,238],
|
|
mediumspringgreen: [0,250,154],
|
|
mediumturquoise: [72,209,204],
|
|
mediumvioletred: [199,21,133],
|
|
midnightblue: [25,25,112],
|
|
mintcream: [245,255,250],
|
|
mistyrose: [255,228,225],
|
|
moccasin: [255,228,181],
|
|
navajowhite: [255,222,173],
|
|
navy: [0,0,128],
|
|
oldlace: [253,245,230],
|
|
olive: [128,128,0],
|
|
olivedrab: [107,142,35],
|
|
orange: [255,165,0],
|
|
orangered: [255,69,0],
|
|
orchid: [218,112,214],
|
|
palegoldenrod: [238,232,170],
|
|
palegreen: [152,251,152],
|
|
paleturquoise: [175,238,238],
|
|
palevioletred: [219,112,147],
|
|
papayawhip: [255,239,213],
|
|
peachpuff: [255,218,185],
|
|
peru: [205,133,63],
|
|
pink: [255,192,203],
|
|
plum: [221,160,221],
|
|
powderblue: [176,224,230],
|
|
purple: [128,0,128],
|
|
red: [255,0,0],
|
|
rosybrown: [188,143,143],
|
|
royalblue: [65,105,225],
|
|
saddlebrown: [139,69,19],
|
|
salmon: [250,128,114],
|
|
sandybrown: [244,164,96],
|
|
seagreen: [46,139,87],
|
|
seashell: [255,245,238],
|
|
sienna: [160,82,45],
|
|
silver: [192,192,192],
|
|
skyblue: [135,206,235],
|
|
slateblue: [106,90,205],
|
|
slategray: [112,128,144],
|
|
slategrey: [112,128,144],
|
|
snow: [255,250,250],
|
|
springgreen: [0,255,127],
|
|
steelblue: [70,130,180],
|
|
tan: [210,180,140],
|
|
teal: [0,128,128],
|
|
thistle: [216,191,216],
|
|
tomato: [255,99,71],
|
|
turquoise: [64,224,208],
|
|
violet: [238,130,238],
|
|
wheat: [245,222,179],
|
|
white: [255,255,255],
|
|
whitesmoke: [245,245,245],
|
|
yellow: [255,255,0],
|
|
yellowgreen: [154,205,50]
|
|
},
|
|
|
|
memoize: function( fn, keyFn ){
|
|
var self = this;
|
|
var cache = {};
|
|
|
|
if( !keyFn ){
|
|
keyFn = function(){
|
|
if( arguments.length === 1 ){
|
|
return arguments[0];
|
|
}
|
|
|
|
var args = [];
|
|
|
|
for( var i = 0; i < arguments.length; i++ ){
|
|
args.push( arguments[i] );
|
|
}
|
|
|
|
return args.join('$');
|
|
};
|
|
}
|
|
|
|
return function memoizedFn(){
|
|
var args = arguments;
|
|
var ret;
|
|
var k = keyFn.apply( self, args );
|
|
|
|
if( !(ret = cache[k]) ){
|
|
ret = cache[k] = fn.apply( self, args );
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
}
|
|
|
|
};
|
|
|
|
$$.util.camel2dash = $$.util.memoize( function( str ){
|
|
var ret = [];
|
|
|
|
for( var i = 0; i < str.length; i++ ){
|
|
var ch = str[i];
|
|
var chLowerCase = ch.toLowerCase();
|
|
var isUpperCase = ch !== chLowerCase;
|
|
|
|
if( isUpperCase ){
|
|
ret.push( '-' );
|
|
ret.push( chLowerCase );
|
|
} else {
|
|
ret.push( ch );
|
|
}
|
|
}
|
|
|
|
var noUpperCases = ret.length === str.length;
|
|
if( noUpperCases ){ return str; } // cheaper than .join()
|
|
|
|
return ret.join('');
|
|
} );
|
|
|
|
$$.util.dash2camel = $$.util.memoize( function( str ){
|
|
var ret = [];
|
|
var nextIsUpper = false;
|
|
|
|
for( var i = 0; i < str.length; i++ ){
|
|
var ch = str[i];
|
|
var isDash = ch === '-';
|
|
|
|
if( isDash ){
|
|
nextIsUpper = true;
|
|
} else {
|
|
if( nextIsUpper ){
|
|
ret.push( ch.toUpperCase() );
|
|
} else {
|
|
ret.push( ch );
|
|
}
|
|
|
|
nextIsUpper = false;
|
|
}
|
|
}
|
|
|
|
return ret.join('');
|
|
} );
|
|
|
|
$$.util.regex = {};
|
|
|
|
$$.util.regex.number = "(?:[-]?\\d*\\.\\d+|[-]?\\d+|[-]?\\d*\\.\\d+[eE]\\d+)";
|
|
|
|
$$.util.regex.rgba = "rgb[a]?\\(("+ $$.util.regex.number +"[%]?)\\s*,\\s*("+ $$.util.regex.number +"[%]?)\\s*,\\s*("+ $$.util.regex.number +"[%]?)(?:\\s*,\\s*("+ $$.util.regex.number +"))?\\)";
|
|
$$.util.regex.rgbaNoBackRefs = "rgb[a]?\\((?:"+ $$.util.regex.number +"[%]?)\\s*,\\s*(?:"+ $$.util.regex.number +"[%]?)\\s*,\\s*(?:"+ $$.util.regex.number +"[%]?)(?:\\s*,\\s*(?:"+ $$.util.regex.number +"))?\\)";
|
|
|
|
$$.util.regex.hsla = "hsl[a]?\\(("+ $$.util.regex.number +")\\s*,\\s*("+ $$.util.regex.number +"[%])\\s*,\\s*("+ $$.util.regex.number +"[%])(?:\\s*,\\s*("+ $$.util.regex.number +"))?\\)";
|
|
$$.util.regex.hslaNoBackRefs = "hsl[a]?\\((?:"+ $$.util.regex.number +")\\s*,\\s*(?:"+ $$.util.regex.number +"[%])\\s*,\\s*(?:"+ $$.util.regex.number +"[%])(?:\\s*,\\s*(?:"+ $$.util.regex.number +"))?\\)";
|
|
|
|
$$.util.regex.hex3 = "\\#[0-9a-fA-F]{3}";
|
|
$$.util.regex.hex6 = "\\#[0-9a-fA-F]{6}";
|
|
|
|
var raf = !window ? null : ( window.requestAnimationFrame || window.mozRequestAnimationFrame ||
|
|
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame );
|
|
|
|
raf = raf || function(fn){ if(fn){ setTimeout(fn, 1000/60); } };
|
|
|
|
$$.util.requestAnimationFrame = function(fn){
|
|
raf( fn );
|
|
};
|
|
|
|
})( cytoscape, typeof window === 'undefined' ? null : window );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.math = {};
|
|
|
|
$$.math.signum = function(x){
|
|
if( x > 0 ){
|
|
return 1;
|
|
} else if( x < 0 ){
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
$$.math.distance = function( p1, p2 ){
|
|
var dx = p2.x - p1.x;
|
|
var dy = p2.y - p1.y;
|
|
|
|
return Math.sqrt( dx*dx + dy*dy );
|
|
};
|
|
|
|
// from http://en.wikipedia.org/wiki/Bézier_curve#Quadratic_curves
|
|
$$.math.qbezierAt = function(p0, p1, p2, t){
|
|
return (1 - t)*(1 - t)*p0 + 2*(1 - t)*t*p1 + t*t*p2;
|
|
};
|
|
|
|
$$.math.qbezierPtAt = function(p0, p1, p2, t){
|
|
return {
|
|
x: $$.math.qbezierAt( p0.x, p1.x, p2.x, t ),
|
|
y: $$.math.qbezierAt( p0.y, p1.y, p2.y, t )
|
|
};
|
|
};
|
|
|
|
$$.math.boundingBoxesIntersect = function( bb1, bb2 ){
|
|
// case: one bb to right of other
|
|
if( bb1.x1 > bb2.x2 ){ return false; }
|
|
if( bb2.x1 > bb1.x2 ){ return false; }
|
|
|
|
// case: one bb to left of other
|
|
if( bb1.x2 < bb2.x1 ){ return false; }
|
|
if( bb2.x2 < bb1.x1 ){ return false; }
|
|
|
|
// case: one bb above other
|
|
if( bb1.y2 < bb2.y1 ){ return false; }
|
|
if( bb2.y2 < bb1.y1 ){ return false; }
|
|
|
|
// case: one bb below other
|
|
if( bb1.y1 > bb2.y2 ){ return false; }
|
|
if( bb2.y1 > bb1.y2 ){ return false; }
|
|
|
|
// otherwise, must have some overlap
|
|
return true;
|
|
};
|
|
|
|
$$.math.inBoundingBox = function( bb, x, y ){
|
|
return bb.x1 <= x && x <= bb.x2 && bb.y1 <= y && y <= bb.y2;
|
|
};
|
|
|
|
$$.math.pointInBoundingBox = function( bb, pt ){
|
|
return this.inBoundingBox( bb, pt.x, pt.y );
|
|
};
|
|
|
|
$$.math.roundRectangleIntersectLine = function(
|
|
x, y, nodeX, nodeY, width, height, padding) {
|
|
|
|
var cornerRadius = this.getRoundRectangleRadius(width, height);
|
|
|
|
var halfWidth = width / 2;
|
|
var halfHeight = height / 2;
|
|
|
|
// Check intersections with straight line segments
|
|
var straightLineIntersections;
|
|
|
|
// Top segment, left to right
|
|
{
|
|
var topStartX = nodeX - halfWidth + cornerRadius - padding;
|
|
var topStartY = nodeY - halfHeight - padding;
|
|
var topEndX = nodeX + halfWidth - cornerRadius + padding;
|
|
var topEndY = topStartY;
|
|
|
|
straightLineIntersections = this.finiteLinesIntersect(
|
|
x, y, nodeX, nodeY, topStartX, topStartY, topEndX, topEndY, false);
|
|
|
|
if (straightLineIntersections.length > 0) {
|
|
return straightLineIntersections;
|
|
}
|
|
}
|
|
|
|
// Right segment, top to bottom
|
|
{
|
|
var rightStartX = nodeX + halfWidth + padding;
|
|
var rightStartY = nodeY - halfHeight + cornerRadius - padding;
|
|
var rightEndX = rightStartX;
|
|
var rightEndY = nodeY + halfHeight - cornerRadius + padding;
|
|
|
|
straightLineIntersections = this.finiteLinesIntersect(
|
|
x, y, nodeX, nodeY, rightStartX, rightStartY, rightEndX, rightEndY, false);
|
|
|
|
if (straightLineIntersections.length > 0) {
|
|
return straightLineIntersections;
|
|
}
|
|
}
|
|
|
|
// Bottom segment, left to right
|
|
{
|
|
var bottomStartX = nodeX - halfWidth + cornerRadius - padding;
|
|
var bottomStartY = nodeY + halfHeight + padding;
|
|
var bottomEndX = nodeX + halfWidth - cornerRadius + padding;
|
|
var bottomEndY = bottomStartY;
|
|
|
|
straightLineIntersections = this.finiteLinesIntersect(
|
|
x, y, nodeX, nodeY, bottomStartX, bottomStartY, bottomEndX, bottomEndY, false);
|
|
|
|
if (straightLineIntersections.length > 0) {
|
|
return straightLineIntersections;
|
|
}
|
|
}
|
|
|
|
// Left segment, top to bottom
|
|
{
|
|
var leftStartX = nodeX - halfWidth - padding;
|
|
var leftStartY = nodeY - halfHeight + cornerRadius - padding;
|
|
var leftEndX = leftStartX;
|
|
var leftEndY = nodeY + halfHeight - cornerRadius + padding;
|
|
|
|
straightLineIntersections = this.finiteLinesIntersect(
|
|
x, y, nodeX, nodeY, leftStartX, leftStartY, leftEndX, leftEndY, false);
|
|
|
|
if (straightLineIntersections.length > 0) {
|
|
return straightLineIntersections;
|
|
}
|
|
}
|
|
|
|
// Check intersections with arc segments
|
|
var arcIntersections;
|
|
|
|
// Top Left
|
|
{
|
|
var topLeftCenterX = nodeX - halfWidth + cornerRadius;
|
|
var topLeftCenterY = nodeY - halfHeight + cornerRadius;
|
|
arcIntersections = this.intersectLineCircle(
|
|
x, y, nodeX, nodeY,
|
|
topLeftCenterX, topLeftCenterY, cornerRadius + padding);
|
|
|
|
// Ensure the intersection is on the desired quarter of the circle
|
|
if (arcIntersections.length > 0
|
|
&& arcIntersections[0] <= topLeftCenterX
|
|
&& arcIntersections[1] <= topLeftCenterY) {
|
|
return [arcIntersections[0], arcIntersections[1]];
|
|
}
|
|
}
|
|
|
|
// Top Right
|
|
{
|
|
var topRightCenterX = nodeX + halfWidth - cornerRadius;
|
|
var topRightCenterY = nodeY - halfHeight + cornerRadius;
|
|
arcIntersections = this.intersectLineCircle(
|
|
x, y, nodeX, nodeY,
|
|
topRightCenterX, topRightCenterY, cornerRadius + padding);
|
|
|
|
// Ensure the intersection is on the desired quarter of the circle
|
|
if (arcIntersections.length > 0
|
|
&& arcIntersections[0] >= topRightCenterX
|
|
&& arcIntersections[1] <= topRightCenterY) {
|
|
return [arcIntersections[0], arcIntersections[1]];
|
|
}
|
|
}
|
|
|
|
// Bottom Right
|
|
{
|
|
var bottomRightCenterX = nodeX + halfWidth - cornerRadius;
|
|
var bottomRightCenterY = nodeY + halfHeight - cornerRadius;
|
|
arcIntersections = this.intersectLineCircle(
|
|
x, y, nodeX, nodeY,
|
|
bottomRightCenterX, bottomRightCenterY, cornerRadius + padding);
|
|
|
|
// Ensure the intersection is on the desired quarter of the circle
|
|
if (arcIntersections.length > 0
|
|
&& arcIntersections[0] >= bottomRightCenterX
|
|
&& arcIntersections[1] >= bottomRightCenterY) {
|
|
return [arcIntersections[0], arcIntersections[1]];
|
|
}
|
|
}
|
|
|
|
// Bottom Left
|
|
{
|
|
var bottomLeftCenterX = nodeX - halfWidth + cornerRadius;
|
|
var bottomLeftCenterY = nodeY + halfHeight - cornerRadius;
|
|
arcIntersections = this.intersectLineCircle(
|
|
x, y, nodeX, nodeY,
|
|
bottomLeftCenterX, bottomLeftCenterY, cornerRadius + padding);
|
|
|
|
// Ensure the intersection is on the desired quarter of the circle
|
|
if (arcIntersections.length > 0
|
|
&& arcIntersections[0] <= bottomLeftCenterX
|
|
&& arcIntersections[1] >= bottomLeftCenterY) {
|
|
return [arcIntersections[0], arcIntersections[1]];
|
|
}
|
|
}
|
|
|
|
return []; // if nothing
|
|
};
|
|
|
|
$$.math.roundRectangleIntersectBox = function(
|
|
boxX1, boxY1, boxX2, boxY2, width, height, centerX, centerY, padding) {
|
|
|
|
// We have the following shpae
|
|
|
|
// _____
|
|
// _| |_
|
|
// | |
|
|
// |_ _|
|
|
// |_____|
|
|
//
|
|
// With a quarter circle at each corner.
|
|
|
|
var cornerRadius = this.getRoundRectangleRadius(width, height);
|
|
|
|
var hBoxTopLeftX = centerX - width / 2 - padding;
|
|
var hBoxTopLeftY = centerY - height / 2 + cornerRadius - padding;
|
|
var hBoxBottomRightX = centerX + width / 2 + padding;
|
|
var hBoxBottomRightY = centerY + height / 2 - cornerRadius + padding;
|
|
|
|
var vBoxTopLeftX = centerX - width / 2 + cornerRadius - padding;
|
|
var vBoxTopLeftY = centerY - height / 2 - padding;
|
|
var vBoxBottomRightX = centerX + width / 2 - cornerRadius + padding;
|
|
var vBoxBottomRightY = centerY + height / 2 + padding;
|
|
|
|
// Check if the box is out of bounds
|
|
var boxMinX = Math.min(boxX1, boxX2);
|
|
var boxMaxX = Math.max(boxX1, boxX2);
|
|
var boxMinY = Math.min(boxY1, boxY2);
|
|
var boxMaxY = Math.max(boxY1, boxY2);
|
|
|
|
if (boxMaxX < hBoxTopLeftX) {
|
|
return false;
|
|
} else if (boxMinX > hBoxBottomRightX) {
|
|
return false;
|
|
}
|
|
|
|
if (boxMaxY < vBoxTopLeftY) {
|
|
return false;
|
|
} else if (boxMinY > vBoxBottomRightY) {
|
|
return false;
|
|
}
|
|
|
|
// Check if an hBox point is in given box
|
|
if (hBoxTopLeftX >= boxMinX && hBoxTopLeftX <= boxMaxX
|
|
&& hBoxTopLeftY >= boxMinY && hBoxTopLeftY <= boxMaxY) {
|
|
return true;
|
|
}
|
|
|
|
if (hBoxBottomRightX >= boxMinX && hBoxBottomRightX <= boxMaxX
|
|
&& hBoxTopLeftY >= boxMinY && hBoxTopLeftY <= boxMaxY) {
|
|
return true;
|
|
}
|
|
|
|
if (hBoxBottomRightX >= boxMinX && hBoxBottomRightX <= boxMaxX
|
|
&& hBoxBottomRightY >= boxMinY && hBoxBottomRightY <= boxMaxY) {
|
|
return true;
|
|
}
|
|
|
|
if (hBoxTopLeftX >= boxMinX && hBoxTopLeftX <= boxMaxX
|
|
&& hBoxBottomRightY >= boxMinY && hBoxBottomRightY <= boxMaxY) {
|
|
return true;
|
|
}
|
|
|
|
// Check if a given point box is in the hBox
|
|
if (boxMinX >= hBoxTopLeftX && boxMinX <= hBoxBottomRightX
|
|
&& boxMinY >= hBoxTopLeftY && boxMinY <= hBoxBottomRightY) {
|
|
return true;
|
|
}
|
|
|
|
if (boxMaxX >= hBoxTopLeftX && boxMaxX <= hBoxBottomRightX
|
|
&& boxMinY >= hBoxTopLeftY && boxMinY <= hBoxBottomRightY) {
|
|
return true;
|
|
}
|
|
|
|
if (boxMaxX >= hBoxTopLeftX && boxMaxX <= hBoxBottomRightX
|
|
&& boxMaxY >= hBoxTopLeftY && boxMaxY <= hBoxBottomRightY) {
|
|
return true;
|
|
}
|
|
|
|
if (boxMinX >= hBoxTopLeftX && boxMinX <= hBoxBottomRightX
|
|
&& boxMaxY >= hBoxTopLeftY && boxMaxY <= hBoxBottomRightY) {
|
|
return true;
|
|
}
|
|
|
|
// Check if an vBox point is in given box
|
|
if (vBoxTopLeftX >= boxMinX && vBoxTopLeftX <= boxMaxX
|
|
&& vBoxTopLeftY >= boxMinY && vBoxTopLeftY <= boxMaxY) {
|
|
return true;
|
|
}
|
|
|
|
if (vBoxBottomRightX >= boxMinX && vBoxBottomRightX <= boxMaxX
|
|
&& vBoxTopLeftY >= boxMinY && vBoxTopLeftY <= boxMaxY) {
|
|
return true;
|
|
}
|
|
|
|
if (vBoxBottomRightX >= boxMinX && vBoxBottomRightX <= boxMaxX
|
|
&& vBoxBottomRightY >= boxMinY && vBoxBottomRightY <= boxMaxY) {
|
|
return true;
|
|
}
|
|
|
|
if (vBoxTopLeftX >= boxMinX && vBoxTopLeftX <= boxMaxX
|
|
&& vBoxBottomRightY >= boxMinY && vBoxBottomRightY <= boxMaxY) {
|
|
return true;
|
|
}
|
|
|
|
// Check if a given point box is in the vBox
|
|
if (boxMinX >= vBoxTopLeftX && boxMinX <= vBoxBottomRightX
|
|
&& boxMinY >= vBoxTopLeftY && boxMinY <= vBoxBottomRightY) {
|
|
return true;
|
|
}
|
|
|
|
if (boxMaxX >= vBoxTopLeftX && boxMaxX <= vBoxBottomRightX
|
|
&& boxMinY >= vBoxTopLeftY && boxMinY <= vBoxBottomRightY) {
|
|
return true;
|
|
}
|
|
|
|
if (boxMaxX >= vBoxTopLeftX && boxMaxX <= vBoxBottomRightX
|
|
&& boxMaxY >= vBoxTopLeftY && boxMaxY <= vBoxBottomRightY) {
|
|
return true;
|
|
}
|
|
|
|
if (boxMinX >= vBoxTopLeftX && boxMinX <= vBoxBottomRightX
|
|
&& boxMaxY >= vBoxTopLeftY && boxMaxY <= vBoxBottomRightY) {
|
|
return true;
|
|
}
|
|
|
|
// Lastly, check if one of the ellipses coincide with the box
|
|
|
|
if (this.boxIntersectEllipse(boxMinX, boxMinY, boxMaxX, boxMaxY, padding,
|
|
cornerRadius * 2, cornerRadius * 2, vBoxTopLeftX + padding, hBoxTopLeftY + padding)) {
|
|
return true;
|
|
}
|
|
|
|
if (this.boxIntersectEllipse(boxMinX, boxMinY, boxMaxX, boxMaxY, padding,
|
|
cornerRadius * 2, cornerRadius * 2, vBoxBottomRightX - padding, hBoxTopLeftY + padding)) {
|
|
return true;
|
|
}
|
|
|
|
if (this.boxIntersectEllipse(boxMinX, boxMinY, boxMaxX, boxMaxY, padding,
|
|
cornerRadius * 2, cornerRadius * 2, vBoxBottomRightX - padding, hBoxBottomRightY - padding)) {
|
|
return true;
|
|
}
|
|
|
|
if (this.boxIntersectEllipse(boxMinX, boxMinY, boxMaxX, boxMaxY, padding,
|
|
cornerRadius * 2, cornerRadius * 2, vBoxTopLeftX + padding, hBoxBottomRightY - padding)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// @O Approximate collision functions
|
|
$$.math.checkInBoundingCircle = function(
|
|
x, y, farthestPointSqDistance, padding, width, height, centerX, centerY) {
|
|
|
|
x = (x - centerX) / (width + padding);
|
|
y = (y - centerY) / (height + padding);
|
|
|
|
return (x * x + y * y) <= farthestPointSqDistance;
|
|
};
|
|
|
|
$$.math.boxInBezierVicinity = function(
|
|
x1box, y1box, x2box, y2box, x1, y1, x2, y2, x3, y3, tolerance) {
|
|
|
|
// Return values:
|
|
// 0 - curve is not in box
|
|
// 1 - curve may be in box; needs precise check
|
|
// 2 - curve is in box
|
|
|
|
// midpoint
|
|
var midX = 0.25 * x1 + 0.5 * x2 + 0.25 * x3;
|
|
var midY = 0.25 * y1 + 0.5 * y2 + 0.25 * y3;
|
|
|
|
var boxMinX = Math.min(x1box, x2box) - tolerance;
|
|
var boxMinY = Math.min(y1box, y2box) - tolerance;
|
|
var boxMaxX = Math.max(x1box, x2box) + tolerance;
|
|
var boxMaxY = Math.max(y1box, y2box) + tolerance;
|
|
|
|
if (x1 >= boxMinX && x1 <= boxMaxX && y1 >= boxMinY && y1 <= boxMaxY) { // (x1, y1) in box
|
|
return 1;
|
|
} else if (x3 >= boxMinX && x3 <= boxMaxX && y3 >= boxMinY && y3 <= boxMaxY) { // (x3, y3) in box
|
|
return 1;
|
|
} else if (midX >= boxMinX && midX <= boxMaxX && midY >= boxMinY && midY <= boxMaxY) { // (midX, midY) in box
|
|
return 1;
|
|
} else if (x2 >= boxMinX && x2 <= boxMaxX && y2 >= boxMinY && y2 <= boxMaxY) { // ctrl pt in box
|
|
return 1;
|
|
}
|
|
|
|
var curveMinX = Math.min(x1, midX, x3);
|
|
var curveMinY = Math.min(y1, midY, y3);
|
|
var curveMaxX = Math.max(x1, midX, x3);
|
|
var curveMaxY = Math.max(y1, midY, y3);
|
|
|
|
/*
|
|
console.log(curveMinX + ", " + curveMinY + ", " + curveMaxX
|
|
+ ", " + curveMaxY);
|
|
if (curveMinX == undefined) {
|
|
console.log("undefined curveMinX: " + x1 + ", " + x2 + ", " + x3);
|
|
}
|
|
*/
|
|
|
|
if (curveMinX > boxMaxX
|
|
|| curveMaxX < boxMinX
|
|
|| curveMinY > boxMaxY
|
|
|| curveMaxY < boxMinY) {
|
|
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
};
|
|
|
|
$$.math.checkBezierInBox = function(
|
|
x1box, y1box, x2box, y2box, x1, y1, x2, y2, x3, y3, tolerance) {
|
|
|
|
function sampleInBox(t){
|
|
var x = $$.math.qbezierAt(x1, x2, x3, t);
|
|
var y = $$.math.qbezierAt(y1, y2, y3, t);
|
|
|
|
return x1box <= x && x <= x2box
|
|
&& y1box <= y && y <= y2box
|
|
;
|
|
}
|
|
|
|
for( var t = 0; t <= 1; t += 0.25 ){
|
|
if( !sampleInBox(t) ){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
$$.math.checkStraightEdgeInBox = function(
|
|
x1box, y1box, x2box, y2box, x1, y1, x2, y2, tolerance) {
|
|
|
|
return x1box <= x1 && x1 <= x2box
|
|
&& x1box <= x2 && x2 <= x2box
|
|
&& y1box <= y1 && y1 <= y2box
|
|
&& y1box <= y2 && y2 <= y2box
|
|
;
|
|
};
|
|
|
|
$$.math.checkStraightEdgeCrossesBox = function(
|
|
x1box, y1box, x2box, y2box, x1, y1, x2, y2, tolerance) {
|
|
|
|
//console.log(arguments);
|
|
|
|
var boxMinX = Math.min(x1box, x2box) - tolerance;
|
|
var boxMinY = Math.min(y1box, y2box) - tolerance;
|
|
var boxMaxX = Math.max(x1box, x2box) + tolerance;
|
|
var boxMaxY = Math.max(y1box, y2box) + tolerance;
|
|
|
|
// Check left + right bounds
|
|
var aX = x2 - x1;
|
|
var bX = x1;
|
|
var yValue;
|
|
|
|
// Top and bottom
|
|
var aY = y2 - y1;
|
|
var bY = y1;
|
|
var xValue;
|
|
|
|
if (Math.abs(aX) < 0.0001) {
|
|
return (x1 >= boxMinX && x1 <= boxMaxX
|
|
&& Math.min(y1, y2) <= boxMinY
|
|
&& Math.max(y1, y2) >= boxMaxY);
|
|
}
|
|
|
|
var tLeft = (boxMinX - bX) / aX;
|
|
if (tLeft > 0 && tLeft <= 1) {
|
|
yValue = aY * tLeft + bY;
|
|
if (yValue >= boxMinY && yValue <= boxMaxY) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var tRight = (boxMaxX - bX) / aX;
|
|
if (tRight > 0 && tRight <= 1) {
|
|
yValue = aY * tRight + bY;
|
|
if (yValue >= boxMinY && yValue <= boxMaxY) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var tTop = (boxMinY - bY) / aY;
|
|
if (tTop > 0 && tTop <= 1) {
|
|
xValue = aX * tTop + bX;
|
|
if (xValue >= boxMinX && xValue <= boxMaxX) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var tBottom = (boxMaxY - bY) / aY;
|
|
if (tBottom > 0 && tBottom <= 1) {
|
|
xValue = aX * tBottom + bX;
|
|
if (xValue >= boxMinX && xValue <= boxMaxX) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
$$.math.checkBezierCrossesBox = function(
|
|
x1box, y1box, x2box, y2box, x1, y1, x2, y2, x3, y3, tolerance) {
|
|
|
|
var boxMinX = Math.min(x1box, x2box) - tolerance;
|
|
var boxMinY = Math.min(y1box, y2box) - tolerance;
|
|
var boxMaxX = Math.max(x1box, x2box) + tolerance;
|
|
var boxMaxY = Math.max(y1box, y2box) + tolerance;
|
|
|
|
if (x1 >= boxMinX && x1 <= boxMaxX && y1 >= boxMinY && y1 <= boxMaxY) {
|
|
return true;
|
|
} else if (x3 >= boxMinX && x3 <= boxMaxX && y3 >= boxMinY && y3 <= boxMaxY) {
|
|
return true;
|
|
}
|
|
|
|
var aX = x1 - 2 * x2 + x3;
|
|
var bX = -2 * x1 + 2 * x2;
|
|
var cX = x1;
|
|
|
|
var xIntervals = [];
|
|
|
|
if (Math.abs(aX) < 0.0001) {
|
|
var leftParam = (boxMinX - x1) / bX;
|
|
var rightParam = (boxMaxX - x1) / bX;
|
|
|
|
xIntervals.push(leftParam, rightParam);
|
|
} else {
|
|
// Find when x coordinate of the curve crosses the left side of the box
|
|
var discriminantX1 = bX * bX - 4 * aX * (cX - boxMinX);
|
|
var tX1, tX2;
|
|
if (discriminantX1 > 0) {
|
|
var sqrt = Math.sqrt(discriminantX1);
|
|
tX1 = (-bX + sqrt) / (2 * aX);
|
|
tX2 = (-bX - sqrt) / (2 * aX);
|
|
|
|
xIntervals.push(tX1, tX2);
|
|
}
|
|
|
|
var discriminantX2 = bX * bX - 4 * aX * (cX - boxMaxX);
|
|
var tX3, tX4;
|
|
if (discriminantX2 > 0) {
|
|
var sqrt = Math.sqrt(discriminantX2);
|
|
tX3 = (-bX + sqrt) / (2 * aX);
|
|
tX4 = (-bX - sqrt) / (2 * aX);
|
|
|
|
xIntervals.push(tX3, tX4);
|
|
}
|
|
}
|
|
|
|
xIntervals.sort(function(a, b) { return a - b; });
|
|
|
|
var aY = y1 - 2 * y2 + y3;
|
|
var bY = -2 * y1 + 2 * y2;
|
|
var cY = y1;
|
|
|
|
var yIntervals = [];
|
|
|
|
if (Math.abs(aY) < 0.0001) {
|
|
var topParam = (boxMinY - y1) / bY;
|
|
var bottomParam = (boxMaxY - y1) / bY;
|
|
|
|
yIntervals.push(topParam, bottomParam);
|
|
} else {
|
|
var discriminantY1 = bY * bY - 4 * aY * (cY - boxMinY);
|
|
|
|
var tY1, tY2;
|
|
if (discriminantY1 > 0) {
|
|
var sqrt = Math.sqrt(discriminantY1);
|
|
tY1 = (-bY + sqrt) / (2 * aY);
|
|
tY2 = (-bY - sqrt) / (2 * aY);
|
|
|
|
yIntervals.push(tY1, tY2);
|
|
}
|
|
|
|
var discriminantY2 = bY * bY - 4 * aY * (cY - boxMaxY);
|
|
|
|
var tY3, tY4;
|
|
if (discriminantY2 > 0) {
|
|
var sqrt = Math.sqrt(discriminantY2);
|
|
tY3 = (-bY + sqrt) / (2 * aY);
|
|
tY4 = (-bY - sqrt) / (2 * aY);
|
|
|
|
yIntervals.push(tY3, tY4);
|
|
}
|
|
}
|
|
|
|
yIntervals.sort(function(a, b) { return a - b; });
|
|
|
|
for (var index = 0; index < xIntervals.length; index += 2) {
|
|
for (var yIndex = 1; yIndex < yIntervals.length; yIndex += 2) {
|
|
|
|
// Check if there exists values for the Bezier curve
|
|
// parameter between 0 and 1 where both the curve's
|
|
// x and y coordinates are within the bounds specified by the box
|
|
if (xIntervals[index] < yIntervals[yIndex]
|
|
&& yIntervals[yIndex] >= 0.0
|
|
&& xIntervals[index] <= 1.0
|
|
&& xIntervals[index + 1] > yIntervals[yIndex - 1]
|
|
&& yIntervals[yIndex - 1] <= 1.0
|
|
&& xIntervals[index + 1] >= 0.0) {
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
$$.math.inLineVicinity = function(x, y, lx1, ly1, lx2, ly2, tolerance){
|
|
var t = tolerance;
|
|
|
|
var x1 = Math.min(lx1, lx2);
|
|
var x2 = Math.max(lx1, lx2);
|
|
var y1 = Math.min(ly1, ly2);
|
|
var y2 = Math.max(ly1, ly2);
|
|
|
|
return x1 - t <= x && x <= x2 + t
|
|
&& y1 - t <= y && y <= y2 + t;
|
|
};
|
|
|
|
$$.math.inBezierVicinity = function(
|
|
x, y, x1, y1, x2, y2, x3, y3, toleranceSquared) {
|
|
|
|
var bb = {
|
|
x1: Math.min( x1, x3, x2 ),
|
|
x2: Math.max( x1, x3, x2 ),
|
|
y1: Math.min( y1, y3, y2 ),
|
|
y2: Math.max( y1, y3, y2 )
|
|
};
|
|
|
|
// if outside the rough bounding box for the bezier, then it can't be a hit
|
|
if( x < bb.x1 || x > bb.x2 || y < bb.y1 || y > bb.y2 ){
|
|
// console.log('bezier out of rough bb')
|
|
return false;
|
|
} else {
|
|
// console.log('do more expensive check');
|
|
return true;
|
|
}
|
|
|
|
};
|
|
|
|
$$.math.solveCubic = function(a, b, c, d, result) {
|
|
|
|
// Solves a cubic function, returns root in form [r1, i1, r2, i2, r3, i3], where
|
|
// r is the real component, i is the imaginary component
|
|
|
|
// An implementation of the Cardano method from the year 1545
|
|
// http://en.wikipedia.org/wiki/Cubic_function#The_nature_of_the_roots
|
|
|
|
b /= a;
|
|
c /= a;
|
|
d /= a;
|
|
|
|
var discriminant, q, r, dum1, s, t, term1, r13;
|
|
|
|
q = (3.0 * c - (b * b)) / 9.0;
|
|
r = -(27.0 * d) + b * (9.0 * c - 2.0 * (b * b));
|
|
r /= 54.0;
|
|
|
|
discriminant = q * q * q + r * r;
|
|
result[1] = 0;
|
|
term1 = (b / 3.0);
|
|
|
|
if (discriminant > 0) {
|
|
s = r + Math.sqrt(discriminant);
|
|
s = ((s < 0) ? -Math.pow(-s, (1.0 / 3.0)) : Math.pow(s, (1.0 / 3.0)));
|
|
t = r - Math.sqrt(discriminant);
|
|
t = ((t < 0) ? -Math.pow(-t, (1.0 / 3.0)) : Math.pow(t, (1.0 / 3.0)));
|
|
result[0] = -term1 + s + t;
|
|
term1 += (s + t) / 2.0;
|
|
result[4] = result[2] = -term1;
|
|
term1 = Math.sqrt(3.0) * (-t + s) / 2;
|
|
result[3] = term1;
|
|
result[5] = -term1;
|
|
return;
|
|
}
|
|
|
|
result[5] = result[3] = 0;
|
|
|
|
if (discriminant === 0) {
|
|
r13 = ((r < 0) ? -Math.pow(-r, (1.0 / 3.0)) : Math.pow(r, (1.0 / 3.0)));
|
|
result[0] = -term1 + 2.0 * r13;
|
|
result[4] = result[2] = -(r13 + term1);
|
|
return;
|
|
}
|
|
|
|
q = -q;
|
|
dum1 = q * q * q;
|
|
dum1 = Math.acos(r / Math.sqrt(dum1));
|
|
r13 = 2.0 * Math.sqrt(q);
|
|
result[0] = -term1 + r13 * Math.cos(dum1 / 3.0);
|
|
result[2] = -term1 + r13 * Math.cos((dum1 + 2.0 * Math.PI) / 3.0);
|
|
result[4] = -term1 + r13 * Math.cos((dum1 + 4.0 * Math.PI) / 3.0);
|
|
|
|
return;
|
|
};
|
|
|
|
$$.math.sqDistanceToQuadraticBezier = function(
|
|
x, y, x1, y1, x2, y2, x3, y3) {
|
|
|
|
// Find minimum distance by using the minimum of the distance
|
|
// function between the given point and the curve
|
|
|
|
// This gives the coefficients of the resulting cubic equation
|
|
// whose roots tell us where a possible minimum is
|
|
// (Coefficients are divided by 4)
|
|
|
|
var a = 1.0 * x1*x1 - 4*x1*x2 + 2*x1*x3 + 4*x2*x2 - 4*x2*x3 + x3*x3
|
|
+ y1*y1 - 4*y1*y2 + 2*y1*y3 + 4*y2*y2 - 4*y2*y3 + y3*y3;
|
|
|
|
var b = 1.0 * 9*x1*x2 - 3*x1*x1 - 3*x1*x3 - 6*x2*x2 + 3*x2*x3
|
|
+ 9*y1*y2 - 3*y1*y1 - 3*y1*y3 - 6*y2*y2 + 3*y2*y3;
|
|
|
|
var c = 1.0 * 3*x1*x1 - 6*x1*x2 + x1*x3 - x1*x + 2*x2*x2 + 2*x2*x - x3*x
|
|
+ 3*y1*y1 - 6*y1*y2 + y1*y3 - y1*y + 2*y2*y2 + 2*y2*y - y3*y;
|
|
|
|
var d = 1.0 * x1*x2 - x1*x1 + x1*x - x2*x
|
|
+ y1*y2 - y1*y1 + y1*y - y2*y;
|
|
|
|
// debug("coefficients: " + a / a + ", " + b / a + ", " + c / a + ", " + d / a);
|
|
|
|
var roots = [];
|
|
|
|
// Use the cubic solving algorithm
|
|
this.solveCubic(a, b, c, d, roots);
|
|
|
|
var zeroThreshold = 0.0000001;
|
|
|
|
var params = [];
|
|
|
|
for (var index = 0; index < 6; index += 2) {
|
|
if (Math.abs(roots[index + 1]) < zeroThreshold
|
|
&& roots[index] >= 0
|
|
&& roots[index] <= 1.0) {
|
|
params.push(roots[index]);
|
|
}
|
|
}
|
|
|
|
params.push(1.0);
|
|
params.push(0.0);
|
|
|
|
var minDistanceSquared = -1;
|
|
var closestParam;
|
|
|
|
var curX, curY, distSquared;
|
|
for (var i = 0; i < params.length; i++) {
|
|
curX = Math.pow(1.0 - params[i], 2.0) * x1
|
|
+ 2.0 * (1 - params[i]) * params[i] * x2
|
|
+ params[i] * params[i] * x3;
|
|
|
|
curY = Math.pow(1 - params[i], 2.0) * y1
|
|
+ 2 * (1.0 - params[i]) * params[i] * y2
|
|
+ params[i] * params[i] * y3;
|
|
|
|
distSquared = Math.pow(curX - x, 2) + Math.pow(curY - y, 2);
|
|
// debug('distance for param ' + params[i] + ": " + Math.sqrt(distSquared));
|
|
if (minDistanceSquared >= 0) {
|
|
if (distSquared < minDistanceSquared) {
|
|
minDistanceSquared = distSquared;
|
|
closestParam = params[i];
|
|
}
|
|
} else {
|
|
minDistanceSquared = distSquared;
|
|
closestParam = params[i];
|
|
}
|
|
}
|
|
|
|
/*
|
|
debugStats.clickX = x;
|
|
debugStats.clickY = y;
|
|
|
|
debugStats.closestX = Math.pow(1.0 - closestParam, 2.0) * x1
|
|
+ 2.0 * (1.0 - closestParam) * closestParam * x2
|
|
+ closestParam * closestParam * x3;
|
|
|
|
debugStats.closestY = Math.pow(1.0 - closestParam, 2.0) * y1
|
|
+ 2.0 * (1.0 - closestParam) * closestParam * y2
|
|
+ closestParam * closestParam * y3;
|
|
*/
|
|
|
|
// debug("given: "
|
|
// + "( " + x + ", " + y + "), "
|
|
// + "( " + x1 + ", " + y1 + "), "
|
|
// + "( " + x2 + ", " + y2 + "), "
|
|
// + "( " + x3 + ", " + y3 + ")");
|
|
|
|
|
|
// debug("roots: " + roots);
|
|
// debug("params: " + params);
|
|
// debug("closest param: " + closestParam);
|
|
return minDistanceSquared;
|
|
};
|
|
|
|
$$.math.sqDistanceToFiniteLine = function(x, y, x1, y1, x2, y2) {
|
|
var offset = [x - x1, y - y1];
|
|
var line = [x2 - x1, y2 - y1];
|
|
|
|
var lineSq = line[0] * line[0] + line[1] * line[1];
|
|
var hypSq = offset[0] * offset[0] + offset[1] * offset[1];
|
|
|
|
var dotProduct = offset[0] * line[0] + offset[1] * line[1];
|
|
var adjSq = dotProduct * dotProduct / lineSq;
|
|
|
|
if (dotProduct < 0) {
|
|
return hypSq;
|
|
}
|
|
|
|
if (adjSq > lineSq) {
|
|
return (x - x2) * (x - x2) + (y - y2) * (y - y2);
|
|
}
|
|
|
|
return hypSq - adjSq;
|
|
};
|
|
|
|
$$.math.pointInsidePolygon = function(
|
|
x, y, basePoints, centerX, centerY, width, height, direction, padding) {
|
|
|
|
//var direction = arguments[6];
|
|
var transformedPoints = new Array(basePoints.length);
|
|
|
|
// Gives negative angle
|
|
var angle = Math.asin(direction[1] / (Math.sqrt(direction[0] * direction[0]
|
|
+ direction[1] * direction[1])));
|
|
|
|
if (direction[0] < 0) {
|
|
angle = angle + Math.PI / 2;
|
|
} else {
|
|
angle = -angle - Math.PI / 2;
|
|
}
|
|
|
|
var cos = Math.cos(-angle);
|
|
var sin = Math.sin(-angle);
|
|
|
|
// console.log("base: " + basePoints);
|
|
for (var i = 0; i < transformedPoints.length / 2; i++) {
|
|
transformedPoints[i * 2] =
|
|
width / 2 * (basePoints[i * 2] * cos
|
|
- basePoints[i * 2 + 1] * sin);
|
|
|
|
transformedPoints[i * 2 + 1] =
|
|
height / 2 * (basePoints[i * 2 + 1] * cos
|
|
+ basePoints[i * 2] * sin);
|
|
|
|
transformedPoints[i * 2] += centerX;
|
|
transformedPoints[i * 2 + 1] += centerY;
|
|
}
|
|
|
|
var points;
|
|
|
|
if (padding > 0) {
|
|
var expandedLineSet = this.expandPolygon(
|
|
transformedPoints,
|
|
-padding);
|
|
|
|
points = this.joinLines(expandedLineSet);
|
|
} else {
|
|
points = transformedPoints;
|
|
}
|
|
|
|
var x1, y1, x2, y2;
|
|
var y3;
|
|
|
|
// Intersect with vertical line through (x, y)
|
|
var up = 0;
|
|
var down = 0;
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
|
|
x1 = points[i * 2];
|
|
y1 = points[i * 2 + 1];
|
|
|
|
if (i + 1 < points.length / 2) {
|
|
x2 = points[(i + 1) * 2];
|
|
y2 = points[(i + 1) * 2 + 1];
|
|
} else {
|
|
x2 = points[(i + 1 - points.length / 2) * 2];
|
|
y2 = points[(i + 1 - points.length / 2) * 2 + 1];
|
|
}
|
|
|
|
//* console.log("line from (" + x1 + ", " + y1 + ") to (" + x2 + ", " + y2 + ")");
|
|
|
|
//& console.log(x1, x, x2);
|
|
|
|
if (x1 == x && x2 == x) {
|
|
|
|
} else if ((x1 >= x && x >= x2)
|
|
|| (x1 <= x && x <= x2)) {
|
|
|
|
y3 = (x - x1) / (x2 - x1) * (y2 - y1) + y1;
|
|
|
|
if (y3 > y) {
|
|
up++;
|
|
}
|
|
|
|
if (y3 < y) {
|
|
down++;
|
|
}
|
|
|
|
//* console.log(y3, y);
|
|
|
|
} else {
|
|
//* console.log('22');
|
|
continue;
|
|
}
|
|
|
|
}
|
|
|
|
//* console.log("up: " + up + ", down: " + down);
|
|
|
|
if (up % 2 === 0) {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
$$.math.joinLines = function(lineSet) {
|
|
|
|
var vertices = new Array(lineSet.length / 2);
|
|
|
|
var currentLineStartX, currentLineStartY, currentLineEndX, currentLineEndY;
|
|
var nextLineStartX, nextLineStartY, nextLineEndX, nextLineEndY;
|
|
|
|
for (var i = 0; i < lineSet.length / 4; i++) {
|
|
currentLineStartX = lineSet[i * 4];
|
|
currentLineStartY = lineSet[i * 4 + 1];
|
|
currentLineEndX = lineSet[i * 4 + 2];
|
|
currentLineEndY = lineSet[i * 4 + 3];
|
|
|
|
if (i < lineSet.length / 4 - 1) {
|
|
nextLineStartX = lineSet[(i + 1) * 4];
|
|
nextLineStartY = lineSet[(i + 1) * 4 + 1];
|
|
nextLineEndX = lineSet[(i + 1) * 4 + 2];
|
|
nextLineEndY = lineSet[(i + 1) * 4 + 3];
|
|
} else {
|
|
nextLineStartX = lineSet[0];
|
|
nextLineStartY = lineSet[1];
|
|
nextLineEndX = lineSet[2];
|
|
nextLineEndY = lineSet[3];
|
|
}
|
|
|
|
var intersection = this.finiteLinesIntersect(
|
|
currentLineStartX, currentLineStartY,
|
|
currentLineEndX, currentLineEndY,
|
|
nextLineStartX, nextLineStartY,
|
|
nextLineEndX, nextLineEndY,
|
|
true);
|
|
|
|
vertices[i * 2] = intersection[0];
|
|
vertices[i * 2 + 1] = intersection[1];
|
|
}
|
|
|
|
return vertices;
|
|
};
|
|
|
|
$$.math.expandPolygon = function(points, pad) {
|
|
|
|
var expandedLineSet = new Array(points.length * 2);
|
|
|
|
var currentPointX, currentPointY, nextPointX, nextPointY;
|
|
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
currentPointX = points[i * 2];
|
|
currentPointY = points[i * 2 + 1];
|
|
|
|
if (i < points.length / 2 - 1) {
|
|
nextPointX = points[(i + 1) * 2];
|
|
nextPointY = points[(i + 1) * 2 + 1];
|
|
} else {
|
|
nextPointX = points[0];
|
|
nextPointY = points[1];
|
|
}
|
|
|
|
// Current line: [currentPointX, currentPointY] to [nextPointX, nextPointY]
|
|
|
|
// Assume CCW polygon winding
|
|
|
|
var offsetX = (nextPointY - currentPointY);
|
|
var offsetY = -(nextPointX - currentPointX);
|
|
|
|
// Normalize
|
|
var offsetLength = Math.sqrt(offsetX * offsetX + offsetY * offsetY);
|
|
var normalizedOffsetX = offsetX / offsetLength;
|
|
var normalizedOffsetY = offsetY / offsetLength;
|
|
|
|
expandedLineSet[i * 4] = currentPointX + normalizedOffsetX * pad;
|
|
expandedLineSet[i * 4 + 1] = currentPointY + normalizedOffsetY * pad;
|
|
expandedLineSet[i * 4 + 2] = nextPointX + normalizedOffsetX * pad;
|
|
expandedLineSet[i * 4 + 3] = nextPointY + normalizedOffsetY * pad;
|
|
}
|
|
|
|
return expandedLineSet;
|
|
};
|
|
|
|
$$.math.intersectLineEllipse = function(
|
|
x, y, centerX, centerY, ellipseWradius, ellipseHradius) {
|
|
|
|
var dispX = centerX - x;
|
|
var dispY = centerY - y;
|
|
|
|
dispX /= ellipseWradius;
|
|
dispY /= ellipseHradius;
|
|
|
|
var len = Math.sqrt(dispX * dispX + dispY * dispY);
|
|
|
|
var newLength = len - 1;
|
|
|
|
if (newLength < 0) {
|
|
return [];
|
|
}
|
|
|
|
var lenProportion = newLength / len;
|
|
|
|
return [(centerX - x) * lenProportion + x, (centerY - y) * lenProportion + y];
|
|
};
|
|
|
|
$$.math.dotProduct = function(
|
|
vec1, vec2) {
|
|
|
|
if (vec1.length != 2 || vec2.length != 2) {
|
|
throw 'dot product: arguments are not vectors';
|
|
}
|
|
|
|
return (vec1[0] * vec2[0] + vec1[1] * vec2[1]);
|
|
};
|
|
|
|
// Returns intersections of increasing distance from line's start point
|
|
$$.math.intersectLineCircle = function(
|
|
x1, y1, x2, y2, centerX, centerY, radius) {
|
|
|
|
// Calculate d, direction vector of line
|
|
var d = [x2 - x1, y2 - y1]; // Direction vector of line
|
|
var c = [centerX, centerY]; // Center of circle
|
|
var f = [x1 - centerX, y1 - centerY];
|
|
|
|
var a = d[0] * d[0] + d[1] * d[1];
|
|
var b = 2 * (f[0] * d[0] + f[1] * d[1]);
|
|
var c = (f[0] * f[0] + f[1] * f[1]) - radius * radius ;
|
|
|
|
var discriminant = b*b-4*a*c;
|
|
|
|
if (discriminant < 0) {
|
|
return [];
|
|
}
|
|
|
|
var t1 = (-b + Math.sqrt(discriminant)) / (2 * a);
|
|
var t2 = (-b - Math.sqrt(discriminant)) / (2 * a);
|
|
|
|
var tMin = Math.min(t1, t2);
|
|
var tMax = Math.max(t1, t2);
|
|
var inRangeParams = [];
|
|
|
|
if (tMin >= 0 && tMin <= 1) {
|
|
inRangeParams.push(tMin);
|
|
}
|
|
|
|
if (tMax >= 0 && tMax <= 1) {
|
|
inRangeParams.push(tMax);
|
|
}
|
|
|
|
if (inRangeParams.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
var nearIntersectionX = inRangeParams[0] * d[0] + x1;
|
|
var nearIntersectionY = inRangeParams[0] * d[1] + y1;
|
|
|
|
if (inRangeParams.length > 1) {
|
|
|
|
if (inRangeParams[0] == inRangeParams[1]) {
|
|
return [nearIntersectionX, nearIntersectionY];
|
|
} else {
|
|
|
|
var farIntersectionX = inRangeParams[1] * d[0] + x1;
|
|
var farIntersectionY = inRangeParams[1] * d[1] + y1;
|
|
|
|
return [nearIntersectionX, nearIntersectionY, farIntersectionX, farIntersectionY];
|
|
}
|
|
|
|
} else {
|
|
return [nearIntersectionX, nearIntersectionY];
|
|
}
|
|
|
|
};
|
|
|
|
$$.math.findCircleNearPoint = function(centerX, centerY,
|
|
radius, farX, farY) {
|
|
|
|
var displacementX = farX - centerX;
|
|
var displacementY = farY - centerY;
|
|
var distance = Math.sqrt(displacementX * displacementX
|
|
+ displacementY * displacementY);
|
|
|
|
var unitDisplacementX = displacementX / distance;
|
|
var unitDisplacementY = displacementY / distance;
|
|
|
|
return [centerX + unitDisplacementX * radius,
|
|
centerY + unitDisplacementY * radius];
|
|
};
|
|
|
|
$$.math.findMaxSqDistanceToOrigin = function(points) {
|
|
var maxSqDistance = 0.000001;
|
|
var sqDistance;
|
|
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
|
|
sqDistance = points[i * 2] * points[i * 2]
|
|
+ points[i * 2 + 1] * points[i * 2 + 1];
|
|
|
|
if (sqDistance > maxSqDistance) {
|
|
maxSqDistance = sqDistance;
|
|
}
|
|
}
|
|
|
|
return maxSqDistance;
|
|
};
|
|
|
|
$$.math.finiteLinesIntersect = function(
|
|
x1, y1, x2, y2, x3, y3, x4, y4, infiniteLines) {
|
|
|
|
var ua_t = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
|
|
var ub_t = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3);
|
|
var u_b = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
|
|
|
|
if (u_b !== 0) {
|
|
var ua = ua_t / u_b;
|
|
var ub = ub_t / u_b;
|
|
|
|
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
|
|
return [x1 + ua * (x2 - x1), y1 + ua * (y2 - y1)];
|
|
|
|
} else {
|
|
if (!infiniteLines) {
|
|
return [];
|
|
} else {
|
|
return [x1 + ua * (x2 - x1), y1 + ua * (y2 - y1)];
|
|
}
|
|
}
|
|
} else {
|
|
if (ua_t === 0 || ub_t === 0) {
|
|
|
|
// Parallel, coincident lines. Check if overlap
|
|
|
|
// Check endpoint of second line
|
|
if ([x1, x2, x4].sort()[1] === x4) {
|
|
return [x4, y4];
|
|
}
|
|
|
|
// Check start point of second line
|
|
if ([x1, x2, x3].sort()[1] === x3) {
|
|
return [x3, y3];
|
|
}
|
|
|
|
// Endpoint of first line
|
|
if ([x3, x4, x2].sort()[1] === x2) {
|
|
return [x2, y2];
|
|
}
|
|
|
|
return [];
|
|
} else {
|
|
|
|
// Parallel, non-coincident
|
|
return [];
|
|
}
|
|
}
|
|
};
|
|
|
|
// (boxMinX, boxMinY, boxMaxX, boxMaxY, padding,
|
|
// cornerRadius * 2, cornerRadius * 2, vBoxTopLeftX + padding, hBoxTopLeftY + padding)) {
|
|
|
|
$$.math.boxIntersectEllipse = function(
|
|
x1, y1, x2, y2, padding, width, height, centerX, centerY) {
|
|
|
|
if (x2 < x1) {
|
|
var oldX1 = x1;
|
|
x1 = x2;
|
|
x2 = oldX1;
|
|
}
|
|
|
|
if (y2 < y1) {
|
|
var oldY1 = y1;
|
|
y1 = y2;
|
|
y2 = oldY1;
|
|
}
|
|
|
|
// 4 ortho extreme points
|
|
var west = [centerX - width / 2 - padding, centerY];
|
|
var east = [centerX + width / 2 + padding, centerY];
|
|
var north = [centerX, centerY - height / 2 - padding];
|
|
var south = [centerX, centerY + height / 2 + padding];
|
|
|
|
// out of bounds: return false
|
|
if (x2 < west[0]) {
|
|
return false;
|
|
}
|
|
|
|
if (x1 > east[0]) {
|
|
return false;
|
|
}
|
|
|
|
if (y1 > south[1]) {
|
|
return false;
|
|
}
|
|
|
|
if (y2 < north[1]) {
|
|
return false;
|
|
}
|
|
|
|
// 1 of 4 ortho extreme points in box: return true
|
|
if (x1 <= east[0] && east[0] <= x2
|
|
&& y1 <= east[1] && east[1] <= y2) {
|
|
return true;
|
|
}
|
|
|
|
if (x1 <= west[0] && west[0] <= x2
|
|
&& y1 <= west[1] && west[1] <= y2) {
|
|
return true;
|
|
}
|
|
|
|
if (x1 <= north[0] && north[0] <= x2
|
|
&& y1 <= north[1] && north[1] <= y2) {
|
|
return true;
|
|
}
|
|
|
|
if (x1 <= south[0] && south[0] <= x2
|
|
&& y1 <= south[1] && south[1] <= y2) {
|
|
return true;
|
|
}
|
|
|
|
// box corner in ellipse: return true
|
|
x1 = (x1 - centerX) / (width / 2 + padding);
|
|
x2 = (x2 - centerX) / (width / 2 + padding);
|
|
|
|
y1 = (y1 - centerY) / (height / 2 + padding);
|
|
y2 = (y2 - centerY) / (height / 2 + padding);
|
|
|
|
if (x1 * x1 + y1 * y1 <= 1) {
|
|
return true;
|
|
}
|
|
|
|
if (x2 * x2 + y1 * y1 <= 1) {
|
|
return true;
|
|
}
|
|
|
|
if (x2 * x2 + y2 * y2 <= 1) {
|
|
return true;
|
|
}
|
|
|
|
if (x1 * x1 + y2 * y2 <= 1) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
$$.math.boxIntersectPolygon = function(
|
|
x1, y1, x2, y2, basePoints, width, height, centerX, centerY, direction, padding) {
|
|
|
|
// console.log(arguments);
|
|
|
|
if (x2 < x1) {
|
|
var oldX1 = x1;
|
|
x1 = x2;
|
|
x2 = oldX1;
|
|
}
|
|
|
|
if (y2 < y1) {
|
|
var oldY1 = y1;
|
|
y1 = y2;
|
|
y2 = oldY1;
|
|
}
|
|
|
|
var transformedPoints = new Array(basePoints.length);
|
|
|
|
// Gives negative of angle
|
|
var angle = Math.asin(direction[1] / (Math.sqrt(direction[0] * direction[0]
|
|
+ direction[1] * direction[1])));
|
|
|
|
if (direction[0] < 0) {
|
|
angle = angle + Math.PI / 2;
|
|
} else {
|
|
angle = -angle - Math.PI / 2;
|
|
}
|
|
|
|
var cos = Math.cos(-angle);
|
|
var sin = Math.sin(-angle);
|
|
|
|
for (var i = 0; i < transformedPoints.length / 2; i++) {
|
|
transformedPoints[i * 2] =
|
|
width / 2 * (basePoints[i * 2] * cos
|
|
- basePoints[i * 2 + 1] * sin);
|
|
|
|
transformedPoints[i * 2 + 1] =
|
|
height / 2 * (basePoints[i * 2 + 1] * cos
|
|
+ basePoints[i * 2] * sin);
|
|
|
|
transformedPoints[i * 2] += centerX;
|
|
transformedPoints[i * 2 + 1] += centerY;
|
|
}
|
|
|
|
// Assume transformedPoints.length > 0, and check if intersection is possible
|
|
var minTransformedX = transformedPoints[0];
|
|
var maxTransformedX = transformedPoints[0];
|
|
var minTransformedY = transformedPoints[1];
|
|
var maxTransformedY = transformedPoints[1];
|
|
|
|
for (var i = 1; i < transformedPoints.length / 2; i++) {
|
|
if (transformedPoints[i * 2] > maxTransformedX) {
|
|
maxTransformedX = transformedPoints[i * 2];
|
|
}
|
|
|
|
if (transformedPoints[i * 2] < minTransformedX) {
|
|
minTransformedX = transformedPoints[i * 2];
|
|
}
|
|
|
|
if (transformedPoints[i * 2 + 1] > maxTransformedY) {
|
|
maxTransformedY = transformedPoints[i * 2 + 1];
|
|
}
|
|
|
|
if (transformedPoints[i * 2 + 1] < minTransformedY) {
|
|
minTransformedY = transformedPoints[i * 2 + 1];
|
|
}
|
|
}
|
|
|
|
if (x2 < minTransformedX - padding) {
|
|
return false;
|
|
}
|
|
|
|
if (x1 > maxTransformedX + padding) {
|
|
return false;
|
|
}
|
|
|
|
if (y2 < minTransformedY - padding) {
|
|
return false;
|
|
}
|
|
|
|
if (y1 > maxTransformedY + padding) {
|
|
return false;
|
|
}
|
|
|
|
// Continue checking with padding-corrected points
|
|
var points;
|
|
|
|
if (padding > 0) {
|
|
var expandedLineSet = $$.math.expandPolygon(
|
|
transformedPoints,
|
|
-padding);
|
|
|
|
points = $$.math.joinLines(expandedLineSet);
|
|
} else {
|
|
points = transformedPoints;
|
|
}
|
|
|
|
// Check if a point is in box
|
|
for (var i = 0; i < transformedPoints.length / 2; i++) {
|
|
if (x1 <= transformedPoints[i * 2]
|
|
&& transformedPoints[i * 2] <= x2) {
|
|
|
|
if (y1 <= transformedPoints[i * 2 + 1]
|
|
&& transformedPoints[i * 2 + 1] <= y2) {
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Check for intersections with the selection box
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
|
|
var currentX = points[i * 2];
|
|
var currentY = points[i * 2 + 1];
|
|
var nextX;
|
|
var nextY;
|
|
|
|
if (i < points.length / 2 - 1) {
|
|
nextX = points[(i + 1) * 2];
|
|
nextY = points[(i + 1) * 2 + 1];
|
|
} else {
|
|
nextX = points[0];
|
|
nextY = points[1];
|
|
}
|
|
|
|
// Intersection with top of selection box
|
|
if ($$.math.finiteLinesIntersect(currentX, currentY, nextX, nextY, x1, y1, x2, y1, false).length > 0) {
|
|
return true;
|
|
}
|
|
|
|
// Intersection with bottom of selection box
|
|
if ($$.math.finiteLinesIntersect(currentX, currentY, nextX, nextY, x1, y2, x2, y2, false).length > 0) {
|
|
return true;
|
|
}
|
|
|
|
// Intersection with left side of selection box
|
|
if ($$.math.finiteLinesIntersect(currentX, currentY, nextX, nextY, x1, y1, x1, y2, false).length > 0) {
|
|
return true;
|
|
}
|
|
|
|
// Intersection with right side of selection box
|
|
if ($$.math.finiteLinesIntersect(currentX, currentY, nextX, nextY, x2, y1, x2, y2, false).length > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
// Check if box corner in the polygon
|
|
if ($$.math.pointInsidePolygon(
|
|
x1, y1, points, 0, 0, 1, 1, 0, direction)) {
|
|
|
|
return true;
|
|
} else if ($$.math.pointInsidePolygon(
|
|
x1, y2, points, 0, 0, 1, 1, 0, direction)) {
|
|
|
|
return true;
|
|
} else if ($$.math.pointInsidePolygon(
|
|
x2, y2, points, 0, 0, 1, 1, 0, direction)) {
|
|
|
|
return true;
|
|
} else if ($$.math.pointInsidePolygon(
|
|
x2, y1, points, 0, 0, 1, 1, 0, direction)) {
|
|
|
|
return true;
|
|
}
|
|
*/
|
|
return false;
|
|
};
|
|
|
|
$$.math.polygonIntersectLine = function(
|
|
x, y, basePoints, centerX, centerY, width, height, padding) {
|
|
|
|
var intersections = [];
|
|
var intersection;
|
|
|
|
var transformedPoints = new Array(basePoints.length);
|
|
|
|
for (var i = 0; i < transformedPoints.length / 2; i++) {
|
|
transformedPoints[i * 2] = basePoints[i * 2] * width + centerX;
|
|
transformedPoints[i * 2 + 1] = basePoints[i * 2 + 1] * height + centerY;
|
|
}
|
|
|
|
var points;
|
|
|
|
if (padding > 0) {
|
|
var expandedLineSet = $$.math.expandPolygon(
|
|
transformedPoints,
|
|
-padding);
|
|
|
|
points = $$.math.joinLines(expandedLineSet);
|
|
} else {
|
|
points = transformedPoints;
|
|
}
|
|
// var points = transformedPoints;
|
|
|
|
var currentX, currentY, nextX, nextY;
|
|
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
|
|
currentX = points[i * 2];
|
|
currentY = points[i * 2 + 1];
|
|
|
|
if (i < points.length / 2 - 1) {
|
|
nextX = points[(i + 1) * 2];
|
|
nextY = points[(i + 1) * 2 + 1];
|
|
} else {
|
|
nextX = points[0];
|
|
nextY = points[1];
|
|
}
|
|
|
|
intersection = this.finiteLinesIntersect(
|
|
x, y, centerX, centerY,
|
|
currentX, currentY,
|
|
nextX, nextY);
|
|
|
|
if (intersection.length !== 0) {
|
|
intersections.push(intersection[0], intersection[1]);
|
|
}
|
|
}
|
|
|
|
return intersections;
|
|
};
|
|
|
|
$$.math.shortenIntersection = function(
|
|
intersection, offset, amount) {
|
|
|
|
var disp = [intersection[0] - offset[0], intersection[1] - offset[1]];
|
|
|
|
var length = Math.sqrt(disp[0] * disp[0] + disp[1] * disp[1]);
|
|
|
|
var lenRatio = (length - amount) / length;
|
|
|
|
if (lenRatio < 0) {
|
|
lenRatio = 0.00001;
|
|
}
|
|
|
|
return [offset[0] + lenRatio * disp[0], offset[1] + lenRatio * disp[1]];
|
|
};
|
|
|
|
$$.math.generateUnitNgonPointsFitToSquare = function(sides, rotationRadians) {
|
|
var points = $$.math.generateUnitNgonPoints(sides, rotationRadians);
|
|
points = $$.math.fitPolygonToSquare(points);
|
|
|
|
return points;
|
|
};
|
|
|
|
$$.math.fitPolygonToSquare = function(points){
|
|
var x, y;
|
|
var sides = points.length/2;
|
|
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
|
|
for (var i = 0; i < sides; i++) {
|
|
x = points[2 * i];
|
|
y = points[2 * i + 1];
|
|
|
|
minX = Math.min( minX, x );
|
|
maxX = Math.max( maxX, x );
|
|
minY = Math.min( minY, y );
|
|
maxY = Math.max( maxY, y );
|
|
}
|
|
|
|
// stretch factors
|
|
var sx = 2 / (maxX - minX);
|
|
var sy = 2 / (maxY - minY);
|
|
|
|
for (var i = 0; i < sides; i++){
|
|
x = points[2 * i] = points[2 * i] * sx;
|
|
y = points[2 * i + 1] = points[2 * i + 1] * sy;
|
|
|
|
minX = Math.min( minX, x );
|
|
maxX = Math.max( maxX, x );
|
|
minY = Math.min( minY, y );
|
|
maxY = Math.max( maxY, y );
|
|
}
|
|
|
|
if( minY < -1 ){
|
|
for (var i = 0; i < sides; i++){
|
|
y = points[2 * i + 1] = points[2 * i + 1] + (-1 -minY);
|
|
}
|
|
}
|
|
|
|
return points;
|
|
};
|
|
|
|
$$.math.generateUnitNgonPoints = function(sides, rotationRadians) {
|
|
|
|
var increment = 1.0 / sides * 2 * Math.PI;
|
|
var startAngle = sides % 2 === 0 ?
|
|
Math.PI / 2.0 + increment / 2.0 : Math.PI / 2.0;
|
|
// console.log(nodeShapes['square']);
|
|
startAngle += rotationRadians;
|
|
|
|
var points = new Array(sides * 2);
|
|
|
|
var currentAngle, x, y;
|
|
for (var i = 0; i < sides; i++) {
|
|
currentAngle = i * increment + startAngle;
|
|
|
|
x = points[2 * i] = Math.cos(currentAngle);// * (1 + i/2);
|
|
y = points[2 * i + 1] = Math.sin(-currentAngle);// * (1 + i/2);
|
|
}
|
|
|
|
return points;
|
|
};
|
|
|
|
$$.math.getRoundRectangleRadius = function(width, height) {
|
|
|
|
// Set the default radius, unless half of width or height is smaller than default
|
|
return Math.min(width / 4, height / 4, 8);
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// registered extensions to cytoscape, indexed by name
|
|
var extensions = {};
|
|
$$.extensions = extensions;
|
|
|
|
// registered modules for extensions, indexed by name
|
|
var modules = {};
|
|
$$.modules = modules;
|
|
|
|
function setExtension(type, name, registrant){
|
|
var impl = {};
|
|
impl[name] = registrant;
|
|
|
|
switch( type ){
|
|
case 'core':
|
|
case 'collection':
|
|
$$.fn[type]( impl );
|
|
}
|
|
|
|
// fill in missing layout functions in the prototype
|
|
if( type === 'layout' ){
|
|
var layoutProto = registrant.prototype;
|
|
var optLayoutFns = [];
|
|
|
|
for( var i = 0; i < optLayoutFns.length; i++ ){
|
|
var fnName = optLayoutFns[i];
|
|
|
|
layoutProto[fnName] = layoutProto[fnName] || function(){ return this; };
|
|
}
|
|
|
|
// either .start() or .run() is defined, so autogen the other
|
|
if( layoutProto.start && !layoutProto.run ){
|
|
layoutProto.run = function(){ this.start(); return this; };
|
|
} else if( !layoutProto.start && layoutProto.run ){
|
|
layoutProto.start = function(){ this.run(); return this; };
|
|
}
|
|
|
|
if( !layoutProto.stop ){
|
|
layoutProto.stop = function(){
|
|
var opts = this.options;
|
|
|
|
if( opts && opts.animate ){
|
|
opts.eles.stop();
|
|
}
|
|
|
|
return this;
|
|
};
|
|
}
|
|
|
|
layoutProto.on = $$.define.on({ layout: true });
|
|
layoutProto.one = $$.define.on({ layout: true, unbindSelfOnTrigger: true });
|
|
layoutProto.once = $$.define.on({ layout: true, unbindAllBindersOnTrigger: true });
|
|
layoutProto.off = $$.define.off({ layout: true });
|
|
layoutProto.trigger = $$.define.trigger({ layout: true });
|
|
|
|
$$.define.eventAliasesOn( layoutProto );
|
|
}
|
|
|
|
return $$.util.setMap({
|
|
map: extensions,
|
|
keys: [ type, name ],
|
|
value: registrant
|
|
});
|
|
}
|
|
|
|
function getExtension(type, name){
|
|
return $$.util.getMap({
|
|
map: extensions,
|
|
keys: [ type, name ]
|
|
});
|
|
}
|
|
|
|
function setModule(type, name, moduleType, moduleName, registrant){
|
|
return $$.util.setMap({
|
|
map: modules,
|
|
keys: [ type, name, moduleType, moduleName ],
|
|
value: registrant
|
|
});
|
|
}
|
|
|
|
function getModule(type, name, moduleType, moduleName){
|
|
return $$.util.getMap({
|
|
map: modules,
|
|
keys: [ type, name, moduleType, moduleName ]
|
|
});
|
|
}
|
|
|
|
$$.extension = function(){
|
|
// e.g. $$.extension('renderer', 'svg')
|
|
if( arguments.length == 2 ){
|
|
return getExtension.apply(this, arguments);
|
|
}
|
|
|
|
// e.g. $$.extension('renderer', 'svg', { ... })
|
|
else if( arguments.length == 3 ){
|
|
return setExtension.apply(this, arguments);
|
|
}
|
|
|
|
// e.g. $$.extension('renderer', 'svg', 'nodeShape', 'ellipse')
|
|
else if( arguments.length == 4 ){
|
|
return getModule.apply(this, arguments);
|
|
}
|
|
|
|
// e.g. $$.extension('renderer', 'svg', 'nodeShape', 'ellipse', { ... })
|
|
else if( arguments.length == 5 ){
|
|
return setModule.apply(this, arguments);
|
|
}
|
|
|
|
else {
|
|
$$.util.error('Invalid extension access syntax');
|
|
}
|
|
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($, $$){ 'use strict';
|
|
|
|
var cyReg = function( $ele ){
|
|
var d = $ele[0]._cyreg = $ele[0]._cyreg || {};
|
|
|
|
return d;
|
|
};
|
|
|
|
$$.registerJquery = function( $ ){
|
|
if( !$ ){ return; } // no jquery => don't need this
|
|
|
|
if( $.fn.cytoscape ){ return; } // already registered
|
|
|
|
// allow calls on a jQuery selector by proxying calls to $.cytoscape
|
|
// e.g. $("#foo").cytoscape(options) => $.cytoscape(options) on #foo
|
|
$.fn.cytoscape = function(opts){
|
|
var $this = $(this);
|
|
|
|
// get object
|
|
if( opts === 'get' ){
|
|
return cyReg( $this ).cy;
|
|
}
|
|
|
|
// bind to ready
|
|
else if( $$.is.fn(opts) ){
|
|
|
|
var ready = opts;
|
|
var cy = cyReg( $this ).cy;
|
|
|
|
if( cy && cy.isReady() ){ // already ready so just trigger now
|
|
cy.trigger('ready', [], ready);
|
|
|
|
} else { // not yet ready, so add to readies list
|
|
var data = cyReg( $this );
|
|
var readies = data.readies = data.readies || [];
|
|
|
|
readies.push( ready );
|
|
}
|
|
|
|
}
|
|
|
|
// proxy to create instance
|
|
else if( $$.is.plainObject(opts) ){
|
|
return $this.each(function(){
|
|
var options = $.extend({}, opts, {
|
|
container: $(this)[0]
|
|
});
|
|
|
|
cytoscape(options);
|
|
});
|
|
}
|
|
};
|
|
|
|
// allow access to the global cytoscape object under jquery for legacy reasons
|
|
$.cytoscape = cytoscape;
|
|
|
|
// use short alias (cy) if not already defined
|
|
if( $.fn.cy == null && $.cy == null ){
|
|
$.fn.cy = $.fn.cytoscape;
|
|
$.cy = $.cytoscape;
|
|
}
|
|
};
|
|
|
|
$$.registerJquery( $ ); // try to register with global jquery for convenience
|
|
|
|
$$.util.require('jquery', function( $ ){
|
|
$$.registerJquery( $ ); // try to register with require()d jquery
|
|
});
|
|
|
|
})(typeof jQuery !== 'undefined' ? jQuery : null , cytoscape);
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// shamelessly taken from jQuery
|
|
// https://github.com/jquery/jquery/blob/master/src/event.js
|
|
|
|
$$.Event = function( src, props ) {
|
|
// Allow instantiation without the 'new' keyword
|
|
if ( !(this instanceof $$.Event) ) {
|
|
return new $$.Event( src, props );
|
|
}
|
|
|
|
// Event object
|
|
if ( src && src.type ) {
|
|
this.originalEvent = src;
|
|
this.type = src.type;
|
|
|
|
// Events bubbling up the document may have been marked as prevented
|
|
// by a handler lower down the tree; reflect the correct value.
|
|
this.isDefaultPrevented = ( src.defaultPrevented ) ? returnTrue : returnFalse;
|
|
|
|
// Event type
|
|
} else {
|
|
this.type = src;
|
|
}
|
|
|
|
// Put explicitly provided properties onto the event object
|
|
if ( props ) {
|
|
// $$.util.extend( this, props );
|
|
|
|
// more efficient to manually copy fields we use
|
|
this.type = props.type !== undefined ? props.type : this.type;
|
|
this.cy = props.cy;
|
|
this.cyTarget = props.cyTarget;
|
|
this.cyPosition = props.cyPosition;
|
|
this.cyRenderedPosition = props.cyRenderedPosition;
|
|
this.namespace = props.namespace;
|
|
this.layout = props.layout;
|
|
this.data = props.data;
|
|
this.message = props.message;
|
|
}
|
|
|
|
// Create a timestamp if incoming event doesn't have one
|
|
this.timeStamp = src && src.timeStamp || +new Date();
|
|
};
|
|
|
|
function returnFalse() {
|
|
return false;
|
|
}
|
|
function returnTrue() {
|
|
return true;
|
|
}
|
|
|
|
// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
|
|
// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
|
|
$$.Event.prototype = {
|
|
preventDefault: function() {
|
|
this.isDefaultPrevented = returnTrue;
|
|
|
|
var e = this.originalEvent;
|
|
if ( !e ) {
|
|
return;
|
|
}
|
|
|
|
// if preventDefault exists run it on the original event
|
|
if ( e.preventDefault ) {
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
stopPropagation: function() {
|
|
this.isPropagationStopped = returnTrue;
|
|
|
|
var e = this.originalEvent;
|
|
if ( !e ) {
|
|
return;
|
|
}
|
|
// if stopPropagation exists run it on the original event
|
|
if ( e.stopPropagation ) {
|
|
e.stopPropagation();
|
|
}
|
|
},
|
|
stopImmediatePropagation: function() {
|
|
this.isImmediatePropagationStopped = returnTrue;
|
|
this.stopPropagation();
|
|
},
|
|
isDefaultPrevented: returnFalse,
|
|
isPropagationStopped: returnFalse,
|
|
isImmediatePropagationStopped: returnFalse
|
|
};
|
|
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// use this module to cherry pick functions into your prototype
|
|
// (useful for functions shared between the core and collections, for example)
|
|
|
|
// e.g.
|
|
// $$.fn.collection({
|
|
// foo: $$.define.foo({ /* params... */ })
|
|
// });
|
|
|
|
$$.define = {
|
|
|
|
// access data field
|
|
data: function( params ){
|
|
var defaults = {
|
|
field: 'data',
|
|
bindingEvent: 'data',
|
|
allowBinding: false,
|
|
allowSetting: false,
|
|
allowGetting: false,
|
|
settingEvent: 'data',
|
|
settingTriggersEvent: false,
|
|
triggerFnName: 'trigger',
|
|
immutableKeys: {}, // key => true if immutable
|
|
updateStyle: false,
|
|
onSet: function( self ){},
|
|
canSet: function( self ){ return true; }
|
|
};
|
|
params = $$.util.extend({}, defaults, params);
|
|
|
|
return function dataImpl( name, value ){
|
|
var p = params;
|
|
var self = this;
|
|
var selfIsArrayLike = self.length !== undefined;
|
|
var all = selfIsArrayLike ? self : [self]; // put in array if not array-like
|
|
var single = selfIsArrayLike ? self[0] : self;
|
|
|
|
// .data('foo', ...)
|
|
if( $$.is.string(name) ){ // set or get property
|
|
|
|
// .data('foo')
|
|
if( p.allowGetting && value === undefined ){ // get
|
|
|
|
var ret;
|
|
if( single ){
|
|
ret = single._private[ p.field ][ name ];
|
|
}
|
|
return ret;
|
|
|
|
// .data('foo', 'bar')
|
|
} else if( p.allowSetting && value !== undefined ) { // set
|
|
var valid = !p.immutableKeys[name];
|
|
if( valid ){
|
|
for( var i = 0, l = all.length; i < l; i++ ){
|
|
if( p.canSet( all[i] ) ){
|
|
all[i]._private[ p.field ][ name ] = value;
|
|
}
|
|
}
|
|
|
|
// update mappers if asked
|
|
if( p.updateStyle ){ self.updateStyle(); }
|
|
|
|
// call onSet callback
|
|
p.onSet( self );
|
|
|
|
if( p.settingTriggersEvent ){
|
|
self[ p.triggerFnName ]( p.settingEvent );
|
|
}
|
|
}
|
|
}
|
|
|
|
// .data({ 'foo': 'bar' })
|
|
} else if( p.allowSetting && $$.is.plainObject(name) ){ // extend
|
|
var obj = name;
|
|
var k, v;
|
|
|
|
for( k in obj ){
|
|
v = obj[ k ];
|
|
|
|
var valid = !p.immutableKeys[k];
|
|
if( valid ){
|
|
for( var i = 0, l = all.length; i < l; i++ ){
|
|
if( p.canSet( all[i] ) ){
|
|
all[i]._private[ p.field ][ k ] = v;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// update mappers if asked
|
|
if( p.updateStyle ){ self.updateStyle(); }
|
|
|
|
// call onSet callback
|
|
p.onSet( self );
|
|
|
|
if( p.settingTriggersEvent ){
|
|
self[ p.triggerFnName ]( p.settingEvent );
|
|
}
|
|
|
|
// .data(function(){ ... })
|
|
} else if( p.allowBinding && $$.is.fn(name) ){ // bind to event
|
|
var fn = name;
|
|
self.bind( p.bindingEvent, fn );
|
|
|
|
// .data()
|
|
} else if( p.allowGetting && name === undefined ){ // get whole object
|
|
var ret;
|
|
if( single ){
|
|
ret = single._private[ p.field ];
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
return self; // maintain chainability
|
|
}; // function
|
|
}, // data
|
|
|
|
// remove data field
|
|
removeData: function( params ){
|
|
var defaults = {
|
|
field: 'data',
|
|
event: 'data',
|
|
triggerFnName: 'trigger',
|
|
triggerEvent: false,
|
|
immutableKeys: {} // key => true if immutable
|
|
};
|
|
params = $$.util.extend({}, defaults, params);
|
|
|
|
return function removeDataImpl( names ){
|
|
var p = params;
|
|
var self = this;
|
|
var selfIsArrayLike = self.length !== undefined;
|
|
var all = selfIsArrayLike ? self : [self]; // put in array if not array-like
|
|
|
|
// .removeData('foo bar')
|
|
if( $$.is.string(names) ){ // then get the list of keys, and delete them
|
|
var keys = names.split(/\s+/);
|
|
var l = keys.length;
|
|
|
|
for( var i = 0; i < l; i++ ){ // delete each non-empty key
|
|
var key = keys[i];
|
|
if( $$.is.emptyString(key) ){ continue; }
|
|
|
|
var valid = !p.immutableKeys[ key ]; // not valid if immutable
|
|
if( valid ){
|
|
for( var i_a = 0, l_a = all.length; i_a < l_a; i_a++ ){
|
|
all[ i_a ]._private[ p.field ][ key ] = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( p.triggerEvent ){
|
|
self[ p.triggerFnName ]( p.event );
|
|
}
|
|
|
|
// .removeData()
|
|
} else if( names === undefined ){ // then delete all keys
|
|
|
|
for( var i_a = 0, l_a = all.length; i_a < l_a; i_a++ ){
|
|
var _privateFields = all[ i_a ]._private[ p.field ];
|
|
|
|
for( var key in _privateFields ){
|
|
var validKeyToDelete = !p.immutableKeys[ key ];
|
|
|
|
if( validKeyToDelete ){
|
|
_privateFields[ key ] = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( p.triggerEvent ){
|
|
self[ p.triggerFnName ]( p.event );
|
|
}
|
|
}
|
|
|
|
return self; // maintain chaining
|
|
}; // function
|
|
}, // removeData
|
|
|
|
// event function reusable stuff
|
|
event: {
|
|
regex: /(\w+)(\.\w+)?/, // regex for matching event strings (e.g. "click.namespace")
|
|
optionalTypeRegex: /(\w+)?(\.\w+)?/,
|
|
falseCallback: function(){ return false; }
|
|
},
|
|
|
|
// event binding
|
|
on: function( params ){
|
|
var defaults = {
|
|
unbindSelfOnTrigger: false,
|
|
unbindAllBindersOnTrigger: false
|
|
};
|
|
params = $$.util.extend({}, defaults, params);
|
|
|
|
return function onImpl(events, selector, data, callback){
|
|
var self = this;
|
|
var selfIsArrayLike = self.length !== undefined;
|
|
var all = selfIsArrayLike ? self : [self]; // put in array if not array-like
|
|
var eventsIsString = $$.is.string(events);
|
|
var p = params;
|
|
|
|
if( $$.is.plainObject(selector) ){ // selector is actually data
|
|
callback = data;
|
|
data = selector;
|
|
selector = undefined;
|
|
} else if( $$.is.fn(selector) || selector === false ){ // selector is actually callback
|
|
callback = selector;
|
|
data = undefined;
|
|
selector = undefined;
|
|
}
|
|
|
|
if( $$.is.fn(data) || data === false ){ // data is actually callback
|
|
callback = data;
|
|
data = undefined;
|
|
}
|
|
|
|
// if there isn't a callback, we can't really do anything
|
|
// (can't speak for mapped events arg version)
|
|
if( !($$.is.fn(callback) || callback === false) && eventsIsString ){
|
|
return self; // maintain chaining
|
|
}
|
|
|
|
if( eventsIsString ){ // then convert to map
|
|
var map = {};
|
|
map[ events ] = callback;
|
|
events = map;
|
|
}
|
|
|
|
for( var evts in events ){
|
|
callback = events[evts];
|
|
if( callback === false ){
|
|
callback = $$.define.event.falseCallback;
|
|
}
|
|
|
|
if( !$$.is.fn(callback) ){ continue; }
|
|
|
|
evts = evts.split(/\s+/);
|
|
for( var i = 0; i < evts.length; i++ ){
|
|
var evt = evts[i];
|
|
if( $$.is.emptyString(evt) ){ continue; }
|
|
|
|
var match = evt.match( $$.define.event.regex ); // type[.namespace]
|
|
|
|
if( match ){
|
|
var type = match[1];
|
|
var namespace = match[2] ? match[2] : undefined;
|
|
|
|
var listener = {
|
|
callback: callback, // callback to run
|
|
data: data, // extra data in eventObj.data
|
|
delegated: selector ? true : false, // whether the evt is delegated
|
|
selector: selector, // the selector to match for delegated events
|
|
selObj: new $$.Selector(selector), // cached selector object to save rebuilding
|
|
type: type, // the event type (e.g. 'click')
|
|
namespace: namespace, // the event namespace (e.g. ".foo")
|
|
unbindSelfOnTrigger: p.unbindSelfOnTrigger,
|
|
unbindAllBindersOnTrigger: p.unbindAllBindersOnTrigger,
|
|
binders: all // who bound together
|
|
};
|
|
|
|
for( var j = 0; j < all.length; j++ ){
|
|
var _p = all[j]._private;
|
|
|
|
_p.listeners = _p.listeners || [];
|
|
_p.listeners.push( listener );
|
|
}
|
|
}
|
|
} // for events array
|
|
} // for events map
|
|
|
|
return self; // maintain chaining
|
|
}; // function
|
|
}, // on
|
|
|
|
eventAliasesOn: function( proto ){
|
|
var p = proto;
|
|
|
|
p.addListener = p.listen = p.bind = p.on;
|
|
p.removeListener = p.unlisten = p.unbind = p.off;
|
|
p.emit = p.trigger;
|
|
|
|
// this is just a wrapper alias of .on()
|
|
p.pon = p.promiseOn = function( events, selector ){
|
|
var self = this;
|
|
var args = Array.prototype.slice.call( arguments, 0 );
|
|
|
|
return new $$.Promise(function( resolve, reject ){
|
|
var callback = function( e ){
|
|
self.off.apply( self, offArgs );
|
|
|
|
resolve( e );
|
|
};
|
|
|
|
var onArgs = args.concat([ callback ]);
|
|
var offArgs = onArgs.concat([]);
|
|
|
|
self.on.apply( self, onArgs );
|
|
});
|
|
};
|
|
},
|
|
|
|
off: function offImpl( params ){
|
|
var defaults = {
|
|
};
|
|
params = $$.util.extend({}, defaults, params);
|
|
|
|
return function(events, selector, callback){
|
|
var self = this;
|
|
var selfIsArrayLike = self.length !== undefined;
|
|
var all = selfIsArrayLike ? self : [self]; // put in array if not array-like
|
|
var eventsIsString = $$.is.string(events);
|
|
|
|
if( arguments.length === 0 ){ // then unbind all
|
|
|
|
for( var i = 0; i < all.length; i++ ){
|
|
all[i]._private.listeners = [];
|
|
}
|
|
|
|
return self; // maintain chaining
|
|
}
|
|
|
|
if( $$.is.fn(selector) || selector === false ){ // selector is actually callback
|
|
callback = selector;
|
|
selector = undefined;
|
|
}
|
|
|
|
if( eventsIsString ){ // then convert to map
|
|
var map = {};
|
|
map[ events ] = callback;
|
|
events = map;
|
|
}
|
|
|
|
for( var evts in events ){
|
|
callback = events[evts];
|
|
|
|
if( callback === false ){
|
|
callback = $$.define.event.falseCallback;
|
|
}
|
|
|
|
evts = evts.split(/\s+/);
|
|
for( var h = 0; h < evts.length; h++ ){
|
|
var evt = evts[h];
|
|
if( $$.is.emptyString(evt) ){ continue; }
|
|
|
|
var match = evt.match( $$.define.event.optionalTypeRegex ); // [type][.namespace]
|
|
if( match ){
|
|
var type = match[1] ? match[1] : undefined;
|
|
var namespace = match[2] ? match[2] : undefined;
|
|
|
|
for( var i = 0; i < all.length; i++ ){ //
|
|
var listeners = all[i]._private.listeners = all[i]._private.listeners || [];
|
|
|
|
for( var j = 0; j < listeners.length; j++ ){
|
|
var listener = listeners[j];
|
|
var nsMatches = !namespace || namespace === listener.namespace;
|
|
var typeMatches = !type || listener.type === type;
|
|
var cbMatches = !callback || callback === listener.callback;
|
|
var listenerMatches = nsMatches && typeMatches && cbMatches;
|
|
|
|
// delete listener if it matches
|
|
if( listenerMatches ){
|
|
listeners.splice(j, 1);
|
|
j--;
|
|
}
|
|
} // for listeners
|
|
} // for all
|
|
} // if match
|
|
} // for events array
|
|
|
|
} // for events map
|
|
|
|
return self; // maintain chaining
|
|
}; // function
|
|
}, // off
|
|
|
|
trigger: function( params ){
|
|
var defaults = {};
|
|
params = $$.util.extend({}, defaults, params);
|
|
|
|
return function triggerImpl(events, extraParams, fnToTrigger){
|
|
var self = this;
|
|
var selfIsArrayLike = self.length !== undefined;
|
|
var all = selfIsArrayLike ? self : [self]; // put in array if not array-like
|
|
var eventsIsString = $$.is.string(events);
|
|
var eventsIsObject = $$.is.plainObject(events);
|
|
var eventsIsEvent = $$.is.event(events);
|
|
var cy = this._private.cy || ( $$.is.core(this) ? this : null );
|
|
var hasCompounds = cy ? cy.hasCompoundNodes() : false;
|
|
|
|
if( eventsIsString ){ // then make a plain event object for each event name
|
|
var evts = events.split(/\s+/);
|
|
events = [];
|
|
|
|
for( var i = 0; i < evts.length; i++ ){
|
|
var evt = evts[i];
|
|
if( $$.is.emptyString(evt) ){ continue; }
|
|
|
|
var match = evt.match( $$.define.event.regex ); // type[.namespace]
|
|
var type = match[1];
|
|
var namespace = match[2] ? match[2] : undefined;
|
|
|
|
events.push( {
|
|
type: type,
|
|
namespace: namespace
|
|
} );
|
|
}
|
|
} else if( eventsIsObject ){ // put in length 1 array
|
|
var eventArgObj = events;
|
|
|
|
events = [ eventArgObj ];
|
|
}
|
|
|
|
if( extraParams ){
|
|
if( !$$.is.array(extraParams) ){ // make sure extra params are in an array if specified
|
|
extraParams = [ extraParams ];
|
|
}
|
|
} else { // otherwise, we've got nothing
|
|
extraParams = [];
|
|
}
|
|
|
|
for( var i = 0; i < events.length; i++ ){ // trigger each event in order
|
|
var evtObj = events[i];
|
|
|
|
for( var j = 0; j < all.length; j++ ){ // for each
|
|
var triggerer = all[j];
|
|
var listeners = triggerer._private.listeners = triggerer._private.listeners || [];
|
|
var triggererIsElement = $$.is.element(triggerer);
|
|
var bubbleUp = triggererIsElement || params.layout;
|
|
|
|
// create the event for this element from the event object
|
|
var evt;
|
|
|
|
if( eventsIsEvent ){ // then just get the object
|
|
evt = evtObj;
|
|
|
|
evt.cyTarget = evt.cyTarget || triggerer;
|
|
evt.cy = evt.cy || cy;
|
|
|
|
} else { // then we have to make one
|
|
evt = new $$.Event( evtObj, {
|
|
cyTarget: triggerer,
|
|
cy: cy,
|
|
namespace: evtObj.namespace
|
|
} );
|
|
}
|
|
|
|
// if a layout was specified, then put it in the typed event
|
|
if( evtObj.layout ){
|
|
evt.layout = evtObj.layout;
|
|
}
|
|
|
|
// if triggered by layout, put in event
|
|
if( params.layout ){
|
|
evt.layout = triggerer;
|
|
}
|
|
|
|
// create a rendered position based on the passed position
|
|
if( evt.cyPosition ){
|
|
var pos = evt.cyPosition;
|
|
var zoom = cy.zoom();
|
|
var pan = cy.pan();
|
|
|
|
evt.cyRenderedPosition = {
|
|
x: pos.x * zoom + pan.x,
|
|
y: pos.y * zoom + pan.y
|
|
};
|
|
}
|
|
|
|
if( fnToTrigger ){ // then override the listeners list with just the one we specified
|
|
listeners = [{
|
|
namespace: evt.namespace,
|
|
type: evt.type,
|
|
callback: fnToTrigger
|
|
}];
|
|
}
|
|
|
|
for( var k = 0; k < listeners.length; k++ ){ // check each listener
|
|
var lis = listeners[k];
|
|
var nsMatches = !lis.namespace || lis.namespace === evt.namespace;
|
|
var typeMatches = lis.type === evt.type;
|
|
var targetMatches = lis.delegated ? ( triggerer !== evt.cyTarget && $$.is.element(evt.cyTarget) && lis.selObj.matches(evt.cyTarget) ) : (true); // we're not going to validate the hierarchy; that's too expensive
|
|
var listenerMatches = nsMatches && typeMatches && targetMatches;
|
|
|
|
if( listenerMatches ){ // then trigger it
|
|
var args = [ evt ];
|
|
args = args.concat( extraParams ); // add extra params to args list
|
|
|
|
if( lis.data ){ // add on data plugged into binding
|
|
evt.data = lis.data;
|
|
} else { // or clear it in case the event obj is reused
|
|
evt.data = undefined;
|
|
}
|
|
|
|
if( lis.unbindSelfOnTrigger || lis.unbindAllBindersOnTrigger ){ // then remove listener
|
|
listeners.splice(k, 1);
|
|
k--;
|
|
}
|
|
|
|
if( lis.unbindAllBindersOnTrigger ){ // then delete the listener for all binders
|
|
var binders = lis.binders;
|
|
for( var l = 0; l < binders.length; l++ ){
|
|
var binder = binders[l];
|
|
if( !binder || binder === triggerer ){ continue; } // already handled triggerer or we can't handle it
|
|
|
|
var binderListeners = binder._private.listeners;
|
|
for( var m = 0; m < binderListeners.length; m++ ){
|
|
var binderListener = binderListeners[m];
|
|
|
|
if( binderListener === lis ){ // delete listener from list
|
|
binderListeners.splice(m, 1);
|
|
m--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// run the callback
|
|
var context = lis.delegated ? evt.cyTarget : triggerer;
|
|
var ret = lis.callback.apply( context, args );
|
|
|
|
if( ret === false || evt.isPropagationStopped() ){
|
|
// then don't bubble
|
|
bubbleUp = false;
|
|
|
|
if( ret === false ){
|
|
// returning false is a shorthand for stopping propagation and preventing the def. action
|
|
evt.stopPropagation();
|
|
evt.preventDefault();
|
|
}
|
|
}
|
|
} // if listener matches
|
|
} // for each listener
|
|
|
|
// bubble up event for elements
|
|
if( bubbleUp ){
|
|
var parent = hasCompounds ? triggerer._private.parent : null;
|
|
var hasParent = parent != null && parent.length !== 0;
|
|
|
|
if( hasParent ){ // then bubble up to parent
|
|
parent = parent[0];
|
|
parent.trigger(evt);
|
|
} else { // otherwise, bubble up to the core
|
|
cy.trigger(evt);
|
|
}
|
|
}
|
|
|
|
} // for each of all
|
|
} // for each event
|
|
|
|
return self; // maintain chaining
|
|
}; // function
|
|
}, // trigger
|
|
|
|
|
|
animated: function( fnParams ){
|
|
var defaults = {};
|
|
fnParams = $$.util.extend({}, defaults, fnParams);
|
|
|
|
return function animatedImpl(){
|
|
var self = this;
|
|
var selfIsArrayLike = self.length !== undefined;
|
|
var all = selfIsArrayLike ? self : [self]; // put in array if not array-like
|
|
var cy = this._private.cy || this;
|
|
|
|
if( !cy.styleEnabled() ){ return false; }
|
|
|
|
var ele = all[0];
|
|
|
|
if( ele ){
|
|
return ele._private.animation.current.length > 0;
|
|
}
|
|
};
|
|
}, // animated
|
|
|
|
clearQueue: function( fnParams ){
|
|
var defaults = {};
|
|
fnParams = $$.util.extend({}, defaults, fnParams);
|
|
|
|
return function clearQueueImpl(){
|
|
var self = this;
|
|
var selfIsArrayLike = self.length !== undefined;
|
|
var all = selfIsArrayLike ? self : [self]; // put in array if not array-like
|
|
var cy = this._private.cy || this;
|
|
|
|
if( !cy.styleEnabled() ){ return this; }
|
|
|
|
for( var i = 0; i < all.length; i++ ){
|
|
var ele = all[i];
|
|
ele._private.animation.queue = [];
|
|
}
|
|
|
|
return this;
|
|
};
|
|
}, // clearQueue
|
|
|
|
delay: function( fnParams ){
|
|
var defaults = {};
|
|
fnParams = $$.util.extend({}, defaults, fnParams);
|
|
|
|
return function delayImpl( time, complete ){
|
|
var cy = this._private.cy || this;
|
|
|
|
if( !cy.styleEnabled() ){ return this; }
|
|
|
|
this.animate({
|
|
delay: time
|
|
}, {
|
|
duration: time,
|
|
complete: complete
|
|
});
|
|
|
|
return this;
|
|
};
|
|
}, // delay
|
|
|
|
animate: function( fnParams ){
|
|
var defaults = {};
|
|
fnParams = $$.util.extend({}, defaults, fnParams);
|
|
|
|
return function animateImpl( properties, params ){
|
|
var self = this;
|
|
var selfIsArrayLike = self.length !== undefined;
|
|
var all = selfIsArrayLike ? self : [self]; // put in array if not array-like
|
|
var cy = this._private.cy || this;
|
|
var isCore = !selfIsArrayLike;
|
|
var isEles = !isCore;
|
|
|
|
if( !cy.styleEnabled() ){ return this; }
|
|
|
|
var callTime = +new Date();
|
|
var style = cy.style();
|
|
var q;
|
|
|
|
if( params === undefined ){
|
|
params = {};
|
|
}
|
|
|
|
if( params.duration === undefined ){
|
|
params.duration = 400;
|
|
}
|
|
|
|
switch( params.duration ){
|
|
case 'slow':
|
|
params.duration = 600;
|
|
break;
|
|
case 'fast':
|
|
params.duration = 200;
|
|
break;
|
|
}
|
|
|
|
var propertiesEmpty = true;
|
|
if( properties ){ for( var i in properties ){
|
|
propertiesEmpty = false;
|
|
break;
|
|
} }
|
|
|
|
if( propertiesEmpty ){
|
|
return this; // nothing to animate
|
|
}
|
|
|
|
if( properties.css && isEles ){
|
|
properties.css = style.getPropsList( properties.css );
|
|
}
|
|
|
|
if( properties.renderedPosition && isEles ){
|
|
var rpos = properties.renderedPosition;
|
|
var pan = cy.pan();
|
|
var zoom = cy.zoom();
|
|
|
|
properties.position = {
|
|
x: ( rpos.x - pan.x ) /zoom,
|
|
y: ( rpos.y - pan.y ) /zoom
|
|
};
|
|
}
|
|
|
|
// override pan w/ panBy if set
|
|
if( properties.panBy && isCore ){
|
|
var panBy = properties.panBy;
|
|
var cyPan = cy.pan();
|
|
|
|
properties.pan = {
|
|
x: cyPan.x + panBy.x,
|
|
y: cyPan.y + panBy.y
|
|
};
|
|
}
|
|
|
|
// override pan w/ center if set
|
|
var center = properties.center || properties.centre;
|
|
if( center && isCore ){
|
|
var centerPan = cy.getCenterPan( center.eles, properties.zoom );
|
|
|
|
if( centerPan ){
|
|
properties.pan = centerPan;
|
|
}
|
|
}
|
|
|
|
// override pan & zoom w/ fit if set
|
|
if( properties.fit && isCore ){
|
|
var fit = properties.fit;
|
|
var fitVp = cy.getFitViewport( fit.eles || fit.boundingBox, fit.padding );
|
|
|
|
if( fitVp ){
|
|
properties.pan = fitVp.pan; //{ x: fitVp.pan.x, y: fitVp.pan.y };
|
|
properties.zoom = fitVp.zoom;
|
|
}
|
|
}
|
|
|
|
for( var i = 0; i < all.length; i++ ){
|
|
var ele = all[i];
|
|
|
|
if( ele.animated() && (params.queue === undefined || params.queue) ){
|
|
q = ele._private.animation.queue;
|
|
} else {
|
|
q = ele._private.animation.current;
|
|
}
|
|
|
|
q.push({
|
|
properties: properties,
|
|
duration: params.duration,
|
|
params: params,
|
|
callTime: callTime
|
|
});
|
|
}
|
|
|
|
if( isEles ){
|
|
cy.addToAnimationPool( this );
|
|
}
|
|
|
|
return this; // chaining
|
|
};
|
|
}, // animate
|
|
|
|
stop: function( fnParams ){
|
|
var defaults = {};
|
|
fnParams = $$.util.extend({}, defaults, fnParams);
|
|
|
|
return function stopImpl( clearQueue, jumpToEnd ){
|
|
var self = this;
|
|
var selfIsArrayLike = self.length !== undefined;
|
|
var all = selfIsArrayLike ? self : [self]; // put in array if not array-like
|
|
var cy = this._private.cy || this;
|
|
|
|
if( !cy.styleEnabled() ){ return this; }
|
|
|
|
for( var i = 0; i < all.length; i++ ){
|
|
var ele = all[i];
|
|
var anis = ele._private.animation.current;
|
|
|
|
for( var j = 0; j < anis.length; j++ ){
|
|
var animation = anis[j];
|
|
if( jumpToEnd ){
|
|
// next iteration of the animation loop, the animation
|
|
// will go straight to the end and be removed
|
|
animation.duration = 0;
|
|
}
|
|
}
|
|
|
|
// clear the queue of future animations
|
|
if( clearQueue ){
|
|
ele._private.animation.queue = [];
|
|
}
|
|
|
|
if( !jumpToEnd ){
|
|
ele._private.animation.current = [];
|
|
}
|
|
}
|
|
|
|
// we have to notify (the animation loop doesn't do it for us on `stop`)
|
|
cy.notify({
|
|
collection: this,
|
|
type: 'draw'
|
|
});
|
|
|
|
return this;
|
|
};
|
|
} // stop
|
|
|
|
}; // define
|
|
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.selector = function(map, options){
|
|
for( var name in map ){
|
|
var fn = map[name];
|
|
$$.Selector.prototype[ name ] = fn;
|
|
}
|
|
};
|
|
|
|
$$.Selector = function(onlyThisGroup, selector){
|
|
|
|
if( !(this instanceof $$.Selector) ){
|
|
return new $$.Selector(onlyThisGroup, selector);
|
|
}
|
|
|
|
if( selector === undefined && onlyThisGroup !== undefined ){
|
|
selector = onlyThisGroup;
|
|
onlyThisGroup = undefined;
|
|
}
|
|
|
|
var self = this;
|
|
|
|
self._private = {
|
|
selectorText: null,
|
|
invalid: true
|
|
};
|
|
|
|
if( !selector || ( $$.is.string(selector) && selector.match(/^\s*$/) ) ){
|
|
|
|
if( onlyThisGroup == null ){
|
|
// ignore
|
|
self.length = 0;
|
|
} else {
|
|
self[0] = newQuery();
|
|
self[0].group = onlyThisGroup;
|
|
self.length = 1;
|
|
}
|
|
|
|
} else if( $$.is.element( selector ) ){
|
|
var collection = new $$.Collection(self.cy(), [ selector ]);
|
|
|
|
self[0] = newQuery();
|
|
self[0].collection = collection;
|
|
self.length = 1;
|
|
|
|
} else if( $$.is.collection( selector ) ){
|
|
self[0] = newQuery();
|
|
self[0].collection = selector;
|
|
self.length = 1;
|
|
|
|
} else if( $$.is.fn( selector ) ) {
|
|
self[0] = newQuery();
|
|
self[0].filter = selector;
|
|
self.length = 1;
|
|
|
|
} else if( $$.is.string( selector ) ){
|
|
|
|
// the current subject in the query
|
|
var currentSubject = null;
|
|
|
|
// storage for parsed queries
|
|
var newQuery = function(){
|
|
return {
|
|
classes: [],
|
|
colonSelectors: [],
|
|
data: [],
|
|
group: null,
|
|
ids: [],
|
|
meta: [],
|
|
|
|
// fake selectors
|
|
collection: null, // a collection to match against
|
|
filter: null, // filter function
|
|
|
|
// these are defined in the upward direction rather than down (e.g. child)
|
|
// because we need to go up in Selector.filter()
|
|
parent: null, // parent query obj
|
|
ancestor: null, // ancestor query obj
|
|
subject: null, // defines subject in compound query (subject query obj; points to self if subject)
|
|
|
|
// use these only when subject has been defined
|
|
child: null,
|
|
descendant: null
|
|
};
|
|
};
|
|
|
|
// tokens in the query language
|
|
var tokens = {
|
|
metaChar: '[\\!\\"\\#\\$\\%\\&\\\'\\(\\)\\*\\+\\,\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\]\\^\\`\\{\\|\\}\\~]', // chars we need to escape in var names, etc
|
|
comparatorOp: '=|\\!=|>|>=|<|<=|\\$=|\\^=|\\*=', // binary comparison op (used in data selectors)
|
|
boolOp: '\\?|\\!|\\^', // boolean (unary) operators (used in data selectors)
|
|
string: '"(?:\\\\"|[^"])+"' + '|' + "'(?:\\\\'|[^'])+'", // string literals (used in data selectors) -- doublequotes | singlequotes
|
|
number: $$.util.regex.number, // number literal (used in data selectors) --- e.g. 0.1234, 1234, 12e123
|
|
meta: 'degree|indegree|outdegree', // allowed metadata fields (i.e. allowed functions to use from $$.Collection)
|
|
separator: '\\s*,\\s*', // queries are separated by commas, e.g. edge[foo = 'bar'], node.someClass
|
|
descendant: '\\s+',
|
|
child: '\\s+>\\s+',
|
|
subject: '\\$'
|
|
};
|
|
tokens.variable = '(?:[\\w-]|(?:\\\\'+ tokens.metaChar +'))+'; // a variable name
|
|
tokens.value = tokens.string + '|' + tokens.number; // a value literal, either a string or number
|
|
tokens.className = tokens.variable; // a class name (follows variable conventions)
|
|
tokens.id = tokens.variable; // an element id (follows variable conventions)
|
|
|
|
// when a token like a variable has escaped meta characters, we need to clean the backslashes out
|
|
// so that values get compared properly in Selector.filter()
|
|
var cleanMetaChars = function(str){
|
|
return str.replace(new RegExp('\\\\(' + tokens.metaChar + ')', 'g'), function(match, $1, offset, original){
|
|
return $1;
|
|
});
|
|
};
|
|
|
|
// add @ variants to comparatorOp
|
|
var ops = tokens.comparatorOp.split('|');
|
|
for( var i = 0; i < ops.length; i++ ){
|
|
var op = ops[i];
|
|
tokens.comparatorOp += '|@' + op;
|
|
}
|
|
|
|
// add ! variants to comparatorOp
|
|
var ops = tokens.comparatorOp.split('|');
|
|
for( var i = 0; i < ops.length; i++ ){
|
|
var op = ops[i];
|
|
|
|
if( op.indexOf('!') >= 0 ){ continue; } // skip ops that explicitly contain !
|
|
if( op === '=' ){ continue; } // skip = b/c != is explicitly defined
|
|
|
|
tokens.comparatorOp += '|\\!' + op;
|
|
}
|
|
|
|
// NOTE: add new expression syntax here to have it recognised by the parser;
|
|
// - a query contains all adjacent (i.e. no separator in between) expressions;
|
|
// - the current query is stored in self[i] --- you can use the reference to `this` in the populate function;
|
|
// - you need to check the query objects in Selector.filter() for it actually filter properly, but that's pretty straight forward
|
|
// - when you add something here, also add to Selector.toString()
|
|
var exprs = {
|
|
group: {
|
|
query: true,
|
|
regex: '(node|edge|\\*)',
|
|
populate: function( group ){
|
|
this.group = group == "*" ? group : group + 's';
|
|
}
|
|
},
|
|
|
|
state: {
|
|
query: true,
|
|
// NB: if one colon selector is a substring of another from its start, place the longer one first
|
|
// e.g. :foobar|:foo
|
|
regex: '(:selected|:unselected|:locked|:unlocked|:visible|:hidden|:transparent|:grabbed|:free|:removed|:inside|:grabbable|:ungrabbable|:animated|:unanimated|:selectable|:unselectable|:orphan|:nonorphan|:parent|:child|:loop|:simple|:active|:inactive|:touch|:backgrounding|:nonbackgrounding)',
|
|
populate: function( state ){
|
|
this.colonSelectors.push( state );
|
|
}
|
|
},
|
|
|
|
id: {
|
|
query: true,
|
|
regex: '\\#('+ tokens.id +')',
|
|
populate: function( id ){
|
|
this.ids.push( cleanMetaChars(id) );
|
|
}
|
|
},
|
|
|
|
className: {
|
|
query: true,
|
|
regex: '\\.('+ tokens.className +')',
|
|
populate: function( className ){
|
|
this.classes.push( cleanMetaChars(className) );
|
|
}
|
|
},
|
|
|
|
dataExists: {
|
|
query: true,
|
|
regex: '\\[\\s*('+ tokens.variable +')\\s*\\]',
|
|
populate: function( variable ){
|
|
this.data.push({
|
|
field: cleanMetaChars(variable)
|
|
});
|
|
}
|
|
},
|
|
|
|
dataCompare: {
|
|
query: true,
|
|
regex: '\\[\\s*('+ tokens.variable +')\\s*('+ tokens.comparatorOp +')\\s*('+ tokens.value +')\\s*\\]',
|
|
populate: function( variable, comparatorOp, value ){
|
|
var valueIsString = new RegExp('^' + tokens.string + '$').exec(value) != null;
|
|
|
|
if( valueIsString ){
|
|
value = value.substring(1, value.length - 1);
|
|
} else {
|
|
value = parseFloat(value);
|
|
}
|
|
|
|
this.data.push({
|
|
field: cleanMetaChars(variable),
|
|
operator: comparatorOp,
|
|
value: value
|
|
});
|
|
}
|
|
},
|
|
|
|
dataBool: {
|
|
query: true,
|
|
regex: '\\[\\s*('+ tokens.boolOp +')\\s*('+ tokens.variable +')\\s*\\]',
|
|
populate: function( boolOp, variable ){
|
|
this.data.push({
|
|
field: cleanMetaChars(variable),
|
|
operator: boolOp
|
|
});
|
|
}
|
|
},
|
|
|
|
metaCompare: {
|
|
query: true,
|
|
regex: '\\[\\[\\s*('+ tokens.meta +')\\s*('+ tokens.comparatorOp +')\\s*('+ tokens.number +')\\s*\\]\\]',
|
|
populate: function( meta, comparatorOp, number ){
|
|
this.meta.push({
|
|
field: cleanMetaChars(meta),
|
|
operator: comparatorOp,
|
|
value: parseFloat(number)
|
|
});
|
|
}
|
|
},
|
|
|
|
nextQuery: {
|
|
separator: true,
|
|
regex: tokens.separator,
|
|
populate: function(){
|
|
// go on to next query
|
|
self[++i] = newQuery();
|
|
currentSubject = null;
|
|
}
|
|
},
|
|
|
|
child: {
|
|
separator: true,
|
|
regex: tokens.child,
|
|
populate: function(){
|
|
// this query is the parent of the following query
|
|
var childQuery = newQuery();
|
|
childQuery.parent = this;
|
|
childQuery.subject = currentSubject;
|
|
|
|
// we're now populating the child query with expressions that follow
|
|
self[i] = childQuery;
|
|
}
|
|
},
|
|
|
|
descendant: {
|
|
separator: true,
|
|
regex: tokens.descendant,
|
|
populate: function(){
|
|
// this query is the ancestor of the following query
|
|
var descendantQuery = newQuery();
|
|
descendantQuery.ancestor = this;
|
|
descendantQuery.subject = currentSubject;
|
|
|
|
// we're now populating the descendant query with expressions that follow
|
|
self[i] = descendantQuery;
|
|
}
|
|
},
|
|
|
|
subject: {
|
|
modifier: true,
|
|
regex: tokens.subject,
|
|
populate: function(){
|
|
if( currentSubject != null && this.subject != this ){
|
|
$$.util.error('Redefinition of subject in selector `' + selector + '`');
|
|
return false;
|
|
}
|
|
|
|
currentSubject = this;
|
|
this.subject = this;
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
var j = 0;
|
|
for( var name in exprs ){
|
|
exprs[j] = exprs[name];
|
|
exprs[j].name = name;
|
|
|
|
j++;
|
|
}
|
|
exprs.length = j;
|
|
|
|
self._private.selectorText = selector;
|
|
var remaining = selector;
|
|
var i = 0;
|
|
|
|
// of all the expressions, find the first match in the remaining text
|
|
var consumeExpr = function( expectation ){
|
|
var expr;
|
|
var match;
|
|
var name;
|
|
|
|
for( var j = 0; j < exprs.length; j++ ){
|
|
var e = exprs[j];
|
|
var n = e.name;
|
|
|
|
// ignore this expression if it doesn't meet the expectation function
|
|
if( $$.is.fn( expectation ) && !expectation(n, e) ){ continue; }
|
|
|
|
var m = remaining.match(new RegExp( '^' + e.regex ));
|
|
|
|
if( m != null ){
|
|
match = m;
|
|
expr = e;
|
|
name = n;
|
|
|
|
var consumed = m[0];
|
|
remaining = remaining.substring( consumed.length );
|
|
|
|
break; // we've consumed one expr, so we can return now
|
|
}
|
|
}
|
|
|
|
return {
|
|
expr: expr,
|
|
match: match,
|
|
name: name
|
|
};
|
|
};
|
|
|
|
// consume all leading whitespace
|
|
var consumeWhitespace = function(){
|
|
var match = remaining.match(/^\s+/);
|
|
|
|
if( match ){
|
|
var consumed = match[0];
|
|
remaining = remaining.substring( consumed.length );
|
|
}
|
|
};
|
|
|
|
self[0] = newQuery(); // get started
|
|
|
|
consumeWhitespace(); // get rid of leading whitespace
|
|
for(;;){
|
|
var check = consumeExpr();
|
|
|
|
if( check.expr == null ){
|
|
$$.util.error('The selector `'+ selector +'`is invalid');
|
|
return;
|
|
} else {
|
|
var args = [];
|
|
for(var j = 1; j < check.match.length; j++){
|
|
args.push( check.match[j] );
|
|
}
|
|
|
|
// let the token populate the selector object (i.e. in self[i])
|
|
var ret = check.expr.populate.apply( self[i], args );
|
|
|
|
if( ret === false ){ return; } // exit if population failed
|
|
}
|
|
|
|
// we're done when there's nothing left to parse
|
|
if( remaining.match(/^\s*$/) ){
|
|
break;
|
|
}
|
|
}
|
|
|
|
self.length = i + 1;
|
|
|
|
// adjust references for subject
|
|
for(j = 0; j < self.length; j++){
|
|
var query = self[j];
|
|
|
|
if( query.subject != null ){
|
|
// go up the tree until we reach the subject
|
|
for(;;){
|
|
if( query.subject == query ){ break; } // done if subject is self
|
|
|
|
if( query.parent != null ){ // swap parent/child reference
|
|
var parent = query.parent;
|
|
var child = query;
|
|
|
|
child.parent = null;
|
|
parent.child = child;
|
|
|
|
query = parent; // go up the tree
|
|
} else if( query.ancestor != null ){ // swap ancestor/descendant
|
|
var ancestor = query.ancestor;
|
|
var descendant = query;
|
|
|
|
descendant.ancestor = null;
|
|
ancestor.descendant = descendant;
|
|
|
|
query = ancestor; // go up the tree
|
|
} else {
|
|
$$.util.error('When adjusting references for the selector `'+ query +'`, neither parent nor ancestor was found');
|
|
break;
|
|
}
|
|
} // for
|
|
|
|
self[j] = query.subject; // subject should be the root query
|
|
} // if
|
|
} // for
|
|
|
|
// make sure for each query that the subject group matches the implicit group if any
|
|
if( onlyThisGroup != null ){
|
|
for(var j = 0; j < self.length; j++){
|
|
if( self[j].group != null && self[j].group != onlyThisGroup ){
|
|
$$.util.error('Group `'+ self[j].group +'` conflicts with implicit group `'+ onlyThisGroup +'` in selector `'+ selector +'`');
|
|
return;
|
|
}
|
|
|
|
self[j].group = onlyThisGroup; // set to implicit group
|
|
}
|
|
}
|
|
|
|
} else {
|
|
$$.util.error('A selector must be created from a string; found ' + selector);
|
|
return;
|
|
}
|
|
|
|
self._private.invalid = false;
|
|
|
|
};
|
|
|
|
$$.selfn = $$.Selector.prototype;
|
|
|
|
$$.selfn.size = function(){
|
|
return this.length;
|
|
};
|
|
|
|
$$.selfn.eq = function(i){
|
|
return this[i];
|
|
};
|
|
|
|
// get elements from the core and then filter them
|
|
$$.selfn.find = function(){
|
|
// TODO impl if we decide to use a DB for storing elements
|
|
};
|
|
|
|
var queryMatches = function(query, element){
|
|
// check group
|
|
if( query.group != null && query.group != '*' && query.group != element._private.group ){
|
|
return false;
|
|
}
|
|
|
|
var cy = element.cy();
|
|
|
|
// check colon selectors
|
|
var allColonSelectorsMatch = true;
|
|
for(var k = 0; k < query.colonSelectors.length; k++){
|
|
var sel = query.colonSelectors[k];
|
|
|
|
switch(sel){
|
|
case ':selected':
|
|
allColonSelectorsMatch = element.selected();
|
|
break;
|
|
case ':unselected':
|
|
allColonSelectorsMatch = !element.selected();
|
|
break;
|
|
case ':selectable':
|
|
allColonSelectorsMatch = element.selectable();
|
|
break;
|
|
case ':unselectable':
|
|
allColonSelectorsMatch = !element.selectable();
|
|
break;
|
|
case ':locked':
|
|
allColonSelectorsMatch = element.locked();
|
|
break;
|
|
case ':unlocked':
|
|
allColonSelectorsMatch = !element.locked();
|
|
break;
|
|
case ':visible':
|
|
allColonSelectorsMatch = element.visible();
|
|
break;
|
|
case ':hidden':
|
|
allColonSelectorsMatch = !element.visible();
|
|
break;
|
|
case ':transparent':
|
|
allColonSelectorsMatch = element.transparent();
|
|
break;
|
|
case ':grabbed':
|
|
allColonSelectorsMatch = element.grabbed();
|
|
break;
|
|
case ':free':
|
|
allColonSelectorsMatch = !element.grabbed();
|
|
break;
|
|
case ':removed':
|
|
allColonSelectorsMatch = element.removed();
|
|
break;
|
|
case ':inside':
|
|
allColonSelectorsMatch = !element.removed();
|
|
break;
|
|
case ':grabbable':
|
|
allColonSelectorsMatch = element.grabbable();
|
|
break;
|
|
case ':ungrabbable':
|
|
allColonSelectorsMatch = !element.grabbable();
|
|
break;
|
|
case ':animated':
|
|
allColonSelectorsMatch = element.animated();
|
|
break;
|
|
case ':unanimated':
|
|
allColonSelectorsMatch = !element.animated();
|
|
break;
|
|
case ':parent':
|
|
allColonSelectorsMatch = element.isNode() && element.children().nonempty();
|
|
break;
|
|
case ':child':
|
|
case ':nonorphan':
|
|
allColonSelectorsMatch = element.isNode() && element.parent().nonempty();
|
|
break;
|
|
case ':orphan':
|
|
allColonSelectorsMatch = element.isNode() && element.parent().empty();
|
|
break;
|
|
case ':loop':
|
|
allColonSelectorsMatch = element.isEdge() && element.data('source') === element.data('target');
|
|
break;
|
|
case ':simple':
|
|
allColonSelectorsMatch = element.isEdge() && element.data('source') !== element.data('target');
|
|
break;
|
|
case ':active':
|
|
allColonSelectorsMatch = element.active();
|
|
break;
|
|
case ':inactive':
|
|
allColonSelectorsMatch = !element.active();
|
|
break;
|
|
case ':touch':
|
|
allColonSelectorsMatch = $$.is.touch();
|
|
break;
|
|
case ':backgrounding':
|
|
allColonSelectorsMatch = element.backgrounding();
|
|
break;
|
|
case ':nonbackgrounding':
|
|
allColonSelectorsMatch = !element.backgrounding();
|
|
break;
|
|
}
|
|
|
|
if( !allColonSelectorsMatch ) break;
|
|
}
|
|
if( !allColonSelectorsMatch ) return false;
|
|
|
|
// check id
|
|
var allIdsMatch = true;
|
|
for(var k = 0; k < query.ids.length; k++){
|
|
var id = query.ids[k];
|
|
var actualId = element._private.data.id;
|
|
|
|
allIdsMatch = allIdsMatch && (id == actualId);
|
|
|
|
if( !allIdsMatch ) break;
|
|
}
|
|
if( !allIdsMatch ) return false;
|
|
|
|
// check classes
|
|
var allClassesMatch = true;
|
|
for(var k = 0; k < query.classes.length; k++){
|
|
var cls = query.classes[k];
|
|
|
|
allClassesMatch = allClassesMatch && element.hasClass(cls);
|
|
|
|
if( !allClassesMatch ) break;
|
|
}
|
|
if( !allClassesMatch ) return false;
|
|
|
|
// generic checking for data/metadata
|
|
var operandsMatch = function(params){
|
|
var allDataMatches = true;
|
|
for(var k = 0; k < query[params.name].length; k++){
|
|
var data = query[params.name][k];
|
|
var operator = data.operator;
|
|
var value = data.value;
|
|
var field = data.field;
|
|
var matches;
|
|
|
|
if( operator != null && value != null ){
|
|
|
|
var fieldVal = params.fieldValue(field);
|
|
var fieldStr = !$$.is.string(fieldVal) && !$$.is.number(fieldVal) ? '' : '' + fieldVal;
|
|
var valStr = '' + value;
|
|
|
|
var caseInsensitive = false;
|
|
if( operator.indexOf('@') >= 0 ){
|
|
fieldStr = fieldStr.toLowerCase();
|
|
valStr = valStr.toLowerCase();
|
|
|
|
operator = operator.replace('@', '');
|
|
caseInsensitive = true;
|
|
}
|
|
|
|
var notExpr = false;
|
|
var handledNotExpr = false;
|
|
if( operator.indexOf('!') >= 0 ){
|
|
operator = operator.replace('!', '');
|
|
notExpr = true;
|
|
}
|
|
|
|
// if we're doing a case insensitive comparison, then we're using a STRING comparison
|
|
// even if we're comparing numbers
|
|
if( caseInsensitive ){
|
|
value = valStr.toLowerCase();
|
|
fieldVal = fieldStr.toLowerCase();
|
|
}
|
|
|
|
switch(operator){
|
|
case '*=':
|
|
matches = fieldStr.search(valStr) >= 0;
|
|
break;
|
|
case '$=':
|
|
matches = new RegExp(valStr + '$').exec(fieldStr) != null;
|
|
break;
|
|
case '^=':
|
|
matches = new RegExp('^' + valStr).exec(fieldStr) != null;
|
|
break;
|
|
case '=':
|
|
matches = fieldVal === value;
|
|
break;
|
|
case '!=':
|
|
matches = fieldVal !== value;
|
|
break;
|
|
case '>':
|
|
matches = !notExpr ? fieldVal > value : fieldVal <= value;
|
|
handledNotExpr = true;
|
|
break;
|
|
case '>=':
|
|
matches = !notExpr ? fieldVal >= value : fieldVal < value;
|
|
handledNotExpr = true;
|
|
break;
|
|
case '<':
|
|
matches = !notExpr ? fieldVal < value : fieldVal >= value;
|
|
handledNotExpr = true;
|
|
break;
|
|
case '<=':
|
|
matches = !notExpr ? fieldVal <= value : fieldVal > value;
|
|
handledNotExpr = true;
|
|
break;
|
|
default:
|
|
matches = false;
|
|
break;
|
|
|
|
}
|
|
} else if( operator != null ){
|
|
switch(operator){
|
|
case '?':
|
|
matches = params.fieldTruthy(field);
|
|
break;
|
|
case '!':
|
|
matches = !params.fieldTruthy(field);
|
|
break;
|
|
case '^':
|
|
matches = params.fieldUndefined(field);
|
|
break;
|
|
}
|
|
} else {
|
|
matches = !params.fieldUndefined(field);
|
|
}
|
|
|
|
if( notExpr && !handledNotExpr ){
|
|
matches = !matches;
|
|
handledNotExpr = true;
|
|
}
|
|
|
|
if( !matches ){
|
|
allDataMatches = false;
|
|
break;
|
|
}
|
|
} // for
|
|
|
|
return allDataMatches;
|
|
}; // operandsMatch
|
|
|
|
// check data matches
|
|
var allDataMatches = operandsMatch({
|
|
name: 'data',
|
|
fieldValue: function(field){
|
|
return element._private.data[field];
|
|
},
|
|
fieldRef: function(field){
|
|
return 'element._private.data.' + field;
|
|
},
|
|
fieldUndefined: function(field){
|
|
return element._private.data[field] === undefined;
|
|
},
|
|
fieldTruthy: function(field){
|
|
if( element._private.data[field] ){
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if( !allDataMatches ){
|
|
return false;
|
|
}
|
|
|
|
// check metadata matches
|
|
var allMetaMatches = operandsMatch({
|
|
name: 'meta',
|
|
fieldValue: function(field){
|
|
return element[field]();
|
|
},
|
|
fieldRef: function(field){
|
|
return 'element.' + field + '()';
|
|
},
|
|
fieldUndefined: function(field){
|
|
return element[field]() == null;
|
|
},
|
|
fieldTruthy: function(field){
|
|
if( element[field]() ){
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if( !allMetaMatches ){
|
|
return false;
|
|
}
|
|
|
|
// check collection
|
|
if( query.collection != null ){
|
|
var matchesAny = query.collection._private.ids[ element.id() ] != null;
|
|
|
|
if( !matchesAny ){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// check filter function
|
|
if( query.filter != null && element.collection().filter( query.filter ).size() === 0 ){
|
|
return false;
|
|
}
|
|
|
|
|
|
// check parent/child relations
|
|
var confirmRelations = function( query, elements ){
|
|
if( query != null ){
|
|
var matches = false;
|
|
|
|
if( !cy.hasCompoundNodes() ){
|
|
return false;
|
|
}
|
|
|
|
elements = elements(); // make elements functional so we save cycles if query == null
|
|
|
|
// query must match for at least one element (may be recursive)
|
|
for(var i = 0; i < elements.length; i++){
|
|
if( queryMatches( query, elements[i] ) ){
|
|
matches = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
} else {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
if (! confirmRelations(query.parent, function(){
|
|
return element.parent();
|
|
}) ){ return false; }
|
|
|
|
if (! confirmRelations(query.ancestor, function(){
|
|
return element.parents();
|
|
}) ){ return false; }
|
|
|
|
if (! confirmRelations(query.child, function(){
|
|
return element.children();
|
|
}) ){ return false; }
|
|
|
|
if (! confirmRelations(query.descendant, function(){
|
|
return element.descendants();
|
|
}) ){ return false; }
|
|
|
|
// we've reached the end, so we've matched everything for this query
|
|
return true;
|
|
}; // queryMatches
|
|
|
|
// filter an existing collection
|
|
$$.selfn.filter = function(collection){
|
|
var self = this;
|
|
var cy = collection.cy();
|
|
|
|
// don't bother trying if it's invalid
|
|
if( self._private.invalid ){
|
|
return new $$.Collection( cy );
|
|
}
|
|
|
|
var selectorFunction = function(i, element){
|
|
for(var j = 0; j < self.length; j++){
|
|
var query = self[j];
|
|
|
|
if( queryMatches(query, element) ){
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
if( self._private.selectorText == null ){
|
|
selectorFunction = function(){ return true; };
|
|
}
|
|
|
|
var filteredCollection = collection.filter( selectorFunction );
|
|
|
|
return filteredCollection;
|
|
}; // filter
|
|
|
|
// does selector match a single element?
|
|
$$.selfn.matches = function(ele){
|
|
var self = this;
|
|
|
|
// don't bother trying if it's invalid
|
|
if( self._private.invalid ){
|
|
return false;
|
|
}
|
|
|
|
for(var j = 0; j < self.length; j++){
|
|
var query = self[j];
|
|
|
|
if( queryMatches(query, ele) ){
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}; // filter
|
|
|
|
// ith query to string
|
|
$$.selfn.toString = $$.selfn.selector = function(){
|
|
|
|
var str = '';
|
|
|
|
var clean = function(obj, isValue){
|
|
if( $$.is.string(obj) ){
|
|
return isValue ? '"' + obj + '"' : obj;
|
|
}
|
|
return '';
|
|
};
|
|
|
|
var queryToString = function(query){
|
|
var str = '';
|
|
|
|
if( query.subject === query ){
|
|
str += '$';
|
|
}
|
|
|
|
var group = clean(query.group);
|
|
str += group.substring(0, group.length - 1);
|
|
|
|
for(var j = 0; j < query.data.length; j++){
|
|
var data = query.data[j];
|
|
|
|
if( data.value ){
|
|
str += '[' + data.field + clean(data.operator) + clean(data.value, true) + ']';
|
|
} else {
|
|
str += '[' + clean(data.operator) + data.field + ']';
|
|
}
|
|
}
|
|
|
|
for(var j = 0; j < query.meta.length; j++){
|
|
var meta = query.meta[j];
|
|
str += '[[' + meta.field + clean(meta.operator) + clean(meta.value, true) + ']]';
|
|
}
|
|
|
|
for(var j = 0; j < query.colonSelectors.length; j++){
|
|
var sel = query.colonSelectors[i];
|
|
str += sel;
|
|
}
|
|
|
|
for(var j = 0; j < query.ids.length; j++){
|
|
var sel = '#' + query.ids[i];
|
|
str += sel;
|
|
}
|
|
|
|
for(var j = 0; j < query.classes.length; j++){
|
|
var sel = '.' + query.classes[i];
|
|
str += sel;
|
|
}
|
|
|
|
if( query.parent != null ){
|
|
str = queryToString( query.parent ) + ' > ' + str;
|
|
}
|
|
|
|
if( query.ancestor != null ){
|
|
str = queryToString( query.ancestor ) + ' ' + str;
|
|
}
|
|
|
|
if( query.child != null ){
|
|
str += ' > ' + queryToString( query.child );
|
|
}
|
|
|
|
if( query.descendant != null ){
|
|
str += ' ' + queryToString( query.descendant );
|
|
}
|
|
|
|
return str;
|
|
};
|
|
|
|
for(var i = 0; i < this.length; i++){
|
|
var query = this[i];
|
|
|
|
str += queryToString( query );
|
|
|
|
if( this.length > 1 && i < this.length - 1 ){
|
|
str += ', ';
|
|
}
|
|
}
|
|
|
|
return str;
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.Style = function( cy ){
|
|
|
|
if( !(this instanceof $$.Style) ){
|
|
return new $$.Style(cy);
|
|
}
|
|
|
|
if( !$$.is.core(cy) ){
|
|
$$.util.error('A style must have a core reference');
|
|
return;
|
|
}
|
|
|
|
this._private = {
|
|
cy: cy,
|
|
coreStyle: {},
|
|
newStyle: true
|
|
};
|
|
|
|
this.length = 0;
|
|
|
|
this.addDefaultStylesheet();
|
|
};
|
|
|
|
// nice-to-have aliases
|
|
$$.style = $$.Style;
|
|
$$.styfn = $$.Style.prototype;
|
|
|
|
// define functions in the Style prototype
|
|
$$.fn.style = function( fnMap, options ){
|
|
for( var fnName in fnMap ){
|
|
var fn = fnMap[ fnName ];
|
|
$$.Style.prototype = fn;
|
|
}
|
|
};
|
|
|
|
(function(){
|
|
var number = $$.util.regex.number;
|
|
var rgba = $$.util.regex.rgbaNoBackRefs;
|
|
var hsla = $$.util.regex.hslaNoBackRefs;
|
|
var hex3 = $$.util.regex.hex3;
|
|
var hex6 = $$.util.regex.hex6;
|
|
var data = function( prefix ){ return '^' + prefix + '\\s*\\(\\s*([\\w\\.]+)\\s*\\)$'; };
|
|
var mapData = function( prefix ){ return '^' + prefix + '\\s*\\(([\\w\\.]+)\\s*\\,\\s*(' + number + ')\\s*\\,\\s*(' + number + ')\\s*,\\s*(' + number + '|\\w+|' + rgba + '|' + hsla + '|' + hex3 + '|' + hex6 + ')\\s*\\,\\s*(' + number + '|\\w+|' + rgba + '|' + hsla + '|' + hex3 + '|' + hex6 + ')\\)$'; };
|
|
|
|
// each visual style property has a type and needs to be validated according to it
|
|
$$.style.types = {
|
|
time: { number: true, min: 0, units: 's|ms', implicitUnits: 'ms' },
|
|
percent: { number: true, min: 0, max: 100, units: '%' },
|
|
zeroOneNumber: { number: true, min: 0, max: 1, unitless: true },
|
|
nOneOneNumber: { number: true, min: -1, max: 1, unitless: true },
|
|
nonNegativeInt: { number: true, min: 0, integer: true, unitless: true },
|
|
position: { enums: ['parent', 'origin'] },
|
|
autoSize: { number: true, min: 0, enums: ['auto'] },
|
|
number: { number: true },
|
|
size: { number: true, min: 0 },
|
|
bgSize: { number: true, min: 0, allowPercent: true },
|
|
bgWH: { number: true, min: 0, allowPercent: true, enums: ['auto'] },
|
|
bgPos: { number: true, allowPercent: true },
|
|
bgRepeat: { enums: ['repeat', 'repeat-x', 'repeat-y', 'no-repeat'] },
|
|
bgFit: { enums: ['none', 'contain', 'cover'] },
|
|
bgClip: { enums: ['none', 'node'] },
|
|
color: { color: true },
|
|
lineStyle: { enums: ['solid', 'dotted', 'dashed'] },
|
|
borderStyle: { enums: ['solid', 'dotted', 'dashed', 'double'] },
|
|
curveStyle: { enums: ['bezier', 'unbundled-bezier', 'haystack'] },
|
|
fontFamily: { regex: '^([\\w- \\"]+(?:\\s*,\\s*[\\w- \\"]+)*)$' },
|
|
fontVariant: { enums: ['small-caps', 'normal'] },
|
|
fontStyle: { enums: ['italic', 'normal', 'oblique'] },
|
|
fontWeight: { enums: ['normal', 'bold', 'bolder', 'lighter', '100', '200', '300', '400', '500', '600', '800', '900', 100, 200, 300, 400, 500, 600, 700, 800, 900] },
|
|
textDecoration: { enums: ['none', 'underline', 'overline', 'line-through'] },
|
|
textTransform: { enums: ['none', 'uppercase', 'lowercase'] },
|
|
textWrap: { enums: ['none', 'wrap'] },
|
|
textBackgroundShape: { enums: ['rectangle', 'roundrectangle']},
|
|
nodeShape: { enums: ['rectangle', 'roundrectangle', 'ellipse', 'triangle', 'square', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'star', 'diamond', 'vee', 'rhomboid'] },
|
|
compoundIncludeLabels: { enums: ['include', 'exclude'] },
|
|
arrowShape: { enums: ['tee', 'triangle', 'triangle-tee', 'triangle-backcurve', 'half-triangle-overshot', 'square', 'circle', 'diamond', 'none'] },
|
|
arrowFill: { enums: ['filled', 'hollow'] },
|
|
display: { enums: ['element', 'none'] },
|
|
visibility: { enums: ['hidden', 'visible'] },
|
|
valign: { enums: ['top', 'center', 'bottom'] },
|
|
halign: { enums: ['left', 'center', 'right'] },
|
|
text: { string: true },
|
|
data: { mapping: true, regex: data('data') },
|
|
layoutData: { mapping: true, regex: data('layoutData') },
|
|
scratch: { mapping: true, regex: data('scratch') },
|
|
mapData: { mapping: true, regex: mapData('mapData') },
|
|
mapLayoutData: { mapping: true, regex: mapData('mapLayoutData') },
|
|
mapScratch: { mapping: true, regex: mapData('mapScratch') },
|
|
fn: { mapping: true, fn: true },
|
|
url: { regex: '^url\\s*\\(\\s*([^\\s]+)\\s*\\s*\\)|none|(.+)$' },
|
|
propList: { propList: true },
|
|
angle: { number: true, units: 'deg|rad' },
|
|
textRotation: { enums: ['none', 'autorotate'] }
|
|
};
|
|
|
|
// define visual style properties
|
|
var t = $$.style.types;
|
|
var props = $$.style.properties = [
|
|
// labels
|
|
{ name: 'text-valign', type: t.valign },
|
|
{ name: 'text-halign', type: t.halign },
|
|
{ name: 'color', type: t.color },
|
|
{ name: 'content', type: t.text },
|
|
{ name: 'text-outline-color', type: t.color },
|
|
{ name: 'text-outline-width', type: t.size },
|
|
{ name: 'text-outline-opacity', type: t.zeroOneNumber },
|
|
{ name: 'text-opacity', type: t.zeroOneNumber },
|
|
{ name: 'text-background-color', type: t.color },
|
|
{ name: 'text-background-opacity', type: t.zeroOneNumber },
|
|
{ name: 'text-border-opacity', type: t.zeroOneNumber },
|
|
{ name: 'text-border-color', type: t.color },
|
|
{ name: 'text-border-width', type: t.size },
|
|
{ name: 'text-border-style', type: t.borderStyle },
|
|
{ name: 'text-background-shape', type: t.textBackgroundShape},
|
|
// { name: 'text-decoration', type: t.textDecoration }, // not supported in canvas
|
|
{ name: 'text-transform', type: t.textTransform },
|
|
{ name: 'text-wrap', type: t.textWrap },
|
|
{ name: 'text-max-width', type: t.size },
|
|
|
|
// { name: 'text-rotation', type: t.angle }, // TODO disabled b/c rotation breaks bounding boxes
|
|
{ name: 'font-family', type: t.fontFamily },
|
|
{ name: 'font-style', type: t.fontStyle },
|
|
// { name: 'font-variant', type: t.fontVariant }, // not useful
|
|
{ name: 'font-weight', type: t.fontWeight },
|
|
{ name: 'font-size', type: t.size },
|
|
{ name: 'min-zoomed-font-size', type: t.size },
|
|
{ name: 'edge-text-rotation', type: t.textRotation },
|
|
|
|
// visibility
|
|
{ name: 'display', type: t.display },
|
|
{ name: 'visibility', type: t.visibility },
|
|
{ name: 'opacity', type: t.zeroOneNumber },
|
|
{ name: 'z-index', type: t.nonNegativeInt },
|
|
|
|
// overlays
|
|
{ name: 'overlay-padding', type: t.size },
|
|
{ name: 'overlay-color', type: t.color },
|
|
{ name: 'overlay-opacity', type: t.zeroOneNumber },
|
|
|
|
// shadows
|
|
{ name: 'shadow-blur', type: t.size },
|
|
{ name: 'shadow-color', type: t.color },
|
|
{ name: 'shadow-opacity', type: t.zeroOneNumber },
|
|
{ name: 'shadow-offset-x', type: t.number },
|
|
{ name: 'shadow-offset-y', type: t.number },
|
|
|
|
// label shadows
|
|
{ name: 'text-shadow-blur', type: t.size },
|
|
{ name: 'text-shadow-color', type: t.color },
|
|
{ name: 'text-shadow-opacity', type: t.zeroOneNumber },
|
|
{ name: 'text-shadow-offset-x', type: t.number },
|
|
{ name: 'text-shadow-offset-y', type: t.number },
|
|
|
|
// transition anis
|
|
{ name: 'transition-property', type: t.propList },
|
|
{ name: 'transition-duration', type: t.time },
|
|
{ name: 'transition-delay', type: t.time },
|
|
|
|
// node body
|
|
{ name: 'height', type: t.autoSize },
|
|
{ name: 'width', type: t.autoSize },
|
|
{ name: 'shape', type: t.nodeShape },
|
|
{ name: 'background-color', type: t.color },
|
|
{ name: 'background-opacity', type: t.zeroOneNumber },
|
|
{ name: 'background-blacken', type: t.nOneOneNumber },
|
|
|
|
// node border
|
|
{ name: 'border-color', type: t.color },
|
|
{ name: 'border-opacity', type: t.zeroOneNumber },
|
|
{ name: 'border-width', type: t.size },
|
|
{ name: 'border-style', type: t.borderStyle },
|
|
|
|
// node background images
|
|
{ name: 'background-image', type: t.url },
|
|
{ name: 'background-image-opacity', type: t.zeroOneNumber },
|
|
{ name: 'background-position-x', type: t.bgPos },
|
|
{ name: 'background-position-y', type: t.bgPos },
|
|
{ name: 'background-repeat', type: t.bgRepeat },
|
|
{ name: 'background-fit', type: t.bgFit },
|
|
{ name: 'background-clip', type: t.bgClip },
|
|
{ name: 'background-width', type: t.bgWH },
|
|
{ name: 'background-height', type: t.bgWH },
|
|
|
|
// compound props
|
|
{ name: 'padding-left', type: t.size },
|
|
{ name: 'padding-right', type: t.size },
|
|
{ name: 'padding-top', type: t.size },
|
|
{ name: 'padding-bottom', type: t.size },
|
|
{ name: 'position', type: t.position },
|
|
{ name: 'compound-sizing-wrt-labels', type: t.compoundIncludeLabels },
|
|
|
|
// edge line
|
|
{ name: 'line-style', type: t.lineStyle },
|
|
{ name: 'line-color', type: t.color },
|
|
{ name: 'control-point-step-size', type: t.size },
|
|
{ name: 'control-point-distance', type: t.number },
|
|
{ name: 'control-point-weight', type: t.zeroOneNumber },
|
|
{ name: 'curve-style', type: t.curveStyle },
|
|
{ name: 'haystack-radius', type: t.zeroOneNumber },
|
|
|
|
// edge arrows
|
|
{ name: 'source-arrow-shape', type: t.arrowShape },
|
|
{ name: 'target-arrow-shape', type: t.arrowShape },
|
|
{ name: 'mid-source-arrow-shape', type: t.arrowShape },
|
|
{ name: 'mid-target-arrow-shape', type: t.arrowShape },
|
|
{ name: 'source-arrow-color', type: t.color },
|
|
{ name: 'target-arrow-color', type: t.color },
|
|
{ name: 'mid-source-arrow-color', type: t.color },
|
|
{ name: 'mid-target-arrow-color', type: t.color },
|
|
{ name: 'source-arrow-fill', type: t.arrowFill },
|
|
{ name: 'target-arrow-fill', type: t.arrowFill },
|
|
{ name: 'mid-source-arrow-fill', type: t.arrowFill },
|
|
{ name: 'mid-target-arrow-fill', type: t.arrowFill },
|
|
|
|
// these are just for the core
|
|
{ name: 'selection-box-color', type: t.color },
|
|
{ name: 'selection-box-opacity', type: t.zeroOneNumber },
|
|
{ name: 'selection-box-border-color', type: t.color },
|
|
{ name: 'selection-box-border-width', type: t.size },
|
|
{ name: 'active-bg-color', type: t.color },
|
|
{ name: 'active-bg-opacity', type: t.zeroOneNumber },
|
|
{ name: 'active-bg-size', type: t.size },
|
|
{ name: 'outside-texture-bg-color', type: t.color },
|
|
{ name: 'outside-texture-bg-opacity', type: t.zeroOneNumber }
|
|
];
|
|
|
|
// pie backgrounds for nodes
|
|
$$.style.pieBackgroundN = 16; // because the pie properties are numbered, give access to a constant N (for renderer use)
|
|
props.push({ name: 'pie-size', type: t.bgSize });
|
|
for( var i = 1; i <= $$.style.pieBackgroundN; i++ ){
|
|
props.push({ name: 'pie-'+i+'-background-color', type: t.color });
|
|
props.push({ name: 'pie-'+i+'-background-size', type: t.percent });
|
|
props.push({ name: 'pie-'+i+'-background-opacity', type: t.zeroOneNumber });
|
|
}
|
|
|
|
// allow access of properties by name ( e.g. $$.style.properties.height )
|
|
for( var i = 0; i < props.length; i++ ){
|
|
var prop = props[i];
|
|
|
|
props[ prop.name ] = prop; // allow lookup by name
|
|
}
|
|
})();
|
|
|
|
// adds the default stylesheet to the current style
|
|
$$.styfn.addDefaultStylesheet = function(){
|
|
// to be nice, we build font related style properties from the core container
|
|
// so that cytoscape matches the style of its container by default
|
|
//
|
|
// unfortunately, this doesn't seem work consistently and can grab the default stylesheet values
|
|
// instead of the developer's values so let's just make it explicit for the dev for now
|
|
//
|
|
// delaying the read of these val's is not an opt'n: that would delay init'l load time
|
|
var fontFamily = 'Helvetica' || this.containerPropertyAsString('font-family') || 'sans-serif';
|
|
var fontStyle = 'normal' || this.containerPropertyAsString('font-style') || 'normal';
|
|
// var fontVariant = 'normal' || this.containerPropertyAsString('font-variant') || 'normal';
|
|
var fontWeight = 'normal' || this.containerPropertyAsString('font-weight') || 'normal';
|
|
var color = '#000' || this.containerPropertyAsString('color') || '#000';
|
|
var textTransform = 'none' || this.containerPropertyAsString('text-transform') || 'none';
|
|
var fontSize = 16 || this.containerPropertyAsString('font-size') || 16;
|
|
var textMaxWidth = 9999 || this.containerPropertyAsString('text-max-width') || 9999;
|
|
|
|
// fill the style with the default stylesheet
|
|
this
|
|
.selector('node, edge') // common properties
|
|
.css({
|
|
'text-valign': 'top',
|
|
'text-halign': 'center',
|
|
'color': color,
|
|
'text-outline-color': '#000',
|
|
'text-outline-width': 0,
|
|
'text-outline-opacity': 1,
|
|
'text-opacity': 1,
|
|
'text-decoration': 'none',
|
|
'text-transform': textTransform,
|
|
'text-wrap': 'none',
|
|
'text-max-width': textMaxWidth,
|
|
'text-background-color': '#000',
|
|
'text-background-opacity': 0,
|
|
'text-border-opacity': 0,
|
|
'text-border-width': 0,
|
|
'text-border-style': 'solid',
|
|
'text-border-color':'#000',
|
|
'text-background-shape':'rectangle',
|
|
'font-family': fontFamily,
|
|
'font-style': fontStyle,
|
|
// 'font-variant': fontVariant,
|
|
'font-weight': fontWeight,
|
|
'font-size': fontSize,
|
|
'min-zoomed-font-size': 0,
|
|
'edge-text-rotation': 'none',
|
|
'visibility': 'visible',
|
|
'display': 'element',
|
|
'opacity': 1,
|
|
'z-index': 0,
|
|
'content': '',
|
|
'overlay-opacity': 0,
|
|
'overlay-color': '#000',
|
|
'overlay-padding': 10,
|
|
'shadow-opacity': 0,
|
|
'shadow-color': '#000',
|
|
'shadow-blur': 10,
|
|
'shadow-offset-x': 0,
|
|
'shadow-offset-y': 0,
|
|
'text-shadow-opacity': 0,
|
|
'text-shadow-color': '#000',
|
|
'text-shadow-blur': 5,
|
|
'text-shadow-offset-x': 0,
|
|
'text-shadow-offset-y': 0,
|
|
'transition-property': 'none',
|
|
'transition-duration': 0,
|
|
'transition-delay': 0,
|
|
|
|
// node props
|
|
'background-blacken': 0,
|
|
'background-color': '#888',
|
|
'background-opacity': 1,
|
|
'background-image': 'none',
|
|
'background-image-opacity': 1,
|
|
'background-position-x': '50%',
|
|
'background-position-y': '50%',
|
|
'background-repeat': 'no-repeat',
|
|
'background-fit': 'none',
|
|
'background-clip': 'node',
|
|
'background-width': 'auto',
|
|
'background-height': 'auto',
|
|
'border-color': '#000',
|
|
'border-opacity': 1,
|
|
'border-width': 0,
|
|
'border-style': 'solid',
|
|
'height': 30,
|
|
'width': 30,
|
|
'shape': 'ellipse',
|
|
|
|
// compound props
|
|
'padding-top': 0,
|
|
'padding-bottom': 0,
|
|
'padding-left': 0,
|
|
'padding-right': 0,
|
|
'position': 'origin',
|
|
'compound-sizing-wrt-labels': 'include',
|
|
|
|
|
|
// node pie bg
|
|
'pie-size': '100%',
|
|
'pie-1-background-color': 'black',
|
|
'pie-2-background-color': 'black',
|
|
'pie-3-background-color': 'black',
|
|
'pie-4-background-color': 'black',
|
|
'pie-5-background-color': 'black',
|
|
'pie-6-background-color': 'black',
|
|
'pie-7-background-color': 'black',
|
|
'pie-8-background-color': 'black',
|
|
'pie-9-background-color': 'black',
|
|
'pie-10-background-color': 'black',
|
|
'pie-11-background-color': 'black',
|
|
'pie-12-background-color': 'black',
|
|
'pie-13-background-color': 'black',
|
|
'pie-14-background-color': 'black',
|
|
'pie-15-background-color': 'black',
|
|
'pie-16-background-color': 'black',
|
|
'pie-1-background-size': '0%',
|
|
'pie-2-background-size': '0%',
|
|
'pie-3-background-size': '0%',
|
|
'pie-4-background-size': '0%',
|
|
'pie-5-background-size': '0%',
|
|
'pie-6-background-size': '0%',
|
|
'pie-7-background-size': '0%',
|
|
'pie-8-background-size': '0%',
|
|
'pie-9-background-size': '0%',
|
|
'pie-10-background-size': '0%',
|
|
'pie-11-background-size': '0%',
|
|
'pie-12-background-size': '0%',
|
|
'pie-13-background-size': '0%',
|
|
'pie-14-background-size': '0%',
|
|
'pie-15-background-size': '0%',
|
|
'pie-16-background-size': '0%',
|
|
'pie-1-background-opacity': 1,
|
|
'pie-2-background-opacity': 1,
|
|
'pie-3-background-opacity': 1,
|
|
'pie-4-background-opacity': 1,
|
|
'pie-5-background-opacity': 1,
|
|
'pie-6-background-opacity': 1,
|
|
'pie-7-background-opacity': 1,
|
|
'pie-8-background-opacity': 1,
|
|
'pie-9-background-opacity': 1,
|
|
'pie-10-background-opacity': 1,
|
|
'pie-11-background-opacity': 1,
|
|
'pie-12-background-opacity': 1,
|
|
'pie-13-background-opacity': 1,
|
|
'pie-14-background-opacity': 1,
|
|
'pie-15-background-opacity': 1,
|
|
'pie-16-background-opacity': 1,
|
|
|
|
// edge props
|
|
'source-arrow-shape': 'none',
|
|
'mid-source-arrow-shape': 'none',
|
|
'target-arrow-shape': 'none',
|
|
'mid-target-arrow-shape': 'none',
|
|
'source-arrow-color': '#ddd',
|
|
'mid-source-arrow-color': '#ddd',
|
|
'target-arrow-color': '#ddd',
|
|
'mid-target-arrow-color': '#ddd',
|
|
'source-arrow-fill': 'filled',
|
|
'mid-source-arrow-fill': 'filled',
|
|
'target-arrow-fill': 'filled',
|
|
'mid-target-arrow-fill': 'filled',
|
|
'line-style': 'solid',
|
|
'line-color': '#ddd',
|
|
'control-point-step-size': 40,
|
|
'control-point-weight': 0.5,
|
|
'curve-style': 'bezier',
|
|
'haystack-radius': 0.8
|
|
})
|
|
.selector('$node > node') // compound (parent) node properties
|
|
.css({
|
|
'width': 'auto',
|
|
'height': 'auto',
|
|
'shape': 'rectangle',
|
|
'background-opacity': 0.5,
|
|
'padding-top': 10,
|
|
'padding-right': 10,
|
|
'padding-left': 10,
|
|
'padding-bottom': 10
|
|
})
|
|
.selector('edge') // just edge properties
|
|
.css({
|
|
'width': 1
|
|
})
|
|
.selector(':active')
|
|
.css({
|
|
'overlay-color': 'black',
|
|
'overlay-padding': 10,
|
|
'overlay-opacity': 0.25
|
|
})
|
|
.selector('core') // just core properties
|
|
.css({
|
|
'selection-box-color': '#ddd',
|
|
'selection-box-opacity': 0.65,
|
|
'selection-box-border-color': '#aaa',
|
|
'selection-box-border-width': 1,
|
|
'active-bg-color': 'black',
|
|
'active-bg-opacity': 0.15,
|
|
'active-bg-size': 30,
|
|
'outside-texture-bg-color': '#000',
|
|
'outside-texture-bg-opacity': 0.125
|
|
})
|
|
;
|
|
|
|
this.defaultLength = this.length;
|
|
};
|
|
|
|
// remove all contexts
|
|
$$.styfn.clear = function(){
|
|
for( var i = 0; i < this.length; i++ ){
|
|
this[i] = undefined;
|
|
}
|
|
this.length = 0;
|
|
this._private.newStyle = true;
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
$$.styfn.resetToDefault = function(){
|
|
this.clear();
|
|
this.addDefaultStylesheet();
|
|
|
|
return this;
|
|
};
|
|
|
|
// builds a style object for the 'core' selector
|
|
$$.styfn.core = function(){
|
|
return this._private.coreStyle;
|
|
};
|
|
|
|
// a caching layer for property parsing
|
|
$$.styfn.parse = function( name, value, propIsBypass, propIsFlat ){
|
|
var argHash = [ name, value, propIsBypass, propIsFlat ].join('$');
|
|
var propCache = this.propCache = this.propCache || {};
|
|
var ret;
|
|
|
|
if( !(ret = propCache[argHash]) ){
|
|
ret = propCache[argHash] = this.parseImpl( name, value, propIsBypass, propIsFlat );
|
|
}
|
|
|
|
// always need a copy since props are mutated later in their lifecycles
|
|
return $$.util.copy( ret );
|
|
};
|
|
|
|
// parse a property; return null on invalid; return parsed property otherwise
|
|
// fields :
|
|
// - name : the name of the property
|
|
// - value : the parsed, native-typed value of the property
|
|
// - strValue : a string value that represents the property value in valid css
|
|
// - bypass : true iff the property is a bypass property
|
|
$$.styfn.parseImpl = function( name, value, propIsBypass, propIsFlat ){
|
|
|
|
name = $$.util.camel2dash( name ); // make sure the property name is in dash form (e.g. 'property-name' not 'propertyName')
|
|
var property = $$.style.properties[ name ];
|
|
var passedValue = value;
|
|
var types = $$.style.types;
|
|
|
|
if( !property ){ return null; } // return null on property of unknown name
|
|
if( value === undefined || value === null ){ return null; } // can't assign null
|
|
|
|
var valueIsString = $$.is.string(value);
|
|
if( valueIsString ){ // trim the value to make parsing easier
|
|
value = $$.util.trim( value );
|
|
}
|
|
|
|
var type = property.type;
|
|
if( !type ){ return null; } // no type, no luck
|
|
|
|
// check if bypass is null or empty string (i.e. indication to delete bypass property)
|
|
if( propIsBypass && (value === '' || value === null) ){
|
|
return {
|
|
name: name,
|
|
value: value,
|
|
bypass: true,
|
|
deleteBypass: true
|
|
};
|
|
}
|
|
|
|
var hasPie = name.match(/pie-(\d+)-background-size/);
|
|
|
|
// check if value is a function used as a mapper
|
|
if( $$.is.fn(value) ){
|
|
return {
|
|
name: name,
|
|
value: value,
|
|
strValue: 'fn',
|
|
mapped: types.fn,
|
|
bypass: propIsBypass,
|
|
hasPie: hasPie
|
|
};
|
|
}
|
|
|
|
// check if value is mapped
|
|
var data, mapData, layoutData, mapLayoutData, scratch, mapScratch;
|
|
if( !valueIsString || propIsFlat ){
|
|
// then don't bother to do the expensive regex checks
|
|
|
|
} else if(
|
|
( data = new RegExp( types.data.regex ).exec( value ) ) ||
|
|
( layoutData = new RegExp( types.layoutData.regex ).exec( value ) ) ||
|
|
( scratch = new RegExp( types.scratch.regex ).exec( value ) )
|
|
){
|
|
if( propIsBypass ){ return false; } // mappers not allowed in bypass
|
|
|
|
var mapped;
|
|
if( data ){
|
|
mapped = types.data;
|
|
} else if( layoutData ){
|
|
mapped = types.layoutData;
|
|
} else {
|
|
mapped = types.scratch;
|
|
}
|
|
|
|
data = data || layoutData || scratch;
|
|
|
|
return {
|
|
name: name,
|
|
value: data,
|
|
strValue: '' + value,
|
|
mapped: mapped,
|
|
field: data[1],
|
|
bypass: propIsBypass,
|
|
hasPie: hasPie
|
|
};
|
|
|
|
} else if(
|
|
( mapData = new RegExp( types.mapData.regex ).exec( value ) ) ||
|
|
( mapLayoutData = new RegExp( types.mapLayoutData.regex ).exec( value ) ) ||
|
|
( mapScratch = new RegExp( types.mapScratch.regex ).exec( value ) )
|
|
){
|
|
if( propIsBypass ){ return false; } // mappers not allowed in bypass
|
|
|
|
var mapped;
|
|
if( mapData ){
|
|
mapped = types.mapData;
|
|
} else if( mapLayoutData ){
|
|
mapped = types.mapLayoutData;
|
|
} else {
|
|
mapped = types.mapScratch;
|
|
}
|
|
|
|
mapData = mapData || mapLayoutData || mapScratch;
|
|
|
|
// we can map only if the type is a colour or a number
|
|
if( !(type.color || type.number) ){ return false; }
|
|
|
|
var valueMin = this.parse( name, mapData[4]); // parse to validate
|
|
if( !valueMin || valueMin.mapped ){ return false; } // can't be invalid or mapped
|
|
|
|
var valueMax = this.parse( name, mapData[5]); // parse to validate
|
|
if( !valueMax || valueMax.mapped ){ return false; } // can't be invalid or mapped
|
|
|
|
// check if valueMin and valueMax are the same
|
|
if( valueMin.value === valueMax.value ){
|
|
return false; // can't make much of a mapper without a range
|
|
|
|
} else if( type.color ){
|
|
var c1 = valueMin.value;
|
|
var c2 = valueMax.value;
|
|
|
|
var same = c1[0] === c2[0] // red
|
|
&& c1[1] === c2[1] // green
|
|
&& c1[2] === c2[2] // blue
|
|
&& ( // optional alpha
|
|
c1[3] === c2[3] // same alpha outright
|
|
|| (
|
|
(c1[3] == null || c1[3] === 1) // full opacity for colour 1?
|
|
&&
|
|
(c2[3] == null || c2[3] === 1) // full opacity for colour 2?
|
|
)
|
|
)
|
|
;
|
|
|
|
if( same ){ return false; } // can't make a mapper without a range
|
|
}
|
|
|
|
return {
|
|
name: name,
|
|
value: mapData,
|
|
strValue: '' + value,
|
|
mapped: mapped,
|
|
field: mapData[1],
|
|
fieldMin: parseFloat( mapData[2] ), // min & max are numeric
|
|
fieldMax: parseFloat( mapData[3] ),
|
|
valueMin: valueMin.value,
|
|
valueMax: valueMax.value,
|
|
bypass: propIsBypass,
|
|
hasPie: hasPie
|
|
};
|
|
}
|
|
|
|
// check the type and return the appropriate object
|
|
if( type.number ){
|
|
var units;
|
|
var implicitUnits = 'px'; // not set => px
|
|
|
|
if( type.units ){ // use specified units if set
|
|
units = type.units;
|
|
}
|
|
|
|
if( type.implicitUnits ){
|
|
implicitUnits = type.implicitUnits;
|
|
}
|
|
|
|
if( !type.unitless ){
|
|
if( valueIsString ){
|
|
var unitsRegex = 'px|em' + (type.allowPercent ? '|\\%' : '');
|
|
if( units ){ unitsRegex = units; } // only allow explicit units if so set
|
|
var match = value.match( '^(' + $$.util.regex.number + ')(' + unitsRegex + ')?' + '$' );
|
|
|
|
if( match ){
|
|
value = match[1];
|
|
units = match[2] || implicitUnits;
|
|
}
|
|
|
|
} else if( !units || type.implicitUnits ) {
|
|
units = implicitUnits; // implicitly px if unspecified
|
|
}
|
|
}
|
|
|
|
value = parseFloat( value );
|
|
|
|
// if not a number and enums not allowed, then the value is invalid
|
|
if( isNaN(value) && type.enums === undefined ){
|
|
return null;
|
|
}
|
|
|
|
// check if this number type also accepts special keywords in place of numbers
|
|
// (i.e. `left`, `auto`, etc)
|
|
if( isNaN(value) && type.enums !== undefined ){
|
|
value = passedValue;
|
|
|
|
for( var i = 0; i < type.enums.length; i++ ){
|
|
var en = type.enums[i];
|
|
|
|
if( en === value ){
|
|
return {
|
|
name: name,
|
|
value: value,
|
|
strValue: '' + value,
|
|
bypass: propIsBypass
|
|
};
|
|
}
|
|
}
|
|
|
|
return null; // failed on enum after failing on number
|
|
}
|
|
|
|
// check if value must be an integer
|
|
if( type.integer && !$$.is.integer(value) ){
|
|
return null;
|
|
}
|
|
|
|
// check value is within range
|
|
if( (type.min !== undefined && value < type.min)
|
|
|| (type.max !== undefined && value > type.max)
|
|
){
|
|
return null;
|
|
}
|
|
|
|
var ret = {
|
|
name: name,
|
|
value: value,
|
|
strValue: '' + value + (units ? units : ''),
|
|
units: units,
|
|
bypass: propIsBypass,
|
|
hasPie: hasPie && value != null && value !== 0 && value !== ''
|
|
};
|
|
|
|
// normalise value in pixels
|
|
if( type.unitless || (units !== 'px' && units !== 'em') ){
|
|
// then pxValue does not apply
|
|
} else {
|
|
ret.pxValue = ( units === 'px' || !units ? (value) : (this.getEmSizeInPixels() * value) );
|
|
}
|
|
|
|
// normalise value in ms
|
|
if( units === 'ms' || units === 's' ){
|
|
ret.msValue = units === 'ms' ? value : 1000 * value;
|
|
}
|
|
|
|
return ret;
|
|
|
|
} else if( type.propList ) {
|
|
|
|
var props = [];
|
|
var propsStr = '' + value;
|
|
|
|
if( propsStr === 'none' ){
|
|
// leave empty
|
|
|
|
} else { // go over each prop
|
|
|
|
var propsSplit = propsStr.split(',');
|
|
for( var i = 0; i < propsSplit.length; i++ ){
|
|
var propName = $$.util.trim( propsSplit[i] );
|
|
|
|
if( $$.style.properties[propName] ){
|
|
props.push( propName );
|
|
}
|
|
}
|
|
|
|
if( props.length === 0 ){ return null; }
|
|
|
|
}
|
|
|
|
return {
|
|
name: name,
|
|
value: props,
|
|
strValue: props.length === 0 ? 'none' : props.join(', '),
|
|
bypass: propIsBypass
|
|
};
|
|
|
|
} else if( type.color ){
|
|
var tuple = $$.util.color2tuple( value );
|
|
|
|
if( !tuple ){ return null; }
|
|
|
|
return {
|
|
name: name,
|
|
value: tuple,
|
|
strValue: '' + value,
|
|
bypass: propIsBypass
|
|
};
|
|
|
|
} else if( type.enums ){
|
|
for( var i = 0; i < type.enums.length; i++ ){
|
|
var en = type.enums[i];
|
|
|
|
if( en === value ){
|
|
return {
|
|
name: name,
|
|
value: value,
|
|
strValue: '' + value,
|
|
bypass: propIsBypass
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
|
|
} else if( type.regex ){
|
|
var regex = new RegExp( type.regex ); // make a regex from the type
|
|
var m = regex.exec( value );
|
|
|
|
if( m ){ // regex matches
|
|
return {
|
|
name: name,
|
|
value: m,
|
|
strValue: '' + value,
|
|
bypass: propIsBypass
|
|
};
|
|
} else { // regex doesn't match
|
|
return null; // didn't match the regex so the value is bogus
|
|
}
|
|
|
|
} else if( type.string ){
|
|
// just return
|
|
return {
|
|
name: name,
|
|
value: value,
|
|
strValue: '' + value,
|
|
bypass: propIsBypass
|
|
};
|
|
|
|
} else {
|
|
return null; // not a type we can handle
|
|
}
|
|
|
|
};
|
|
|
|
// create a new context from the specified selector string and switch to that context
|
|
$$.styfn.selector = function( selectorStr ){
|
|
// 'core' is a special case and does not need a selector
|
|
var selector = selectorStr === 'core' ? null : new $$.Selector( selectorStr );
|
|
|
|
var i = this.length++; // new context means new index
|
|
this[i] = {
|
|
selector: selector,
|
|
properties: [],
|
|
mappedProperties: [],
|
|
index: i
|
|
};
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
// add one or many css rules to the current context
|
|
$$.styfn.css = function(){
|
|
var args = arguments;
|
|
|
|
switch( args.length ){
|
|
case 1:
|
|
var map = args[0];
|
|
|
|
for( var i = 0; i < $$.style.properties.length; i++ ){
|
|
var prop = $$.style.properties[i];
|
|
var mapVal = map[ prop.name ];
|
|
|
|
if( mapVal === undefined ){
|
|
mapVal = map[ $$.util.dash2camel(prop.name) ];
|
|
}
|
|
|
|
if( mapVal !== undefined ){
|
|
this.cssRule( prop.name, mapVal );
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case 2:
|
|
this.cssRule( args[0], args[1] );
|
|
break;
|
|
|
|
default:
|
|
break; // do nothing if args are invalid
|
|
}
|
|
|
|
return this; // chaining
|
|
};
|
|
$$.styfn.style = $$.styfn.css;
|
|
|
|
// add a single css rule to the current context
|
|
$$.styfn.cssRule = function( name, value ){
|
|
// name-value pair
|
|
var property = this.parse( name, value );
|
|
|
|
// add property to current context if valid
|
|
if( property ){
|
|
var i = this.length - 1;
|
|
this[i].properties.push( property );
|
|
this[i].properties[ property.name ] = property; // allow access by name as well
|
|
|
|
if( property.hasPie ){
|
|
this._private.hasPie = true;
|
|
}
|
|
|
|
if( property.mapped ){
|
|
this[i].mappedProperties.push( property );
|
|
}
|
|
|
|
// add to core style if necessary
|
|
var currentSelectorIsCore = !this[i].selector;
|
|
if( currentSelectorIsCore ){
|
|
this._private.coreStyle[ property.name ] = property;
|
|
}
|
|
}
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// (potentially expensive calculation)
|
|
// apply the style to the element based on
|
|
// - its bypass
|
|
// - what selectors match it
|
|
$$.styfn.apply = function( eles ){
|
|
var self = this;
|
|
|
|
if( self._private.newStyle ){ // clear style caches
|
|
this._private.contextStyles = {};
|
|
this._private.propDiffs = {};
|
|
}
|
|
|
|
for( var ie = 0; ie < eles.length; ie++ ){
|
|
var ele = eles[ie];
|
|
var cxtMeta = self.getContextMeta( ele );
|
|
var cxtStyle = self.getContextStyle( cxtMeta );
|
|
var app = self.applyContextStyle( cxtMeta, cxtStyle, ele );
|
|
|
|
self.updateTransitions( ele, app.diffProps );
|
|
self.updateStyleHints( ele );
|
|
|
|
} // for elements
|
|
|
|
self._private.newStyle = false;
|
|
};
|
|
|
|
$$.styfn.getPropertiesDiff = function( oldCxtKey, newCxtKey ){
|
|
var self = this;
|
|
var cache = self._private.propDiffs = self._private.propDiffs || {};
|
|
var dualCxtKey = oldCxtKey + '-' + newCxtKey;
|
|
var cachedVal = cache[dualCxtKey];
|
|
|
|
if( cachedVal ){
|
|
return cachedVal;
|
|
}
|
|
|
|
var diffProps = [];
|
|
var addedProp = {};
|
|
|
|
for( var i = 0; i < self.length; i++ ){
|
|
var cxt = self[i];
|
|
var oldHasCxt = oldCxtKey[i] === 't';
|
|
var newHasCxt = newCxtKey[i] === 't';
|
|
var cxtHasDiffed = oldHasCxt !== newHasCxt;
|
|
var cxtHasMappedProps = cxt.mappedProperties.length > 0;
|
|
|
|
if( cxtHasDiffed || cxtHasMappedProps ){
|
|
var props;
|
|
|
|
if( cxtHasDiffed && cxtHasMappedProps ){
|
|
props = cxt.properties; // suffices b/c mappedProperties is a subset of properties
|
|
} else if( cxtHasDiffed ){
|
|
props = cxt.properties; // need to check them all
|
|
} else if( cxtHasMappedProps ){
|
|
props = cxt.mappedProperties; // only need to check mapped
|
|
}
|
|
|
|
for( var j = 0; j < props.length; j++ ){
|
|
var prop = props[j];
|
|
var name = prop.name;
|
|
|
|
// if a later context overrides this property, then the fact that this context has switched/diffed doesn't matter
|
|
// (semi expensive check since it makes this function O(n^2) on context length, but worth it since overall result
|
|
// is cached)
|
|
var laterCxtOverrides = false;
|
|
for( var k = i + 1; k < self.length; k++ ){
|
|
var laterCxt = self[k];
|
|
var hasLaterCxt = newCxtKey[k] === 't';
|
|
|
|
if( !hasLaterCxt ){ continue; } // can't override unless the context is active
|
|
|
|
laterCxtOverrides = laterCxt.properties[ prop.name ] != null;
|
|
|
|
if( laterCxtOverrides ){ break; } // exit early as long as one later context overrides
|
|
}
|
|
|
|
if( !addedProp[name] && !laterCxtOverrides ){
|
|
addedProp[name] = true;
|
|
diffProps.push( name );
|
|
}
|
|
} // for props
|
|
} // if
|
|
|
|
} // for contexts
|
|
|
|
cache[ dualCxtKey ] = diffProps;
|
|
return diffProps;
|
|
};
|
|
|
|
$$.styfn.getContextMeta = function( ele ){
|
|
var self = this;
|
|
var cxtKey = '';
|
|
var diffProps;
|
|
var prevKey = ele._private.styleCxtKey || '';
|
|
|
|
if( self._private.newStyle ){
|
|
prevKey = ''; // since we need to apply all style if a fresh stylesheet
|
|
}
|
|
|
|
// get the cxt key
|
|
for( var i = 0; i < self.length; i++ ){
|
|
var context = self[i];
|
|
var contextSelectorMatches = context.selector && context.selector.matches( ele ); // NB: context.selector may be null for 'core'
|
|
|
|
if( contextSelectorMatches ){
|
|
cxtKey += 't';
|
|
} else {
|
|
cxtKey += 'f';
|
|
}
|
|
} // for context
|
|
|
|
diffProps = self.getPropertiesDiff( prevKey, cxtKey );
|
|
|
|
ele._private.styleCxtKey = cxtKey;
|
|
|
|
return {
|
|
key: cxtKey,
|
|
diffPropNames: diffProps
|
|
};
|
|
};
|
|
|
|
// gets a computed ele style object based on matched contexts
|
|
$$.styfn.getContextStyle = function( cxtMeta ){
|
|
var cxtKey = cxtMeta.key;
|
|
var self = this;
|
|
var cxtStyles = this._private.contextStyles = this._private.contextStyles || {};
|
|
|
|
// if already computed style, returned cached copy
|
|
if( cxtStyles[cxtKey] ){ return cxtStyles[cxtKey]; }
|
|
|
|
var style = {
|
|
_private: {
|
|
key: cxtKey
|
|
}
|
|
};
|
|
|
|
for( var i = 0; i < self.length; i++ ){
|
|
var cxt = self[i];
|
|
var hasCxt = cxtKey[i] === 't';
|
|
|
|
if( !hasCxt ){ continue; }
|
|
|
|
for( var j = 0; j < cxt.properties.length; j++ ){
|
|
var prop = cxt.properties[j];
|
|
var styProp = style[ prop.name ] = prop;
|
|
|
|
styProp.context = cxt;
|
|
}
|
|
}
|
|
|
|
cxtStyles[cxtKey] = style;
|
|
return style;
|
|
};
|
|
|
|
$$.styfn.applyContextStyle = function( cxtMeta, cxtStyle, ele ){
|
|
var self = this;
|
|
var diffProps = cxtMeta.diffPropNames;
|
|
var retDiffProps = {};
|
|
|
|
for( var i = 0; i < diffProps.length; i++ ){
|
|
var diffPropName = diffProps[i];
|
|
var cxtProp = cxtStyle[ diffPropName ];
|
|
var eleProp = ele._private.style[ diffPropName ];
|
|
|
|
// save cycles when the context prop doesn't need to be applied
|
|
if( !cxtProp || eleProp === cxtProp ){ continue; }
|
|
|
|
var retDiffProp = retDiffProps[ diffPropName ] = {
|
|
prev: eleProp
|
|
};
|
|
|
|
self.applyParsedProperty( ele, cxtProp );
|
|
|
|
retDiffProp.next = ele._private.style[ diffPropName ];
|
|
|
|
if( retDiffProp.next && retDiffProp.next.bypass ){
|
|
retDiffProp.next = retDiffProp.next.bypassed;
|
|
}
|
|
}
|
|
|
|
return {
|
|
diffProps: retDiffProps
|
|
};
|
|
};
|
|
|
|
$$.styfn.updateStyleHints = function(ele){
|
|
var _p = ele._private;
|
|
var self = this;
|
|
var style = _p.style;
|
|
|
|
// set whether has pie or not; for greater efficiency
|
|
var hasPie = false;
|
|
if( _p.group === 'nodes' && self._private.hasPie ){
|
|
for( var i = 1; i <= $$.style.pieBackgroundN; i++ ){ // 1..N
|
|
var size = _p.style['pie-' + i + '-background-size'].value;
|
|
|
|
if( size > 0 ){
|
|
hasPie = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
_p.hasPie = hasPie;
|
|
|
|
var transform = style['text-transform'].strValue;
|
|
var content = style['content'].strValue;
|
|
var fStyle = style['font-style'].strValue;
|
|
var size = style['font-size'].pxValue + 'px';
|
|
var family = style['font-family'].strValue;
|
|
// var variant = style['font-variant'].strValue;
|
|
var weight = style['font-weight'].strValue;
|
|
var valign = style['text-valign'].strValue;
|
|
var halign = style['text-valign'].strValue;
|
|
var oWidth = style['text-outline-width'].pxValue;
|
|
var wrap = style['text-wrap'].strValue;
|
|
var wrapW = style['text-max-width'].pxValue;
|
|
_p.labelKey = fStyle +'$'+ size +'$'+ family +'$'+ weight +'$'+ content +'$'+ transform +'$'+ valign +'$'+ halign +'$'+ oWidth + '$' + wrap + '$' + wrapW;
|
|
_p.fontKey = fStyle +'$'+ weight +'$'+ size +'$'+ family;
|
|
|
|
var width = style['width'].pxValue;
|
|
var height = style['height'].pxValue;
|
|
var borderW = style['border-width'].pxValue;
|
|
_p.boundingBoxKey = width +'$'+ height +'$'+ borderW;
|
|
|
|
if( ele._private.group === 'edges' ){
|
|
var cpss = style['control-point-step-size'].pxValue;
|
|
var cpd = style['control-point-distance'] ? style['control-point-distance'].pxValue : undefined;
|
|
var cpw = style['control-point-weight'].value;
|
|
var curve = style['curve-style'].strValue;
|
|
|
|
_p.boundingBoxKey += '$'+ cpss +'$'+ cpd +'$'+ cpw +'$'+ curve;
|
|
}
|
|
|
|
_p.styleKey = Date.now(); // probably safe to use applied time and much faster
|
|
// for( var i = 0; i < $$.style.properties.length; i++ ){
|
|
// var prop = $$.style.properties[i];
|
|
// var eleProp = _p.style[ prop.name ];
|
|
// var val = eleProp && eleProp.strValue ? eleProp.strValue : 'undefined';
|
|
|
|
// _p.styleKey += '$' + val;
|
|
// }
|
|
};
|
|
|
|
// apply a property to the style (for internal use)
|
|
// returns whether application was successful
|
|
//
|
|
// now, this function flattens the property, and here's how:
|
|
//
|
|
// for parsedProp:{ bypass: true, deleteBypass: true }
|
|
// no property is generated, instead the bypass property in the
|
|
// element's style is replaced by what's pointed to by the `bypassed`
|
|
// field in the bypass property (i.e. restoring the property the
|
|
// bypass was overriding)
|
|
//
|
|
// for parsedProp:{ mapped: truthy }
|
|
// the generated flattenedProp:{ mapping: prop }
|
|
//
|
|
// for parsedProp:{ bypass: true }
|
|
// the generated flattenedProp:{ bypassed: parsedProp }
|
|
$$.styfn.applyParsedProperty = function( ele, parsedProp ){
|
|
var prop = parsedProp;
|
|
var style = ele._private.style;
|
|
var fieldVal, flatProp;
|
|
var types = $$.style.types;
|
|
var type = $$.style.properties[ prop.name ].type;
|
|
var propIsBypass = prop.bypass;
|
|
var origProp = style[ prop.name ];
|
|
var origPropIsBypass = origProp && origProp.bypass;
|
|
var _p = ele._private;
|
|
|
|
// can't apply auto to width or height unless it's a parent node
|
|
if( (parsedProp.name === 'height' || parsedProp.name === 'width') && ele.isNode() ){
|
|
if( parsedProp.value === 'auto' && !ele.isParent() ){
|
|
return false;
|
|
} else if( parsedProp.value !== 'auto' && ele.isParent() ){
|
|
prop = parsedProp = this.parse( parsedProp.name, 'auto', propIsBypass );
|
|
}
|
|
}
|
|
|
|
// check if we need to delete the current bypass
|
|
if( propIsBypass && prop.deleteBypass ){ // then this property is just here to indicate we need to delete
|
|
var currentProp = style[ prop.name ];
|
|
|
|
// can only delete if the current prop is a bypass and it points to the property it was overriding
|
|
if( !currentProp ){
|
|
return true; // property is already not defined
|
|
} else if( currentProp.bypass && currentProp.bypassed ){ // then replace the bypass property with the original
|
|
|
|
// because the bypassed property was already applied (and therefore parsed), we can just replace it (no reapplying necessary)
|
|
style[ prop.name ] = currentProp.bypassed;
|
|
return true;
|
|
|
|
} else {
|
|
return false; // we're unsuccessful deleting the bypass
|
|
}
|
|
}
|
|
|
|
var printMappingErr = function(){
|
|
$$.util.error('Do not assign mappings to elements without corresponding data (e.g. ele `'+ ele.id() +'` for property `'+ prop.name +'` with data field `'+ prop.field +'`); try a `['+ prop.field +']` selector to limit scope to elements with `'+ prop.field +'` defined');
|
|
};
|
|
|
|
// put the property in the style objects
|
|
switch( prop.mapped ){ // flatten the property if mapped
|
|
case types.mapData:
|
|
case types.mapLayoutData:
|
|
case types.mapScratch:
|
|
|
|
var isLayout = prop.mapped === types.mapLayoutData;
|
|
var isScratch = prop.mapped === types.mapScratch;
|
|
|
|
// flatten the field (e.g. data.foo.bar)
|
|
var fields = prop.field.split(".");
|
|
var fieldVal;
|
|
|
|
if( isScratch || isLayout ){
|
|
fieldVal = _p.scratch;
|
|
} else {
|
|
fieldVal = _p.data;
|
|
}
|
|
|
|
for( var i = 0; i < fields.length && fieldVal; i++ ){
|
|
var field = fields[i];
|
|
fieldVal = fieldVal[ field ];
|
|
}
|
|
|
|
var percent;
|
|
if( !$$.is.number(fieldVal) ){ // then keep the mapping but assume 0% for now
|
|
percent = 0;
|
|
} else {
|
|
percent = (fieldVal - prop.fieldMin) / (prop.fieldMax - prop.fieldMin);
|
|
}
|
|
|
|
// make sure to bound percent value
|
|
if( percent < 0 ){
|
|
percent = 0;
|
|
} else if( percent > 1 ){
|
|
percent = 1;
|
|
}
|
|
|
|
if( type.color ){
|
|
var r1 = prop.valueMin[0];
|
|
var r2 = prop.valueMax[0];
|
|
var g1 = prop.valueMin[1];
|
|
var g2 = prop.valueMax[1];
|
|
var b1 = prop.valueMin[2];
|
|
var b2 = prop.valueMax[2];
|
|
var a1 = prop.valueMin[3] == null ? 1 : prop.valueMin[3];
|
|
var a2 = prop.valueMax[3] == null ? 1 : prop.valueMax[3];
|
|
|
|
var clr = [
|
|
Math.round( r1 + (r2 - r1)*percent ),
|
|
Math.round( g1 + (g2 - g1)*percent ),
|
|
Math.round( b1 + (b2 - b1)*percent ),
|
|
Math.round( a1 + (a2 - a1)*percent )
|
|
];
|
|
|
|
flatProp = { // colours are simple, so just create the flat property instead of expensive string parsing
|
|
bypass: prop.bypass, // we're a bypass if the mapping property is a bypass
|
|
name: prop.name,
|
|
value: clr,
|
|
strValue: 'rgb(' + clr[0] + ', ' + clr[1] + ', ' + clr[2] + ')'
|
|
};
|
|
|
|
} else if( type.number ){
|
|
var calcValue = prop.valueMin + (prop.valueMax - prop.valueMin) * percent;
|
|
flatProp = this.parse( prop.name, calcValue, prop.bypass, true );
|
|
|
|
} else {
|
|
return false; // can only map to colours and numbers
|
|
}
|
|
|
|
if( !flatProp ){ // if we can't flatten the property, then use the origProp so we still keep the mapping itself
|
|
flatProp = this.parse( prop.name, origProp.strValue, prop.bypass, true );
|
|
}
|
|
|
|
if( !flatProp ){ printMappingErr(); }
|
|
flatProp.mapping = prop; // keep a reference to the mapping
|
|
prop = flatProp; // the flattened (mapped) property is the one we want
|
|
|
|
break;
|
|
|
|
// direct mapping
|
|
case types.data:
|
|
case types.layoutData:
|
|
case types.scratch:
|
|
var isLayout = prop.mapped === types.layoutData;
|
|
var isScratch = prop.mapped === types.scratch;
|
|
|
|
// flatten the field (e.g. data.foo.bar)
|
|
var fields = prop.field.split(".");
|
|
var fieldVal;
|
|
|
|
if( isScratch || isLayout ){
|
|
fieldVal = _p.scratch;
|
|
} else {
|
|
fieldVal = _p.data;
|
|
}
|
|
|
|
if( fieldVal ){ for( var i = 0; i < fields.length; i++ ){
|
|
var field = fields[i];
|
|
fieldVal = fieldVal[ field ];
|
|
} }
|
|
|
|
flatProp = this.parse( prop.name, fieldVal, prop.bypass, true );
|
|
|
|
if( !flatProp ){ // if we can't flatten the property, then use the origProp so we still keep the mapping itself
|
|
var flatPropVal = origProp ? origProp.strValue : '';
|
|
|
|
flatProp = this.parse( prop.name, flatPropVal, prop.bypass, true );
|
|
}
|
|
|
|
if( !flatProp ){ printMappingErr(); }
|
|
flatProp.mapping = prop; // keep a reference to the mapping
|
|
prop = flatProp; // the flattened (mapped) property is the one we want
|
|
|
|
break;
|
|
|
|
case types.fn:
|
|
var fn = prop.value;
|
|
var fnRetVal = fn( ele );
|
|
|
|
flatProp = this.parse( prop.name, fnRetVal, prop.bypass, true );
|
|
flatProp.mapping = prop; // keep a reference to the mapping
|
|
prop = flatProp; // the flattened (mapped) property is the one we want
|
|
|
|
break;
|
|
|
|
case undefined:
|
|
break; // just set the property
|
|
|
|
default:
|
|
return false; // not a valid mapping
|
|
}
|
|
|
|
// if the property is a bypass property, then link the resultant property to the original one
|
|
if( propIsBypass ){
|
|
if( origPropIsBypass ){ // then this bypass overrides the existing one
|
|
prop.bypassed = origProp.bypassed; // steal bypassed prop from old bypass
|
|
} else { // then link the orig prop to the new bypass
|
|
prop.bypassed = origProp;
|
|
}
|
|
|
|
style[ prop.name ] = prop; // and set
|
|
|
|
} else { // prop is not bypass
|
|
if( origPropIsBypass ){ // then keep the orig prop (since it's a bypass) and link to the new prop
|
|
origProp.bypassed = prop;
|
|
} else { // then just replace the old prop with the new one
|
|
style[ prop.name ] = prop;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// updates the visual style for all elements (useful for manual style modification after init)
|
|
$$.styfn.update = function(){
|
|
var cy = this._private.cy;
|
|
var eles = cy.elements();
|
|
|
|
eles.updateStyle();
|
|
};
|
|
|
|
// just update the functional properties (i.e. mappings) in the elements'
|
|
// styles (less expensive than recalculation)
|
|
$$.styfn.updateMappers = function( eles ){
|
|
for( var i = 0; i < eles.length; i++ ){ // for each ele
|
|
var ele = eles[i];
|
|
var style = ele._private.style;
|
|
|
|
for( var j = 0; j < $$.style.properties.length; j++ ){ // for each prop
|
|
var prop = $$.style.properties[j];
|
|
var propInStyle = style[ prop.name ];
|
|
|
|
if( propInStyle && propInStyle.mapping ){
|
|
var mapping = propInStyle.mapping;
|
|
this.applyParsedProperty( ele, mapping ); // reapply the mapping property
|
|
}
|
|
}
|
|
|
|
this.updateStyleHints( ele );
|
|
}
|
|
};
|
|
|
|
// diffProps : { name => { prev, next } }
|
|
$$.styfn.updateTransitions = function( ele, diffProps, isBypass ){
|
|
var self = this;
|
|
var style = ele._private.style;
|
|
|
|
var props = style['transition-property'].value;
|
|
var duration = style['transition-duration'].msValue;
|
|
var delay = style['transition-delay'].msValue;
|
|
var css = {};
|
|
|
|
if( props.length > 0 && duration > 0 ){
|
|
|
|
// build up the style to animate towards
|
|
var anyPrev = false;
|
|
for( var i = 0; i < props.length; i++ ){
|
|
var prop = props[i];
|
|
var styProp = style[ prop ];
|
|
var diffProp = diffProps[ prop ];
|
|
|
|
if( !diffProp ){ continue; }
|
|
|
|
var prevProp = diffProp.prev;
|
|
var fromProp = prevProp;
|
|
var toProp = diffProp.next != null ? diffProp.next : styProp;
|
|
var diff = false;
|
|
var initVal;
|
|
var initDt = 0.000001; // delta time % value for initVal (allows animating out of init zero opacity)
|
|
|
|
if( !fromProp ){ continue; }
|
|
|
|
// consider px values
|
|
if( $$.is.number( fromProp.pxValue ) && $$.is.number( toProp.pxValue ) ){
|
|
diff = toProp.pxValue - fromProp.pxValue; // nonzero is truthy
|
|
initVal = fromProp.pxValue + initDt * diff;
|
|
|
|
// consider numerical values
|
|
} else if( $$.is.number( fromProp.value ) && $$.is.number( toProp.value ) ){
|
|
diff = toProp.value - fromProp.value; // nonzero is truthy
|
|
initVal = fromProp.value + initDt * diff;
|
|
|
|
// consider colour values
|
|
} else if( $$.is.array( fromProp.value ) && $$.is.array( toProp.value ) ){
|
|
diff = fromProp.value[0] !== toProp.value[0]
|
|
|| fromProp.value[1] !== toProp.value[1]
|
|
|| fromProp.value[2] !== toProp.value[2]
|
|
;
|
|
|
|
initVal = fromProp.strValue;
|
|
}
|
|
|
|
// the previous value is good for an animation only if it's different
|
|
if( diff ){
|
|
css[ prop ] = toProp.strValue; // to val
|
|
this.applyBypass( ele, prop, initVal ); // from val
|
|
anyPrev = true;
|
|
}
|
|
|
|
} // end if props allow ani
|
|
|
|
// can't transition if there's nothing previous to transition from
|
|
if( !anyPrev ){ return; }
|
|
|
|
ele._private.transitioning = true;
|
|
|
|
ele.stop();
|
|
|
|
if( delay > 0 ){
|
|
ele.delay( delay );
|
|
}
|
|
|
|
ele.animate({
|
|
css: css
|
|
}, {
|
|
duration: duration,
|
|
queue: false,
|
|
complete: function(){
|
|
if( !isBypass ){
|
|
self.removeBypasses( ele, props );
|
|
}
|
|
|
|
ele._private.transitioning = false;
|
|
}
|
|
});
|
|
|
|
} else if( ele._private.transitioning ){
|
|
ele.stop();
|
|
|
|
this.removeBypasses( ele, props );
|
|
|
|
ele._private.transitioning = false;
|
|
}
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// bypasses are applied to an existing style on an element, and just tacked on temporarily
|
|
// returns true iff application was successful for at least 1 specified property
|
|
$$.styfn.applyBypass = function( eles, name, value, updateTransitions ){
|
|
var props = [];
|
|
var isBypass = true;
|
|
|
|
// put all the properties (can specify one or many) in an array after parsing them
|
|
if( name === "*" || name === "**" ){ // apply to all property names
|
|
|
|
if( value !== undefined ){
|
|
for( var i = 0; i < $$.style.properties.length; i++ ){
|
|
var prop = $$.style.properties[i];
|
|
var name = prop.name;
|
|
|
|
var parsedProp = this.parse(name, value, true);
|
|
|
|
if( parsedProp ){
|
|
props.push( parsedProp );
|
|
}
|
|
}
|
|
}
|
|
|
|
} else if( $$.is.string(name) ){ // then parse the single property
|
|
var parsedProp = this.parse(name, value, true);
|
|
|
|
if( parsedProp ){
|
|
props.push( parsedProp );
|
|
}
|
|
} else if( $$.is.plainObject(name) ){ // then parse each property
|
|
var specifiedProps = name;
|
|
updateTransitions = value;
|
|
|
|
for( var i = 0; i < $$.style.properties.length; i++ ){
|
|
var prop = $$.style.properties[i];
|
|
var name = prop.name;
|
|
var value = specifiedProps[ name ];
|
|
|
|
if( value === undefined ){ // try camel case name too
|
|
value = specifiedProps[ $$.util.dash2camel(name) ];
|
|
}
|
|
|
|
if( value !== undefined ){
|
|
var parsedProp = this.parse(name, value, true);
|
|
|
|
if( parsedProp ){
|
|
props.push( parsedProp );
|
|
}
|
|
}
|
|
}
|
|
} else { // can't do anything without well defined properties
|
|
return false;
|
|
}
|
|
|
|
// we've failed if there are no valid properties
|
|
if( props.length === 0 ){ return false; }
|
|
|
|
// now, apply the bypass properties on the elements
|
|
var ret = false; // return true if at least one succesful bypass applied
|
|
for( var i = 0; i < eles.length; i++ ){ // for each ele
|
|
var ele = eles[i];
|
|
var style = ele._private.style;
|
|
var diffProps = {};
|
|
var diffProp;
|
|
|
|
for( var j = 0; j < props.length; j++ ){ // for each prop
|
|
var prop = props[j];
|
|
|
|
if( updateTransitions ){
|
|
var prevProp = style[ prop.name ];
|
|
diffProp = diffProps[ prop.name ] = { prev: prevProp };
|
|
}
|
|
|
|
ret = this.applyParsedProperty( ele, prop ) || ret;
|
|
|
|
if( updateTransitions ){
|
|
diffProp.next = style[ prop.name ];
|
|
}
|
|
|
|
} // for props
|
|
|
|
if( updateTransitions ){
|
|
this.updateTransitions( ele, diffProps, isBypass );
|
|
}
|
|
} // for eles
|
|
|
|
return ret;
|
|
};
|
|
|
|
// only useful in specific cases like animation
|
|
$$.styfn.overrideBypass = function( eles, name, value ){
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
var prop = ele._private.style[ $$.util.camel2dash(name) ];
|
|
|
|
if( !prop.bypass ){ // need a bypass if one doesn't exist
|
|
this.applyBypass( ele, name, value );
|
|
continue;
|
|
}
|
|
|
|
prop.value = value;
|
|
prop.pxValue = value;
|
|
}
|
|
};
|
|
|
|
$$.styfn.removeAllBypasses = function( eles, updateTransitions ){
|
|
var isBypass = true;
|
|
|
|
for( var j = 0; j < eles.length; j++ ){
|
|
var ele = eles[j];
|
|
var diffProps = {};
|
|
var style = ele._private.style;
|
|
|
|
for( var i = 0; i < $$.style.properties.length; i++ ){
|
|
var prop = $$.style.properties[i];
|
|
var name = prop.name;
|
|
var value = ''; // empty => remove bypass
|
|
var parsedProp = this.parse(name, value, true);
|
|
var prevProp = style[ prop.name ];
|
|
var diffProp = diffProps[ prop.name ] = { prev: prevProp };
|
|
|
|
this.applyParsedProperty(ele, parsedProp);
|
|
|
|
diffProp.next = style[ prop.name ];
|
|
} // for props
|
|
|
|
if( updateTransitions ){
|
|
this.updateTransitions( ele, diffProps, isBypass );
|
|
}
|
|
} // for eles
|
|
};
|
|
|
|
$$.styfn.removeBypasses = function( eles, props, updateTransitions ){
|
|
var isBypass = true;
|
|
|
|
for( var j = 0; j < eles.length; j++ ){
|
|
var ele = eles[j];
|
|
var diffProps = {};
|
|
var style = ele._private.style;
|
|
|
|
for( var i = 0; i < props.length; i++ ){
|
|
var name = props[i];
|
|
var prop = $$.style.properties[ name ];
|
|
var value = ''; // empty => remove bypass
|
|
var parsedProp = this.parse(name, value, true);
|
|
var prevProp = style[ prop.name ];
|
|
var diffProp = diffProps[ prop.name ] = { prev: prevProp };
|
|
|
|
this.applyParsedProperty(ele, parsedProp);
|
|
|
|
diffProp.next = style[ prop.name ];
|
|
} // for props
|
|
|
|
if( updateTransitions ){
|
|
this.updateTransitions( ele, diffProps, isBypass );
|
|
}
|
|
} // for eles
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$, window){ 'use strict';
|
|
|
|
// gets what an em size corresponds to in pixels relative to a dom element
|
|
$$.styfn.getEmSizeInPixels = function(){
|
|
var cy = this._private.cy;
|
|
var domElement = cy.container();
|
|
|
|
if( window && domElement && window.getComputedStyle ){
|
|
var pxAsStr = window.getComputedStyle(domElement).getPropertyValue('font-size');
|
|
var px = parseFloat( pxAsStr );
|
|
return px;
|
|
} else {
|
|
return 1; // in case we're running outside of the browser
|
|
}
|
|
};
|
|
|
|
// gets css property from the core container
|
|
$$.styfn.containerCss = function( propName ){
|
|
var cy = this._private.cy;
|
|
var domElement = cy.container();
|
|
|
|
if( window && domElement && window.getComputedStyle ){
|
|
return window.getComputedStyle(domElement).getPropertyValue( propName );
|
|
}
|
|
};
|
|
|
|
$$.styfn.containerProperty = function( propName ){
|
|
var propStr = this.containerCss( propName );
|
|
var prop = this.parse( propName, propStr );
|
|
return prop;
|
|
};
|
|
|
|
$$.styfn.containerPropertyAsString = function( propName ){
|
|
var prop = this.containerProperty( propName );
|
|
|
|
if( prop ){
|
|
return prop.strValue;
|
|
}
|
|
};
|
|
|
|
})( cytoscape, typeof window === 'undefined' ? null : window );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// gets the rendered style for an element
|
|
$$.styfn.getRenderedStyle = function( ele ){
|
|
var ele = ele[0]; // insure it's an element
|
|
|
|
if( ele ){
|
|
var rstyle = {};
|
|
var style = ele._private.style;
|
|
var cy = this._private.cy;
|
|
var zoom = cy.zoom();
|
|
|
|
for( var i = 0; i < $$.style.properties.length; i++ ){
|
|
var prop = $$.style.properties[i];
|
|
var styleProp = style[ prop.name ];
|
|
|
|
if( styleProp ){
|
|
var val = styleProp.unitless ? styleProp.strValue : (styleProp.pxValue * zoom) + 'px';
|
|
rstyle[ prop.name ] = val;
|
|
rstyle[ $$.util.dash2camel(prop.name) ] = val;
|
|
}
|
|
}
|
|
|
|
return rstyle;
|
|
}
|
|
};
|
|
|
|
// gets the raw style for an element
|
|
$$.styfn.getRawStyle = function( ele ){
|
|
var ele = ele[0]; // insure it's an element
|
|
|
|
if( ele ){
|
|
var rstyle = {};
|
|
var style = ele._private.style;
|
|
|
|
for( var i = 0; i < $$.style.properties.length; i++ ){
|
|
var prop = $$.style.properties[i];
|
|
var styleProp = style[ prop.name ];
|
|
|
|
if( styleProp ){
|
|
rstyle[ prop.name ] = styleProp.strValue;
|
|
rstyle[ $$.util.dash2camel(prop.name) ] = styleProp.strValue;
|
|
}
|
|
}
|
|
|
|
return rstyle;
|
|
}
|
|
};
|
|
|
|
// gets the value style for an element (useful for things like animations)
|
|
$$.styfn.getValueStyle = function( ele ){
|
|
var rstyle = {};
|
|
var style;
|
|
var isEle = $$.is.element(ele);
|
|
|
|
if( isEle ){
|
|
style = ele._private.style;
|
|
} else {
|
|
style = ele; // just passed the style itself
|
|
}
|
|
|
|
if( style ){
|
|
for( var i = 0; i < $$.style.properties.length; i++ ){
|
|
var prop = $$.style.properties[i];
|
|
var styleProp = style[ prop.name ] || style[ $$.util.dash2camel(prop.name) ];
|
|
|
|
if( styleProp !== undefined && !$$.is.plainObject( styleProp ) ){ // then make a prop of it
|
|
styleProp = this.parse(prop.name, styleProp);
|
|
}
|
|
|
|
if( styleProp ){
|
|
rstyle[ prop.name ] = styleProp;
|
|
rstyle[ $$.util.dash2camel(prop.name) ] = styleProp;
|
|
}
|
|
}
|
|
}
|
|
|
|
return rstyle;
|
|
};
|
|
|
|
$$.styfn.getPropsList = function( propsObj ){
|
|
var rstyle = [];
|
|
var style = propsObj;
|
|
var props = $$.style.properties;
|
|
|
|
if( style ){
|
|
for( var name in style ){
|
|
var val = style[name];
|
|
var prop = props[name] || props[ $$.util.camel2dash(name) ];
|
|
var styleProp = this.parse( prop.name, val );
|
|
|
|
rstyle.push( styleProp );
|
|
}
|
|
}
|
|
|
|
return rstyle;
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.style.applyFromJson = function( style, json ){
|
|
for( var i = 0; i < json.length; i++ ){
|
|
var context = json[i];
|
|
var selector = context.selector;
|
|
var props = context.style || context.css;
|
|
|
|
style.selector( selector ); // apply selector
|
|
|
|
for( var name in props ){
|
|
var value = props[name];
|
|
|
|
style.css( name, value ); // apply property
|
|
}
|
|
}
|
|
|
|
return style;
|
|
};
|
|
|
|
// static function
|
|
$$.style.fromJson = function( cy, json ){
|
|
var style = new $$.Style(cy);
|
|
|
|
$$.style.applyFromJson( style, json );
|
|
|
|
return style;
|
|
};
|
|
|
|
// accessible cy.style() function
|
|
$$.styfn.fromJson = function( json ){
|
|
var style = this;
|
|
|
|
style.resetToDefault();
|
|
|
|
$$.style.applyFromJson( style, json );
|
|
|
|
return style;
|
|
};
|
|
|
|
// get json from cy.style() api
|
|
$$.styfn.json = function(){
|
|
var json = [];
|
|
|
|
for( var i = this.defaultLength; i < this.length; i++ ){
|
|
var cxt = this[i];
|
|
var selector = cxt.selector;
|
|
var props = cxt.properties;
|
|
var css = {};
|
|
|
|
for( var j = 0; j < props.length; j++ ){
|
|
var prop = props[j];
|
|
css[ prop.name ] = prop.strValue;
|
|
}
|
|
|
|
json.push({
|
|
selector: !selector ? 'core' : selector.toString(),
|
|
style: css
|
|
});
|
|
}
|
|
|
|
return json;
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.style.applyFromString = function( style, string ){
|
|
var remaining = '' + string;
|
|
var selAndBlockStr;
|
|
var blockRem;
|
|
var propAndValStr;
|
|
|
|
// remove comments from the style string
|
|
remaining = remaining.replace(/[/][*](\s|.)+?[*][/]/g, '');
|
|
|
|
function removeSelAndBlockFromRemaining(){
|
|
// remove the parsed selector and block from the remaining text to parse
|
|
if( remaining.length > selAndBlockStr.length ){
|
|
remaining = remaining.substr( selAndBlockStr.length );
|
|
} else {
|
|
remaining = '';
|
|
}
|
|
}
|
|
|
|
function removePropAndValFromRem(){
|
|
// remove the parsed property and value from the remaining block text to parse
|
|
if( blockRem.length > propAndValStr.length ){
|
|
blockRem = blockRem.substr( propAndValStr.length );
|
|
} else {
|
|
blockRem = '';
|
|
}
|
|
}
|
|
|
|
while(true){
|
|
var nothingLeftToParse = remaining.match(/^\s*$/);
|
|
if( nothingLeftToParse ){ break; }
|
|
|
|
var selAndBlock = remaining.match(/^\s*((?:.|\s)+?)\s*\{((?:.|\s)+?)\}/);
|
|
|
|
if( !selAndBlock ){
|
|
$$.util.error('Halting stylesheet parsing: String stylesheet contains more to parse but no selector and block found in: ' + remaining);
|
|
break;
|
|
}
|
|
|
|
selAndBlockStr = selAndBlock[0];
|
|
|
|
// parse the selector
|
|
var selectorStr = selAndBlock[1];
|
|
if( selectorStr !== 'core' ){
|
|
var selector = new $$.Selector( selectorStr );
|
|
if( selector._private.invalid ){
|
|
$$.util.error('Skipping parsing of block: Invalid selector found in string stylesheet: ' + selectorStr);
|
|
|
|
// skip this selector and block
|
|
removeSelAndBlockFromRemaining();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// parse the block of properties and values
|
|
var blockStr = selAndBlock[2];
|
|
var invalidBlock = false;
|
|
blockRem = blockStr;
|
|
var props = [];
|
|
|
|
while(true){
|
|
var nothingLeftToParse = blockRem.match(/^\s*$/);
|
|
if( nothingLeftToParse ){ break; }
|
|
|
|
var propAndVal = blockRem.match(/^\s*(.+?)\s*:\s*(.+?)\s*;/);
|
|
|
|
if( !propAndVal ){
|
|
$$.util.error('Skipping parsing of block: Invalid formatting of style property and value definitions found in:' + blockStr);
|
|
invalidBlock = true;
|
|
break;
|
|
}
|
|
|
|
propAndValStr = propAndVal[0];
|
|
var propStr = propAndVal[1];
|
|
var valStr = propAndVal[2];
|
|
|
|
var prop = $$.style.properties[ propStr ];
|
|
if( !prop ){
|
|
$$.util.error('Skipping property: Invalid property name in: ' + propAndValStr);
|
|
|
|
// skip this property in the block
|
|
removePropAndValFromRem();
|
|
continue;
|
|
}
|
|
|
|
var parsedProp = style.parse( propStr, valStr );
|
|
|
|
if( !parsedProp ){
|
|
$$.util.error('Skipping property: Invalid property definition in: ' + propAndValStr);
|
|
|
|
// skip this property in the block
|
|
removePropAndValFromRem();
|
|
continue;
|
|
}
|
|
|
|
props.push({
|
|
name: propStr,
|
|
val: valStr
|
|
});
|
|
removePropAndValFromRem();
|
|
}
|
|
|
|
if( invalidBlock ){
|
|
removeSelAndBlockFromRemaining();
|
|
break;
|
|
}
|
|
|
|
// put the parsed block in the style
|
|
style.selector( selectorStr );
|
|
for( var i = 0; i < props.length; i++ ){
|
|
var prop = props[i];
|
|
style.css( prop.name, prop.val );
|
|
}
|
|
|
|
removeSelAndBlockFromRemaining();
|
|
}
|
|
|
|
return style;
|
|
};
|
|
|
|
$$.style.fromString = function( cy, string ){
|
|
var style = new $$.Style(cy);
|
|
|
|
$$.style.applyFromString( style, string );
|
|
|
|
return style;
|
|
};
|
|
|
|
$$.styfn.fromString = function( string ){
|
|
var style = this;
|
|
|
|
style.resetToDefault();
|
|
|
|
$$.style.applyFromString( style, string );
|
|
|
|
return style;
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// a dummy stylesheet object that doesn't need a reference to the core
|
|
// (useful for init)
|
|
$$.stylesheet = $$.Stylesheet = function(){
|
|
if( !(this instanceof $$.Stylesheet) ){
|
|
return new $$.Stylesheet();
|
|
}
|
|
|
|
this.length = 0;
|
|
};
|
|
|
|
$$.sheetfn = $$.Stylesheet.prototype;
|
|
|
|
// just store the selector to be parsed later
|
|
$$.sheetfn.selector = function( selector ){
|
|
var i = this.length++;
|
|
|
|
this[i] = {
|
|
selector: selector,
|
|
properties: []
|
|
};
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
// just store the property to be parsed later
|
|
$$.sheetfn.css = function( name, value ){
|
|
var i = this.length - 1;
|
|
|
|
if( $$.is.string(name) ){
|
|
this[i].properties.push({
|
|
name: name,
|
|
value: value
|
|
});
|
|
} else if( $$.is.plainObject(name) ){
|
|
var map = name;
|
|
|
|
for( var j = 0; j < $$.style.properties.length; j++ ){
|
|
var prop = $$.style.properties[j];
|
|
var mapVal = map[ prop.name ];
|
|
|
|
if( mapVal === undefined ){ // also try camel case name
|
|
mapVal = map[ $$.util.dash2camel(prop.name) ];
|
|
}
|
|
|
|
if( mapVal !== undefined ){
|
|
var name = prop.name;
|
|
var value = mapVal;
|
|
|
|
this[i].properties.push({
|
|
name: name,
|
|
value: value
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
$$.sheetfn.style = $$.sheetfn.css;
|
|
|
|
// generate a real style object from the dummy stylesheet
|
|
$$.sheetfn.generateStyle = function( cy ){
|
|
var style = new $$.Style(cy);
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var context = this[i];
|
|
var selector = context.selector;
|
|
var props = context.properties;
|
|
|
|
style.selector(selector); // apply selector
|
|
|
|
for( var j = 0; j < props.length; j++ ){
|
|
var prop = props[j];
|
|
|
|
style.css( prop.name, prop.value ); // apply property
|
|
}
|
|
}
|
|
|
|
return style;
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
// cross-env thread/worker
|
|
// NB : uses (heavyweight) processes on nodejs so best not to create too many threads
|
|
|
|
;(function($$, window){ 'use strict';
|
|
|
|
$$.Thread = function( fn ){
|
|
if( !(this instanceof $$.Thread) ){
|
|
return new $$.Thread( fn );
|
|
}
|
|
|
|
this._private = {
|
|
requires: [],
|
|
files: [],
|
|
queue: null,
|
|
pass: []
|
|
};
|
|
|
|
if( fn ){
|
|
this.run( fn );
|
|
}
|
|
|
|
};
|
|
|
|
$$.thread = $$.Thread;
|
|
$$.thdfn = $$.Thread.prototype; // short alias
|
|
|
|
$$.fn.thread = function( fnMap, options ){
|
|
for( var name in fnMap ){
|
|
var fn = fnMap[name];
|
|
$$.Thread.prototype[ name ] = fn;
|
|
}
|
|
};
|
|
|
|
var stringifyFieldVal = function( val ){
|
|
var valStr = $$.is.fn( val ) ? val.toString() : 'JSON.parse("' + JSON.stringify(val) + '")';
|
|
|
|
return valStr;
|
|
};
|
|
|
|
// allows for requires with prototypes and subobjs etc
|
|
var fnAsRequire = function( fn ){
|
|
var req;
|
|
var fnName;
|
|
|
|
if( $$.is.object(fn) && fn.fn ){ // manual fn
|
|
req = fnAs( fn.fn, fn.name );
|
|
fnName = fn.name;
|
|
fn = fn.fn;
|
|
} else if( $$.is.fn(fn) ){ // auto fn
|
|
req = fn.toString();
|
|
fnName = fn.name;
|
|
} else if( $$.is.string(fn) ){ // stringified fn
|
|
req = fn;
|
|
} else if( $$.is.object(fn) ){ // plain object
|
|
if( fn.proto ){
|
|
req = '';
|
|
} else {
|
|
req = fn.name + ' = {};';
|
|
}
|
|
|
|
fnName = fn.name;
|
|
fn = fn.obj;
|
|
}
|
|
|
|
req += '\n';
|
|
|
|
var protoreq = function( val, subname ){
|
|
if( val.prototype ){
|
|
var protoNonempty = false;
|
|
for( var prop in val.prototype ){ protoNonempty = true; break; }
|
|
|
|
if( protoNonempty ){
|
|
req += fnAsRequire( {
|
|
name: subname,
|
|
obj: val,
|
|
proto: true
|
|
}, val );
|
|
}
|
|
}
|
|
};
|
|
|
|
// pull in prototype
|
|
if( fn.prototype && fnName != null ){
|
|
|
|
for( var name in fn.prototype ){
|
|
var protoStr = '';
|
|
|
|
var val = fn.prototype[ name ];
|
|
var valStr = stringifyFieldVal( val );
|
|
var subname = fnName + '.prototype.' + name;
|
|
|
|
protoStr += subname + ' = ' + valStr + ';\n';
|
|
|
|
if( protoStr ){
|
|
req += protoStr;
|
|
}
|
|
|
|
protoreq( val, subname ); // subobject with prototype
|
|
}
|
|
|
|
}
|
|
|
|
// pull in properties for obj/fns
|
|
if( !$$.is.string(fn) ){ for( var name in fn ){
|
|
var propsStr = '';
|
|
|
|
if( fn.hasOwnProperty(name) ){
|
|
var val = fn[ name ];
|
|
var valStr = stringifyFieldVal( val );
|
|
var subname = fnName + '["' + name + '"]';
|
|
|
|
propsStr += subname + ' = ' + valStr + ';\n';
|
|
}
|
|
|
|
if( propsStr ){
|
|
req += propsStr;
|
|
}
|
|
|
|
protoreq( val, subname ); // subobject with prototype
|
|
} }
|
|
|
|
return req;
|
|
};
|
|
|
|
var isPathStr = function( str ){
|
|
return $$.is.string(str) && str.match(/\.js$/);
|
|
};
|
|
|
|
$$.fn.thread({
|
|
|
|
require: function( fn, as ){
|
|
if( isPathStr(fn) ){
|
|
this._private.files.push( fn );
|
|
|
|
return this;
|
|
}
|
|
|
|
if( as ){
|
|
if( $$.is.fn(fn) ){
|
|
// disabled b/c doesn't work with forced names on functions w/ prototypes
|
|
//fn = fnAs( fn, as );
|
|
|
|
as = as || fn.name;
|
|
|
|
fn = { name: as, fn: fn };
|
|
} else {
|
|
fn = { name: as, obj: fn };
|
|
}
|
|
}
|
|
|
|
this._private.requires.push( fn );
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
pass: function( data ){
|
|
this._private.pass.push( data );
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
run: function( fn, pass ){ // fn used like main()
|
|
var self = this;
|
|
var _p = this._private;
|
|
pass = pass || _p.pass.shift();
|
|
|
|
if( _p.stopped ){
|
|
$$.util.error('Attempted to run a stopped thread! Start a new thread or do not stop the existing thread and reuse it.');
|
|
return;
|
|
}
|
|
|
|
if( _p.running ){
|
|
return _p.queue = _p.queue.then(function(){ // inductive step
|
|
return self.run( fn, pass );
|
|
});
|
|
}
|
|
|
|
var useWW = window != null;
|
|
var useNode = typeof module !== 'undefined';
|
|
|
|
self.trigger('run');
|
|
|
|
var runP = new $$.Promise(function( resolve, reject ){
|
|
|
|
_p.running = true;
|
|
|
|
var threadTechAlreadyExists = _p.ran;
|
|
|
|
var fnImplStr = $$.is.string( fn ) ? fn : fn.toString();
|
|
|
|
// worker code to exec
|
|
var fnStr = '\n' + ( _p.requires.map(function( r ){
|
|
return fnAsRequire( r );
|
|
}) ).concat( _p.files.map(function( f ){
|
|
if( useWW ){
|
|
var wwifyFile = function( file ){
|
|
if( file.match(/^\.\//) || file.match(/^\.\./) ){
|
|
return window.location.origin + window.location.pathname + file;
|
|
} else if( file.match(/^\//) ){
|
|
return window.location.origin + '/' + file;
|
|
}
|
|
return file;
|
|
};
|
|
|
|
return 'importScripts("' + wwifyFile(f) + '");';
|
|
} else if( useNode ) {
|
|
return 'eval( require("fs").readFileSync("' + f + '", { encoding: "utf8" }) );';
|
|
}
|
|
}) ).concat([
|
|
'( function(){',
|
|
'var ret = (' + fnImplStr + ')(' + JSON.stringify(pass) + ');',
|
|
'if( ret !== undefined ){ resolve(ret); }', // assume if ran fn returns defined value (incl. null), that we want to resolve to it
|
|
'} )()\n'
|
|
]).join('\n');
|
|
|
|
// because we've now consumed the requires, empty the list so we don't dupe on next run()
|
|
_p.requires = [];
|
|
_p.files = [];
|
|
|
|
if( useWW ){
|
|
var fnBlob, fnUrl;
|
|
|
|
// add normalised thread api functions
|
|
if( !threadTechAlreadyExists ){
|
|
var fnPre = fnStr + '';
|
|
|
|
fnStr = [
|
|
'function broadcast(m){ return message(m); };', // alias
|
|
'function message(m){ postMessage(m); };',
|
|
'function listen(fn){',
|
|
' self.addEventListener("message", function(m){ ',
|
|
' if( typeof m === "object" && (m.data.$$eval || m.data === "$$start") ){',
|
|
' } else { ',
|
|
' fn( m.data );',
|
|
' }',
|
|
' });',
|
|
'};',
|
|
'self.addEventListener("message", function(m){ if( m.data.$$eval ){ eval( m.data.$$eval ); } });',
|
|
'function resolve(v){ postMessage({ $$resolve: v }); };',
|
|
'function reject(v){ postMessage({ $$reject: v }); };'
|
|
].join('\n');
|
|
|
|
fnStr += fnPre;
|
|
|
|
fnBlob = new Blob([ fnStr ], {
|
|
type: 'application/javascript'
|
|
});
|
|
fnUrl = window.URL.createObjectURL( fnBlob );
|
|
}
|
|
// create webworker and let it exec the serialised code
|
|
var ww = _p.webworker = _p.webworker || new Worker( fnUrl );
|
|
|
|
if( threadTechAlreadyExists ){ // then just exec new run() code
|
|
ww.postMessage({
|
|
$$eval: fnStr
|
|
});
|
|
}
|
|
|
|
// worker messages => events
|
|
var cb;
|
|
ww.addEventListener('message', cb = function( m ){
|
|
var isObject = $$.is.object(m) && $$.is.object( m.data );
|
|
|
|
if( isObject && ('$$resolve' in m.data) ){
|
|
ww.removeEventListener('message', cb); // done listening b/c resolve()
|
|
|
|
resolve( m.data.$$resolve );
|
|
} else if( isObject && ('$$reject' in m.data) ){
|
|
ww.removeEventListener('message', cb); // done listening b/c reject()
|
|
|
|
reject( m.data.$$reject );
|
|
} else {
|
|
self.trigger( new $$.Event(m, { type: 'message', message: m.data }) );
|
|
}
|
|
}, false);
|
|
|
|
if( !threadTechAlreadyExists ){
|
|
ww.postMessage('$$start'); // start up the worker
|
|
}
|
|
|
|
} else if( useNode ){
|
|
// create a new process
|
|
var path = require('path');
|
|
var child_process = require('child_process');
|
|
var child = _p.child = _p.child || child_process.fork( path.join(__dirname, 'thread-node-fork') );
|
|
|
|
// child process messages => events
|
|
var cb;
|
|
child.on('message', cb = function( m ){
|
|
if( $$.is.object(m) && ('$$resolve' in m) ){
|
|
child.removeListener('message', cb); // done listening b/c resolve()
|
|
|
|
resolve( m.$$resolve );
|
|
} else if( $$.is.object(m) && ('$$reject' in m) ){
|
|
child.removeListener('message', cb); // done listening b/c reject()
|
|
|
|
reject( m.$$reject );
|
|
} else {
|
|
self.trigger( new $$.Event({}, { type: 'message', message: m }) );
|
|
}
|
|
});
|
|
|
|
// ask the child process to eval the worker code
|
|
child.send({
|
|
$$eval: fnStr
|
|
});
|
|
} else {
|
|
$$.error('Tried to create thread but no underlying tech found!');
|
|
// TODO fallback on main JS thread?
|
|
}
|
|
|
|
}).then(function( v ){
|
|
_p.running = false;
|
|
_p.ran = true;
|
|
|
|
self.trigger('ran');
|
|
|
|
return v;
|
|
});
|
|
|
|
if( _p.queue == null ){
|
|
_p.queue = runP; // i.e. first step of inductive promise chain (for queue)
|
|
}
|
|
|
|
return runP;
|
|
},
|
|
|
|
// send the thread a message
|
|
message: function( m ){
|
|
var _p = this._private;
|
|
|
|
if( _p.webworker ){
|
|
_p.webworker.postMessage( m );
|
|
}
|
|
|
|
if( _p.child ){
|
|
_p.child.send( m );
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
stop: function(){
|
|
var _p = this._private;
|
|
|
|
if( _p.webworker ){
|
|
_p.webworker.terminate();
|
|
}
|
|
|
|
if( _p.child ){
|
|
_p.child.kill();
|
|
}
|
|
|
|
_p.stopped = true;
|
|
|
|
return this.trigger('stop'); // chaining
|
|
},
|
|
|
|
stopped: function(){
|
|
return this._private.stopped;
|
|
}
|
|
|
|
});
|
|
|
|
var fnAs = function( fn, name ){
|
|
var fnStr = fn.toString();
|
|
fnStr = fnStr.replace(/function.*\(/, 'function ' + name + '(');
|
|
|
|
return fnStr;
|
|
};
|
|
|
|
var defineFnal = function( opts ){
|
|
opts = opts || {};
|
|
|
|
return function fnalImpl( fn, arg1 ){
|
|
var fnStr = fnAs( fn, '_$_$_' + opts.name );
|
|
|
|
this.require( fnStr );
|
|
|
|
return this.run( [
|
|
'function( data ){',
|
|
' var origResolve = resolve;',
|
|
' var res = [];',
|
|
' ',
|
|
' resolve = function( val ){',
|
|
' res.push( val );',
|
|
' };',
|
|
' ',
|
|
' var ret = data.' + opts.name + '( _$_$_' + opts.name + ( arguments.length > 1 ? ', ' + JSON.stringify(arg1) : '' ) + ' );',
|
|
' ',
|
|
' resolve = origResolve;',
|
|
' resolve( res.length > 0 ? res : ret );',
|
|
'}'
|
|
].join('\n') );
|
|
};
|
|
};
|
|
|
|
$$.fn.thread({
|
|
reduce: defineFnal({ name: 'reduce' }),
|
|
|
|
reduceRight: defineFnal({ name: 'reduceRight' }),
|
|
|
|
map: defineFnal({ name: 'map' })
|
|
});
|
|
|
|
// aliases
|
|
var fn = $$.thdfn;
|
|
fn.promise = fn.run;
|
|
fn.terminate = fn.halt = fn.stop;
|
|
fn.include = fn.require;
|
|
|
|
// higher level alias (in case you like the worker metaphor)
|
|
$$.worker = $$.Worker = $$.Thread;
|
|
|
|
// pull in event apis
|
|
$$.fn.thread({
|
|
on: $$.define.on(),
|
|
one: $$.define.on({ unbindSelfOnTrigger: true }),
|
|
off: $$.define.off(),
|
|
trigger: $$.define.trigger()
|
|
});
|
|
|
|
$$.define.eventAliasesOn( $$.thdfn );
|
|
|
|
})( cytoscape, typeof window === 'undefined' ? null : window );
|
|
|
|
;(function($$, window){ 'use strict';
|
|
|
|
$$.Fabric = function( N ){
|
|
if( !(this instanceof $$.Fabric) ){
|
|
return new $$.Fabric( N );
|
|
}
|
|
|
|
this._private = {
|
|
pass: []
|
|
};
|
|
|
|
var defN = 4;
|
|
|
|
if( $$.is.number(N) ){
|
|
// then use the specified number of threads
|
|
} if( typeof navigator !== 'undefined' && navigator.hardwareConcurrency != null ){
|
|
N = navigator.hardwareConcurrency;
|
|
} else if( typeof module !== 'undefined' ){
|
|
N = require('os').cpus().length;
|
|
} else { // TODO could use an estimation here but would the additional expense be worth it?
|
|
N = defN;
|
|
}
|
|
|
|
for( var i = 0; i < N; i++ ){
|
|
this[i] = $$.Thread();
|
|
}
|
|
|
|
this.length = N;
|
|
};
|
|
|
|
$$.fabric = $$.Fabric;
|
|
$$.fabfn = $$.Fabric.prototype; // short alias
|
|
|
|
$$.fn.fabric = function( fnMap, options ){
|
|
for( var name in fnMap ){
|
|
var fn = fnMap[name];
|
|
$$.Fabric.prototype[ name ] = fn;
|
|
}
|
|
};
|
|
|
|
$$.fn.fabric({
|
|
|
|
// require fn in all threads
|
|
require: function( fn, as ){
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var thread = this[i];
|
|
|
|
thread.require( fn, as );
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
// get a random thread
|
|
random: function(){
|
|
var i = Math.round( (this.length - 1) * Math.random() );
|
|
var thread = this[i];
|
|
|
|
return thread;
|
|
},
|
|
|
|
// run on random thread
|
|
run: function( fn ){
|
|
var pass = this._private.pass.shift();
|
|
|
|
return this.random().pass( pass ).run( fn );
|
|
},
|
|
|
|
// sends a random thread a message
|
|
message: function( m ){
|
|
return this.random().message( m );
|
|
},
|
|
|
|
// send all threads a message
|
|
broadcast: function( m ){
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var thread = this[i];
|
|
|
|
thread.message( m );
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
// stop all threads
|
|
stop: function(){
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var thread = this[i];
|
|
|
|
thread.stop();
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
// pass data to be used with .spread() etc.
|
|
pass: function( data ){
|
|
var pass = this._private.pass;
|
|
|
|
if( $$.is.array(data) ){
|
|
pass.push( data );
|
|
} else {
|
|
$$.util.error('Only arrays or collections may be used with fabric.pass()');
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
spreadSize: function(){
|
|
var subsize = Math.ceil( this._private.pass[0].length / this.length );
|
|
|
|
subsize = Math.max( 1, subsize ); // don't pass less than one ele to each thread
|
|
|
|
return subsize;
|
|
},
|
|
|
|
// split the data into slices to spread the data equally among threads
|
|
spread: function( fn ){
|
|
var self = this;
|
|
var _p = self._private;
|
|
var subsize = self.spreadSize(); // number of pass eles to handle in each thread
|
|
var pass = _p.pass.shift().concat([]); // keep a copy
|
|
var runPs = [];
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var thread = this[i];
|
|
var slice = pass.splice( 0, subsize );
|
|
|
|
var runP = thread.pass( slice ).run( fn );
|
|
|
|
runPs.push( runP );
|
|
|
|
var doneEarly = pass.length === 0;
|
|
if( doneEarly ){ break; }
|
|
}
|
|
|
|
return $$.Promise.all( runPs ).then(function( thens ){
|
|
var postpass = [];
|
|
var p = 0;
|
|
|
|
// fill postpass with the total result joined from all threads
|
|
for( var i = 0; i < thens.length; i++ ){
|
|
var then = thens[i]; // array result from thread i
|
|
|
|
for( var j = 0; j < then.length; j++ ){
|
|
var t = then[j]; // array element
|
|
|
|
postpass[ p++ ] = t;
|
|
}
|
|
}
|
|
|
|
return postpass;
|
|
});
|
|
},
|
|
|
|
// parallel version of array.map()
|
|
map: function( fn ){
|
|
var self = this;
|
|
|
|
self.require( fn, '_$_$_fabmap' );
|
|
|
|
return self.spread(function( split ){
|
|
var mapped = [];
|
|
var origResolve = resolve;
|
|
|
|
resolve = function( val ){
|
|
mapped.push( val );
|
|
};
|
|
|
|
for( var i = 0; i < split.length; i++ ){
|
|
var oldLen = mapped.length;
|
|
var ret = _$_$_fabmap( split[i] );
|
|
var nothingInsdByResolve = oldLen === mapped.length;
|
|
|
|
if( nothingInsdByResolve ){
|
|
mapped.push( ret );
|
|
}
|
|
}
|
|
|
|
resolve = origResolve;
|
|
|
|
return mapped;
|
|
});
|
|
|
|
},
|
|
|
|
// parallel version of array.filter()
|
|
filter: function( fn ){
|
|
var _p = this._private;
|
|
var pass = _p.pass[0];
|
|
|
|
return this.map( fn ).then(function( include ){
|
|
var ret = [];
|
|
|
|
for( var i = 0; i < pass.length; i++ ){
|
|
var datum = pass[i];
|
|
var incDatum = include[i];
|
|
|
|
if( incDatum ){
|
|
ret.push( datum );
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
});
|
|
},
|
|
|
|
// sorts the passed array using a divide and conquer strategy
|
|
sort: function( cmp ){
|
|
var self = this;
|
|
var P = this._private.pass[0].length;
|
|
var subsize = this.spreadSize();
|
|
|
|
cmp = cmp || function( a, b ){ // default comparison function
|
|
if( a < b ){
|
|
return -1;
|
|
} else if( a > b ){
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
self.require( cmp, '_$_$_cmp' );
|
|
|
|
return self.spread(function( split ){ // sort each split normally
|
|
var sortedSplit = split.sort( _$_$_cmp );
|
|
resolve( sortedSplit );
|
|
|
|
}).then(function( joined ){
|
|
// do all the merging in the main thread to minimise data transfer
|
|
|
|
// TODO could do merging in separate threads but would incur add'l cost of data transfer
|
|
// for each level of the merge
|
|
|
|
var merge = function( i, j, max ){
|
|
// don't overflow array
|
|
j = Math.min( j, P );
|
|
max = Math.min( max, P );
|
|
|
|
// left and right sides of merge
|
|
var l = i;
|
|
var r = j;
|
|
|
|
var sorted = [];
|
|
|
|
for( var k = l; k < max; k++ ){
|
|
|
|
var eleI = joined[i];
|
|
var eleJ = joined[j];
|
|
|
|
if( i < r && ( j >= max || cmp(eleI, eleJ) <= 0 ) ){
|
|
sorted.push( eleI );
|
|
i++;
|
|
} else {
|
|
sorted.push( eleJ );
|
|
j++;
|
|
}
|
|
|
|
}
|
|
|
|
// in the array proper, put the sorted values
|
|
for( var k = 0; k < sorted.length; k++ ){ // kth sorted item
|
|
var index = l + k;
|
|
|
|
joined[ index ] = sorted[k];
|
|
}
|
|
};
|
|
|
|
for( var splitL = subsize; splitL < P; splitL *= 2 ){ // merge until array is "split" as 1
|
|
|
|
for( var i = 0; i < P; i += 2*splitL ){
|
|
merge( i, i + splitL, i + 2*splitL );
|
|
}
|
|
|
|
}
|
|
|
|
return joined;
|
|
});
|
|
}
|
|
|
|
|
|
});
|
|
|
|
var defineRandomPasser = function( opts ){
|
|
opts = opts || {};
|
|
|
|
return function( fn, arg1 ){
|
|
var pass = this._private.pass.shift();
|
|
|
|
return this.random().pass( pass )[ opts.threadFn ]( fn, arg1 );
|
|
};
|
|
};
|
|
|
|
$$.fn.fabric({
|
|
randomMap: defineRandomPasser({ threadFn: 'map' }),
|
|
|
|
reduce: defineRandomPasser({ threadFn: 'reduce' }),
|
|
|
|
reduceRight: defineRandomPasser({ threadFn: 'reduceRight' })
|
|
});
|
|
|
|
// aliases
|
|
var fn = $$.fabfn;
|
|
fn.promise = fn.run;
|
|
fn.terminate = fn.halt = fn.stop;
|
|
fn.include = fn.require;
|
|
|
|
// pull in event apis
|
|
$$.fn.fabric({
|
|
on: $$.define.on(),
|
|
one: $$.define.on({ unbindSelfOnTrigger: true }),
|
|
off: $$.define.off(),
|
|
trigger: $$.define.trigger()
|
|
});
|
|
|
|
$$.define.eventAliasesOn( $$.fabfn );
|
|
|
|
})( cytoscape, typeof window === 'undefined' ? null : window );
|
|
|
|
;(function($$, window){ 'use strict';
|
|
|
|
var defaults = {
|
|
};
|
|
|
|
var origDefaults = $$.util.copy( defaults );
|
|
|
|
$$.defaults = function( opts ){
|
|
defaults = $$.util.extend({}, origDefaults, opts);
|
|
};
|
|
|
|
$$.fn.core = function( fnMap, options ){
|
|
for( var name in fnMap ){
|
|
var fn = fnMap[name];
|
|
$$.Core.prototype[ name ] = fn;
|
|
}
|
|
};
|
|
|
|
$$.Core = function( opts ){
|
|
if( !(this instanceof $$.Core) ){
|
|
return new $$.Core(opts);
|
|
}
|
|
var cy = this;
|
|
|
|
opts = $$.util.extend({}, defaults, opts);
|
|
|
|
var container = opts.container;
|
|
var reg = container ? container._cyreg : null; // e.g. already registered some info (e.g. readies) via jquery
|
|
reg = reg || {};
|
|
|
|
if( reg && reg.cy ){
|
|
if( container ){
|
|
while( container.firstChild ){ // clean the container
|
|
container.removeChild( container.firstChild );
|
|
}
|
|
}
|
|
|
|
reg.cy.notify({ type: 'destroy' }); // destroy the renderer
|
|
|
|
reg = {}; // old instance => replace reg completely
|
|
}
|
|
|
|
var readies = reg.readies = reg.readies || [];
|
|
|
|
if( container ){ container._cyreg = reg; } // make sure container assoc'd reg points to this cy
|
|
reg.cy = cy;
|
|
|
|
var head = window !== undefined && container !== undefined && !opts.headless;
|
|
var options = opts;
|
|
options.layout = $$.util.extend( { name: head ? 'grid' : 'null' }, options.layout );
|
|
options.renderer = $$.util.extend( { name: head ? 'canvas' : 'null' }, options.renderer );
|
|
|
|
var defVal = function( def, val, altVal ){
|
|
if( val !== undefined ){
|
|
return val;
|
|
} else if( altVal !== undefined ){
|
|
return altVal;
|
|
} else {
|
|
return def;
|
|
}
|
|
};
|
|
|
|
var _p = this._private = {
|
|
container: options.container, // html dom ele container
|
|
ready: false, // whether ready has been triggered
|
|
initrender: false, // has initrender has been triggered
|
|
options: options, // cached options
|
|
elements: [], // array of elements
|
|
id2index: {}, // element id => index in elements array
|
|
listeners: [], // list of listeners
|
|
onRenders: [], // rendering listeners
|
|
aniEles: $$.Collection(this), // elements being animated
|
|
scratch: {}, // scratch object for core
|
|
layout: null,
|
|
renderer: null,
|
|
notificationsEnabled: true, // whether notifications are sent to the renderer
|
|
minZoom: 1e-50,
|
|
maxZoom: 1e50,
|
|
zoomingEnabled: defVal(true, options.zoomingEnabled),
|
|
userZoomingEnabled: defVal(true, options.userZoomingEnabled),
|
|
panningEnabled: defVal(true, options.panningEnabled),
|
|
userPanningEnabled: defVal(true, options.userPanningEnabled),
|
|
boxSelectionEnabled: defVal(false, options.boxSelectionEnabled),
|
|
autolock: defVal(false, options.autolock, options.autolockNodes),
|
|
autoungrabify: defVal(false, options.autoungrabify, options.autoungrabifyNodes),
|
|
autounselectify: defVal(false, options.autounselectify),
|
|
styleEnabled: options.styleEnabled === undefined ? head : options.styleEnabled,
|
|
zoom: $$.is.number(options.zoom) ? options.zoom : 1,
|
|
pan: {
|
|
x: $$.is.plainObject(options.pan) && $$.is.number(options.pan.x) ? options.pan.x : 0,
|
|
y: $$.is.plainObject(options.pan) && $$.is.number(options.pan.y) ? options.pan.y : 0
|
|
},
|
|
animation: { // object for currently-running animations
|
|
current: [],
|
|
queue: []
|
|
},
|
|
hasCompoundNodes: false,
|
|
deferredExecQueue: []
|
|
};
|
|
|
|
// set selection type
|
|
var selType = options.selectionType;
|
|
if( selType === undefined || (selType !== 'additive' && selType !== 'single') ){
|
|
// then set default
|
|
|
|
_p.selectionType = 'single';
|
|
} else {
|
|
_p.selectionType = selType;
|
|
}
|
|
|
|
// init zoom bounds
|
|
if( $$.is.number(options.minZoom) && $$.is.number(options.maxZoom) && options.minZoom < options.maxZoom ){
|
|
_p.minZoom = options.minZoom;
|
|
_p.maxZoom = options.maxZoom;
|
|
} else if( $$.is.number(options.minZoom) && options.maxZoom === undefined ){
|
|
_p.minZoom = options.minZoom;
|
|
} else if( $$.is.number(options.maxZoom) && options.minZoom === undefined ){
|
|
_p.maxZoom = options.maxZoom;
|
|
}
|
|
|
|
var loadExtData = function( next ){
|
|
var anyIsPromise = false;
|
|
|
|
for( var i = 0; i < extData.length; i++ ){
|
|
var datum = extData[i];
|
|
|
|
if( $$.is.promise(datum) ){
|
|
anyIsPromise = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( anyIsPromise ){
|
|
return $$.Promise.all( extData ).then( next ); // load all data asynchronously, then exec rest of init
|
|
} else {
|
|
next( extData ); // exec synchronously for convenience
|
|
}
|
|
};
|
|
|
|
var extData = [ options.style, options.elements ];
|
|
loadExtData(function( thens ){
|
|
var initStyle = thens[0];
|
|
var initEles = thens[1];
|
|
|
|
// init style
|
|
if( _p.styleEnabled ){
|
|
cy.setStyle( initStyle );
|
|
}
|
|
|
|
// create the renderer
|
|
cy.initRenderer( $$.util.extend({
|
|
hideEdgesOnViewport: options.hideEdgesOnViewport,
|
|
hideLabelsOnViewport: options.hideLabelsOnViewport,
|
|
textureOnViewport: options.textureOnViewport,
|
|
wheelSensitivity: $$.is.number(options.wheelSensitivity) && options.wheelSensitivity > 0 ? options.wheelSensitivity : 1,
|
|
motionBlur: options.motionBlur === undefined ? true : options.motionBlur, // on by default
|
|
motionBlurOpacity: options.motionBlurOpacity === undefined ? 0.05 : options.motionBlurOpacity,
|
|
pixelRatio: $$.is.number(options.pixelRatio) && options.pixelRatio > 0 ? options.pixelRatio : (options.pixelRatio === 'auto' ? undefined : 1),
|
|
desktopTapThreshold: options.desktopTapThreshold === undefined ? 4 : options.desktopTapThreshold,
|
|
touchTapThreshold: options.touchTapThreshold === undefined ? 8 : options.touchTapThreshold
|
|
}, options.renderer) );
|
|
|
|
// trigger the passed function for the `initrender` event
|
|
if( options.initrender ){
|
|
cy.on('initrender', options.initrender);
|
|
cy.on('initrender', function(){
|
|
cy._private.initrender = true;
|
|
});
|
|
}
|
|
|
|
// initial load
|
|
cy.load(initEles, function(){ // onready
|
|
cy.startAnimationLoop();
|
|
cy._private.ready = true;
|
|
|
|
// if a ready callback is specified as an option, the bind it
|
|
if( $$.is.fn( options.ready ) ){
|
|
cy.on('ready', options.ready);
|
|
}
|
|
|
|
// bind all the ready handlers registered before creating this instance
|
|
for( var i = 0; i < readies.length; i++ ){
|
|
var fn = readies[i];
|
|
cy.on('ready', fn);
|
|
}
|
|
if( reg ){ reg.readies = []; } // clear b/c we've bound them all and don't want to keep it around in case a new core uses the same div etc
|
|
|
|
cy.trigger('ready');
|
|
}, options.done);
|
|
|
|
});
|
|
};
|
|
|
|
$$.corefn = $$.Core.prototype; // short alias
|
|
|
|
|
|
$$.fn.core({
|
|
isReady: function(){
|
|
return this._private.ready;
|
|
},
|
|
|
|
ready: function( fn ){
|
|
if( this.isReady() ){
|
|
this.trigger('ready', [], fn); // just calls fn as though triggered via ready event
|
|
} else {
|
|
this.on('ready', fn);
|
|
}
|
|
},
|
|
|
|
initrender: function(){
|
|
return this._private.initrender;
|
|
},
|
|
|
|
destroy: function(){
|
|
this.notify({ type: 'destroy' }); // destroy the renderer
|
|
|
|
var domEle = this.container();
|
|
var parEle = domEle.parentNode;
|
|
if( parEle ){
|
|
try{
|
|
parEle.removeChild( domEle );
|
|
} catch(e){
|
|
// ie10 issue #1014
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
getElementById: function( id ){
|
|
var index = this._private.id2index[ id ];
|
|
if( index !== undefined ){
|
|
return this._private.elements[ index ];
|
|
}
|
|
|
|
// worst case, return an empty collection
|
|
return new $$.Collection( this );
|
|
},
|
|
|
|
selectionType: function(){
|
|
return this._private.selectionType;
|
|
},
|
|
|
|
hasCompoundNodes: function(){
|
|
return this._private.hasCompoundNodes;
|
|
},
|
|
|
|
styleEnabled: function(){
|
|
return this._private.styleEnabled;
|
|
},
|
|
|
|
addToPool: function( eles ){
|
|
var elements = this._private.elements;
|
|
var id2index = this._private.id2index;
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
|
|
var id = ele._private.data.id;
|
|
var index = id2index[ id ];
|
|
var alreadyInPool = index !== undefined;
|
|
|
|
if( !alreadyInPool ){
|
|
index = elements.length;
|
|
elements.push( ele );
|
|
id2index[ id ] = index;
|
|
ele._private.index = index;
|
|
}
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
removeFromPool: function( eles ){
|
|
var elements = this._private.elements;
|
|
var id2index = this._private.id2index;
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
|
|
var id = ele._private.data.id;
|
|
var index = id2index[ id ];
|
|
var inPool = index !== undefined;
|
|
|
|
if( inPool ){
|
|
this._private.id2index[ id ] = undefined;
|
|
elements.splice(index, 1);
|
|
|
|
// adjust the index of all elements past this index
|
|
for( var j = index; j < elements.length; j++ ){
|
|
var jid = elements[j]._private.data.id;
|
|
id2index[ jid ]--;
|
|
elements[j]._private.index--;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
container: function(){
|
|
return this._private.container;
|
|
},
|
|
|
|
options: function(){
|
|
return $$.util.copy( this._private.options );
|
|
},
|
|
|
|
json: function(params){
|
|
var json = {};
|
|
var cy = this;
|
|
|
|
json.elements = {};
|
|
cy.elements().each(function(i, ele){
|
|
var group = ele.group();
|
|
|
|
if( !json.elements[group] ){
|
|
json.elements[group] = [];
|
|
}
|
|
|
|
json.elements[group].push( ele.json() );
|
|
});
|
|
|
|
if( this._private.styleEnabled ){
|
|
json.style = cy.style().json();
|
|
}
|
|
|
|
json.zoomingEnabled = cy._private.zoomingEnabled;
|
|
json.userZoomingEnabled = cy._private.userZoomingEnabled;
|
|
json.zoom = cy._private.zoom;
|
|
json.minZoom = cy._private.minZoom;
|
|
json.maxZoom = cy._private.maxZoom;
|
|
json.panningEnabled = cy._private.panningEnabled;
|
|
json.userPanningEnabled = cy._private.userPanningEnabled;
|
|
json.pan = cy._private.pan;
|
|
json.boxSelectionEnabled = cy._private.boxSelectionEnabled;
|
|
json.layout = cy._private.options.layout;
|
|
json.renderer = cy._private.options.renderer;
|
|
json.hideEdgesOnViewport = cy._private.options.hideEdgesOnViewport;
|
|
json.hideLabelsOnViewport = cy._private.options.hideLabelsOnViewport;
|
|
json.textureOnViewport = cy._private.options.textureOnViewport;
|
|
json.wheelSensitivity = cy._private.options.wheelSensitivity;
|
|
json.motionBlur = cy._private.options.motionBlur;
|
|
|
|
return json;
|
|
},
|
|
|
|
// defer execution until not busy and guarantee relative execution order of deferred functions
|
|
defer: function( fn ){
|
|
var cy = this;
|
|
var _p = cy._private;
|
|
var q = _p.deferredExecQueue;
|
|
|
|
q.push( fn );
|
|
|
|
if( !_p.deferredTimeout ){
|
|
_p.deferredTimeout = setTimeout(function(){
|
|
while( q.length > 0 ){
|
|
( q.shift() )();
|
|
}
|
|
|
|
_p.deferredTimeout = null;
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
})( cytoscape, typeof window === 'undefined' ? null : window );
|
|
|
|
(function($$, window){ 'use strict';
|
|
|
|
function ready(f) {
|
|
var fn = ( document && (document.readyState === 'interactive' || document.readyState === 'complete') ) ? f : ready;
|
|
|
|
setTimeout(fn, 9, f);
|
|
}
|
|
|
|
$$.fn.core({
|
|
add: function(opts){
|
|
|
|
var elements;
|
|
var cy = this;
|
|
|
|
// add the elements
|
|
if( $$.is.elementOrCollection(opts) ){
|
|
var eles = opts;
|
|
|
|
if( eles._private.cy === cy ){ // same instance => just restore
|
|
elements = eles.restore();
|
|
|
|
} else { // otherwise, copy from json
|
|
var jsons = [];
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
jsons.push( ele.json() );
|
|
}
|
|
|
|
elements = new $$.Collection( cy, jsons );
|
|
}
|
|
}
|
|
|
|
// specify an array of options
|
|
else if( $$.is.array(opts) ){
|
|
var jsons = opts;
|
|
|
|
elements = new $$.Collection(cy, jsons);
|
|
}
|
|
|
|
// specify via opts.nodes and opts.edges
|
|
else if( $$.is.plainObject(opts) && ($$.is.array(opts.nodes) || $$.is.array(opts.edges)) ){
|
|
var elesByGroup = opts;
|
|
var jsons = [];
|
|
|
|
var grs = ['nodes', 'edges'];
|
|
for( var i = 0, il = grs.length; i < il; i++ ){
|
|
var group = grs[i];
|
|
var elesArray = elesByGroup[group];
|
|
|
|
if( $$.is.array(elesArray) ){
|
|
|
|
for( var j = 0, jl = elesArray.length; j < jl; j++ ){
|
|
var json = elesArray[j];
|
|
json.group = group;
|
|
|
|
jsons.push( json );
|
|
}
|
|
}
|
|
}
|
|
|
|
elements = new $$.Collection(cy, jsons);
|
|
}
|
|
|
|
// specify options for one element
|
|
else {
|
|
var json = opts;
|
|
elements = (new $$.Element( cy, json )).collection();
|
|
}
|
|
|
|
return elements;
|
|
},
|
|
|
|
remove: function(collection){
|
|
if( $$.is.elementOrCollection(collection) ){
|
|
collection = collection;
|
|
} else if( $$.is.string(collection) ){
|
|
var selector = collection;
|
|
collection = this.$( selector );
|
|
}
|
|
|
|
return collection.remove();
|
|
},
|
|
|
|
load: function(elements, onload, ondone){
|
|
var cy = this;
|
|
|
|
cy.notifications(false);
|
|
|
|
// remove old elements
|
|
var oldEles = cy.elements();
|
|
if( oldEles.length > 0 ){
|
|
oldEles.remove();
|
|
}
|
|
|
|
if( elements != null ){
|
|
if( $$.is.plainObject(elements) || $$.is.array(elements) ){
|
|
cy.add( elements );
|
|
}
|
|
}
|
|
|
|
function callback(){
|
|
cy.one('layoutready', function(e){
|
|
cy.notifications(true);
|
|
cy.trigger(e); // we missed this event by turning notifications off, so pass it on
|
|
|
|
cy.notify({
|
|
type: 'load',
|
|
collection: cy.elements()
|
|
});
|
|
|
|
cy.one('load', onload);
|
|
cy.trigger('load');
|
|
}).one('layoutstop', function(){
|
|
cy.one('done', ondone);
|
|
cy.trigger('done');
|
|
});
|
|
|
|
var layoutOpts = $$.util.extend({}, cy._private.options.layout);
|
|
layoutOpts.eles = cy.$();
|
|
|
|
cy.layout( layoutOpts );
|
|
|
|
}
|
|
|
|
if( window ){
|
|
ready( callback );
|
|
} else {
|
|
callback();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
});
|
|
|
|
})( cytoscape, typeof window === 'undefined' ? null : window );
|
|
|
|
;(function($$, window){ 'use strict';
|
|
|
|
$$.fn.core({
|
|
|
|
// pull in animation functions
|
|
animated: $$.define.animated(),
|
|
clearQueue: $$.define.clearQueue(),
|
|
delay: $$.define.delay(),
|
|
animate: $$.define.animate(),
|
|
stop: $$.define.stop(),
|
|
|
|
addToAnimationPool: function( eles ){
|
|
var cy = this;
|
|
|
|
if( !cy.styleEnabled() ){ return; } // save cycles when no style used
|
|
|
|
cy._private.aniEles.merge( eles );
|
|
},
|
|
|
|
startAnimationLoop: function(){
|
|
var cy = this;
|
|
|
|
if( !cy.styleEnabled() ){ return; } // save cycles when no style used
|
|
|
|
// don't execute the animation loop in headless environments
|
|
if( !window ){
|
|
return;
|
|
}
|
|
|
|
function globalAnimationStep(){
|
|
$$.util.requestAnimationFrame(function(now){
|
|
handleElements(now);
|
|
globalAnimationStep();
|
|
});
|
|
}
|
|
|
|
globalAnimationStep(); // first call
|
|
|
|
function handleElements(now){
|
|
now = +new Date();
|
|
|
|
var eles = cy._private.aniEles;
|
|
var doneEles = [];
|
|
var startedSomeAniThisTick = false;
|
|
|
|
function handleElement( ele, isCore ){
|
|
var current = ele._private.animation.current;
|
|
var queue = ele._private.animation.queue;
|
|
var ranAnis = false;
|
|
|
|
// if nothing currently animating, get something from the queue
|
|
if( current.length === 0 ){
|
|
var next = queue.length > 0 ? queue.shift() : null;
|
|
|
|
if( next ){
|
|
next.callTime = now; // was queued, so update call time
|
|
current.push( next );
|
|
}
|
|
}
|
|
|
|
// step and remove if done
|
|
var completes = [];
|
|
for(var i = current.length - 1; i >= 0; i--){
|
|
var ani = current[i];
|
|
|
|
// start if need be
|
|
if( !ani.started ){
|
|
startAnimation( ele, ani );
|
|
startedSomeAniThisTick = true;
|
|
}
|
|
|
|
step( ele, ani, now, isCore );
|
|
|
|
if( ani.done ){
|
|
completes.push( ani );
|
|
|
|
// remove current[i]
|
|
current.splice(i, 1);
|
|
}
|
|
|
|
ranAnis = true;
|
|
}
|
|
|
|
// call complete callbacks
|
|
for( var i = 0; i < completes.length; i++ ){
|
|
var ani = completes[i];
|
|
var complete = ani.params.complete;
|
|
|
|
if( $$.is.fn(complete) ){
|
|
complete.apply( ele, [ now ] );
|
|
}
|
|
}
|
|
|
|
if( !isCore && current.length === 0 && queue.length === 0 ){
|
|
doneEles.push( ele );
|
|
}
|
|
|
|
return ranAnis;
|
|
} // handleElements
|
|
|
|
// handle all eles
|
|
for( var e = 0; e < eles.length; e++ ){
|
|
var ele = eles[e];
|
|
|
|
handleElement( ele );
|
|
} // each element
|
|
|
|
var ranCoreAni = handleElement( cy, true );
|
|
|
|
// notify renderer
|
|
if( eles.length > 0 || ranCoreAni ){
|
|
var toNotify;
|
|
|
|
if( eles.length > 0 ){
|
|
var updatedEles = eles.updateCompoundBounds();
|
|
toNotify = updatedEles.length > 0 ? eles.add( updatedEles ) : eles;
|
|
}
|
|
|
|
cy.notify({
|
|
type: startedSomeAniThisTick ? 'style' : 'draw',
|
|
collection: toNotify
|
|
});
|
|
}
|
|
|
|
// remove elements from list of currently animating if its queues are empty
|
|
eles.unmerge( doneEles );
|
|
|
|
} // handleElements
|
|
|
|
function startAnimation( self, ani ){
|
|
var isCore = $$.is.core( self );
|
|
var isEles = !isCore;
|
|
var ele = self;
|
|
var style = cy._private.style;
|
|
|
|
if( isEles ){
|
|
var pos = ele._private.position;
|
|
var startPosition = {
|
|
x: pos.x,
|
|
y: pos.y
|
|
};
|
|
var startStyle = style.getValueStyle( ele );
|
|
}
|
|
|
|
if( isCore ){
|
|
var pan = cy._private.pan;
|
|
var startPan = {
|
|
x: pan.x,
|
|
y: pan.y
|
|
};
|
|
|
|
var startZoom = cy._private.zoom;
|
|
}
|
|
|
|
ani.started = true;
|
|
ani.startTime = Date.now();
|
|
ani.startPosition = startPosition;
|
|
ani.startStyle = startStyle;
|
|
ani.startPan = startPan;
|
|
ani.startZoom = startZoom;
|
|
}
|
|
|
|
function step( self, animation, now, isCore ){
|
|
var style = cy._private.style;
|
|
var properties = animation.properties;
|
|
var params = animation.params;
|
|
var startTime = animation.startTime;
|
|
var percent;
|
|
var isEles = !isCore;
|
|
|
|
if( animation.duration === 0 ){
|
|
percent = 1;
|
|
} else {
|
|
percent = Math.min(1, (now - startTime)/animation.duration);
|
|
}
|
|
|
|
if( percent < 0 ){
|
|
percent = 0;
|
|
} else if( percent > 1 ){
|
|
percent = 1;
|
|
}
|
|
|
|
if( properties.delay == null ){ // then update
|
|
|
|
var startPos = animation.startPosition;
|
|
var endPos = properties.position;
|
|
var pos = self._private.position;
|
|
if( endPos && isEles ){
|
|
if( valid( startPos.x, endPos.x ) ){
|
|
pos.x = ease( startPos.x, endPos.x, percent );
|
|
}
|
|
|
|
if( valid( startPos.y, endPos.y ) ){
|
|
pos.y = ease( startPos.y, endPos.y, percent );
|
|
}
|
|
}
|
|
|
|
var startPan = animation.startPan;
|
|
var endPan = properties.pan;
|
|
var pan = self._private.pan;
|
|
var animatingPan = endPan != null && isCore;
|
|
if( animatingPan ){
|
|
if( valid( startPan.x, endPan.x ) ){
|
|
pan.x = ease( startPan.x, endPan.x, percent );
|
|
}
|
|
|
|
if( valid( startPan.y, endPan.y ) ){
|
|
pan.y = ease( startPan.y, endPan.y, percent );
|
|
}
|
|
|
|
self.trigger('pan');
|
|
}
|
|
|
|
var startZoom = animation.startZoom;
|
|
var endZoom = properties.zoom;
|
|
var animatingZoom = endZoom != null && isCore;
|
|
if( animatingZoom ){
|
|
if( valid( startZoom, endZoom ) ){
|
|
self._private.zoom = ease( startZoom, endZoom, percent );
|
|
}
|
|
|
|
self.trigger('zoom');
|
|
}
|
|
|
|
if( animatingPan || animatingZoom ){
|
|
self.trigger('viewport');
|
|
}
|
|
|
|
var props = properties.style || properties.css;
|
|
if( props && isEles ){
|
|
|
|
for( var i = 0; i < props.length; i++ ){
|
|
var name = props[i].name;
|
|
var prop = props[i];
|
|
var end = prop;
|
|
|
|
var start = animation.startStyle[ name ];
|
|
var easedVal = ease( start, end, percent );
|
|
|
|
style.overrideBypass( self, name, easedVal );
|
|
} // for props
|
|
|
|
} // if
|
|
|
|
}
|
|
|
|
if( $$.is.fn(params.step) ){
|
|
params.step.apply( self, [ now ] );
|
|
}
|
|
|
|
if( percent >= 1 ){
|
|
animation.done = true;
|
|
}
|
|
|
|
return percent;
|
|
}
|
|
|
|
function valid(start, end){
|
|
if( start == null || end == null ){
|
|
return false;
|
|
}
|
|
|
|
if( $$.is.number(start) && $$.is.number(end) ){
|
|
return true;
|
|
} else if( (start) && (end) ){
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function ease(startProp, endProp, percent){
|
|
if( percent < 0 ){
|
|
percent = 0;
|
|
} else if( percent > 1 ){
|
|
percent = 1;
|
|
}
|
|
|
|
var start, end;
|
|
|
|
if( startProp.pxValue != null || startProp.value != null ){
|
|
start = startProp.pxValue != null ? startProp.pxValue : startProp.value;
|
|
} else {
|
|
start = startProp;
|
|
}
|
|
|
|
if( endProp.pxValue != null || endProp.value != null ){
|
|
end = endProp.pxValue != null ? endProp.pxValue : endProp.value;
|
|
} else {
|
|
end = endProp;
|
|
}
|
|
|
|
if( $$.is.number(start) && $$.is.number(end) ){
|
|
return start + (end - start) * percent;
|
|
|
|
} else if( $$.is.number(start[0]) && $$.is.number(end[0]) ){ // then assume a colour
|
|
var c1 = start;
|
|
var c2 = end;
|
|
|
|
var ch = function(ch1, ch2){
|
|
var diff = ch2 - ch1;
|
|
var min = ch1;
|
|
return Math.round( percent * diff + min );
|
|
};
|
|
|
|
var r = ch( c1[0], c2[0] );
|
|
var g = ch( c1[1], c2[1] );
|
|
var b = ch( c1[2], c2[2] );
|
|
|
|
return [r, g, b];
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
})( cytoscape, typeof window === 'undefined' ? null : window );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.core({
|
|
data: $$.define.data({
|
|
field: 'data',
|
|
bindingEvent: 'data',
|
|
allowBinding: true,
|
|
allowSetting: true,
|
|
settingEvent: 'data',
|
|
settingTriggersEvent: true,
|
|
triggerFnName: 'trigger',
|
|
allowGetting: true
|
|
}),
|
|
|
|
removeData: $$.define.removeData({
|
|
field: 'data',
|
|
event: 'data',
|
|
triggerFnName: 'trigger',
|
|
triggerEvent: true
|
|
}),
|
|
|
|
scratch: $$.define.data({
|
|
field: 'scratch',
|
|
allowBinding: false,
|
|
allowSetting: true,
|
|
settingTriggersEvent: false,
|
|
allowGetting: true
|
|
}),
|
|
|
|
removeScratch: $$.define.removeData({
|
|
field: 'scratch',
|
|
triggerEvent: false
|
|
})
|
|
});
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.core({
|
|
on: $$.define.on(), // .on( events [, selector] [, data], handler)
|
|
one: $$.define.on({ unbindSelfOnTrigger: true }),
|
|
once: $$.define.on({ unbindAllBindersOnTrigger: true }),
|
|
off: $$.define.off(), // .off( events [, selector] [, handler] )
|
|
trigger: $$.define.trigger() // .trigger( events [, extraParams] )
|
|
});
|
|
|
|
$$.define.eventAliasesOn( $$.corefn );
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.core({
|
|
|
|
png: function( options ){
|
|
var renderer = this._private.renderer;
|
|
options = options || {};
|
|
|
|
return renderer.png( options );
|
|
},
|
|
|
|
jpg: function( options ){
|
|
var renderer = this._private.renderer;
|
|
options = options || {};
|
|
|
|
options.bg = options.bg || '#fff';
|
|
|
|
return renderer.jpg( options );
|
|
}
|
|
|
|
});
|
|
|
|
$$.corefn.jpeg = $$.corefn.jpg;
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.core({
|
|
|
|
layout: function( params ){
|
|
var layout;
|
|
|
|
// always use a new layout w/ init opts; slightly different backwards compatibility
|
|
// but fixes layout reuse issues like dagre #819
|
|
if( params == null ){
|
|
params = $$.util.extend({}, this._private.options.layout);
|
|
params.eles = this.$();
|
|
}
|
|
|
|
layout = this.initLayout( params );
|
|
layout.run();
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
makeLayout: function( params ){
|
|
return this.initLayout( params );
|
|
},
|
|
|
|
initLayout: function( options ){
|
|
if( options == null ){
|
|
$$.util.error('Layout options must be specified to make a layout');
|
|
return;
|
|
}
|
|
|
|
if( options.name == null ){
|
|
$$.util.error('A `name` must be specified to make a layout');
|
|
return;
|
|
}
|
|
|
|
var name = options.name;
|
|
var LayoutProto = $$.extension('layout', name);
|
|
|
|
if( LayoutProto == null ){
|
|
$$.util.error('Can not apply layout: No such layout `' + name + '` found; did you include its JS file?');
|
|
return;
|
|
}
|
|
|
|
options.eles = options.eles != null ? options.eles : this.$();
|
|
|
|
if( $$.is.string( options.eles ) ){
|
|
options.eles = this.$( options.eles );
|
|
}
|
|
|
|
var layout = new LayoutProto( $$.util.extend({}, options, {
|
|
cy: this
|
|
}) );
|
|
|
|
// make sure layout has _private for use w/ std apis like .on()
|
|
if( !$$.is.plainObject(layout._private) ){
|
|
layout._private = {};
|
|
}
|
|
|
|
layout._private.cy = this;
|
|
layout._private.listeners = [];
|
|
|
|
return layout;
|
|
}
|
|
|
|
});
|
|
|
|
$$.corefn.createLayout = $$.corefn.makeLayout;
|
|
|
|
})( cytoscape );
|
|
|
|
(function($$){ 'use strict';
|
|
|
|
$$.fn.core({
|
|
notify: function( params ){
|
|
if( this._private.batchingNotify ){
|
|
var bEles = this._private.batchNotifyEles;
|
|
var bTypes = this._private.batchNotifyTypes;
|
|
|
|
if( params.collection ){ for( var i = 0; i < params.collection.length; i++ ){
|
|
var ele = params.collection[i];
|
|
|
|
if( !bEles.ids[ ele._private.id ] ){
|
|
bEles.push( ele );
|
|
}
|
|
} }
|
|
|
|
if( !bTypes.ids[ params.type ] ){
|
|
bTypes.push( params.type );
|
|
}
|
|
|
|
return; // notifications are disabled during batching
|
|
}
|
|
|
|
if( !this._private.notificationsEnabled ){ return; } // exit on disabled
|
|
|
|
var renderer = this.renderer();
|
|
|
|
renderer.notify(params);
|
|
},
|
|
|
|
notifications: function( bool ){
|
|
var p = this._private;
|
|
|
|
if( bool === undefined ){
|
|
return p.notificationsEnabled;
|
|
} else {
|
|
p.notificationsEnabled = bool ? true : false;
|
|
}
|
|
},
|
|
|
|
noNotifications: function( callback ){
|
|
this.notifications(false);
|
|
callback();
|
|
this.notifications(true);
|
|
},
|
|
|
|
startBatch: function(){
|
|
var _p = this._private;
|
|
|
|
_p.batchingStyle = _p.batchingNotify = true;
|
|
_p.batchStyleEles = [];
|
|
_p.batchNotifyEles = [];
|
|
_p.batchNotifyTypes = [];
|
|
|
|
_p.batchStyleEles.ids = {};
|
|
_p.batchNotifyEles.ids = {};
|
|
_p.batchNotifyTypes.ids = {};
|
|
|
|
return this;
|
|
},
|
|
|
|
endBatch: function(){
|
|
var _p = this._private;
|
|
|
|
// update style for dirty eles
|
|
_p.batchingStyle = false;
|
|
new $$.Collection(this, _p.batchStyleEles).updateStyle();
|
|
|
|
// notify the renderer of queued eles and event types
|
|
_p.batchingNotify = false;
|
|
this.notify({
|
|
type: _p.batchNotifyTypes,
|
|
collection: _p.batchNotifyEles
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
batch: function( callback ){
|
|
this.startBatch();
|
|
callback();
|
|
this.endBatch();
|
|
|
|
return this;
|
|
},
|
|
|
|
// for backwards compatibility
|
|
batchData: function( map ){
|
|
var cy = this;
|
|
|
|
return this.batch(function(){
|
|
for( var id in map ){
|
|
var data = map[id];
|
|
var ele = cy.getElementById( id );
|
|
|
|
ele.data( data );
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.core({
|
|
|
|
renderTo: function( context, zoom, pan, pxRatio ){
|
|
var r = this._private.renderer;
|
|
|
|
r.renderTo( context, zoom, pan, pxRatio );
|
|
return this;
|
|
},
|
|
|
|
renderer: function(){
|
|
return this._private.renderer;
|
|
},
|
|
|
|
forceRender: function(){
|
|
this.notify({
|
|
type: 'draw'
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
resize: function(){
|
|
this.notify({
|
|
type: 'resize'
|
|
});
|
|
|
|
this.trigger('resize');
|
|
|
|
return this;
|
|
},
|
|
|
|
initRenderer: function( options ){
|
|
var cy = this;
|
|
|
|
var RendererProto = $$.extension('renderer', options.name);
|
|
if( RendererProto == null ){
|
|
$$.util.error('Can not initialise: No such renderer `%s` found; did you include its JS file?', options.name);
|
|
return;
|
|
}
|
|
|
|
this._private.renderer = new RendererProto(
|
|
$$.util.extend({}, options, {
|
|
cy: cy,
|
|
style: cy._private.style
|
|
})
|
|
);
|
|
|
|
},
|
|
|
|
triggerOnRender: function(){
|
|
var cbs = this._private.onRenders;
|
|
|
|
for( var i = 0; i < cbs.length; i++ ){
|
|
var cb = cbs[i];
|
|
|
|
cb();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
onRender: function( cb ){
|
|
this._private.onRenders.push( cb );
|
|
|
|
return this;
|
|
},
|
|
|
|
offRender: function( fn ){
|
|
var cbs = this._private.onRenders;
|
|
|
|
if( fn == null ){ // unbind all
|
|
this._private.onRenders = [];
|
|
return this;
|
|
}
|
|
|
|
for( var i = 0; i < cbs.length; i++ ){ // unbind specified
|
|
var cb = cbs[i];
|
|
|
|
if( fn === cb ){
|
|
cbs.splice( i, 1 );
|
|
break;
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
});
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.core({
|
|
|
|
// get a collection
|
|
// - empty collection on no args
|
|
// - collection of elements in the graph on selector arg
|
|
// - guarantee a returned collection when elements or collection specified
|
|
collection: function( eles ){
|
|
|
|
if( $$.is.string( eles ) ){
|
|
return this.$( eles );
|
|
|
|
} else if( $$.is.elementOrCollection( eles ) ){
|
|
return eles.collection();
|
|
|
|
} else if( $$.is.array( eles ) ){
|
|
return new $$.Collection( this, eles );
|
|
}
|
|
|
|
return new $$.Collection( this );
|
|
},
|
|
|
|
nodes: function( selector ){
|
|
var nodes = this.$(function(){
|
|
return this.isNode();
|
|
});
|
|
|
|
if( selector ){
|
|
return nodes.filter( selector );
|
|
}
|
|
|
|
return nodes;
|
|
},
|
|
|
|
edges: function( selector ){
|
|
var edges = this.$(function(){
|
|
return this.isEdge();
|
|
});
|
|
|
|
if( selector ){
|
|
return edges.filter( selector );
|
|
}
|
|
|
|
return edges;
|
|
},
|
|
|
|
// search the graph like jQuery
|
|
$: function( selector ){
|
|
var eles = new $$.Collection( this, this._private.elements );
|
|
|
|
if( selector ){
|
|
return eles.filter( selector );
|
|
}
|
|
|
|
return eles;
|
|
}
|
|
|
|
});
|
|
|
|
// aliases
|
|
$$.corefn.elements = $$.corefn.filter = $$.corefn.$;
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.core({
|
|
|
|
style: function( newStyle ){
|
|
if( newStyle ){
|
|
var s = this.setStyle( newStyle );
|
|
|
|
s.update();
|
|
}
|
|
|
|
return this._private.style;
|
|
},
|
|
|
|
setStyle: function( style ){
|
|
var _p = this._private;
|
|
|
|
if( $$.is.stylesheet(style) ){
|
|
_p.style = style.generateStyle(this);
|
|
|
|
} else if( $$.is.array(style) ) {
|
|
_p.style = $$.style.fromJson(this, style);
|
|
|
|
} else if( $$.is.string(style) ){
|
|
_p.style = $$.style.fromString(this, style);
|
|
|
|
} else {
|
|
_p.style = new $$.Style( this );
|
|
}
|
|
|
|
return _p.style;
|
|
}
|
|
});
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.core({
|
|
|
|
autolock: function(bool){
|
|
if( bool !== undefined ){
|
|
this._private.autolock = bool ? true : false;
|
|
} else {
|
|
return this._private.autolock;
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
autoungrabify: function(bool){
|
|
if( bool !== undefined ){
|
|
this._private.autoungrabify = bool ? true : false;
|
|
} else {
|
|
return this._private.autoungrabify;
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
autounselectify: function(bool){
|
|
if( bool !== undefined ){
|
|
this._private.autounselectify = bool ? true : false;
|
|
} else {
|
|
return this._private.autounselectify;
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
panningEnabled: function( bool ){
|
|
if( bool !== undefined ){
|
|
this._private.panningEnabled = bool ? true : false;
|
|
} else {
|
|
return this._private.panningEnabled;
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
userPanningEnabled: function( bool ){
|
|
if( bool !== undefined ){
|
|
this._private.userPanningEnabled = bool ? true : false;
|
|
} else {
|
|
return this._private.userPanningEnabled;
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
zoomingEnabled: function( bool ){
|
|
if( bool !== undefined ){
|
|
this._private.zoomingEnabled = bool ? true : false;
|
|
} else {
|
|
return this._private.zoomingEnabled;
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
userZoomingEnabled: function( bool ){
|
|
if( bool !== undefined ){
|
|
this._private.userZoomingEnabled = bool ? true : false;
|
|
} else {
|
|
return this._private.userZoomingEnabled;
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
boxSelectionEnabled: function( bool ){
|
|
if( bool !== undefined ){
|
|
this._private.boxSelectionEnabled = bool ? true : false;
|
|
} else {
|
|
return this._private.boxSelectionEnabled;
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
pan: function(){
|
|
var args = arguments;
|
|
var pan = this._private.pan;
|
|
var dim, val, dims, x, y;
|
|
|
|
switch( args.length ){
|
|
case 0: // .pan()
|
|
return pan;
|
|
|
|
case 1:
|
|
|
|
if( $$.is.string( args[0] ) ){ // .pan('x')
|
|
dim = args[0];
|
|
return pan[ dim ];
|
|
|
|
} else if( $$.is.plainObject( args[0] ) ) { // .pan({ x: 0, y: 100 })
|
|
if( !this._private.panningEnabled ){
|
|
return this;
|
|
}
|
|
|
|
dims = args[0];
|
|
x = dims.x;
|
|
y = dims.y;
|
|
|
|
if( $$.is.number(x) ){
|
|
pan.x = x;
|
|
}
|
|
|
|
if( $$.is.number(y) ){
|
|
pan.y = y;
|
|
}
|
|
|
|
this.trigger('pan viewport');
|
|
}
|
|
break;
|
|
|
|
case 2: // .pan('x', 100)
|
|
if( !this._private.panningEnabled ){
|
|
return this;
|
|
}
|
|
|
|
dim = args[0];
|
|
val = args[1];
|
|
|
|
if( (dim === 'x' || dim === 'y') && $$.is.number(val) ){
|
|
pan[dim] = val;
|
|
}
|
|
|
|
this.trigger('pan viewport');
|
|
break;
|
|
|
|
default:
|
|
break; // invalid
|
|
}
|
|
|
|
this.notify({ // notify the renderer that the viewport changed
|
|
type: 'viewport'
|
|
});
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
panBy: function(params){
|
|
var args = arguments;
|
|
var pan = this._private.pan;
|
|
var dim, val, dims, x, y;
|
|
|
|
if( !this._private.panningEnabled ){
|
|
return this;
|
|
}
|
|
|
|
switch( args.length ){
|
|
case 1:
|
|
|
|
if( $$.is.plainObject( args[0] ) ) { // .panBy({ x: 0, y: 100 })
|
|
dims = args[0];
|
|
x = dims.x;
|
|
y = dims.y;
|
|
|
|
if( $$.is.number(x) ){
|
|
pan.x += x;
|
|
}
|
|
|
|
if( $$.is.number(y) ){
|
|
pan.y += y;
|
|
}
|
|
|
|
this.trigger('pan viewport');
|
|
}
|
|
break;
|
|
|
|
case 2: // .panBy('x', 100)
|
|
dim = args[0];
|
|
val = args[1];
|
|
|
|
if( (dim === 'x' || dim === 'y') && $$.is.number(val) ){
|
|
pan[dim] += val;
|
|
}
|
|
|
|
this.trigger('pan viewport');
|
|
break;
|
|
|
|
default:
|
|
break; // invalid
|
|
}
|
|
|
|
this.notify({ // notify the renderer that the viewport changed
|
|
type: 'viewport'
|
|
});
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
fit: function( elements, padding ){
|
|
var viewportState = this.getFitViewport( elements, padding );
|
|
|
|
if( viewportState ){
|
|
var _p = this._private;
|
|
_p.zoom = viewportState.zoom;
|
|
_p.pan = viewportState.pan;
|
|
|
|
this.trigger('pan zoom viewport');
|
|
|
|
this.notify({ // notify the renderer that the viewport changed
|
|
type: 'viewport'
|
|
});
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
getFitViewport: function( elements, padding ){
|
|
if( $$.is.number(elements) && padding === undefined ){ // elements is optional
|
|
padding = elements;
|
|
elements = undefined;
|
|
}
|
|
|
|
if( !this._private.panningEnabled || !this._private.zoomingEnabled ){
|
|
return;
|
|
}
|
|
|
|
var bb;
|
|
|
|
if( $$.is.string(elements) ){
|
|
var sel = elements;
|
|
elements = this.$( sel );
|
|
|
|
} else if( $$.is.boundingBox(elements) ){ // assume bb
|
|
var bbe = elements;
|
|
bb = {
|
|
x1: bbe.x1,
|
|
y1: bbe.y1,
|
|
x2: bbe.x2,
|
|
y2: bbe.y2
|
|
};
|
|
|
|
bb.w = bb.x2 - bb.x1;
|
|
bb.h = bb.y2 - bb.y1;
|
|
|
|
} else if( !$$.is.elementOrCollection(elements) ){
|
|
elements = this.elements();
|
|
}
|
|
|
|
bb = bb || elements.boundingBox();
|
|
|
|
var w = this.width();
|
|
var h = this.height();
|
|
var zoom;
|
|
padding = $$.is.number(padding) ? padding : 0;
|
|
|
|
if( !isNaN(w) && !isNaN(h) && w > 0 && h > 0 && !isNaN(bb.w) && !isNaN(bb.h) && bb.w > 0 && bb.h > 0 ){
|
|
zoom = Math.min( (w - 2*padding)/bb.w, (h - 2*padding)/bb.h );
|
|
|
|
// crop zoom
|
|
zoom = zoom > this._private.maxZoom ? this._private.maxZoom : zoom;
|
|
zoom = zoom < this._private.minZoom ? this._private.minZoom : zoom;
|
|
|
|
var pan = { // now pan to middle
|
|
x: (w - zoom*( bb.x1 + bb.x2 ))/2,
|
|
y: (h - zoom*( bb.y1 + bb.y2 ))/2
|
|
};
|
|
|
|
return {
|
|
zoom: zoom,
|
|
pan: pan
|
|
};
|
|
}
|
|
|
|
return;
|
|
},
|
|
|
|
minZoom: function( zoom ){
|
|
if( zoom === undefined ){
|
|
return this._private.minZoom;
|
|
} else if( $$.is.number(zoom) ){
|
|
this._private.minZoom = zoom;
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
maxZoom: function( zoom ){
|
|
if( zoom === undefined ){
|
|
return this._private.maxZoom;
|
|
} else if( $$.is.number(zoom) ){
|
|
this._private.maxZoom = zoom;
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
zoom: function( params ){
|
|
var pos; // in rendered px
|
|
var zoom;
|
|
|
|
if( params === undefined ){ // then get the zoom
|
|
return this._private.zoom;
|
|
|
|
} else if( $$.is.number(params) ){ // then set the zoom
|
|
zoom = params;
|
|
|
|
} else if( $$.is.plainObject(params) ){ // then zoom about a point
|
|
zoom = params.level;
|
|
|
|
if( params.position ){
|
|
var p = params.position;
|
|
var pan = this._private.pan;
|
|
var z = this._private.zoom;
|
|
|
|
pos = { // convert to rendered px
|
|
x: p.x * z + pan.x,
|
|
y: p.y * z + pan.y
|
|
};
|
|
} else if( params.renderedPosition ){
|
|
pos = params.renderedPosition;
|
|
}
|
|
|
|
if( pos && !this._private.panningEnabled ){
|
|
return this; // panning disabled
|
|
}
|
|
}
|
|
|
|
if( !this._private.zoomingEnabled ){
|
|
return this; // zooming disabled
|
|
}
|
|
|
|
if( !$$.is.number(zoom) || ( pos && (!$$.is.number(pos.x) || !$$.is.number(pos.y)) ) ){
|
|
return this; // can't zoom with invalid params
|
|
}
|
|
|
|
// crop zoom
|
|
zoom = zoom > this._private.maxZoom ? this._private.maxZoom : zoom;
|
|
zoom = zoom < this._private.minZoom ? this._private.minZoom : zoom;
|
|
|
|
if( pos ){ // set zoom about position
|
|
var pan1 = this._private.pan;
|
|
var zoom1 = this._private.zoom;
|
|
var zoom2 = zoom;
|
|
|
|
var pan2 = {
|
|
x: -zoom2/zoom1 * (pos.x - pan1.x) + pos.x,
|
|
y: -zoom2/zoom1 * (pos.y - pan1.y) + pos.y
|
|
};
|
|
|
|
this._private.zoom = zoom;
|
|
this._private.pan = pan2;
|
|
|
|
var posChanged = pan1.x !== pan2.x || pan1.y !== pan2.y;
|
|
this.trigger(' zoom ' + (posChanged ? ' pan ' : '') + ' viewport ' );
|
|
|
|
} else { // just set the zoom
|
|
this._private.zoom = zoom;
|
|
this.trigger('zoom viewport');
|
|
}
|
|
|
|
this.notify({ // notify the renderer that the viewport changed
|
|
type: 'viewport'
|
|
});
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
viewport: function( opts ){
|
|
var _p = this._private;
|
|
var zoomDefd = true;
|
|
var panDefd = true;
|
|
var events = []; // to trigger
|
|
var zoomFailed = false;
|
|
var panFailed = false;
|
|
|
|
if( !opts ){ return this; }
|
|
if( !$$.is.number(opts.zoom) ){ zoomDefd = false; }
|
|
if( !$$.is.plainObject(opts.pan) ){ panDefd = false; }
|
|
if( !zoomDefd && !panDefd ){ return this; }
|
|
|
|
if( zoomDefd ){
|
|
var z = opts.zoom;
|
|
|
|
if( z < _p.minZoom || z > _p.maxZoom || !_p.zoomingEnabled ){
|
|
zoomFailed = true;
|
|
|
|
} else {
|
|
_p.zoom = z;
|
|
|
|
events.push('zoom');
|
|
}
|
|
}
|
|
|
|
if( panDefd && (!zoomFailed || !opts.cancelOnFailedZoom) && _p.panningEnabled ){
|
|
var p = opts.pan;
|
|
|
|
if( $$.is.number(p.x) ){
|
|
_p.pan.x = p.x;
|
|
panFailed = false;
|
|
}
|
|
|
|
if( $$.is.number(p.y) ){
|
|
_p.pan.y = p.y;
|
|
panFailed = false;
|
|
}
|
|
|
|
if( !panFailed ){
|
|
events.push('pan');
|
|
}
|
|
}
|
|
|
|
if( events.length > 0 ){
|
|
events.push('viewport');
|
|
this.trigger( events.join(' ') );
|
|
|
|
this.notify({
|
|
type: 'viewport'
|
|
});
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
center: function( elements ){
|
|
var pan = this.getCenterPan( elements );
|
|
|
|
if( pan ){
|
|
this._private.pan = pan;
|
|
|
|
this.trigger('pan viewport');
|
|
|
|
this.notify({ // notify the renderer that the viewport changed
|
|
type: 'viewport'
|
|
});
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
getCenterPan: function( elements, zoom ){
|
|
if( !this._private.panningEnabled ){
|
|
return;
|
|
}
|
|
|
|
if( $$.is.string(elements) ){
|
|
var selector = elements;
|
|
elements = this.elements( selector );
|
|
} else if( !$$.is.elementOrCollection(elements) ){
|
|
elements = this.elements();
|
|
}
|
|
|
|
var bb = elements.boundingBox();
|
|
var w = this.width();
|
|
var h = this.height();
|
|
zoom = zoom === undefined ? this._private.zoom : zoom;
|
|
|
|
var pan = { // middle
|
|
x: (w - zoom*( bb.x1 + bb.x2 ))/2,
|
|
y: (h - zoom*( bb.y1 + bb.y2 ))/2
|
|
};
|
|
|
|
return pan;
|
|
},
|
|
|
|
reset: function(){
|
|
if( !this._private.panningEnabled || !this._private.zoomingEnabled ){
|
|
return this;
|
|
}
|
|
|
|
this.viewport({
|
|
pan: { x: 0, y: 0 },
|
|
zoom: 1
|
|
});
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
width: function(){
|
|
var container = this._private.container;
|
|
|
|
if( container ){
|
|
return container.clientWidth;
|
|
}
|
|
|
|
return 1; // fallback if no container (not 0 b/c can be used for dividing etc)
|
|
},
|
|
|
|
height: function(){
|
|
var container = this._private.container;
|
|
|
|
if( container ){
|
|
return container.clientHeight;
|
|
}
|
|
|
|
return 1; // fallback if no container (not 0 b/c can be used for dividing etc)
|
|
},
|
|
|
|
extent: function(){
|
|
var pan = this._private.pan;
|
|
var zoom = this._private.zoom;
|
|
var rb = this.renderedExtent();
|
|
|
|
var b = {
|
|
x1: ( rb.x1 - pan.x )/zoom,
|
|
x2: ( rb.x2 - pan.x )/zoom,
|
|
y1: ( rb.y1 - pan.y )/zoom,
|
|
y2: ( rb.y2 - pan.y )/zoom,
|
|
};
|
|
|
|
b.w = b.x2 - b.x1;
|
|
b.h = b.y2 - b.y1;
|
|
|
|
return b;
|
|
},
|
|
|
|
renderedExtent: function(){
|
|
var width = this.width();
|
|
var height = this.height();
|
|
|
|
return {
|
|
x1: 0,
|
|
y1: 0,
|
|
x2: width,
|
|
y2: height,
|
|
w: width,
|
|
h: height
|
|
};
|
|
}
|
|
});
|
|
|
|
// aliases
|
|
$$.corefn.centre = $$.corefn.center;
|
|
|
|
// backwards compatibility
|
|
$$.corefn.autolockNodes = $$.corefn.autolock;
|
|
$$.corefn.autoungrabifyNodes = $$.corefn.autoungrabify;
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// Use this interface to define functions for collections/elements.
|
|
// This interface is good, because it forces you to think in terms
|
|
// of the collections case (more than 1 element), so we don't need
|
|
// notification blocking nonsense everywhere.
|
|
//
|
|
// Other collection-*.js files depend on this being defined first.
|
|
// It's a trade off: It simplifies the code for Collection and
|
|
// Element integration so much that it's worth it to create the
|
|
// JS dependency.
|
|
//
|
|
// Having this integration guarantees that we can call any
|
|
// collection function on an element and vice versa.
|
|
|
|
// e.g. $$.fn.collection({ someFunc: function(){ /* ... */ } })
|
|
$$.fn.collection = $$.fn.eles = function( fnMap, options ){
|
|
for( var name in fnMap ){
|
|
var fn = fnMap[name];
|
|
|
|
$$.Collection.prototype[ name ] = fn;
|
|
}
|
|
};
|
|
|
|
// factory for generating edge ids when no id is specified for a new element
|
|
var idFactory = {
|
|
prefix: {
|
|
nodes: 'n',
|
|
edges: 'e'
|
|
},
|
|
id: {
|
|
nodes: 0,
|
|
edges: 0
|
|
},
|
|
generate: function(cy, element, tryThisId){
|
|
var json = $$.is.element( element ) ? element._private : element;
|
|
var group = json.group;
|
|
var id = tryThisId != null ? tryThisId : this.prefix[group] + this.id[group];
|
|
|
|
if( cy.getElementById(id).empty() ){
|
|
this.id[group]++; // we've used the current id, so move it up
|
|
} else { // otherwise keep trying successive unused ids
|
|
while( !cy.getElementById(id).empty() ){
|
|
id = this.prefix[group] + ( ++this.id[group] );
|
|
}
|
|
}
|
|
|
|
return id;
|
|
}
|
|
};
|
|
|
|
// Element
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// represents a node or an edge
|
|
$$.Element = function(cy, params, restore){
|
|
if( !(this instanceof $$.Element) ){
|
|
return new $$.Element(cy, params, restore);
|
|
}
|
|
|
|
var self = this;
|
|
restore = (restore === undefined || restore ? true : false);
|
|
|
|
if( cy === undefined || params === undefined || !$$.is.core(cy) ){
|
|
$$.util.error('An element must have a core reference and parameters set');
|
|
return;
|
|
}
|
|
|
|
// validate group
|
|
if( params.group !== 'nodes' && params.group !== 'edges' ){
|
|
$$.util.error('An element must be of type `nodes` or `edges`; you specified `' + params.group + '`');
|
|
return;
|
|
}
|
|
|
|
// make the element array-like, just like a collection
|
|
this.length = 1;
|
|
this[0] = this;
|
|
|
|
// NOTE: when something is added here, add also to ele.json()
|
|
this._private = {
|
|
cy: cy,
|
|
single: true, // indicates this is an element
|
|
data: params.data || {}, // data object
|
|
position: params.position || {}, // (x, y) position pair
|
|
autoWidth: undefined, // width and height of nodes calculated by the renderer when set to special 'auto' value
|
|
autoHeight: undefined,
|
|
listeners: [], // array of bound listeners
|
|
group: params.group, // string; 'nodes' or 'edges'
|
|
style: {}, // properties as set by the style
|
|
rstyle: {}, // properties for style sent from the renderer to the core
|
|
styleCxts: [], // applied style contexts from the styler
|
|
removed: true, // whether it's inside the vis; true if removed (set true here since we call restore)
|
|
selected: params.selected ? true : false, // whether it's selected
|
|
selectable: params.selectable === undefined ? true : ( params.selectable ? true : false ), // whether it's selectable
|
|
locked: params.locked ? true : false, // whether the element is locked (cannot be moved)
|
|
grabbed: false, // whether the element is grabbed by the mouse; renderer sets this privately
|
|
grabbable: params.grabbable === undefined ? true : ( params.grabbable ? true : false ), // whether the element can be grabbed
|
|
active: false, // whether the element is active from user interaction
|
|
classes: {}, // map ( className => true )
|
|
animation: { // object for currently-running animations
|
|
current: [],
|
|
queue: []
|
|
},
|
|
rscratch: {}, // object in which the renderer can store information
|
|
scratch: params.scratch || {}, // scratch objects
|
|
edges: [], // array of connected edges
|
|
children: [] // array of children
|
|
};
|
|
|
|
// renderedPosition overrides if specified
|
|
if( params.renderedPosition ){
|
|
var rpos = params.renderedPosition;
|
|
var pan = cy.pan();
|
|
var zoom = cy.zoom();
|
|
|
|
this._private.position = {
|
|
x: (rpos.x - pan.x)/zoom,
|
|
y: (rpos.y - pan.y)/zoom
|
|
};
|
|
}
|
|
|
|
if( $$.is.string(params.classes) ){
|
|
var classes = params.classes.split(/\s+/);
|
|
for( var i = 0, l = classes.length; i < l; i++ ){
|
|
var cls = classes[i];
|
|
if( !cls || cls === '' ){ continue; }
|
|
|
|
self._private.classes[cls] = true;
|
|
}
|
|
}
|
|
|
|
if( params.css ){
|
|
cy.style().applyBypass( this, params.css );
|
|
}
|
|
|
|
if( restore === undefined || restore ){
|
|
this.restore();
|
|
}
|
|
|
|
};
|
|
|
|
|
|
// Collection
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// represents a set of nodes, edges, or both together
|
|
$$.Collection = function(cy, elements, options){
|
|
if( !(this instanceof $$.Collection) ){
|
|
return new $$.Collection(cy, elements);
|
|
}
|
|
|
|
if( cy === undefined || !$$.is.core(cy) ){
|
|
$$.util.error('A collection must have a reference to the core');
|
|
return;
|
|
}
|
|
|
|
var ids = {};
|
|
var indexes = {};
|
|
var createdElements = false;
|
|
|
|
if( !elements ){
|
|
elements = [];
|
|
} else if( elements.length > 0 && $$.is.plainObject( elements[0] ) && !$$.is.element( elements[0] ) ){
|
|
createdElements = true;
|
|
|
|
// make elements from json and restore all at once later
|
|
var eles = [];
|
|
var elesIds = {};
|
|
|
|
for( var i = 0, l = elements.length; i < l; i++ ){
|
|
var json = elements[i];
|
|
|
|
if( json.data == null ){
|
|
json.data = {};
|
|
}
|
|
|
|
var data = json.data;
|
|
|
|
// make sure newly created elements have valid ids
|
|
if( data.id == null ){
|
|
data.id = idFactory.generate( cy, json );
|
|
} else if( cy.getElementById( data.id ).length !== 0 || elesIds[ data.id ] ){
|
|
continue; // can't create element if prior id already exists
|
|
}
|
|
|
|
var ele = new $$.Element( cy, json, false );
|
|
eles.push( ele );
|
|
elesIds[ data.id ] = true;
|
|
}
|
|
|
|
elements = eles;
|
|
}
|
|
|
|
this.length = 0;
|
|
|
|
for( var i = 0, l = elements.length; i < l; i++ ){
|
|
var element = elements[i];
|
|
if( !element ){ continue; }
|
|
|
|
var id = element._private.data.id;
|
|
|
|
if( !options || (options.unique && !ids[ id ] ) ){
|
|
ids[ id ] = element;
|
|
indexes[ id ] = this.length;
|
|
|
|
this[ this.length ] = element;
|
|
this.length++;
|
|
}
|
|
}
|
|
|
|
this._private = {
|
|
cy: cy,
|
|
ids: ids,
|
|
indexes: indexes
|
|
};
|
|
|
|
// restore the elements if we created them from json
|
|
if( createdElements ){
|
|
this.restore();
|
|
}
|
|
};
|
|
|
|
|
|
// Functions
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// keep the prototypes in sync (an element has the same functions as a collection)
|
|
// and use $$.elefn and $$.elesfn as shorthands to the prototypes
|
|
$$.elefn = $$.elesfn = $$.Element.prototype = $$.Collection.prototype;
|
|
|
|
$$.elesfn.cy = function(){
|
|
return this._private.cy;
|
|
};
|
|
|
|
$$.elesfn.element = function(){
|
|
return this[0];
|
|
};
|
|
|
|
$$.elesfn.collection = function(){
|
|
if( $$.is.collection(this) ){
|
|
return this;
|
|
} else { // an element
|
|
return new $$.Collection( this._private.cy, [this] );
|
|
}
|
|
};
|
|
|
|
$$.elesfn.unique = function(){
|
|
return new $$.Collection( this._private.cy, this, { unique: true } );
|
|
};
|
|
|
|
$$.elesfn.getElementById = function( id ){
|
|
var cy = this._private.cy;
|
|
var ele = this._private.ids[ id ];
|
|
|
|
return ele ? ele : $$.Collection(cy); // get ele or empty collection
|
|
};
|
|
|
|
$$.elesfn.json = function(){
|
|
var ele = this.element();
|
|
if( ele == null ){ return undefined; }
|
|
|
|
var p = ele._private;
|
|
|
|
var json = $$.util.copy({
|
|
data: p.data,
|
|
position: p.position,
|
|
group: p.group,
|
|
bypass: p.bypass,
|
|
removed: p.removed,
|
|
selected: p.selected,
|
|
selectable: p.selectable,
|
|
locked: p.locked,
|
|
grabbed: p.grabbed,
|
|
grabbable: p.grabbable,
|
|
classes: ''
|
|
});
|
|
|
|
var classes = [];
|
|
for( var cls in p.classes ){
|
|
if( p.classes[cls] ){
|
|
classes.push(cls);
|
|
}
|
|
}
|
|
|
|
for( var i = 0; i < classes.length; i++ ){
|
|
var cls = classes[i];
|
|
json.classes += cls + ( i < classes.length - 1 ? ' ' : '' );
|
|
}
|
|
|
|
return json;
|
|
};
|
|
|
|
$$.elesfn.jsons = function(){
|
|
var jsons = [];
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
var json = ele.json();
|
|
|
|
jsons.push( json );
|
|
}
|
|
|
|
return jsons;
|
|
};
|
|
|
|
$$.elesfn.clone = function(){
|
|
var cy = this.cy();
|
|
var elesArr = [];
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
var json = ele.json();
|
|
var clone = new $$.Element(cy, json, false); // NB no restore
|
|
|
|
elesArr.push( clone );
|
|
}
|
|
|
|
return new $$.Collection( cy, elesArr );
|
|
};
|
|
$$.elesfn.copy = $$.elesfn.clone;
|
|
|
|
$$.elesfn.restore = function( notifyRenderer ){
|
|
var self = this;
|
|
var restored = [];
|
|
var cy = self.cy();
|
|
|
|
if( notifyRenderer === undefined ){
|
|
notifyRenderer = true;
|
|
}
|
|
|
|
// create arrays of nodes and edges, since we need to
|
|
// restore the nodes first
|
|
var elements = [];
|
|
var nodes = [], edges = [];
|
|
var numNodes = 0;
|
|
var numEdges = 0;
|
|
for( var i = 0, l = self.length; i < l; i++ ){
|
|
var ele = self[i];
|
|
|
|
// keep nodes first in the array and edges after
|
|
if( ele.isNode() ){ // put to front of array if node
|
|
nodes.push( ele );
|
|
numNodes++;
|
|
} else { // put to end of array if edge
|
|
edges.push( ele );
|
|
numEdges++;
|
|
}
|
|
}
|
|
|
|
elements = nodes.concat( edges );
|
|
|
|
// now, restore each element
|
|
for( var i = 0, l = elements.length; i < l; i++ ){
|
|
var ele = elements[i];
|
|
|
|
if( !ele.removed() ){
|
|
// don't need to do anything
|
|
continue;
|
|
}
|
|
|
|
var _private = ele._private;
|
|
var data = _private.data;
|
|
|
|
// set id and validate
|
|
if( data.id === undefined ){
|
|
data.id = idFactory.generate( cy, ele );
|
|
} else if( $$.is.emptyString(data.id) || !$$.is.string(data.id) ){
|
|
$$.util.error('Can not create element with invalid string ID `' + data.id + '`');
|
|
|
|
// can't create element if it has empty string as id or non-string id
|
|
continue;
|
|
} else if( cy.getElementById( data.id ).length !== 0 ){
|
|
$$.util.error('Can not create second element with ID `' + data.id + '`');
|
|
|
|
// can't create element if one already has that id
|
|
continue;
|
|
}
|
|
|
|
var id = data.id; // id is finalised, now let's keep a ref
|
|
|
|
if( ele.isEdge() ){ // extra checks for edges
|
|
|
|
var edge = ele;
|
|
var fields = ['source', 'target'];
|
|
var fieldsLength = fields.length;
|
|
var badSourceOrTarget = false;
|
|
for(var j = 0; j < fieldsLength; j++){
|
|
|
|
var field = fields[j];
|
|
var val = data[field];
|
|
|
|
if( val == null || val === '' ){
|
|
// can't create if source or target is not defined properly
|
|
$$.util.error('Can not create edge `' + id + '` with unspecified ' + field);
|
|
badSourceOrTarget = true;
|
|
} else if( cy.getElementById(val).empty() ){
|
|
// can't create edge if one of its nodes doesn't exist
|
|
$$.util.error('Can not create edge `' + id + '` with nonexistant ' + field + ' `' + val + '`');
|
|
badSourceOrTarget = true;
|
|
}
|
|
}
|
|
|
|
if( badSourceOrTarget ){ continue; } // can't create this
|
|
|
|
var src = cy.getElementById( data.source );
|
|
var tgt = cy.getElementById( data.target );
|
|
|
|
src._private.edges.push( edge );
|
|
tgt._private.edges.push( edge );
|
|
|
|
edge._private.source = src;
|
|
edge._private.target = tgt;
|
|
|
|
} // if is edge
|
|
|
|
// create mock ids map for element so it can be used like collections
|
|
_private.ids = {};
|
|
_private.ids[ id ] = ele;
|
|
|
|
_private.removed = false;
|
|
cy.addToPool( ele );
|
|
|
|
restored.push( ele );
|
|
} // for each element
|
|
|
|
// do compound node sanity checks
|
|
for( var i = 0; i < numNodes; i++ ){ // each node
|
|
var node = elements[i];
|
|
var data = node._private.data;
|
|
|
|
var parentId = node._private.data.parent;
|
|
var specifiedParent = parentId != null;
|
|
|
|
if( specifiedParent ){
|
|
var parent = cy.getElementById( parentId );
|
|
|
|
if( parent.empty() ){
|
|
// non-existant parent; just remove it
|
|
data.parent = undefined;
|
|
} else {
|
|
var selfAsParent = false;
|
|
var ancestor = parent;
|
|
while( !ancestor.empty() ){
|
|
if( node.same(ancestor) ){
|
|
// mark self as parent and remove from data
|
|
selfAsParent = true;
|
|
data.parent = undefined; // remove parent reference
|
|
|
|
// exit or we loop forever
|
|
break;
|
|
}
|
|
|
|
ancestor = ancestor.parent();
|
|
}
|
|
|
|
if( !selfAsParent ){
|
|
// connect with children
|
|
parent[0]._private.children.push( node );
|
|
node._private.parent = parent[0];
|
|
|
|
// let the core know we have a compound graph
|
|
cy._private.hasCompoundNodes = true;
|
|
}
|
|
} // else
|
|
} // if specified parent
|
|
} // for each node
|
|
|
|
restored = new $$.Collection( cy, restored );
|
|
if( restored.length > 0 ){
|
|
|
|
var toUpdateStyle = restored.add( restored.connectedNodes() ).add( restored.parent() );
|
|
toUpdateStyle.updateStyle( notifyRenderer );
|
|
|
|
if( notifyRenderer ){
|
|
restored.rtrigger('add');
|
|
} else {
|
|
restored.trigger('add');
|
|
}
|
|
}
|
|
|
|
return self; // chainability
|
|
};
|
|
|
|
$$.elesfn.removed = function(){
|
|
var ele = this[0];
|
|
return ele && ele._private.removed;
|
|
};
|
|
|
|
$$.elesfn.inside = function(){
|
|
var ele = this[0];
|
|
return ele && !ele._private.removed;
|
|
};
|
|
|
|
$$.elesfn.remove = function( notifyRenderer ){
|
|
var self = this;
|
|
var removed = [];
|
|
var elesToRemove = [];
|
|
var elesToRemoveIds = {};
|
|
var cy = self._private.cy;
|
|
|
|
if( notifyRenderer === undefined ){
|
|
notifyRenderer = true;
|
|
}
|
|
|
|
// add connected edges
|
|
function addConnectedEdges(node){
|
|
var edges = node._private.edges;
|
|
for( var i = 0; i < edges.length; i++ ){
|
|
add( edges[i] );
|
|
}
|
|
}
|
|
|
|
|
|
// add descendant nodes
|
|
function addChildren(node){
|
|
var children = node._private.children;
|
|
|
|
for( var i = 0; i < children.length; i++ ){
|
|
add( children[i] );
|
|
}
|
|
}
|
|
|
|
function add( ele ){
|
|
var alreadyAdded = elesToRemoveIds[ ele.id() ];
|
|
if( alreadyAdded ){
|
|
return;
|
|
} else {
|
|
elesToRemoveIds[ ele.id() ] = true;
|
|
}
|
|
|
|
if( ele.isNode() ){
|
|
elesToRemove.push( ele ); // nodes are removed last
|
|
|
|
addConnectedEdges( ele );
|
|
addChildren( ele );
|
|
} else {
|
|
elesToRemove.unshift( ele ); // edges are removed first
|
|
}
|
|
}
|
|
|
|
// make the list of elements to remove
|
|
// (may be removing more than specified due to connected edges etc)
|
|
|
|
for( var i = 0, l = self.length; i < l; i++ ){
|
|
var ele = self[i];
|
|
|
|
add( ele );
|
|
}
|
|
|
|
function removeEdgeRef(node, edge){
|
|
var connectedEdges = node._private.edges;
|
|
for( var j = 0; j < connectedEdges.length; j++ ){
|
|
var connectedEdge = connectedEdges[j];
|
|
|
|
if( edge === connectedEdge ){
|
|
connectedEdges.splice( j, 1 );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeChildRef(parent, ele){
|
|
ele = ele[0];
|
|
parent = parent[0];
|
|
var children = parent._private.children;
|
|
|
|
for( var j = 0; j < children.length; j++ ){
|
|
if( children[j][0] === ele[0] ){
|
|
children.splice(j, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for( var i = 0; i < elesToRemove.length; i++ ){
|
|
var ele = elesToRemove[i];
|
|
|
|
// mark as removed
|
|
ele._private.removed = true;
|
|
|
|
// remove from core pool
|
|
cy.removeFromPool( ele );
|
|
|
|
// add to list of removed elements
|
|
removed.push( ele );
|
|
|
|
if( ele.isEdge() ){ // remove references to this edge in its connected nodes
|
|
var src = ele.source()[0];
|
|
var tgt = ele.target()[0];
|
|
|
|
removeEdgeRef( src, ele );
|
|
removeEdgeRef( tgt, ele );
|
|
|
|
} else { // remove reference to parent
|
|
var parent = ele.parent();
|
|
|
|
if( parent.length !== 0 ){
|
|
removeChildRef(parent, ele);
|
|
}
|
|
}
|
|
}
|
|
|
|
// check to see if we have a compound graph or not
|
|
var elesStillInside = cy._private.elements;
|
|
cy._private.hasCompoundNodes = false;
|
|
for( var i = 0; i < elesStillInside.length; i++ ){
|
|
var ele = elesStillInside[i];
|
|
|
|
if( ele.isParent() ){
|
|
cy._private.hasCompoundNodes = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var removedElements = new $$.Collection( this.cy(), removed );
|
|
if( removedElements.size() > 0 ){
|
|
// must manually notify since trigger won't do this automatically once removed
|
|
|
|
if( notifyRenderer ){
|
|
this.cy().notify({
|
|
type: 'remove',
|
|
collection: removedElements
|
|
});
|
|
}
|
|
|
|
removedElements.trigger('remove');
|
|
}
|
|
|
|
// check for empty remaining parent nodes
|
|
var checkedParentId = {};
|
|
for( var i = 0; i < elesToRemove.length; i++ ){
|
|
var ele = elesToRemove[i];
|
|
var isNode = ele._private.group === 'nodes';
|
|
var parentId = ele._private.data.parent;
|
|
|
|
if( isNode && parentId !== undefined && !checkedParentId[ parentId ] ){
|
|
checkedParentId[ parentId ] = true;
|
|
var parent = cy.getElementById( parentId );
|
|
|
|
if( parent && parent.length !== 0 && !parent._private.removed && parent.children().length === 0 ){
|
|
parent.updateStyle();
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
$$.elesfn.move = function( struct ){
|
|
var cy = this._private.cy;
|
|
|
|
if( struct.source !== undefined || struct.target !== undefined ){
|
|
var srcId = struct.source;
|
|
var tgtId = struct.target;
|
|
var srcExists = cy.getElementById( srcId ).length > 0;
|
|
var tgtExists = cy.getElementById( tgtId ).length > 0;
|
|
|
|
if( srcExists || tgtExists ){
|
|
var jsons = this.jsons();
|
|
|
|
this.remove();
|
|
|
|
for( var i = 0; i < jsons.length; i++ ){
|
|
var json = jsons[i];
|
|
|
|
if( json.group === 'edges' ){
|
|
if( srcExists ){ json.data.source = srcId; }
|
|
if( tgtExists ){ json.data.target = tgtId; }
|
|
}
|
|
}
|
|
|
|
return cy.add( jsons );
|
|
}
|
|
|
|
} else if( struct.parent !== undefined ){ // move node to new parent
|
|
var parentId = struct.parent;
|
|
var parentExists = parentId === null || cy.getElementById( parentId ).length > 0;
|
|
|
|
if( parentExists ){
|
|
var jsons = this.jsons();
|
|
var descs = this.descendants();
|
|
var descsEtc = descs.merge( descs.add(this).connectedEdges() );
|
|
|
|
this.remove(); // NB: also removes descendants and their connected edges
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var json = jsons[i];
|
|
|
|
if( json.group === 'nodes' ){
|
|
json.data.parent = parentId === null ? undefined : parentId;
|
|
}
|
|
}
|
|
}
|
|
|
|
return cy.add( jsons ).merge( descsEtc.restore() );
|
|
}
|
|
|
|
return this; // if nothing done
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// search, spanning trees, etc
|
|
$$.fn.eles({
|
|
|
|
// std functional ele first callback style
|
|
stdBreadthFirstSearch: function( options ){
|
|
options = $$.util.extend( {}, options, {
|
|
std: true
|
|
} );
|
|
|
|
return this.breadthFirstSearch( options );
|
|
},
|
|
|
|
// do a breadth first search from the nodes in the collection
|
|
// from pseudocode on wikipedia
|
|
breadthFirstSearch: function( roots, fn, directed ){
|
|
var options;
|
|
var std;
|
|
var thisArg;
|
|
if( $$.is.plainObject(roots) && !$$.is.elementOrCollection(roots) ){
|
|
options = roots;
|
|
roots = options.roots;
|
|
fn = options.visit;
|
|
directed = options.directed;
|
|
std = options.std;
|
|
thisArg = options.thisArg;
|
|
}
|
|
|
|
directed = arguments.length === 2 && !$$.is.fn(fn) ? fn : directed;
|
|
fn = $$.is.fn(fn) ? fn : function(){};
|
|
|
|
var cy = this._private.cy;
|
|
var v = $$.is.string(roots) ? this.filter(roots) : roots;
|
|
var Q = [];
|
|
var connectedNodes = [];
|
|
var connectedBy = {};
|
|
var id2depth = {};
|
|
var V = {};
|
|
var j = 0;
|
|
var found;
|
|
var nodes = this.nodes();
|
|
var edges = this.edges();
|
|
|
|
// enqueue v
|
|
for( var i = 0; i < v.length; i++ ){
|
|
if( v[i].isNode() ){
|
|
Q.unshift( v[i] );
|
|
V[ v[i].id() ] = true;
|
|
|
|
connectedNodes.push( v[i] );
|
|
id2depth[ v[i].id() ] = 0;
|
|
}
|
|
}
|
|
|
|
while( Q.length !== 0 ){
|
|
var v = Q.shift();
|
|
var depth = id2depth[ v.id() ];
|
|
var prevEdge = connectedBy[ v.id() ];
|
|
var prevNode = prevEdge == null ? undefined : prevEdge.connectedNodes().not( v )[0];
|
|
var ret;
|
|
|
|
if( std ){
|
|
ret = fn.call(thisArg, v, prevEdge, prevNode, j++, depth);
|
|
} else {
|
|
ret = fn.call(v, j++, depth, v, prevEdge, prevNode);
|
|
}
|
|
|
|
if( ret === true ){
|
|
found = v;
|
|
break;
|
|
}
|
|
|
|
if( ret === false ){
|
|
break;
|
|
}
|
|
|
|
var vwEdges = v.connectedEdges(directed ? function(){ return this.data('source') === v.id(); } : undefined).intersect( edges );
|
|
for( var i = 0; i < vwEdges.length; i++ ){
|
|
var e = vwEdges[i];
|
|
var w = e.connectedNodes(function(){ return this.id() !== v.id(); }).intersect( nodes );
|
|
|
|
if( w.length !== 0 && !V[ w.id() ] ){
|
|
w = w[0];
|
|
|
|
Q.push( w );
|
|
V[ w.id() ] = true;
|
|
|
|
id2depth[ w.id() ] = id2depth[ v.id() ] + 1;
|
|
|
|
connectedNodes.push( w );
|
|
connectedBy[ w.id() ] = e;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
var connectedEles = [];
|
|
|
|
for( var i = 0; i < connectedNodes.length; i++ ){
|
|
var node = connectedNodes[i];
|
|
var edge = connectedBy[ node.id() ];
|
|
|
|
if( edge ){
|
|
connectedEles.push( edge );
|
|
}
|
|
|
|
connectedEles.push( node );
|
|
}
|
|
|
|
return {
|
|
path: new $$.Collection( cy, connectedEles, { unique: true } ),
|
|
found: new $$.Collection( cy, found, { unique: true } )
|
|
};
|
|
},
|
|
|
|
// std functional ele first callback style
|
|
stdDepthFirstSearch: function( options ){
|
|
options = $$.util.extend( {}, options, {
|
|
std: true
|
|
} );
|
|
|
|
return this.depthFirstSearch( options );
|
|
},
|
|
|
|
// do a depth first search on the nodes in the collection
|
|
// from pseudocode on wikipedia (iterative impl)
|
|
depthFirstSearch: function( roots, fn, directed ){
|
|
var options;
|
|
var std;
|
|
var thisArg;
|
|
if( $$.is.plainObject(roots) && !$$.is.elementOrCollection(roots) ){
|
|
options = roots;
|
|
roots = options.roots;
|
|
fn = options.visit;
|
|
directed = options.directed;
|
|
std = options.std;
|
|
thisArg = options.thisArg;
|
|
}
|
|
|
|
directed = arguments.length === 2 && !$$.is.fn(fn) ? fn : directed;
|
|
fn = $$.is.fn(fn) ? fn : function(){};
|
|
var cy = this._private.cy;
|
|
var v = $$.is.string(roots) ? this.filter(roots) : roots;
|
|
var S = [];
|
|
var connectedNodes = [];
|
|
var connectedBy = {};
|
|
var id2depth = {};
|
|
var discovered = {};
|
|
var j = 0;
|
|
var found;
|
|
var edges = this.edges();
|
|
var nodes = this.nodes();
|
|
|
|
// push v
|
|
for( var i = 0; i < v.length; i++ ){
|
|
if( v[i].isNode() ){
|
|
S.push( v[i] );
|
|
|
|
connectedNodes.push( v[i] );
|
|
id2depth[ v[i].id() ] = 0;
|
|
}
|
|
}
|
|
|
|
while( S.length !== 0 ){
|
|
var v = S.pop();
|
|
|
|
if( !discovered[ v.id() ] ){
|
|
discovered[ v.id() ] = true;
|
|
|
|
var depth = id2depth[ v.id() ];
|
|
var prevEdge = connectedBy[ v.id() ];
|
|
var prevNode = prevEdge == null ? undefined : prevEdge.connectedNodes().not( v )[0];
|
|
var ret;
|
|
|
|
if( std ){
|
|
ret = fn.call(thisArg, v, prevEdge, prevNode, j++, depth);
|
|
} else {
|
|
ret = fn.call(v, j++, depth, v, prevEdge, prevNode);
|
|
}
|
|
|
|
if( ret === true ){
|
|
found = v;
|
|
break;
|
|
}
|
|
|
|
if( ret === false ){
|
|
break;
|
|
}
|
|
|
|
var vwEdges = v.connectedEdges(directed ? function(){ return this.data('source') === v.id(); } : undefined).intersect( edges );
|
|
|
|
for( var i = 0; i < vwEdges.length; i++ ){
|
|
var e = vwEdges[i];
|
|
var w = e.connectedNodes(function(){ return this.id() !== v.id(); }).intersect( nodes );
|
|
|
|
if( w.length !== 0 && !discovered[ w.id() ] ){
|
|
w = w[0];
|
|
|
|
S.push( w );
|
|
|
|
id2depth[ w.id() ] = id2depth[ v.id() ] + 1;
|
|
|
|
connectedNodes.push( w );
|
|
connectedBy[ w.id() ] = e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var connectedEles = [];
|
|
|
|
for( var i = 0; i < connectedNodes.length; i++ ){
|
|
var node = connectedNodes[i];
|
|
var edge = connectedBy[ node.id() ];
|
|
|
|
if( edge ){
|
|
connectedEles.push( edge );
|
|
}
|
|
|
|
connectedEles.push( node );
|
|
}
|
|
|
|
return {
|
|
path: new $$.Collection( cy, connectedEles, { unique: true } ),
|
|
found: new $$.Collection( cy, found, { unique: true } )
|
|
};
|
|
},
|
|
|
|
// kruskal's algorithm (finds min spanning tree, assuming undirected graph)
|
|
// implemented from pseudocode from wikipedia
|
|
kruskal: function( weightFn ){
|
|
weightFn = $$.is.fn(weightFn) ? weightFn : function(){ return 1; }; // if not specified, assume each edge has equal weight (1)
|
|
|
|
function findSet(ele){
|
|
for( var i = 0; i < forest.length; i++ ){
|
|
var eles = forest[i];
|
|
|
|
if( eles.anySame(ele) ){
|
|
return {
|
|
eles: eles,
|
|
index: i
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
var A = new $$.Collection(this._private.cy, []);
|
|
var forest = [];
|
|
var nodes = this.nodes();
|
|
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
forest.push( nodes[i].collection() );
|
|
}
|
|
|
|
var edges = this.edges();
|
|
var S = edges.toArray().sort(function(a, b){
|
|
var weightA = weightFn.call(a, a);
|
|
var weightB = weightFn.call(b, b);
|
|
|
|
return weightA - weightB;
|
|
});
|
|
|
|
for(var i = 0; i < S.length; i++){
|
|
var edge = S[i];
|
|
var u = edge.source()[0];
|
|
var v = edge.target()[0];
|
|
var setU = findSet(u);
|
|
var setV = findSet(v);
|
|
|
|
if( setU.index !== setV.index ){
|
|
A = A.add( edge );
|
|
|
|
// combine forests for u and v
|
|
forest[ setU.index ] = setU.eles.add( setV.eles );
|
|
forest.splice( setV.index, 1 );
|
|
}
|
|
}
|
|
|
|
return nodes.add( A );
|
|
|
|
},
|
|
|
|
dijkstra: function( root, weightFn, directed ){
|
|
var options;
|
|
if( $$.is.plainObject(root) && !$$.is.elementOrCollection(root) ){
|
|
options = root;
|
|
root = options.root;
|
|
weightFn = options.weight;
|
|
directed = options.directed;
|
|
}
|
|
|
|
var cy = this._private.cy;
|
|
weightFn = $$.is.fn(weightFn) ? weightFn : function(){ return 1; }; // if not specified, assume each edge has equal weight (1)
|
|
|
|
var source = $$.is.string(root) ? this.filter(root)[0] : root[0];
|
|
var dist = {};
|
|
var prev = {};
|
|
var knownDist = {};
|
|
|
|
var edges = this.edges().filter(function(){ return !this.isLoop(); });
|
|
var nodes = this.nodes();
|
|
var Q = [];
|
|
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
dist[ nodes[i].id() ] = nodes[i].same( source ) ? 0 : Infinity;
|
|
Q.push( nodes[i] );
|
|
}
|
|
|
|
var valueFn = function(node) {
|
|
return dist[ node.id() ];
|
|
};
|
|
|
|
Q = new $$.Collection(cy, Q);
|
|
|
|
var heap = $$.Minheap(cy, Q, valueFn);
|
|
|
|
var distBetween = function(u, v){
|
|
var uvs = ( directed ? u.edgesTo(v) : u.edgesWith(v) ).intersect(edges);
|
|
var smallestDistance = Infinity;
|
|
var smallestEdge;
|
|
|
|
for( var i = 0; i < uvs.length; i++ ){
|
|
var edge = uvs[i];
|
|
var weight = weightFn.apply( edge, [edge] );
|
|
|
|
if( weight < smallestDistance || !smallestEdge ){
|
|
smallestDistance = weight;
|
|
smallestEdge = edge;
|
|
}
|
|
}
|
|
|
|
return {
|
|
edge: smallestEdge,
|
|
dist: smallestDistance
|
|
};
|
|
};
|
|
|
|
while(heap.size() > 0){
|
|
var smallestEl = heap.pop(),
|
|
smalletsDist = smallestEl.value,
|
|
uid = smallestEl.id,
|
|
u = cy.getElementById(uid);
|
|
|
|
knownDist[uid] = smalletsDist;
|
|
|
|
if( smalletsDist === Math.Infinite ){
|
|
break;
|
|
}
|
|
|
|
var neighbors = u.neighborhood().intersect(nodes);
|
|
for( var i = 0; i < neighbors.length; i++ ){
|
|
var v = neighbors[i];
|
|
var vid = v.id();
|
|
var vDist = distBetween(u, v);
|
|
|
|
var alt = smalletsDist + vDist.dist;
|
|
|
|
if( alt < heap.getValueById(vid) ){
|
|
heap.edit(vid, alt);
|
|
prev[ vid ] = {
|
|
node: u,
|
|
edge: vDist.edge
|
|
};
|
|
}
|
|
} // for
|
|
} // while
|
|
|
|
return {
|
|
distanceTo: function(node){
|
|
var target = $$.is.string(node) ? nodes.filter(node)[0] : node[0];
|
|
|
|
return knownDist[ target.id() ];
|
|
},
|
|
|
|
pathTo: function(node){
|
|
var target = $$.is.string(node) ? nodes.filter(node)[0] : node[0];
|
|
var S = [];
|
|
var u = target;
|
|
|
|
if( target.length > 0 ){
|
|
S.unshift( target );
|
|
|
|
while( prev[ u.id() ] ){
|
|
var p = prev[ u.id() ];
|
|
|
|
S.unshift( p.edge );
|
|
S.unshift( p.node );
|
|
|
|
u = p.node;
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( cy, S );
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
// nice, short mathemathical alias
|
|
$$.elesfn.bfs = $$.elesfn.breadthFirstSearch;
|
|
$$.elesfn.dfs = $$.elesfn.depthFirstSearch;
|
|
$$.elesfn.stdBfs = $$.elesfn.stdBreadthFirstSearch;
|
|
$$.elesfn.stdDfs = $$.elesfn.stdDepthFirstSearch;
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$) {
|
|
'use strict';
|
|
|
|
// Additional graph analysis algorithms
|
|
$$.fn.eles({
|
|
|
|
// Implemented from pseudocode from wikipedia
|
|
|
|
// options => options object
|
|
// root // starting node (either element or selector string)
|
|
// weight: function( edge ){} // specifies weight to use for `edge`/`this`. If not present, it will be asumed a weight of 1 for all edges
|
|
// heuristic: function( node ){} // specifies heuristic value for `node`/`this`
|
|
// directed // default false
|
|
// goal // target node (either element or selector string). Mandatory.
|
|
|
|
// retObj => returned object by function
|
|
// found : true/false // whether a path from root to goal has been found
|
|
// distance // Distance for the shortest path from root to goal
|
|
// path // Array of ids of nodes in shortest path
|
|
aStar: function(options) {
|
|
options = options || {};
|
|
|
|
// var logDebug = function() {
|
|
// if (debug) {
|
|
// console.log.apply(console, arguments);
|
|
// }
|
|
// };
|
|
|
|
// Reconstructs the path from Start to End, acumulating the result in pathAcum
|
|
var reconstructPath = function(start, end, cameFromMap, pathAcum) {
|
|
// Base case
|
|
if (start == end) {
|
|
pathAcum.push( cy.getElementById(end) );
|
|
return pathAcum;
|
|
}
|
|
|
|
if (end in cameFromMap) {
|
|
// We know which node is before the last one
|
|
var previous = cameFromMap[end];
|
|
var previousEdge = cameFromEdge[end];
|
|
|
|
pathAcum.push( cy.getElementById(end) );
|
|
pathAcum.push( cy.getElementById(previousEdge) );
|
|
|
|
|
|
return reconstructPath(start,
|
|
previous,
|
|
cameFromMap,
|
|
pathAcum);
|
|
}
|
|
|
|
// We should not reach here!
|
|
return undefined;
|
|
};
|
|
|
|
// Returns the index of the element in openSet which has minimum fScore
|
|
var findMin = function(openSet, fScore) {
|
|
if (openSet.length === 0) {
|
|
// Should never be the case
|
|
return undefined;
|
|
}
|
|
var minPos = 0;
|
|
var tempScore = fScore[openSet[0]];
|
|
for (var i = 1; i < openSet.length; i++) {
|
|
var s = fScore[openSet[i]];
|
|
if (s < tempScore) {
|
|
tempScore = s;
|
|
minPos = i;
|
|
}
|
|
}
|
|
return minPos;
|
|
};
|
|
|
|
// Parse options
|
|
// debug - optional
|
|
// if (options.debug != null) {
|
|
// var debug = options.debug;
|
|
// } else {
|
|
// var debug = false;
|
|
// }
|
|
|
|
// logDebug("Starting aStar...");
|
|
var cy = this._private.cy;
|
|
|
|
// root - mandatory!
|
|
if (options != null && options.root != null) {
|
|
var source = $$.is.string(options.root) ?
|
|
// use it as a selector, e.g. "#rootID
|
|
this.filter(options.root)[0] :
|
|
options.root[0];
|
|
// logDebug("Source node: %s", source.id());
|
|
} else {
|
|
return undefined;
|
|
}
|
|
|
|
// goal - mandatory!
|
|
if (options.goal != null) {
|
|
var target = $$.is.string(options.goal) ?
|
|
// use it as a selector, e.g. "#goalID
|
|
this.filter(options.goal)[0] :
|
|
options.goal[0];
|
|
// logDebug("Target node: %s", target.id());
|
|
} else {
|
|
return undefined;
|
|
}
|
|
|
|
// Heuristic function - optional
|
|
if (options.heuristic != null && $$.is.fn(options.heuristic)) {
|
|
var heuristic = options.heuristic;
|
|
} else {
|
|
var heuristic = function(){ return 0; }; // use constant if unspecified
|
|
// $$.util.error("Missing required parameter (heuristic)! Aborting.");
|
|
// return;
|
|
}
|
|
|
|
// Weight function - optional
|
|
if (options.weight != null && $$.is.fn(options.weight)) {
|
|
var weightFn = options.weight;
|
|
} else {
|
|
// If not specified, assume each edge has equal weight (1)
|
|
var weightFn = function(e) {return 1;};
|
|
}
|
|
|
|
// directed - optional
|
|
if (options.directed != null) {
|
|
var directed = options.directed;
|
|
} else {
|
|
var directed = false;
|
|
}
|
|
|
|
var closedSet = [];
|
|
var openSet = [source.id()];
|
|
var cameFrom = {};
|
|
var cameFromEdge = {};
|
|
var gScore = {};
|
|
var fScore = {};
|
|
|
|
gScore[source.id()] = 0;
|
|
fScore[source.id()] = heuristic(source);
|
|
|
|
var edges = this.edges().stdFilter(function(e){ return !e.isLoop(); });
|
|
var nodes = this.nodes();
|
|
|
|
// Counter
|
|
var steps = 0;
|
|
|
|
// Main loop
|
|
while (openSet.length > 0) {
|
|
var minPos = findMin(openSet, fScore);
|
|
var cMin = cy.getElementById( openSet[minPos] );
|
|
steps++;
|
|
|
|
// logDebug("\nStep: %s", steps);
|
|
// logDebug("Processing node: %s, fScore = %s", cMin.id(), fScore[cMin.id()]);
|
|
|
|
// If we've found our goal, then we are done
|
|
if (cMin.id() == target.id()) {
|
|
// logDebug("Found goal node!");
|
|
var rPath = reconstructPath(source.id(), target.id(), cameFrom, []);
|
|
rPath.reverse();
|
|
// logDebug("Path: %s", rPath);
|
|
return {
|
|
found : true,
|
|
distance : gScore[cMin.id()],
|
|
path : new $$.Collection(cy, rPath),
|
|
steps : steps
|
|
};
|
|
}
|
|
|
|
// Add cMin to processed nodes
|
|
closedSet.push(cMin.id());
|
|
// Remove cMin from boundary nodes
|
|
openSet.splice(minPos, 1);
|
|
// logDebug("Added node to closedSet, removed from openSet.");
|
|
// logDebug("Processing neighbors...");
|
|
|
|
// Update scores for neighbors of cMin
|
|
// Take into account if graph is directed or not
|
|
var vwEdges = cMin.connectedEdges();
|
|
if( directed ){ vwEdges = vwEdges.stdFilter(function(ele){ return ele.data('source') === cMin.id(); }); }
|
|
vwEdges = vwEdges.intersect(edges);
|
|
|
|
for (var i = 0; i < vwEdges.length; i++) {
|
|
var e = vwEdges[i];
|
|
var w = e.connectedNodes().stdFilter(function(n){ return n.id() !== cMin.id(); }).intersect(nodes);
|
|
|
|
// logDebug(" processing neighbor: %s", w.id());
|
|
// if node is in closedSet, ignore it
|
|
if (closedSet.indexOf(w.id()) != -1) {
|
|
// logDebug(" already in closedSet, ignoring it.");
|
|
continue;
|
|
}
|
|
|
|
// New tentative score for node w
|
|
var tempScore = gScore[cMin.id()] + weightFn.apply(e, [e]);
|
|
// logDebug(" tentative gScore: %d", tempScore);
|
|
|
|
// Update gScore for node w if:
|
|
// w not present in openSet
|
|
// OR
|
|
// tentative gScore is less than previous value
|
|
|
|
// w not in openSet
|
|
if (openSet.indexOf(w.id()) == -1) {
|
|
gScore[w.id()] = tempScore;
|
|
fScore[w.id()] = tempScore + heuristic(w);
|
|
openSet.push(w.id()); // Add node to openSet
|
|
cameFrom[w.id()] = cMin.id();
|
|
cameFromEdge[w.id()] = e.id();
|
|
// logDebug(" not in openSet, adding it. ");
|
|
// logDebug(" fScore(%s) = %s", w.id(), tempScore);
|
|
continue;
|
|
}
|
|
// w already in openSet, but with greater gScore
|
|
if (tempScore < gScore[w.id()]) {
|
|
gScore[w.id()] = tempScore;
|
|
fScore[w.id()] = tempScore + heuristic(w);
|
|
cameFrom[w.id()] = cMin.id();
|
|
// logDebug(" better score, replacing gScore. ");
|
|
// logDebug(" fScore(%s) = %s", w.id(), tempScore);
|
|
}
|
|
|
|
} // End of neighbors update
|
|
|
|
} // End of main loop
|
|
|
|
// If we've reached here, then we've not reached our goal
|
|
// logDebug("Reached end of computation without finding our goal");
|
|
return {
|
|
found : false,
|
|
distance : undefined,
|
|
path : undefined,
|
|
steps : steps
|
|
};
|
|
}, // aStar()
|
|
|
|
|
|
// Implemented from pseudocode from wikipedia
|
|
// options => options object
|
|
// weight: function( edge ){} // specifies weight to use for `edge`/`this`. If not present, it will be asumed a weight of 1 for all edges
|
|
// directed // default false
|
|
// retObj => returned object by function
|
|
// pathTo : function(fromId, toId) // Returns the shortest path from node with ID "fromID" to node with ID "toId", as an array of node IDs
|
|
// distanceTo: function(fromId, toId) // Returns the distance of the shortest path from node with ID "fromID" to node with ID "toId"
|
|
floydWarshall: function(options) {
|
|
options = options || {};
|
|
|
|
// var logDebug = function() {
|
|
// if (debug) {
|
|
// console.log.apply(console, arguments);
|
|
// }
|
|
// };
|
|
|
|
// Parse options
|
|
// debug - optional
|
|
// if (options.debug != null) {
|
|
// var debug = options.debug;
|
|
// } else {
|
|
// var debug = false;
|
|
// }
|
|
// logDebug("Starting floydWarshall...");
|
|
|
|
var cy = this._private.cy;
|
|
|
|
// Weight function - optional
|
|
if (options.weight != null && $$.is.fn(options.weight)) {
|
|
var weightFn = options.weight;
|
|
} else {
|
|
// If not specified, assume each edge has equal weight (1)
|
|
var weightFn = function(e) {return 1;};
|
|
}
|
|
|
|
// directed - optional
|
|
if (options.directed != null) {
|
|
var directed = options.directed;
|
|
} else {
|
|
var directed = false;
|
|
}
|
|
|
|
var edges = this.edges().stdFilter(function(e){ return !e.isLoop(); });
|
|
var nodes = this.nodes();
|
|
var numNodes = nodes.length;
|
|
|
|
// mapping: node id -> position in nodes array
|
|
var id2position = {};
|
|
for (var i = 0; i < numNodes; i++) {
|
|
id2position[nodes[i].id()] = i;
|
|
}
|
|
|
|
// Initialize distance matrix
|
|
var dist = [];
|
|
for (var i = 0; i < numNodes; i++) {
|
|
var newRow = new Array(numNodes);
|
|
for (var j = 0; j < numNodes; j++) {
|
|
if (i == j) {
|
|
newRow[j] = 0;
|
|
} else {
|
|
newRow[j] = Infinity;
|
|
}
|
|
}
|
|
dist.push(newRow);
|
|
}
|
|
|
|
// Initialize matrix used for path reconstruction
|
|
// Initialize distance matrix
|
|
var next = [];
|
|
var edgeNext = [];
|
|
|
|
var initMatrix = function(next){
|
|
for (var i = 0; i < numNodes; i++) {
|
|
var newRow = new Array(numNodes);
|
|
for (var j = 0; j < numNodes; j++) {
|
|
newRow[j] = undefined;
|
|
}
|
|
next.push(newRow);
|
|
}
|
|
};
|
|
|
|
initMatrix(next);
|
|
initMatrix(edgeNext);
|
|
|
|
// Process edges
|
|
for (var i = 0; i < edges.length ; i++) {
|
|
var sourceIndex = id2position[edges[i].source().id()];
|
|
var targetIndex = id2position[edges[i].target().id()];
|
|
var weight = weightFn.apply(edges[i], [edges[i]]);
|
|
|
|
// Check if already process another edge between same 2 nodes
|
|
if (dist[sourceIndex][targetIndex] > weight) {
|
|
dist[sourceIndex][targetIndex] = weight;
|
|
next[sourceIndex][targetIndex] = targetIndex;
|
|
edgeNext[sourceIndex][targetIndex] = edges[i];
|
|
}
|
|
}
|
|
|
|
// If undirected graph, process 'reversed' edges
|
|
if (!directed) {
|
|
for (var i = 0; i < edges.length ; i++) {
|
|
var sourceIndex = id2position[edges[i].target().id()];
|
|
var targetIndex = id2position[edges[i].source().id()];
|
|
var weight = weightFn.apply(edges[i], [edges[i]]);
|
|
|
|
// Check if already process another edge between same 2 nodes
|
|
if (dist[sourceIndex][targetIndex] > weight) {
|
|
dist[sourceIndex][targetIndex] = weight;
|
|
next[sourceIndex][targetIndex] = targetIndex;
|
|
edgeNext[sourceIndex][targetIndex] = edges[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Main loop
|
|
for (var k = 0; k < numNodes; k++) {
|
|
for (var i = 0; i < numNodes; i++) {
|
|
for (var j = 0; j < numNodes; j++) {
|
|
if (dist[i][k] + dist[k][j] < dist[i][j]) {
|
|
dist[i][j] = dist[i][k] + dist[k][j];
|
|
next[i][j] = next[i][k];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build result object
|
|
var position2id = [];
|
|
for (var i = 0; i < numNodes; i++) {
|
|
position2id.push(nodes[i].id());
|
|
}
|
|
|
|
var res = {
|
|
distance: function(from, to) {
|
|
if ($$.is.string(from)) {
|
|
// from is a selector string
|
|
var fromId = (cy.filter(from)[0]).id();
|
|
} else {
|
|
// from is a node
|
|
var fromId = from.id();
|
|
}
|
|
|
|
if ($$.is.string(to)) {
|
|
// to is a selector string
|
|
var toId = (cy.filter(to)[0]).id();
|
|
} else {
|
|
// to is a node
|
|
var toId = to.id();
|
|
}
|
|
|
|
return dist[id2position[fromId]][id2position[toId]];
|
|
},
|
|
|
|
path: function(from, to) {
|
|
var reconstructPathAux = function(from, to, next, position2id, edgeNext) {
|
|
if (from === to) {
|
|
return cy.getElementById( position2id[from] );
|
|
}
|
|
if (next[from][to] === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
var path = [ cy.getElementById(position2id[from]) ];
|
|
var prev = from;
|
|
while (from !== to) {
|
|
prev = from;
|
|
from = next[from][to];
|
|
|
|
var edge = edgeNext[prev][from];
|
|
path.push( edge );
|
|
|
|
path.push( cy.getElementById(position2id[from]) );
|
|
}
|
|
return path;
|
|
};
|
|
|
|
if ($$.is.string(from)) {
|
|
// from is a selector string
|
|
var fromId = (cy.filter(from)[0]).id();
|
|
} else {
|
|
// from is a node
|
|
var fromId = from.id();
|
|
}
|
|
|
|
if ($$.is.string(to)) {
|
|
// to is a selector string
|
|
var toId = (cy.filter(to)[0]).id();
|
|
} else {
|
|
// to is a node
|
|
var toId = to.id();
|
|
}
|
|
|
|
var pathArr = reconstructPathAux(id2position[fromId],
|
|
id2position[toId],
|
|
next,
|
|
position2id,
|
|
edgeNext);
|
|
|
|
return new $$.Collection( cy, pathArr );
|
|
},
|
|
};
|
|
|
|
return res;
|
|
|
|
}, // floydWarshall
|
|
|
|
|
|
// Implemented from pseudocode from wikipedia
|
|
// options => options object
|
|
// root: starting node (either element or selector string)
|
|
// weight: function( edge ){} // specifies weight to use for `edge`/`this`. If not present, it will be asumed a weight of 1 for all edges
|
|
// directed // default false
|
|
// retObj => returned object by function
|
|
// pathTo : function(toId) // Returns the shortest path from root node to node with ID "toId", as an array of node IDs
|
|
// distanceTo: function(toId) // Returns the distance of the shortest path from root node to node with ID "toId"
|
|
// hasNegativeWeightCycle: true/false (if true, pathTo and distanceTo will be undefined)
|
|
bellmanFord: function(options) {
|
|
options = options || {};
|
|
|
|
// var logDebug = function() {
|
|
// if (debug) {
|
|
// console.log.apply(console, arguments);
|
|
// }
|
|
// };
|
|
|
|
// Parse options
|
|
// debug - optional
|
|
// if (options.debug != null) {
|
|
// var debug = options.debug;
|
|
// } else {
|
|
// var debug = false;
|
|
// }
|
|
// logDebug("Starting bellmanFord...");
|
|
|
|
// Weight function - optional
|
|
if (options.weight != null && $$.is.fn(options.weight)) {
|
|
var weightFn = options.weight;
|
|
} else {
|
|
// If not specified, assume each edge has equal weight (1)
|
|
var weightFn = function(e) {return 1;};
|
|
}
|
|
|
|
// directed - optional
|
|
if (options.directed != null) {
|
|
var directed = options.directed;
|
|
} else {
|
|
var directed = false;
|
|
}
|
|
|
|
// root - mandatory!
|
|
if (options.root != null) {
|
|
if ($$.is.string(options.root)) {
|
|
// use it as a selector, e.g. "#rootID
|
|
var source = this.filter(options.root)[0];
|
|
} else {
|
|
var source = options.root[0];
|
|
}
|
|
// logDebug("Source node: %s", source.id());
|
|
} else {
|
|
$$.util.error("options.root required");
|
|
return undefined;
|
|
}
|
|
|
|
var cy = this._private.cy;
|
|
var edges = this.edges().stdFilter(function(e){ return !e.isLoop(); });
|
|
var nodes = this.nodes();
|
|
var numNodes = nodes.length;
|
|
|
|
// mapping: node id -> position in nodes array
|
|
var id2position = {};
|
|
for (var i = 0; i < numNodes; i++) {
|
|
id2position[nodes[i].id()] = i;
|
|
}
|
|
|
|
// Initializations
|
|
var cost = [];
|
|
var predecessor = [];
|
|
var predEdge = [];
|
|
|
|
for (var i = 0; i < numNodes; i++) {
|
|
if (nodes[i].id() === source.id()) {
|
|
cost[i] = 0;
|
|
} else {
|
|
cost[i] = Infinity;
|
|
}
|
|
predecessor[i] = undefined;
|
|
}
|
|
|
|
// Edges relaxation
|
|
var flag = false;
|
|
for (var i = 1; i < numNodes; i++) {
|
|
flag = false;
|
|
for (var e = 0; e < edges.length; e++) {
|
|
var sourceIndex = id2position[edges[e].source().id()];
|
|
var targetIndex = id2position[edges[e].target().id()];
|
|
var weight = weightFn.apply(edges[e], [edges[e]]);
|
|
|
|
var temp = cost[sourceIndex] + weight;
|
|
if (temp < cost[targetIndex]) {
|
|
cost[targetIndex] = temp;
|
|
predecessor[targetIndex] = sourceIndex;
|
|
predEdge[targetIndex] = edges[e];
|
|
flag = true;
|
|
}
|
|
|
|
// If undirected graph, we need to take into account the 'reverse' edge
|
|
if (!directed) {
|
|
var temp = cost[targetIndex] + weight;
|
|
if (temp < cost[sourceIndex]) {
|
|
cost[sourceIndex] = temp;
|
|
predecessor[sourceIndex] = targetIndex;
|
|
predEdge[sourceIndex] = edges[e];
|
|
flag = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!flag) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (flag) {
|
|
// Check for negative weight cycles
|
|
for (var e = 0; e < edges.length; e++) {
|
|
var sourceIndex = id2position[edges[e].source().id()];
|
|
var targetIndex = id2position[edges[e].target().id()];
|
|
var weight = weightFn.apply(edges[e], [edges[e]]);
|
|
|
|
if (cost[sourceIndex] + weight < cost[targetIndex]) {
|
|
$$.util.error("Error: graph contains a negative weigth cycle!");
|
|
return { pathTo: undefined,
|
|
distanceTo: undefined,
|
|
hasNegativeWeightCycle: true};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build result object
|
|
var position2id = [];
|
|
for (var i = 0; i < numNodes; i++) {
|
|
position2id.push(nodes[i].id());
|
|
}
|
|
|
|
|
|
var res = {
|
|
distanceTo : function(to) {
|
|
if ($$.is.string(to)) {
|
|
// to is a selector string
|
|
var toId = (cy.filter(to)[0]).id();
|
|
} else {
|
|
// to is a node
|
|
var toId = to.id();
|
|
}
|
|
|
|
return cost[id2position[toId]];
|
|
},
|
|
|
|
pathTo : function(to) {
|
|
|
|
var reconstructPathAux = function(predecessor, fromPos, toPos, position2id, acumPath, predEdge) {
|
|
for(;;){
|
|
// Add toId to path
|
|
acumPath.push( cy.getElementById(position2id[toPos]) );
|
|
acumPath.push( predEdge[toPos] );
|
|
|
|
if (fromPos === toPos) {
|
|
// reached starting node
|
|
return acumPath;
|
|
}
|
|
|
|
// If no path exists, discart acumulated path and return undefined
|
|
var predPos = predecessor[toPos];
|
|
if (typeof predPos === "undefined") {
|
|
return undefined;
|
|
}
|
|
|
|
toPos = predPos;
|
|
}
|
|
|
|
};
|
|
|
|
if ($$.is.string(to)) {
|
|
// to is a selector string
|
|
var toId = (cy.filter(to)[0]).id();
|
|
} else {
|
|
// to is a node
|
|
var toId = to.id();
|
|
}
|
|
var path = [];
|
|
|
|
// This returns a reversed path
|
|
var res = reconstructPathAux(predecessor,
|
|
id2position[source.id()],
|
|
id2position[toId],
|
|
position2id,
|
|
path,
|
|
predEdge);
|
|
|
|
// Get it in the correct order and return it
|
|
if (res != null) {
|
|
res.reverse();
|
|
}
|
|
|
|
return new $$.Collection(cy, res);
|
|
},
|
|
|
|
hasNegativeWeightCycle: false
|
|
};
|
|
|
|
return res;
|
|
|
|
}, // bellmanFord
|
|
|
|
|
|
// Computes the minimum cut of an undirected graph
|
|
// Returns the correct answer with high probability
|
|
// options => options object
|
|
//
|
|
// retObj => returned object by function
|
|
// cut : list of IDs of edges in the cut,
|
|
// partition1: list of IDs of nodes in one partition
|
|
// partition2: list of IDs of nodes in the other partition
|
|
kargerStein: function(options) {
|
|
options = options || {};
|
|
|
|
// var logDebug = function() {
|
|
// if (debug) {
|
|
// console.log.apply(console, arguments);
|
|
// }
|
|
// };
|
|
|
|
// Function which colapses 2 (meta) nodes into one
|
|
// Updates the remaining edge lists
|
|
// Receives as a paramater the edge which causes the collapse
|
|
var colapse = function(edgeIndex, nodeMap, remainingEdges) {
|
|
var edgeInfo = remainingEdges[edgeIndex];
|
|
var sourceIn = edgeInfo[1];
|
|
var targetIn = edgeInfo[2];
|
|
var partition1 = nodeMap[sourceIn];
|
|
var partition2 = nodeMap[targetIn];
|
|
|
|
// Delete all edges between partition1 and partition2
|
|
var newEdges = remainingEdges.filter(function(edge) {
|
|
if (nodeMap[edge[1]] === partition1 && nodeMap[edge[2]] === partition2) {
|
|
return false;
|
|
}
|
|
if (nodeMap[edge[1]] === partition2 && nodeMap[edge[2]] === partition1) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// All edges pointing to partition2 should now point to partition1
|
|
for (var i = 0; i < newEdges.length; i++) {
|
|
var edge = newEdges[i];
|
|
if (edge[1] === partition2) { // Check source
|
|
newEdges[i] = edge.slice(0);
|
|
newEdges[i][1] = partition1;
|
|
} else if (edge[2] === partition2) { // Check target
|
|
newEdges[i] = edge.slice(0);
|
|
newEdges[i][2] = partition1;
|
|
}
|
|
}
|
|
|
|
// Move all nodes from partition2 to partition1
|
|
for (var i = 0; i < nodeMap.length; i++) {
|
|
if (nodeMap[i] === partition2) {
|
|
nodeMap[i] = partition1;
|
|
}
|
|
}
|
|
|
|
return newEdges;
|
|
};
|
|
|
|
|
|
// Contracts a graph until we reach a certain number of meta nodes
|
|
var contractUntil = function(metaNodeMap,
|
|
remainingEdges,
|
|
size,
|
|
sizeLimit) {
|
|
// Stop condition
|
|
if (size <= sizeLimit) {
|
|
return remainingEdges;
|
|
}
|
|
|
|
// Choose an edge randomly
|
|
var edgeIndex = Math.floor((Math.random() * remainingEdges.length));
|
|
|
|
// Colapse graph based on edge
|
|
var newEdges = colapse(edgeIndex, metaNodeMap, remainingEdges);
|
|
|
|
return contractUntil(metaNodeMap,
|
|
newEdges,
|
|
size - 1,
|
|
sizeLimit);
|
|
};
|
|
|
|
|
|
// Parse options
|
|
// debug - optional
|
|
// if (options != null && options.debug != null) {
|
|
// var debug = options.debug;
|
|
// } else {
|
|
// var debug = false;
|
|
// }
|
|
// logDebug("Starting kargerStein...");
|
|
|
|
var cy = this._private.cy;
|
|
var edges = this.edges().stdFilter(function(e){ return !e.isLoop(); });
|
|
var nodes = this.nodes();
|
|
var numNodes = nodes.length;
|
|
var numEdges = edges.length;
|
|
var numIter = Math.ceil(Math.pow(Math.log(numNodes) / Math.LN2, 2));
|
|
var stopSize = Math.floor(numNodes / Math.sqrt(2));
|
|
|
|
if (numNodes < 2) {
|
|
$$.util.error("At least 2 nodes are required for KargerSteing algorithm!");
|
|
return undefined;
|
|
}
|
|
|
|
// Create numerical identifiers for each node
|
|
// mapping: node id -> position in nodes array
|
|
// for reverse mapping, simply use nodes array
|
|
var id2position = {};
|
|
for (var i = 0; i < numNodes; i++) {
|
|
id2position[nodes[i].id()] = i;
|
|
}
|
|
|
|
// Now store edge destination as indexes
|
|
// Format for each edge (edge index, source node index, target node index)
|
|
var edgeIndexes = [];
|
|
for (var i = 0; i < numEdges; i++) {
|
|
var e = edges[i];
|
|
edgeIndexes.push([i, id2position[e.source().id()], id2position[e.target().id()]]);
|
|
}
|
|
|
|
// We will store the best cut found here
|
|
var minCutSize = Infinity;
|
|
var minCut;
|
|
|
|
// Initial meta node partition
|
|
var originalMetaNode = [];
|
|
for (var i = 0; i < numNodes; i++) {
|
|
originalMetaNode.push(i);
|
|
}
|
|
|
|
// Main loop
|
|
for (var iter = 0; iter <= numIter; iter++) {
|
|
// Create new meta node partition
|
|
var metaNodeMap = originalMetaNode.slice(0);
|
|
|
|
// Contract until stop point (stopSize nodes)
|
|
var edgesState = contractUntil(metaNodeMap, edgeIndexes, numNodes, stopSize);
|
|
|
|
// Create a copy of the colapsed nodes state
|
|
var metaNodeMap2 = metaNodeMap.slice(0);
|
|
|
|
// Run 2 iterations starting in the stop state
|
|
var res1 = contractUntil(metaNodeMap, edgesState, stopSize, 2);
|
|
var res2 = contractUntil(metaNodeMap2, edgesState, stopSize, 2);
|
|
|
|
// Is any of the 2 results the best cut so far?
|
|
if (res1.length <= res2.length && res1.length < minCutSize) {
|
|
minCutSize = res1.length;
|
|
minCut = [res1, metaNodeMap];
|
|
} else if (res2.length <= res1.length && res2.length < minCutSize) {
|
|
minCutSize = res2.length;
|
|
minCut = [res2, metaNodeMap2];
|
|
}
|
|
} // end of main loop
|
|
|
|
|
|
// Construct result
|
|
var resEdges = (minCut[0]).map(function(e){ return edges[e[0]]; });
|
|
var partition1 = [];
|
|
var partition2 = [];
|
|
|
|
// traverse metaNodeMap for best cut
|
|
var witnessNodePartition = minCut[1][0];
|
|
for (var i = 0; i < minCut[1].length; i++) {
|
|
var partitionId = minCut[1][i];
|
|
if (partitionId === witnessNodePartition) {
|
|
partition1.push(nodes[i]);
|
|
} else {
|
|
partition2.push(nodes[i]);
|
|
}
|
|
}
|
|
|
|
var ret = {
|
|
cut: new $$.Collection(cy, resEdges),
|
|
partition1: new $$.Collection(cy, partition1),
|
|
partition2: new $$.Collection(cy, partition2)
|
|
};
|
|
|
|
return ret;
|
|
},
|
|
|
|
|
|
//
|
|
// options => options object
|
|
// dampingFactor: optional
|
|
// precision: optional
|
|
// iterations : optional
|
|
// retObj => returned object by function
|
|
// rank : function that returns the pageRank of a given node (object or selector string)
|
|
pageRank: function(options) {
|
|
options = options || {};
|
|
|
|
var normalizeVector = function(vector) {
|
|
var length = vector.length;
|
|
|
|
// First, get sum of all elements
|
|
var total = 0;
|
|
for (var i = 0; i < length; i++) {
|
|
total += vector[i];
|
|
}
|
|
|
|
// Now, divide each by the sum of all elements
|
|
for (var i = 0; i < length; i++) {
|
|
vector[i] = vector[i] / total;
|
|
}
|
|
};
|
|
|
|
// var logDebug = function() {
|
|
// if (debug) {
|
|
// console.log.apply(console, arguments);
|
|
// }
|
|
// };
|
|
|
|
// Parse options
|
|
// debug - optional
|
|
// if (options != null &&
|
|
// options.debug != null) {
|
|
// var debug = options.debug;
|
|
// } else {
|
|
// var debug = false;
|
|
// }
|
|
// logDebug("Starting pageRank...");
|
|
|
|
// dampingFactor - optional
|
|
if (options != null &&
|
|
options.dampingfactor != null) {
|
|
var dampingFactor = options.dampingFactor;
|
|
} else {
|
|
var dampingFactor = 0.8; // Default damping factor
|
|
}
|
|
|
|
// desired precision - optional
|
|
if (options != null &&
|
|
options.precision != null) {
|
|
var epsilon = options.precision;
|
|
} else {
|
|
var epsilon = 0.000001; // Default precision
|
|
}
|
|
|
|
// Max number of iterations - optional
|
|
if (options != null &&
|
|
options.iterations != null) {
|
|
var numIter = options.iterations;
|
|
} else {
|
|
var numIter = 200; // Default number of iterations
|
|
}
|
|
|
|
// Weight function - optional
|
|
if (options != null &&
|
|
options.weight != null &&
|
|
$$.is.fn(options.weight)) {
|
|
var weightFn = options.weight;
|
|
} else {
|
|
// If not specified, assume each edge has equal weight (1)
|
|
var weightFn = function(e) {return 1;};
|
|
}
|
|
|
|
var cy = this._private.cy;
|
|
var edges = this.edges().stdFilter(function(e){ return !e.isLoop(); });
|
|
var nodes = this.nodes();
|
|
var numNodes = nodes.length;
|
|
var numEdges = edges.length;
|
|
|
|
// Create numerical identifiers for each node
|
|
// mapping: node id -> position in nodes array
|
|
// for reverse mapping, simply use nodes array
|
|
var id2position = {};
|
|
for (var i = 0; i < numNodes; i++) {
|
|
id2position[nodes[i].id()] = i;
|
|
}
|
|
|
|
// Construct transposed adjacency matrix
|
|
// First lets have a zeroed matrix of the right size
|
|
// We'll also keep track of the sum of each column
|
|
var matrix = [];
|
|
var columnSum = [];
|
|
var additionalProb = (1 - dampingFactor) / numNodes;
|
|
|
|
// Create null matric
|
|
for (var i = 0; i < numNodes; i++) {
|
|
var newRow = [];
|
|
for (var j = 0; j < numNodes; j++) {
|
|
newRow.push(0.0);
|
|
}
|
|
matrix.push(newRow);
|
|
columnSum.push(0.0);
|
|
}
|
|
|
|
// Now, process edges
|
|
for (var i = 0; i < numEdges; i++) {
|
|
var edge = edges[i];
|
|
var s = id2position[edge.source().id()];
|
|
var t = id2position[edge.target().id()];
|
|
var w = weightFn.apply(edge, [edge]);
|
|
|
|
// Update matrix
|
|
matrix[t][s] += w;
|
|
|
|
// Update column sum
|
|
columnSum[s] += w;
|
|
}
|
|
|
|
// Add additional probability based on damping factor
|
|
// Also, take into account columns that have sum = 0
|
|
var p = 1.0 / numNodes + additionalProb; // Shorthand
|
|
// Traverse matrix, column by column
|
|
for (var j = 0; j < numNodes; j++) {
|
|
if (columnSum[j] === 0) {
|
|
// No 'links' out from node jth, assume equal probability for each possible node
|
|
for (var i = 0; i < numNodes; i++) {
|
|
matrix[i][j] = p;
|
|
}
|
|
} else {
|
|
// Node jth has outgoing link, compute normalized probabilities
|
|
for (var i = 0; i < numNodes; i++) {
|
|
matrix[i][j] = matrix[i][j] / columnSum[j] + additionalProb;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compute dominant eigenvector using power method
|
|
var eigenvector = [];
|
|
var nullVector = [];
|
|
var previous;
|
|
|
|
// Start with a vector of all 1's
|
|
// Also, initialize a null vector which will be used as shorthand
|
|
for (var i = 0; i < numNodes; i++) {
|
|
eigenvector.push(1.0);
|
|
nullVector.push(0.0);
|
|
}
|
|
|
|
for (var iter = 0; iter < numIter; iter++) {
|
|
// New array with all 0's
|
|
var temp = nullVector.slice(0);
|
|
|
|
// Multiply matrix with previous result
|
|
for (var i = 0; i < numNodes; i++) {
|
|
for (var j = 0; j < numNodes; j++) {
|
|
temp[i] += matrix[i][j] * eigenvector[j];
|
|
}
|
|
}
|
|
|
|
normalizeVector(temp);
|
|
previous = eigenvector;
|
|
eigenvector = temp;
|
|
|
|
var diff = 0;
|
|
// Compute difference (squared module) of both vectors
|
|
for (var i = 0; i < numNodes; i++) {
|
|
diff += Math.pow(previous[i] - eigenvector[i], 2);
|
|
}
|
|
|
|
// If difference is less than the desired threshold, stop iterating
|
|
if (diff < epsilon) {
|
|
// logDebug("Stoped at iteration %s", iter);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// logDebug("Result:\n" + eigenvector);
|
|
|
|
// Construct result
|
|
var res = {
|
|
rank : function(node) {
|
|
if ($$.is.string(node)) {
|
|
// is a selector string
|
|
var nodeId = (cy.filter(node)[0]).id();
|
|
} else {
|
|
// is a node object
|
|
var nodeId = node.id();
|
|
}
|
|
return eigenvector[id2position[nodeId]];
|
|
}
|
|
};
|
|
|
|
|
|
return res;
|
|
}, // pageRank
|
|
|
|
|
|
// options => options object
|
|
// weight: function( edge ){} // specifies weight to use for `edge`/`this`. If not present, it will be asumed a weight of 1 for all edges
|
|
// directed // default false
|
|
// retObj => returned object by function
|
|
// if directed
|
|
// indegree : function(node) // Returns the normalized indegree of the given node
|
|
// outdegree: function(node) // Returns the normalized outdegree of the given node
|
|
// if undirected
|
|
// degree : function(node) // Returns the normalized degree of the given node
|
|
degreeCentralityNormalized: function (options) {
|
|
options = options || {};
|
|
|
|
// var logDebug = function () {
|
|
// if (debug) {
|
|
// console.log.apply(console, arguments);
|
|
// }
|
|
// };
|
|
|
|
// Parse options
|
|
// debug - optional
|
|
// if (options.debug != null) {
|
|
// var debug = options.debug;
|
|
// } else {
|
|
// var debug = false;
|
|
// }
|
|
|
|
// directed - optional
|
|
if (options.directed != null) {
|
|
var directed = options.directed;
|
|
} else {
|
|
var directed = false;
|
|
}
|
|
|
|
// logDebug("Starting degree centrality...");
|
|
var nodes = this.nodes();
|
|
var numNodes = nodes.length;
|
|
|
|
if (!directed) {
|
|
var degrees = {};
|
|
var maxDegree = 0;
|
|
|
|
for (var i = 0; i < numNodes; i++) {
|
|
var node = nodes[i];
|
|
// add current node to the current options object and call degreeCentrality
|
|
var currDegree = this.degreeCentrality($$.util.extend({}, options, {root: node}));
|
|
if (maxDegree < currDegree.degree)
|
|
maxDegree = currDegree.degree;
|
|
|
|
degrees[node.id()] = currDegree.degree;
|
|
}
|
|
|
|
return {
|
|
degree: function (node) {
|
|
if ($$.is.string(node)) {
|
|
// from is a selector string
|
|
var node = (cy.filter(node)[0]).id();
|
|
} else {
|
|
// from is a node
|
|
var node = node.id();
|
|
}
|
|
|
|
return degrees[node] / maxDegree;
|
|
}
|
|
};
|
|
} else {
|
|
var indegrees = {};
|
|
var outdegrees = {};
|
|
var maxIndegree = 0;
|
|
var maxOutdegree = 0;
|
|
|
|
for (var i = 0; i < numNodes; i++) {
|
|
var node = nodes[i];
|
|
// add current node to the current options object and call degreeCentrality
|
|
var currDegree = this.degreeCentrality($$.util.extend({}, options, {root: node}));
|
|
|
|
if (maxIndegree < currDegree.indegree)
|
|
maxIndegree = currDegree.indegree;
|
|
|
|
if (maxOutdegree < currDegree.outdegree)
|
|
maxOutdegree = currDegree.outdegree;
|
|
|
|
indegrees[node.id()] = currDegree.indegree;
|
|
outdegrees[node.id()] = currDegree.outdegree;
|
|
}
|
|
|
|
return {
|
|
indegree: function (node) {
|
|
if ($$.is.string(node)) {
|
|
// from is a selector string
|
|
var node = (cy.filter(node)[0]).id();
|
|
} else {
|
|
// from is a node
|
|
var node = node.id();
|
|
}
|
|
|
|
return indegrees[node] / maxIndegree;
|
|
},
|
|
outdegree: function (node) {
|
|
if ($$.is.string(node)) {
|
|
// from is a selector string
|
|
var node = (cy.filter(node)[0]).id();
|
|
} else {
|
|
// from is a node
|
|
var node = node.id();
|
|
}
|
|
|
|
return outdegrees[node] / maxOutdegree;
|
|
}
|
|
|
|
};
|
|
}
|
|
|
|
}, // degreeCentralityNormalized
|
|
|
|
// Implemented from the algorithm in Opsahl's paper "Node centrality in weighted networks: Generalizing degree and shortest paths" check the heading 2 "Degree"
|
|
// options => options object
|
|
// node : focal node
|
|
// weight: function( edge ){} // specifies weight to use for `edge`/`this`. If not present, it will be asumed a weight of 1 for all edges
|
|
// alpha : alpha value for the algorithm (Benchmark values of alpha: 0 -> disregards the weights focuses on number of edges
|
|
// 1 -> disregards the number of edges focuses on total amount of weight
|
|
// directed // default false
|
|
// retObj => returned object by function
|
|
// if directed
|
|
// indegree : indegree of the given node
|
|
// outdegree: outdegree of the given node
|
|
// if undirected
|
|
// degree : degree of the given node
|
|
degreeCentrality: function (options) {
|
|
options = options || {};
|
|
|
|
var callingEles = this;
|
|
|
|
// var logDebug = function () {
|
|
// if (debug) {
|
|
// console.log.apply(console, arguments);
|
|
// }
|
|
// };
|
|
|
|
// Parse options
|
|
// debug - optional
|
|
// if (options.debug != null) {
|
|
// var debug = options.debug;
|
|
// } else {
|
|
// var debug = false;
|
|
// }
|
|
|
|
// logDebug("Starting degree centrality...");
|
|
|
|
// root - mandatory!
|
|
if (options != null && options.root != null) {
|
|
var root = $$.is.string(options.root) ? this.filter(options.root)[0] : options.root[0];
|
|
// logDebug("Source node: %s", root.id());
|
|
} else {
|
|
return undefined;
|
|
}
|
|
|
|
// weight - optional
|
|
if (options.weight != null && $$.is.fn(options.weight)) {
|
|
var weightFn = options.weight;
|
|
} else {
|
|
// If not specified, assume each edge has equal weight (1)
|
|
var weightFn = function (e) {
|
|
return 1;
|
|
};
|
|
}
|
|
|
|
// directed - optional
|
|
if (options.directed != null) {
|
|
var directed = options.directed;
|
|
} else {
|
|
var directed = false;
|
|
}
|
|
|
|
// alpha - optional
|
|
if (options.alpha != null && $$.is.number(options.alpha)) {
|
|
var alpha = options.alpha;
|
|
} else {
|
|
alpha = 0;
|
|
}
|
|
|
|
|
|
if (!directed) {
|
|
var connEdges = root.connectedEdges().intersection( callingEles );
|
|
var k = connEdges.length;
|
|
var s = 0;
|
|
|
|
// Now, sum edge weights
|
|
for (var i = 0; i < connEdges.length; i++) {
|
|
var edge = connEdges[i];
|
|
s += weightFn.apply(edge, [edge]);
|
|
}
|
|
|
|
return {
|
|
degree: Math.pow(k, 1 - alpha) * Math.pow(s, alpha)
|
|
};
|
|
} else {
|
|
var incoming = root.connectedEdges('edge[target = "' + root.id() + '"]').intersection( callingEles );
|
|
var outgoing = root.connectedEdges('edge[source = "' + root.id() + '"]').intersection( callingEles );
|
|
var k_in = incoming.length;
|
|
var k_out = outgoing.length;
|
|
var s_in = 0;
|
|
var s_out = 0;
|
|
|
|
// Now, sum incoming edge weights
|
|
for (var i = 0; i < incoming.length; i++) {
|
|
var edge = incoming[i];
|
|
s_in += weightFn.apply(edge, [edge]);
|
|
}
|
|
|
|
// Now, sum outgoing edge weights
|
|
for (var i = 0; i < outgoing.length; i++) {
|
|
var edge = outgoing[i];
|
|
s_out += weightFn.apply(edge, [edge]);
|
|
}
|
|
|
|
return {
|
|
indegree: Math.pow(k_in, 1 - alpha) * Math.pow(s_in, alpha),
|
|
outdegree: Math.pow(k_out, 1 - alpha) * Math.pow(s_out, alpha)
|
|
};
|
|
}
|
|
}, // degreeCentrality
|
|
|
|
// options => options object
|
|
// weight: function( edge ){} // specifies weight to use for `edge`/`this`. If not present, it will be asumed a weight of 1 for all edges
|
|
// directed // default false
|
|
// harmonic // use harmonic mean instead of arithmetic mean
|
|
// retObj => returned object by function
|
|
// closeness : function(node) // Returns the normalized closeness of the given node
|
|
closenessCentralityNormalized: function (options) {
|
|
options = options || {};
|
|
|
|
// var logDebug = function () {
|
|
// if (debug) {
|
|
// console.log.apply(console, arguments);
|
|
// }
|
|
// };
|
|
|
|
// Parse options
|
|
// debug - optional
|
|
// if (options.debug != null) {
|
|
// var debug = options.debug;
|
|
// } else {
|
|
// var debug = false;
|
|
// }
|
|
|
|
// logDebug("Starting closeness centrality...");
|
|
|
|
var harmonic = options.harmonic;
|
|
if( harmonic === undefined ){
|
|
harmonic = true;
|
|
}
|
|
|
|
var closenesses = {};
|
|
var maxCloseness = 0;
|
|
var nodes = this.nodes();
|
|
var fw = this.floydWarshall({ weight: options.weight, directed: options.directed });
|
|
|
|
// Compute closeness for every node and find the maximum closeness
|
|
for(var i = 0; i < nodes.length; i++){
|
|
var currCloseness = 0;
|
|
for (var j = 0; j < nodes.length; j++) {
|
|
if (i != j) {
|
|
var d = fw.distance(nodes[i], nodes[j]);
|
|
|
|
if( harmonic ){
|
|
currCloseness += 1 / d;
|
|
} else {
|
|
currCloseness += d;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( !harmonic ){
|
|
currCloseness = 1 / currCloseness;
|
|
}
|
|
|
|
if (maxCloseness < currCloseness){
|
|
maxCloseness = currCloseness;
|
|
}
|
|
|
|
closenesses[nodes[i].id()] = currCloseness;
|
|
}
|
|
|
|
return {
|
|
closeness: function (node) {
|
|
if ($$.is.string(node)) {
|
|
// from is a selector string
|
|
var node = (cy.filter(node)[0]).id();
|
|
} else {
|
|
// from is a node
|
|
var node = node.id();
|
|
}
|
|
|
|
return closenesses[node] / maxCloseness;
|
|
}
|
|
};
|
|
},
|
|
// Implemented from pseudocode from wikipedia
|
|
// options => options object
|
|
// root : focal node
|
|
// weight: function( edge ){} // specifies weight to use for `edge`/`this`. If not present, it will be asumed a weight of 1 for all edges
|
|
// directed // default false
|
|
// closeness => returned value by the function. Closeness value of the given node.
|
|
closenessCentrality: function (options) {
|
|
options = options || {};
|
|
|
|
// var logDebug = function () {
|
|
// if (debug) {
|
|
// console.log.apply(console, arguments);
|
|
// }
|
|
// };
|
|
|
|
// Parse options
|
|
// debug - optional
|
|
// if (options.debug != null) {
|
|
// var debug = options.debug;
|
|
// } else {
|
|
// var debug = false;
|
|
// }
|
|
|
|
// logDebug("Starting closeness centrality...");
|
|
|
|
// root - mandatory!
|
|
if (options.root != null) {
|
|
if ($$.is.string(options.root)) {
|
|
// use it as a selector, e.g. "#rootID
|
|
var root = this.filter(options.root)[0];
|
|
} else {
|
|
var root = options.root[0];
|
|
}
|
|
// logDebug("Source node: %s", root.id());
|
|
} else {
|
|
$$.util.error("options.root required");
|
|
return undefined;
|
|
}
|
|
|
|
// weight - optional
|
|
if (options.weight != null && $$.is.fn(options.weight)) {
|
|
var weight = options.weight;
|
|
} else {
|
|
var weight = function(){return 1;};
|
|
}
|
|
|
|
// directed - optional
|
|
if (options.directed != null && $$.is.bool(options.directed)) {
|
|
var directed = options.directed;
|
|
} else {
|
|
var directed = false;
|
|
}
|
|
|
|
var harmonic = options.harmonic;
|
|
if( harmonic === undefined ){
|
|
harmonic = true;
|
|
}
|
|
|
|
// we need distance from this node to every other node
|
|
var dijkstra = this.dijkstra({
|
|
root: root,
|
|
weight: weight,
|
|
directed: directed
|
|
});
|
|
var totalDistance = 0;
|
|
|
|
var nodes = this.nodes();
|
|
for (var i = 0; i < nodes.length; i++){
|
|
if (nodes[i].id() != root.id()){
|
|
var d = dijkstra.distanceTo(nodes[i]);
|
|
|
|
if( harmonic ){
|
|
totalDistance += 1 / d;
|
|
} else {
|
|
totalDistance += d;
|
|
}
|
|
}
|
|
}
|
|
|
|
return harmonic ? totalDistance : 1 / totalDistance;
|
|
}, // closenessCentrality
|
|
|
|
// Implemented from the algorithm in the paper "On Variants of Shortest-Path Betweenness Centrality and their Generic Computation" by Ulrik Brandes
|
|
// options => options object
|
|
// weight: function( edge ){} // specifies weight to use for `edge`/`this`. If not present, it will be asumed a weight of 1 for all edges
|
|
// directed // default false
|
|
// retObj => returned object by function
|
|
// betweenness : function(node) // Returns the betweenness centrality of the given node
|
|
// betweennessNormalized : function(node) // Returns the normalized betweenness centrality of the given node
|
|
betweennessCentrality: function (options) {
|
|
options = options || {};
|
|
|
|
// var logDebug = function () {
|
|
// if (debug) {
|
|
// console.log.apply(console, arguments);
|
|
// }
|
|
// };
|
|
|
|
// Parse options
|
|
// debug - optional
|
|
// if (options.debug != null) {
|
|
// var debug = options.debug;
|
|
// } else {
|
|
// var debug = false;
|
|
// }
|
|
|
|
// logDebug("Starting betweenness centrality...");
|
|
|
|
// Weight - optional
|
|
if (options.weight != null && $$.is.fn(options.weight)) {
|
|
var weightFn = options.weight;
|
|
var weighted = true;
|
|
} else {
|
|
var weighted = false;
|
|
}
|
|
|
|
// Directed - default false
|
|
if (options.directed != null && $$.is.bool(options.directed)) {
|
|
var directed = options.directed;
|
|
} else {
|
|
var directed = false;
|
|
}
|
|
|
|
var priorityInsert = function (queue, ele) {
|
|
queue.unshift(ele);
|
|
for (var i = 0; d[queue[i]] < d[queue[i + 1]] && i < queue.length - 1; i++) {
|
|
var tmp = queue[i];
|
|
queue[i] = queue[i + 1];
|
|
queue[i + 1] = tmp;
|
|
}
|
|
};
|
|
|
|
var cy = this._private.cy;
|
|
|
|
// starting
|
|
var V = this.nodes();
|
|
var A = {};
|
|
var C = {};
|
|
|
|
// A contains the neighborhoods of every node
|
|
for (var i = 0; i < V.length; i++) {
|
|
if (directed) {
|
|
A[V[i].id()] = V[i].outgoers("node"); // get outgoers of every node
|
|
} else {
|
|
A[V[i].id()] = V[i].openNeighborhood("node"); // get neighbors of every node
|
|
}
|
|
}
|
|
|
|
// C contains the betweenness values
|
|
for (var i = 0; i < V.length; i++) {
|
|
C[V[i].id()] = 0;
|
|
}
|
|
|
|
for (var s = 0; s < V.length; s++) {
|
|
var S = []; // stack
|
|
var P = {};
|
|
var g = {};
|
|
var d = {};
|
|
var Q = []; // queue
|
|
|
|
// init dictionaries
|
|
for (var i = 0; i < V.length; i++) {
|
|
P[V[i].id()] = [];
|
|
g[V[i].id()] = 0;
|
|
d[V[i].id()] = Number.POSITIVE_INFINITY;
|
|
}
|
|
|
|
g[V[s].id()] = 1; // sigma
|
|
d[V[s].id()] = 0; // distance to s
|
|
|
|
Q.unshift(V[s].id());
|
|
|
|
while (Q.length > 0) {
|
|
var v = Q.pop();
|
|
S.push(v);
|
|
if (weighted) {
|
|
A[v].forEach(function (w) {
|
|
if (cy.$('#' + v).edgesTo(w).length > 0) {
|
|
var edge = cy.$('#' + v).edgesTo(w)[0];
|
|
} else {
|
|
var edge = w.edgesTo('#' + v)[0];
|
|
}
|
|
|
|
var edgeWeight = weightFn.apply(edge, [edge]);
|
|
|
|
if (d[w.id()] > d[v] + edgeWeight) {
|
|
d[w.id()] = d[v] + edgeWeight;
|
|
if (Q.indexOf(w.id()) < 0) { //if w is not in Q
|
|
priorityInsert(Q, w.id());
|
|
} else { // update position if w is in Q
|
|
Q.splice(Q.indexOf(w.id()), 1);
|
|
priorityInsert(Q, w.id());
|
|
}
|
|
g[w.id()] = 0;
|
|
P[w.id()] = [];
|
|
}
|
|
if (d[w.id()] == d[v] + edgeWeight) {
|
|
g[w.id()] = g[w.id()] + g[v];
|
|
P[w.id()].push(v);
|
|
}
|
|
});
|
|
} else {
|
|
A[v].forEach(function (w) {
|
|
if (d[w.id()] == Number.POSITIVE_INFINITY) {
|
|
Q.unshift(w.id());
|
|
d[w.id()] = d[v] + 1;
|
|
}
|
|
if (d[w.id()] == d[v] + 1) {
|
|
g[w.id()] = g[w.id()] + g[v];
|
|
P[w.id()].push(v);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
var e = {};
|
|
for (var i = 0; i < V.length; i++) {
|
|
e[V[i].id()] = 0;
|
|
}
|
|
|
|
while (S.length > 0) {
|
|
var w = S.pop();
|
|
P[w].forEach(function (v) {
|
|
e[v] = e[v] + (g[v] / g[w]) * (1 + e[w]);
|
|
if (w != V[s].id())
|
|
C[w] = C[w] + e[w];
|
|
});
|
|
}
|
|
}
|
|
|
|
var max = 0;
|
|
for (var key in C) {
|
|
if (max < C[key])
|
|
max = C[key];
|
|
}
|
|
|
|
var ret = {
|
|
betweenness: function (node) {
|
|
if ($$.is.string(node)) {
|
|
var node = (cy.filter(node)[0]).id();
|
|
} else {
|
|
var node = node.id();
|
|
}
|
|
|
|
return C[node];
|
|
},
|
|
|
|
betweennessNormalized: function (node) {
|
|
if ($$.is.string(node)) {
|
|
var node = (cy.filter(node)[0]).id();
|
|
} else {
|
|
var node = node.id();
|
|
}
|
|
|
|
return C[node] / max;
|
|
}
|
|
};
|
|
|
|
// alias
|
|
ret.betweennessNormalised = ret.betweennessNormalized;
|
|
|
|
return ret;
|
|
} // betweennessCentrality
|
|
}); // $$.fn.eles
|
|
|
|
// nice, short mathemathical alias
|
|
$$.elesfn.dc = $$.elesfn.degreeCentrality;
|
|
$$.elesfn.dcn = $$.elesfn.degreeCentralityNormalised = $$.elesfn.degreeCentralityNormalized;
|
|
$$.elesfn.cc = $$.elesfn.closenessCentrality;
|
|
$$.elesfn.ccn = $$.elesfn.closenessCentralityNormalised = $$.elesfn.closenessCentralityNormalized;
|
|
$$.elesfn.bc = $$.elesfn.betweennessCentrality;
|
|
}) (cytoscape);
|
|
|
|
;(function( $$ ){ 'use strict';
|
|
|
|
$$.fn.eles({
|
|
animated: $$.define.animated(),
|
|
clearQueue: $$.define.clearQueue(),
|
|
delay: $$.define.delay(),
|
|
animate: $$.define.animate(),
|
|
stop: $$.define.stop()
|
|
});
|
|
|
|
})( cytoscape );
|
|
|
|
;(function( $$ ){ 'use strict';
|
|
|
|
$$.fn.eles({
|
|
addClass: function(classes){
|
|
classes = classes.split(/\s+/);
|
|
var self = this;
|
|
var changed = [];
|
|
|
|
for( var i = 0; i < classes.length; i++ ){
|
|
var cls = classes[i];
|
|
if( $$.is.emptyString(cls) ){ continue; }
|
|
|
|
for( var j = 0; j < self.length; j++ ){
|
|
var ele = self[j];
|
|
var hasClass = ele._private.classes[cls];
|
|
ele._private.classes[cls] = true;
|
|
|
|
if( !hasClass ){ // if didn't already have, add to list of changed
|
|
changed.push( ele );
|
|
}
|
|
}
|
|
}
|
|
|
|
// trigger update style on those eles that had class changes
|
|
if( changed.length > 0 ){
|
|
new $$.Collection(this._private.cy, changed)
|
|
.updateStyle()
|
|
.trigger('class')
|
|
;
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
hasClass: function(className){
|
|
var ele = this[0];
|
|
return ( ele != null && ele._private.classes[className] ) ? true : false;
|
|
},
|
|
|
|
toggleClass: function(classesStr, toggle){
|
|
var classes = classesStr.split(/\s+/);
|
|
var self = this;
|
|
var changed = []; // eles who had classes changed
|
|
|
|
for( var i = 0, il = self.length; i < il; i++ ){
|
|
var ele = self[i];
|
|
|
|
for( var j = 0; j < classes.length; j++ ){
|
|
var cls = classes[j];
|
|
|
|
if( $$.is.emptyString(cls) ){ continue; }
|
|
|
|
var hasClass = ele._private.classes[cls];
|
|
var shouldAdd = toggle || (toggle === undefined && !hasClass);
|
|
|
|
if( shouldAdd ){
|
|
ele._private.classes[cls] = true;
|
|
|
|
if( !hasClass ){ changed.push(ele); }
|
|
} else { // then remove
|
|
ele._private.classes[cls] = false;
|
|
|
|
if( hasClass ){ changed.push(ele); }
|
|
}
|
|
|
|
} // for j classes
|
|
} // for i eles
|
|
|
|
// trigger update style on those eles that had class changes
|
|
if( changed.length > 0 ){
|
|
new $$.Collection(this._private.cy, changed)
|
|
.updateStyle()
|
|
.trigger('class')
|
|
;
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
removeClass: function(classes){
|
|
classes = classes.split(/\s+/);
|
|
var self = this;
|
|
var changed = [];
|
|
|
|
for( var i = 0; i < self.length; i++ ){
|
|
var ele = self[i];
|
|
|
|
for( var j = 0; j < classes.length; j++ ){
|
|
var cls = classes[j];
|
|
if( !cls || cls === '' ){ continue; }
|
|
|
|
var hasClass = ele._private.classes[cls];
|
|
ele._private.classes[cls] = undefined;
|
|
|
|
if( hasClass ){ // then we changed its set of classes
|
|
changed.push( ele );
|
|
}
|
|
}
|
|
}
|
|
|
|
// trigger update style on those eles that had class changes
|
|
if( changed.length > 0 ){
|
|
new $$.Collection(self._private.cy, changed).updateStyle();
|
|
}
|
|
|
|
self.trigger('class');
|
|
return self;
|
|
},
|
|
|
|
flashClass: function(classes, duration){
|
|
var self = this;
|
|
|
|
if( duration == null ){
|
|
duration = 250;
|
|
} else if( duration === 0 ){
|
|
return self; // nothing to do really
|
|
}
|
|
|
|
self.addClass( classes );
|
|
setTimeout(function(){
|
|
self.removeClass( classes );
|
|
}, duration);
|
|
|
|
return self;
|
|
}
|
|
});
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.eles({
|
|
allAre: function( selector ){
|
|
return this.filter(selector).length === this.length;
|
|
},
|
|
|
|
is: function( selector ){
|
|
return this.filter(selector).length > 0;
|
|
},
|
|
|
|
some: function( fn, thisArg ){
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ret = !thisArg ? fn( this[i], i, this ) : fn.apply( thisArg, [ this[i], i, this ] );
|
|
|
|
if( ret ){
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
every: function( fn, thisArg ){
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ret = !thisArg ? fn( this[i], i, this ) : fn.apply( thisArg, [ this[i], i, this ] );
|
|
|
|
if( !ret ){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
same: function( collection ){
|
|
collection = this.cy().collection( collection );
|
|
|
|
// cheap extra check
|
|
if( this.length !== collection.length ){
|
|
return false;
|
|
}
|
|
|
|
return this.intersect( collection ).length === this.length;
|
|
},
|
|
|
|
anySame: function( collection ){
|
|
collection = this.cy().collection( collection );
|
|
|
|
return this.intersect( collection ).length > 0;
|
|
},
|
|
|
|
allAreNeighbors: function( collection ){
|
|
collection = this.cy().collection( collection );
|
|
|
|
return this.neighborhood().intersect( collection ).length === collection.length;
|
|
}
|
|
});
|
|
|
|
$$.elesfn.allAreNeighbours = $$.elesfn.allAreNeighbors;
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// Compound functions
|
|
/////////////////////
|
|
|
|
$$.fn.eles({
|
|
parent: function( selector ){
|
|
var parents = [];
|
|
var cy = this._private.cy;
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
var parent = cy.getElementById( ele._private.data.parent );
|
|
|
|
if( parent.size() > 0 ){
|
|
parents.push( parent );
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( cy, parents, { unique: true } ).filter( selector );
|
|
},
|
|
|
|
parents: function( selector ){
|
|
var parents = [];
|
|
|
|
var eles = this.parent();
|
|
while( eles.nonempty() ){
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
parents.push( ele );
|
|
}
|
|
|
|
eles = eles.parent();
|
|
}
|
|
|
|
return new $$.Collection( this.cy(), parents, { unique: true } ).filter( selector );
|
|
},
|
|
|
|
commonAncestors: function( selector ){
|
|
var ancestors;
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
var parents = ele.parents();
|
|
|
|
ancestors = ancestors || parents;
|
|
|
|
ancestors = ancestors.intersect( parents ); // current list must be common with current ele parents set
|
|
}
|
|
|
|
return ancestors.filter( selector );
|
|
},
|
|
|
|
orphans: function( selector ){
|
|
return this.stdFilter(function( ele ){
|
|
return ele.isNode() && ele.parent().empty();
|
|
}).filter( selector );
|
|
},
|
|
|
|
nonorphans: function( selector ){
|
|
return this.stdFilter(function( ele ){
|
|
return ele.isNode() && ele.parent().nonempty();
|
|
}).filter( selector );
|
|
},
|
|
|
|
children: function( selector ){
|
|
var children = [];
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
children = children.concat( ele._private.children );
|
|
}
|
|
|
|
return new $$.Collection( this.cy(), children, { unique: true } ).filter( selector );
|
|
},
|
|
|
|
siblings: function( selector ){
|
|
return this.parent().children().not( this ).filter( selector );
|
|
},
|
|
|
|
isParent: function(){
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
return ele._private.children.length !== 0;
|
|
}
|
|
},
|
|
|
|
isChild: function(){
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
return ele._private.data.parent !== undefined && ele.parent().length !== 0;
|
|
}
|
|
},
|
|
|
|
descendants: function( selector ){
|
|
var elements = [];
|
|
|
|
function add( eles ){
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
|
|
elements.push( ele );
|
|
|
|
if( ele.children().nonempty() ){
|
|
add( ele.children() );
|
|
}
|
|
}
|
|
}
|
|
|
|
add( this.children() );
|
|
|
|
return new $$.Collection( this.cy(), elements, { unique: true } ).filter( selector );
|
|
}
|
|
});
|
|
|
|
// aliases
|
|
$$.elesfn.ancestors = $$.elesfn.parents;
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var borderWidthMultiplier = 2 * 0.5;
|
|
var borderWidthAdjustment = 0;
|
|
|
|
$$.fn.eles({
|
|
|
|
data: $$.define.data({
|
|
field: 'data',
|
|
bindingEvent: 'data',
|
|
allowBinding: true,
|
|
allowSetting: true,
|
|
settingEvent: 'data',
|
|
settingTriggersEvent: true,
|
|
triggerFnName: 'trigger',
|
|
allowGetting: true,
|
|
immutableKeys: {
|
|
'id': true,
|
|
'source': true,
|
|
'target': true,
|
|
'parent': true
|
|
},
|
|
updateStyle: true
|
|
}),
|
|
|
|
removeData: $$.define.removeData({
|
|
field: 'data',
|
|
event: 'data',
|
|
triggerFnName: 'trigger',
|
|
triggerEvent: true,
|
|
immutableKeys: {
|
|
'id': true,
|
|
'source': true,
|
|
'target': true,
|
|
'parent': true
|
|
},
|
|
updateStyle: true
|
|
}),
|
|
|
|
scratch: $$.define.data({
|
|
field: 'scratch',
|
|
bindingEvent: 'scratch',
|
|
allowBinding: true,
|
|
allowSetting: true,
|
|
settingEvent: 'scratch',
|
|
settingTriggersEvent: true,
|
|
triggerFnName: 'trigger',
|
|
allowGetting: true,
|
|
updateStyle: true
|
|
}),
|
|
|
|
removeScratch: $$.define.removeData({
|
|
field: 'scratch',
|
|
event: 'scratch',
|
|
triggerFnName: 'trigger',
|
|
triggerEvent: true,
|
|
updateStyle: true
|
|
}),
|
|
|
|
rscratch: $$.define.data({
|
|
field: 'rscratch',
|
|
allowBinding: false,
|
|
allowSetting: true,
|
|
settingTriggersEvent: false,
|
|
allowGetting: true
|
|
}),
|
|
|
|
removeRscratch: $$.define.removeData({
|
|
field: 'rscratch',
|
|
triggerEvent: false
|
|
}),
|
|
|
|
id: function(){
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
return ele._private.data.id;
|
|
}
|
|
},
|
|
|
|
position: $$.define.data({
|
|
field: 'position',
|
|
bindingEvent: 'position',
|
|
allowBinding: true,
|
|
allowSetting: true,
|
|
settingEvent: 'position',
|
|
settingTriggersEvent: true,
|
|
triggerFnName: 'rtrigger',
|
|
allowGetting: true,
|
|
validKeys: ['x', 'y'],
|
|
onSet: function( eles ){
|
|
var updatedEles = eles.updateCompoundBounds();
|
|
updatedEles.rtrigger('position');
|
|
},
|
|
canSet: function( ele ){
|
|
return !ele.locked();
|
|
}
|
|
}),
|
|
|
|
// position but no notification to renderer
|
|
silentPosition: $$.define.data({
|
|
field: 'position',
|
|
bindingEvent: 'position',
|
|
allowBinding: false,
|
|
allowSetting: true,
|
|
settingEvent: 'position',
|
|
settingTriggersEvent: false,
|
|
triggerFnName: 'trigger',
|
|
allowGetting: true,
|
|
validKeys: ['x', 'y'],
|
|
onSet: function( eles ){
|
|
eles.updateCompoundBounds();
|
|
},
|
|
canSet: function( ele ){
|
|
return !ele.locked();
|
|
}
|
|
}),
|
|
|
|
positions: function( pos, silent ){
|
|
if( $$.is.plainObject(pos) ){
|
|
this.position(pos);
|
|
|
|
} else if( $$.is.fn(pos) ){
|
|
var fn = pos;
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
|
|
var pos = fn.apply(ele, [i, ele]);
|
|
|
|
if( pos && !ele.locked() ){
|
|
var elePos = ele._private.position;
|
|
elePos.x = pos.x;
|
|
elePos.y = pos.y;
|
|
}
|
|
}
|
|
|
|
var updatedEles = this.updateCompoundBounds();
|
|
var toTrigger = updatedEles.length > 0 ? this.add( updatedEles ) : this;
|
|
|
|
if( silent ){
|
|
toTrigger.trigger('position');
|
|
} else {
|
|
toTrigger.rtrigger('position');
|
|
}
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
silentPositions: function( pos ){
|
|
return this.positions( pos, true );
|
|
},
|
|
|
|
updateCompoundBounds: function(){
|
|
var cy = this.cy();
|
|
|
|
if( !cy.styleEnabled() || !cy.hasCompoundNodes() ){ return cy.collection(); } // save cycles for non compound graphs or when style disabled
|
|
|
|
var updated = [];
|
|
|
|
function update( parent ){
|
|
var children = parent.children();
|
|
var style = parent._private.style;
|
|
var includeLabels = style['compound-sizing-wrt-labels'].value === 'include';
|
|
var bb = children.boundingBox({ includeLabels: includeLabels, includeEdges: true });
|
|
var padding = {
|
|
top: style['padding-top'].pxValue,
|
|
bottom: style['padding-bottom'].pxValue,
|
|
left: style['padding-left'].pxValue,
|
|
right: style['padding-right'].pxValue
|
|
};
|
|
var pos = parent._private.position;
|
|
var didUpdate = false;
|
|
|
|
if( style['width'].value === 'auto' ){
|
|
parent._private.autoWidth = bb.w + padding.left + padding.right;
|
|
pos.x = (bb.x1 + bb.x2 - padding.left + padding.right)/2;
|
|
didUpdate = true;
|
|
}
|
|
|
|
if( style['height'].value === 'auto' ){
|
|
parent._private.autoHeight = bb.h + padding.top + padding.bottom;
|
|
pos.y = (bb.y1 + bb.y2 - padding.top + padding.bottom)/2;
|
|
didUpdate = true;
|
|
}
|
|
|
|
if( didUpdate ){
|
|
updated.push( parent );
|
|
}
|
|
}
|
|
|
|
// go up, level by level
|
|
var eles = this.parent();
|
|
while( eles.nonempty() ){
|
|
|
|
// update each parent node in this level
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
|
|
update( ele );
|
|
}
|
|
|
|
// next level
|
|
eles = eles.parent();
|
|
}
|
|
|
|
// return changed
|
|
return new $$.Collection( cy, updated );
|
|
},
|
|
|
|
// get/set the rendered (i.e. on screen) positon of the element
|
|
renderedPosition: function( dim, val ){
|
|
var ele = this[0];
|
|
var cy = this.cy();
|
|
var zoom = cy.zoom();
|
|
var pan = cy.pan();
|
|
var rpos = $$.is.plainObject( dim ) ? dim : undefined;
|
|
var setting = rpos !== undefined || ( val !== undefined && $$.is.string(dim) );
|
|
|
|
if( ele && ele.isNode() ){ // must have an element and must be a node to return position
|
|
if( setting ){
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
|
|
if( val !== undefined ){ // set one dimension
|
|
ele._private.position[dim] = ( val - pan[dim] )/zoom;
|
|
} else if( rpos !== undefined ){ // set whole position
|
|
ele._private.position = {
|
|
x: ( rpos.x - pan.x ) /zoom,
|
|
y: ( rpos.y - pan.y ) /zoom
|
|
};
|
|
}
|
|
}
|
|
|
|
this.rtrigger('position');
|
|
} else { // getting
|
|
var pos = ele._private.position;
|
|
rpos = {
|
|
x: pos.x * zoom + pan.x,
|
|
y: pos.y * zoom + pan.y
|
|
};
|
|
|
|
if( dim === undefined ){ // then return the whole rendered position
|
|
return rpos;
|
|
} else { // then return the specified dimension
|
|
return rpos[ dim ];
|
|
}
|
|
}
|
|
} else if( !setting ){
|
|
return undefined; // for empty collection case
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
// get/set the position relative to the parent
|
|
relativePosition: function( dim, val ){
|
|
var ele = this[0];
|
|
var cy = this.cy();
|
|
var ppos = $$.is.plainObject( dim ) ? dim : undefined;
|
|
var setting = ppos !== undefined || ( val !== undefined && $$.is.string(dim) );
|
|
var hasCompoundNodes = cy.hasCompoundNodes();
|
|
|
|
if( ele && ele.isNode() ){ // must have an element and must be a node to return position
|
|
if( setting ){
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
var parent = hasCompoundNodes ? ele.parent() : null;
|
|
var hasParent = parent && parent.length > 0;
|
|
var relativeToParent = hasParent;
|
|
|
|
if( hasParent ){
|
|
parent = parent[0];
|
|
}
|
|
|
|
var origin = relativeToParent ? parent._private.position : { x: 0, y: 0 };
|
|
|
|
if( val !== undefined ){ // set one dimension
|
|
ele._private.position[dim] = val + origin[dim];
|
|
} else if( ppos !== undefined ){ // set whole position
|
|
ele._private.position = {
|
|
x: ppos.x + origin.x,
|
|
y: ppos.y + origin.y,
|
|
};
|
|
}
|
|
}
|
|
|
|
this.rtrigger('position');
|
|
|
|
} else { // getting
|
|
var pos = ele._private.position;
|
|
var parent = hasCompoundNodes ? ele.parent() : null;
|
|
var hasParent = parent && parent.length > 0;
|
|
var relativeToParent = hasParent;
|
|
|
|
if( hasParent ){
|
|
parent = parent[0];
|
|
}
|
|
|
|
var origin = relativeToParent ? parent._private.position : { x: 0, y: 0 };
|
|
|
|
ppos = {
|
|
x: pos.x - origin.x,
|
|
y: pos.y - origin.y
|
|
};
|
|
|
|
if( dim === undefined ){ // then return the whole rendered position
|
|
return ppos;
|
|
} else { // then return the specified dimension
|
|
return ppos[ dim ];
|
|
}
|
|
}
|
|
} else if( !setting ){
|
|
return undefined; // for empty collection case
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
// convenience function to get a numerical value for the width of the node/edge
|
|
width: function(){
|
|
var ele = this[0];
|
|
var cy = ele._private.cy;
|
|
var styleEnabled = cy._private.styleEnabled;
|
|
|
|
if( ele ){
|
|
if( styleEnabled ){
|
|
var w = ele._private.style.width;
|
|
return w.strValue === 'auto' ? ele._private.autoWidth : w.pxValue;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
},
|
|
|
|
outerWidth: function(){
|
|
var ele = this[0];
|
|
var cy = ele._private.cy;
|
|
var styleEnabled = cy._private.styleEnabled;
|
|
|
|
if( ele ){
|
|
if( styleEnabled ){
|
|
var style = ele._private.style;
|
|
var width = style.width.strValue === 'auto' ? ele._private.autoWidth : style.width.pxValue;
|
|
var border = style['border-width'] ? style['border-width'].pxValue * borderWidthMultiplier + borderWidthAdjustment : 0;
|
|
|
|
return width + border;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
},
|
|
|
|
renderedWidth: function(){
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
var width = ele.width();
|
|
return width * this.cy().zoom();
|
|
}
|
|
},
|
|
|
|
renderedOuterWidth: function(){
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
var owidth = ele.outerWidth();
|
|
return owidth * this.cy().zoom();
|
|
}
|
|
},
|
|
|
|
// convenience function to get a numerical value for the height of the node
|
|
height: function(){
|
|
var ele = this[0];
|
|
var cy = ele._private.cy;
|
|
var styleEnabled = cy._private.styleEnabled;
|
|
|
|
if( ele && ele._private.group === 'nodes' ){
|
|
if( styleEnabled ){
|
|
var h = ele._private.style.height;
|
|
return h.strValue === 'auto' ? ele._private.autoHeight : h.pxValue;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
},
|
|
|
|
outerHeight: function(){
|
|
var ele = this[0];
|
|
var cy = ele._private.cy;
|
|
var styleEnabled = cy._private.styleEnabled;
|
|
|
|
if( ele && ele._private.group === 'nodes' ){
|
|
if( styleEnabled ){
|
|
var style = ele._private.style;
|
|
var height = style.height.strValue === 'auto' ? ele._private.autoHeight : style.height.pxValue;
|
|
var border = style['border-width'] ? style['border-width'].pxValue * borderWidthMultiplier + borderWidthAdjustment : 0;
|
|
} else {
|
|
return 1;
|
|
}
|
|
|
|
return height + border;
|
|
}
|
|
},
|
|
|
|
renderedHeight: function(){
|
|
var ele = this[0];
|
|
|
|
if( ele && ele._private.group === 'nodes' ){
|
|
var height = ele.height();
|
|
return height * this.cy().zoom();
|
|
}
|
|
},
|
|
|
|
renderedOuterHeight: function(){
|
|
var ele = this[0];
|
|
|
|
if( ele && ele._private.group === 'nodes' ){
|
|
var oheight = ele.outerHeight();
|
|
return oheight * this.cy().zoom();
|
|
}
|
|
},
|
|
|
|
renderedBoundingBox: function( options ){
|
|
var bb = this.boundingBox( options );
|
|
var cy = this.cy();
|
|
var zoom = cy.zoom();
|
|
var pan = cy.pan();
|
|
|
|
var x1 = bb.x1 * zoom + pan.x;
|
|
var x2 = bb.x2 * zoom + pan.x;
|
|
var y1 = bb.y1 * zoom + pan.y;
|
|
var y2 = bb.y2 * zoom + pan.y;
|
|
|
|
return {
|
|
x1: x1,
|
|
x2: x2,
|
|
y1: y1,
|
|
y2: y2,
|
|
w: x2 - x1,
|
|
h: y2 - y1
|
|
};
|
|
},
|
|
|
|
// get the bounding box of the elements (in raw model position)
|
|
boundingBox: function( options ){
|
|
var eles = this;
|
|
var cy = eles._private.cy;
|
|
var cy_p = cy._private;
|
|
var styleEnabled = cy_p.styleEnabled;
|
|
|
|
options = options || {};
|
|
|
|
var includeNodes = options.includeNodes === undefined ? true : options.includeNodes;
|
|
var includeEdges = options.includeEdges === undefined ? true : options.includeEdges;
|
|
var includeLabels = options.includeLabels === undefined ? true : options.includeLabels;
|
|
|
|
// recalculate projections etc
|
|
if( styleEnabled ){
|
|
cy_p.renderer.recalculateRenderedStyle( this );
|
|
}
|
|
|
|
var x1 = Infinity;
|
|
var x2 = -Infinity;
|
|
var y1 = Infinity;
|
|
var y2 = -Infinity;
|
|
|
|
// find bounds of elements
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
var _p = ele._private;
|
|
var style = _p.style;
|
|
var display = styleEnabled ? _p.style['display'].value : 'element';
|
|
var isNode = _p.group === 'nodes';
|
|
var ex1, ex2, ey1, ey2, x, y;
|
|
var includedEle = false;
|
|
|
|
if( display === 'none' ){ continue; } // then ele doesn't take up space
|
|
|
|
if( isNode && includeNodes ){
|
|
includedEle = true;
|
|
|
|
var pos = _p.position;
|
|
x = pos.x;
|
|
y = pos.y;
|
|
var w = ele.outerWidth();
|
|
var halfW = w/2;
|
|
var h = ele.outerHeight();
|
|
var halfH = h/2;
|
|
|
|
// handle node dimensions
|
|
/////////////////////////
|
|
|
|
ex1 = x - halfW;
|
|
ex2 = x + halfW;
|
|
ey1 = y - halfH;
|
|
ey2 = y + halfH;
|
|
|
|
x1 = ex1 < x1 ? ex1 : x1;
|
|
x2 = ex2 > x2 ? ex2 : x2;
|
|
y1 = ey1 < y1 ? ey1 : y1;
|
|
y2 = ey2 > y2 ? ey2 : y2;
|
|
|
|
} else if( ele.isEdge() && includeEdges ){
|
|
includedEle = true;
|
|
|
|
var n1 = _p.source;
|
|
var n1_p = n1._private;
|
|
var n1pos = n1_p.position;
|
|
|
|
var n2 = _p.target;
|
|
var n2_p = n2._private;
|
|
var n2pos = n2_p.position;
|
|
|
|
|
|
// handle edge dimensions (rough box estimate)
|
|
//////////////////////////////////////////////
|
|
|
|
var rstyle = _p.rstyle || {};
|
|
|
|
ex1 = n1pos.x;
|
|
ex2 = n2pos.x;
|
|
ey1 = n1pos.y;
|
|
ey2 = n2pos.y;
|
|
|
|
if( ex1 > ex2 ){
|
|
var temp = ex1;
|
|
ex1 = ex2;
|
|
ex2 = temp;
|
|
}
|
|
|
|
if( ey1 > ey2 ){
|
|
var temp = ey1;
|
|
ey1 = ey2;
|
|
ey2 = temp;
|
|
}
|
|
|
|
x1 = ex1 < x1 ? ex1 : x1;
|
|
x2 = ex2 > x2 ? ex2 : x2;
|
|
y1 = ey1 < y1 ? ey1 : y1;
|
|
y2 = ey2 > y2 ? ey2 : y2;
|
|
|
|
// handle points along edge (sanity check)
|
|
//////////////////////////////////////////
|
|
|
|
if( styleEnabled ){
|
|
var bpts = rstyle.bezierPts || [];
|
|
|
|
var w = style['width'].pxValue;
|
|
var wHalf = w/2;
|
|
|
|
for( var j = 0; j < bpts.length; j++ ){
|
|
var bpt = bpts[j];
|
|
|
|
ex1 = bpt.x - wHalf;
|
|
ex2 = bpt.x + wHalf;
|
|
ey1 = bpt.y - wHalf;
|
|
ey2 = bpt.y + wHalf;
|
|
|
|
x1 = ex1 < x1 ? ex1 : x1;
|
|
x2 = ex2 > x2 ? ex2 : x2;
|
|
y1 = ey1 < y1 ? ey1 : y1;
|
|
y2 = ey2 > y2 ? ey2 : y2;
|
|
}
|
|
}
|
|
|
|
// precise haystacks (sanity check)
|
|
///////////////////////////////////
|
|
|
|
if( styleEnabled && style['curve-style'].strValue === 'haystack' ){
|
|
var hpts = _p.rscratch.haystackPts;
|
|
|
|
ex1 = hpts[0];
|
|
ey1 = hpts[1];
|
|
ex2 = hpts[2];
|
|
ey2 = hpts[3];
|
|
|
|
if( ex1 > ex2 ){
|
|
var temp = ex1;
|
|
ex1 = ex2;
|
|
ex2 = temp;
|
|
}
|
|
|
|
if( ey1 > ey2 ){
|
|
var temp = ey1;
|
|
ey1 = ey2;
|
|
ey2 = temp;
|
|
}
|
|
|
|
x1 = ex1 < x1 ? ex1 : x1;
|
|
x2 = ex2 > x2 ? ex2 : x2;
|
|
y1 = ey1 < y1 ? ey1 : y1;
|
|
y2 = ey2 > y2 ? ey2 : y2;
|
|
}
|
|
|
|
} // edges
|
|
|
|
|
|
// handle label dimensions
|
|
//////////////////////////
|
|
|
|
if( styleEnabled ){
|
|
|
|
var style = ele._private.style;
|
|
var rstyle = ele._private.rstyle;
|
|
var label = style['content'].strValue;
|
|
var fontSize = style['font-size'];
|
|
var halign = style['text-halign'];
|
|
var valign = style['text-valign'];
|
|
var labelWidth = rstyle.labelWidth;
|
|
var labelHeight = rstyle.labelHeight;
|
|
var labelX = rstyle.labelX;
|
|
var labelY = rstyle.labelY;
|
|
|
|
if( includedEle && includeLabels && label && fontSize && labelHeight != null && labelWidth != null && labelX != null && labelY != null && halign && valign ){
|
|
var lh = labelHeight;
|
|
var lw = labelWidth;
|
|
var lx1, lx2, ly1, ly2;
|
|
|
|
if( ele.isEdge() ){
|
|
lx1 = labelX - lw/2;
|
|
lx2 = labelX + lw/2;
|
|
ly1 = labelY - lh/2;
|
|
ly2 = labelY + lh/2;
|
|
} else {
|
|
switch( halign.value ){
|
|
case 'left':
|
|
lx1 = labelX - lw;
|
|
lx2 = labelX;
|
|
break;
|
|
|
|
case 'center':
|
|
lx1 = labelX - lw/2;
|
|
lx2 = labelX + lw/2;
|
|
break;
|
|
|
|
case 'right':
|
|
lx1 = labelX;
|
|
lx2 = labelX + lw;
|
|
break;
|
|
}
|
|
|
|
switch( valign.value ){
|
|
case 'top':
|
|
ly1 = labelY - lh;
|
|
ly2 = labelY;
|
|
break;
|
|
|
|
case 'center':
|
|
ly1 = labelY - lh/2;
|
|
ly2 = labelY + lh/2;
|
|
break;
|
|
|
|
case 'bottom':
|
|
ly1 = labelY;
|
|
ly2 = labelY + lh;
|
|
break;
|
|
}
|
|
}
|
|
|
|
x1 = lx1 < x1 ? lx1 : x1;
|
|
x2 = lx2 > x2 ? lx2 : x2;
|
|
y1 = ly1 < y1 ? ly1 : y1;
|
|
y2 = ly2 > y2 ? ly2 : y2;
|
|
}
|
|
} // style enabled
|
|
} // for
|
|
|
|
var noninf = function(x){
|
|
if( x === Infinity || x === -Infinity ){
|
|
return 0;
|
|
}
|
|
|
|
return x;
|
|
};
|
|
|
|
x1 = noninf(x1);
|
|
x2 = noninf(x2);
|
|
y1 = noninf(y1);
|
|
y2 = noninf(y2);
|
|
|
|
return {
|
|
x1: x1,
|
|
x2: x2,
|
|
y1: y1,
|
|
y2: y2,
|
|
w: x2 - x1,
|
|
h: y2 - y1
|
|
};
|
|
}
|
|
});
|
|
|
|
// aliases
|
|
var fn = $$.elesfn;
|
|
fn.attr = fn.data;
|
|
fn.removeAttr = fn.removeData;
|
|
fn.modelPosition = fn.point = fn.position;
|
|
fn.modelPositions = fn.points = fn.positions;
|
|
fn.renderedPoint = fn.renderedPosition;
|
|
fn.relativePoint = fn.relativePosition;
|
|
fn.boundingbox = fn.boundingBox;
|
|
fn.renderedBoundingbox = fn.renderedBoundingBox;
|
|
|
|
})( cytoscape );
|
|
|
|
;(function( $$ ){ 'use strict';
|
|
|
|
// Regular degree functions (works on single element)
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function defineDegreeFunction(callback){
|
|
return function( includeLoops ){
|
|
var self = this;
|
|
|
|
if( includeLoops === undefined ){
|
|
includeLoops = true;
|
|
}
|
|
|
|
if( self.length === 0 ){ return; }
|
|
|
|
if( self.isNode() && !self.removed() ){
|
|
var degree = 0;
|
|
var node = self[0];
|
|
var connectedEdges = node._private.edges;
|
|
|
|
for( var i = 0; i < connectedEdges.length; i++ ){
|
|
var edge = connectedEdges[i];
|
|
|
|
if( !includeLoops && edge.isLoop() ){
|
|
continue;
|
|
}
|
|
|
|
degree += callback( node, edge );
|
|
}
|
|
|
|
return degree;
|
|
} else {
|
|
return;
|
|
}
|
|
};
|
|
}
|
|
|
|
$$.fn.eles({
|
|
degree: defineDegreeFunction(function(node, edge){
|
|
if( edge.source().same( edge.target() ) ){
|
|
return 2;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}),
|
|
|
|
indegree: defineDegreeFunction(function(node, edge){
|
|
if( edge.target().same(node) ){
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}),
|
|
|
|
outdegree: defineDegreeFunction(function(node, edge){
|
|
if( edge.source().same(node) ){
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
})
|
|
});
|
|
|
|
|
|
// Collection degree stats
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function defineDegreeBoundsFunction(degreeFn, callback){
|
|
return function( includeLoops ){
|
|
var ret;
|
|
var nodes = this.nodes();
|
|
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var ele = nodes[i];
|
|
var degree = ele[degreeFn]( includeLoops );
|
|
if( degree !== undefined && (ret === undefined || callback(degree, ret)) ){
|
|
ret = degree;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
}
|
|
|
|
$$.fn.eles({
|
|
minDegree: defineDegreeBoundsFunction('degree', function(degree, min){
|
|
return degree < min;
|
|
}),
|
|
|
|
maxDegree: defineDegreeBoundsFunction('degree', function(degree, max){
|
|
return degree > max;
|
|
}),
|
|
|
|
minIndegree: defineDegreeBoundsFunction('indegree', function(degree, min){
|
|
return degree < min;
|
|
}),
|
|
|
|
maxIndegree: defineDegreeBoundsFunction('indegree', function(degree, max){
|
|
return degree > max;
|
|
}),
|
|
|
|
minOutdegree: defineDegreeBoundsFunction('outdegree', function(degree, min){
|
|
return degree < min;
|
|
}),
|
|
|
|
maxOutdegree: defineDegreeBoundsFunction('outdegree', function(degree, max){
|
|
return degree > max;
|
|
})
|
|
});
|
|
|
|
$$.fn.eles({
|
|
totalDegree: function( includeLoops ){
|
|
var total = 0;
|
|
var nodes = this.nodes();
|
|
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
total += nodes[i].degree( includeLoops );
|
|
}
|
|
|
|
return total;
|
|
}
|
|
});
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// Functions for binding & triggering events
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
$$.fn.eles({
|
|
on: $$.define.on(), // .on( events [, selector] [, data], handler)
|
|
one: $$.define.on({ unbindSelfOnTrigger: true }),
|
|
once: $$.define.on({ unbindAllBindersOnTrigger: true }),
|
|
off: $$.define.off(), // .off( events [, selector] [, handler] )
|
|
trigger: $$.define.trigger(), // .trigger( events [, extraParams] )
|
|
|
|
rtrigger: function(event, extraParams){ // for internal use only
|
|
if( this.length === 0 ){ return; } // empty collections don't need to notify anything
|
|
|
|
// notify renderer
|
|
this.cy().notify({
|
|
type: event,
|
|
collection: this
|
|
});
|
|
|
|
this.trigger(event, extraParams);
|
|
return this;
|
|
}
|
|
});
|
|
|
|
// aliases:
|
|
$$.define.eventAliasesOn( $$.elesfn );
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.eles({
|
|
nodes: function( selector ){
|
|
return this.filter(function(i, element){
|
|
return element.isNode();
|
|
}).filter(selector);
|
|
},
|
|
|
|
edges: function( selector ){
|
|
return this.filter(function(i, element){
|
|
return element.isEdge();
|
|
}).filter(selector);
|
|
},
|
|
|
|
filter: function( filter ){
|
|
var cy = this._private.cy;
|
|
|
|
if( $$.is.fn(filter) ){
|
|
var elements = [];
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
|
|
if( filter.apply(ele, [i, ele]) ){
|
|
elements.push(ele);
|
|
}
|
|
}
|
|
|
|
return new $$.Collection(cy, elements);
|
|
|
|
} else if( $$.is.string(filter) || $$.is.elementOrCollection(filter) ){
|
|
return new $$.Selector(filter).filter(this);
|
|
|
|
} else if( filter === undefined ){
|
|
return this;
|
|
}
|
|
|
|
return new $$.Collection( cy ); // if not handled by above, give 'em an empty collection
|
|
},
|
|
|
|
not: function( toRemove ){
|
|
var cy = this._private.cy;
|
|
|
|
if( !toRemove ){
|
|
return this;
|
|
} else {
|
|
|
|
if( $$.is.string( toRemove ) ){
|
|
toRemove = this.filter( toRemove );
|
|
}
|
|
|
|
var elements = [];
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var element = this[i];
|
|
|
|
var remove = toRemove._private.ids[ element.id() ];
|
|
if( !remove ){
|
|
elements.push( element );
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( cy, elements );
|
|
}
|
|
|
|
},
|
|
|
|
absoluteComplement: function(){
|
|
var cy = this._private.cy;
|
|
|
|
return cy.elements().not( this );
|
|
},
|
|
|
|
intersect: function( other ){
|
|
var cy = this._private.cy;
|
|
|
|
// if a selector is specified, then filter by it instead
|
|
if( $$.is.string(other) ){
|
|
var selector = other;
|
|
return this.filter( selector );
|
|
}
|
|
|
|
var elements = [];
|
|
var col1 = this;
|
|
var col2 = other;
|
|
var col1Smaller = this.length < other.length;
|
|
// var ids1 = col1Smaller ? col1._private.ids : col2._private.ids;
|
|
var ids2 = col1Smaller ? col2._private.ids : col1._private.ids;
|
|
var col = col1Smaller ? col1 : col2;
|
|
|
|
for( var i = 0; i < col.length; i++ ){
|
|
var id = col[i]._private.data.id;
|
|
var ele = ids2[ id ];
|
|
|
|
if( ele ){
|
|
elements.push( ele );
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( cy, elements );
|
|
},
|
|
|
|
xor: function( other ){
|
|
var cy = this._private.cy;
|
|
|
|
if( $$.is.string(other) ){
|
|
other = cy.$( other );
|
|
}
|
|
|
|
var elements = [];
|
|
var col1 = this;
|
|
var col2 = other;
|
|
|
|
var add = function( col, other ){
|
|
|
|
for( var i = 0; i < col.length; i++ ){
|
|
var ele = col[i];
|
|
var id = ele._private.data.id;
|
|
var inOther = other._private.ids[ id ];
|
|
|
|
if( !inOther ){
|
|
elements.push( ele );
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
add( col1, col2 );
|
|
add( col2, col1 );
|
|
|
|
return new $$.Collection( cy, elements );
|
|
},
|
|
|
|
diff: function( other ){
|
|
var cy = this._private.cy;
|
|
|
|
if( $$.is.string(other) ){
|
|
other = cy.$( other );
|
|
}
|
|
|
|
var left = [];
|
|
var right = [];
|
|
var both = [];
|
|
var col1 = this;
|
|
var col2 = other;
|
|
|
|
var add = function( col, other, retEles ){
|
|
|
|
for( var i = 0; i < col.length; i++ ){
|
|
var ele = col[i];
|
|
var id = ele._private.data.id;
|
|
var inOther = other._private.ids[ id ];
|
|
|
|
if( inOther ){
|
|
both.push( ele );
|
|
} else {
|
|
retEles.push( ele );
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
add( col1, col2, left );
|
|
add( col2, col1, right );
|
|
|
|
return {
|
|
left: new $$.Collection( cy, left, { unique: true } ),
|
|
right: new $$.Collection( cy, right, { unique: true } ),
|
|
both: new $$.Collection( cy, both, { unique: true } )
|
|
};
|
|
},
|
|
|
|
add: function( toAdd ){
|
|
var cy = this._private.cy;
|
|
|
|
if( !toAdd ){
|
|
return this;
|
|
}
|
|
|
|
if( $$.is.string(toAdd) ){
|
|
var selector = toAdd;
|
|
toAdd = cy.elements(selector);
|
|
}
|
|
|
|
var elements = [];
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
elements.push( this[i] );
|
|
}
|
|
|
|
for( var i = 0; i < toAdd.length; i++ ){
|
|
|
|
var add = !this._private.ids[ toAdd[i].id() ];
|
|
if( add ){
|
|
elements.push( toAdd[i] );
|
|
}
|
|
}
|
|
|
|
return new $$.Collection(cy, elements);
|
|
},
|
|
|
|
// in place merge on calling collection
|
|
merge: function( toAdd ){
|
|
var _p = this._private;
|
|
var cy = _p.cy;
|
|
|
|
if( !toAdd ){
|
|
return this;
|
|
}
|
|
|
|
if( $$.is.string(toAdd) ){
|
|
var selector = toAdd;
|
|
toAdd = cy.elements(selector);
|
|
}
|
|
|
|
for( var i = 0; i < toAdd.length; i++ ){
|
|
var toAddEle = toAdd[i];
|
|
var id = toAddEle.id();
|
|
var add = !_p.ids[ id ];
|
|
|
|
if( add ){
|
|
var index = this.length++;
|
|
|
|
this[ index ] = toAddEle;
|
|
_p.ids[ id ] = toAddEle;
|
|
_p.indexes[ id ] = index;
|
|
}
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
// remove single ele in place in calling collection
|
|
unmergeOne: function( ele ){
|
|
ele = ele[0];
|
|
|
|
var _p = this._private;
|
|
var id = ele.id();
|
|
var i = _p.indexes[ id ];
|
|
|
|
if( i == null ){
|
|
return this; // no need to remove
|
|
}
|
|
|
|
// remove ele
|
|
this[i] = undefined;
|
|
_p.ids[ id ] = undefined;
|
|
_p.indexes[ id ] = undefined;
|
|
|
|
var unmergedLastEle = i === this.length - 1;
|
|
|
|
// replace empty spot with last ele in collection
|
|
if( this.length > 1 && !unmergedLastEle ){
|
|
var lastEleI = this.length - 1;
|
|
var lastEle = this[ lastEleI ];
|
|
|
|
this[ lastEleI ] = undefined;
|
|
this[i] = lastEle;
|
|
_p.indexes[ lastEle.id() ] = i;
|
|
}
|
|
|
|
// the collection is now 1 ele smaller
|
|
this.length--;
|
|
|
|
return this;
|
|
},
|
|
|
|
// remove eles in place on calling collection
|
|
unmerge: function( toRemove ){
|
|
var cy = this._private.cy;
|
|
|
|
if( !toRemove ){
|
|
return this;
|
|
}
|
|
|
|
if( $$.is.string(toRemove) ){
|
|
var selector = toRemove;
|
|
toRemove = cy.elements(selector);
|
|
}
|
|
|
|
for( var i = 0; i < toRemove.length; i++ ){
|
|
this.unmergeOne( toRemove[i] );
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
map: function( mapFn, thisArg ){
|
|
var arr = [];
|
|
var eles = this;
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
var ret = thisArg ? mapFn.apply( thisArg, [ele, i, eles] ) : mapFn( ele, i, eles );
|
|
|
|
arr.push( ret );
|
|
}
|
|
|
|
return arr;
|
|
},
|
|
|
|
stdFilter: function( fn, thisArg ){
|
|
var filterEles = [];
|
|
var eles = this;
|
|
var cy = this._private.cy;
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
var include = thisArg ? fn.apply( thisArg, [ele, i, eles] ) : fn( ele, i, eles );
|
|
|
|
if( include ){
|
|
filterEles.push( ele );
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( cy, filterEles );
|
|
},
|
|
|
|
max: function( valFn, thisArg ){
|
|
var max = -Infinity;
|
|
var maxEle;
|
|
var eles = this;
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
var val = thisArg ? valFn.apply( thisArg, [ ele, i, eles ] ) : valFn( ele, i, eles );
|
|
|
|
if( val > max ){
|
|
max = val;
|
|
maxEle = ele;
|
|
}
|
|
}
|
|
|
|
return {
|
|
value: max,
|
|
ele: maxEle
|
|
};
|
|
},
|
|
|
|
min: function( valFn, thisArg ){
|
|
var min = Infinity;
|
|
var minEle;
|
|
var eles = this;
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
var val = thisArg ? valFn.apply( thisArg, [ ele, i, eles ] ) : valFn( ele, i, eles );
|
|
|
|
if( val < min ){
|
|
min = val;
|
|
minEle = ele;
|
|
}
|
|
}
|
|
|
|
return {
|
|
value: min,
|
|
ele: minEle
|
|
};
|
|
}
|
|
});
|
|
|
|
// aliases
|
|
var fn = $$.elesfn;
|
|
fn['u'] = fn['|'] = fn['+'] = fn.union = fn.or = fn.add;
|
|
fn['\\'] = fn['!'] = fn['-'] = fn.difference = fn.relativeComplement = fn.not;
|
|
fn['n'] = fn['&'] = fn['.'] = fn.and = fn.intersection = fn.intersect;
|
|
fn['^'] = fn['(+)'] = fn['(-)'] = fn.symmetricDifference = fn.symdiff = fn.xor;
|
|
fn.fnFilter = fn.filterFn = fn.stdFilter;
|
|
fn.complement = fn.abscomp = fn.absoluteComplement;
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.eles({
|
|
isNode: function(){
|
|
return this.group() === 'nodes';
|
|
},
|
|
|
|
isEdge: function(){
|
|
return this.group() === 'edges';
|
|
},
|
|
|
|
isLoop: function(){
|
|
return this.isEdge() && this.source().id() === this.target().id();
|
|
},
|
|
|
|
isSimple: function(){
|
|
return this.isEdge() && this.source().id() !== this.target().id();
|
|
},
|
|
|
|
group: function(){
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
return ele._private.group;
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// Functions for iterating over collections
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
$$.fn.eles({
|
|
each: function(fn){
|
|
if( $$.is.fn(fn) ){
|
|
for(var i = 0; i < this.length; i++){
|
|
var ele = this[i];
|
|
var ret = fn.apply( ele, [ i, ele ] );
|
|
|
|
if( ret === false ){ break; } // exit each early on return false
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
forEach: function(fn, thisArg){
|
|
if( $$.is.fn(fn) ){
|
|
|
|
for(var i = 0; i < this.length; i++){
|
|
var ele = this[i];
|
|
var ret = thisArg ? fn.apply( thisArg, [ ele, i, this ] ) : fn( ele, i, this );
|
|
|
|
if( ret === false ){ break; } // exit each early on return false
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
toArray: function(){
|
|
var array = [];
|
|
|
|
for(var i = 0; i < this.length; i++){
|
|
array.push( this[i] );
|
|
}
|
|
|
|
return array;
|
|
},
|
|
|
|
slice: function(start, end){
|
|
var array = [];
|
|
var thisSize = this.length;
|
|
|
|
if( end == null ){
|
|
end = thisSize;
|
|
}
|
|
|
|
if( start == null ){
|
|
start = 0;
|
|
}
|
|
|
|
if( start < 0 ){
|
|
start = thisSize + start;
|
|
}
|
|
|
|
if( end < 0 ){
|
|
end = thisSize + end;
|
|
}
|
|
|
|
for(var i = start; i >= 0 && i < end && i < thisSize; i++){
|
|
array.push( this[i] );
|
|
}
|
|
|
|
return new $$.Collection(this.cy(), array);
|
|
},
|
|
|
|
size: function(){
|
|
return this.length;
|
|
},
|
|
|
|
eq: function(i){
|
|
return this[i] || new $$.Collection( this.cy() );
|
|
},
|
|
|
|
first: function(){
|
|
return this[0] || new $$.Collection( this.cy() );
|
|
},
|
|
|
|
last: function(){
|
|
return this[ this.length - 1 ] || new $$.Collection( this.cy() );
|
|
},
|
|
|
|
empty: function(){
|
|
return this.length === 0;
|
|
},
|
|
|
|
nonempty: function(){
|
|
return !this.empty();
|
|
},
|
|
|
|
sort: function( sortFn ){
|
|
if( !$$.is.fn( sortFn ) ){
|
|
return this;
|
|
}
|
|
|
|
var cy = this.cy();
|
|
var sorted = this.toArray().sort( sortFn );
|
|
|
|
return new $$.Collection(cy, sorted);
|
|
},
|
|
|
|
sortByZIndex: function(){
|
|
return this.sort( $$.Collection.zIndexSort );
|
|
},
|
|
|
|
zDepth: function(){
|
|
var ele = this[0];
|
|
if( !ele ){ return undefined; }
|
|
|
|
// var cy = ele.cy();
|
|
var _p = ele._private;
|
|
var group = _p.group;
|
|
|
|
if( group === 'nodes' ){
|
|
var depth = _p.data.parent ? ele.parents().size() : 0;
|
|
|
|
if( !ele.isParent() ){
|
|
return Number.MAX_VALUE; // childless nodes always on top
|
|
}
|
|
|
|
return depth;
|
|
} else {
|
|
var src = _p.source;
|
|
var tgt = _p.target;
|
|
var srcDepth = src.zDepth();
|
|
var tgtDepth = tgt.zDepth();
|
|
|
|
return Math.max( srcDepth, tgtDepth, 0 ); // depth of deepest parent
|
|
}
|
|
}
|
|
});
|
|
|
|
$$.Collection.zIndexSort = function(a, b){
|
|
var cy = a.cy();
|
|
var a_p = a._private;
|
|
var b_p = b._private;
|
|
var zDiff = a_p.style['z-index'].value - b_p.style['z-index'].value;
|
|
var depthA = 0;
|
|
var depthB = 0;
|
|
var hasCompoundNodes = cy.hasCompoundNodes();
|
|
var aIsNode = a_p.group === 'nodes';
|
|
var aIsEdge = a_p.group === 'edges';
|
|
var bIsNode = b_p.group === 'nodes';
|
|
var bIsEdge = b_p.group === 'edges';
|
|
|
|
// no need to calculate element depth if there is no compound node
|
|
if( hasCompoundNodes ){
|
|
depthA = a.zDepth();
|
|
depthB = b.zDepth();
|
|
}
|
|
|
|
var depthDiff = depthA - depthB;
|
|
var sameDepth = depthDiff === 0;
|
|
|
|
if( sameDepth ){
|
|
|
|
if( aIsNode && bIsEdge ){
|
|
return 1; // 'a' is a node, it should be drawn later
|
|
|
|
} else if( aIsEdge && bIsNode ){
|
|
return -1; // 'a' is an edge, it should be drawn first
|
|
|
|
} else { // both nodes or both edges
|
|
if( zDiff === 0 ){ // same z-index => compare indices in the core (order added to graph w/ last on top)
|
|
return a_p.index - b_p.index;
|
|
} else {
|
|
return zDiff;
|
|
}
|
|
}
|
|
|
|
// elements on different level
|
|
} else {
|
|
return depthDiff; // deeper element should be drawn later
|
|
}
|
|
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// Functions for layouts on nodes
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
$$.fn.eles({
|
|
|
|
// using standard layout options, apply position function (w/ or w/o animation)
|
|
layoutPositions: function( layout, options, fn ){
|
|
var nodes = this.nodes();
|
|
var cy = this.cy();
|
|
|
|
layout.trigger({ type: 'layoutstart', layout: layout });
|
|
|
|
if( options.animate ){
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var node = nodes[i];
|
|
var lastNode = i === nodes.length - 1;
|
|
|
|
var newPos = fn.call( node, i, node );
|
|
var pos = node.position();
|
|
|
|
if( !$$.is.number(pos.x) || !$$.is.number(pos.y) ){
|
|
node.silentPosition({ x: 0, y: 0 });
|
|
}
|
|
|
|
node.animate({
|
|
position: newPos
|
|
}, {
|
|
duration: options.animationDuration,
|
|
step: !lastNode ? undefined : function(){
|
|
if( options.fit ){
|
|
cy.fit( options.eles, options.padding );
|
|
}
|
|
},
|
|
complete: !lastNode ? undefined : function(){
|
|
if( options.zoom != null ){
|
|
cy.zoom( options.zoom );
|
|
}
|
|
|
|
if( options.pan ){
|
|
cy.pan( options.pan );
|
|
}
|
|
|
|
if( options.fit ){
|
|
cy.fit( options.eles, options.padding );
|
|
}
|
|
|
|
layout.one('layoutstop', options.stop);
|
|
layout.trigger({ type: 'layoutstop', layout: layout });
|
|
}
|
|
});
|
|
}
|
|
|
|
layout.one('layoutready', options.ready);
|
|
layout.trigger({ type: 'layoutready', layout: layout });
|
|
} else {
|
|
nodes.positions( fn );
|
|
|
|
if( options.fit ){
|
|
cy.fit( options.eles, options.padding );
|
|
}
|
|
|
|
if( options.zoom != null ){
|
|
cy.zoom( options.zoom );
|
|
}
|
|
|
|
if( options.pan ){
|
|
cy.pan( options.pan );
|
|
}
|
|
|
|
layout.one('layoutready', options.ready);
|
|
layout.trigger({ type: 'layoutready', layout: layout });
|
|
|
|
layout.one('layoutstop', options.stop);
|
|
layout.trigger({ type: 'layoutstop', layout: layout });
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
layout: function( options ){
|
|
var cy = this.cy();
|
|
|
|
cy.layout( $$.util.extend({}, options, {
|
|
eles: this
|
|
}) );
|
|
|
|
return this;
|
|
},
|
|
|
|
makeLayout: function( options ){
|
|
var cy = this.cy();
|
|
|
|
return cy.makeLayout( $$.util.extend({}, options, {
|
|
eles: this
|
|
}) );
|
|
}
|
|
|
|
});
|
|
|
|
// aliases:
|
|
$$.elesfn.createLayout = $$.elesfn.makeLayout;
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.eles({
|
|
|
|
// fully updates (recalculates) the style for the elements
|
|
updateStyle: function( notifyRenderer ){
|
|
var cy = this._private.cy;
|
|
|
|
if( !cy.styleEnabled() ){ return this; }
|
|
|
|
if( cy._private.batchingStyle ){
|
|
var bEles = cy._private.batchStyleEles;
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
|
|
if( !bEles.ids[ ele._private.id ] ){
|
|
bEles.push( ele );
|
|
}
|
|
}
|
|
|
|
return this; // chaining and exit early when batching
|
|
}
|
|
|
|
var style = cy.style();
|
|
notifyRenderer = notifyRenderer || notifyRenderer === undefined ? true : false;
|
|
|
|
style.apply( this );
|
|
|
|
var updatedCompounds = this.updateCompoundBounds();
|
|
var toNotify = updatedCompounds.length > 0 ? this.add( updatedCompounds ) : this;
|
|
|
|
if( notifyRenderer ){
|
|
toNotify.rtrigger('style'); // let renderer know we changed style
|
|
} else {
|
|
toNotify.trigger('style'); // just fire the event
|
|
}
|
|
return this; // chaining
|
|
},
|
|
|
|
// just update the mappers in the elements' styles; cheaper than eles.updateStyle()
|
|
updateMappers: function( notifyRenderer ){
|
|
var cy = this._private.cy;
|
|
var style = cy.style();
|
|
notifyRenderer = notifyRenderer || notifyRenderer === undefined ? true : false;
|
|
|
|
if( !cy.styleEnabled() ){ return this; }
|
|
|
|
style.updateMappers( this );
|
|
|
|
var updatedCompounds = this.updateCompoundBounds();
|
|
var toNotify = updatedCompounds.length > 0 ? this.add( updatedCompounds ) : this;
|
|
|
|
if( notifyRenderer ){
|
|
toNotify.rtrigger('style'); // let renderer know we changed style
|
|
} else {
|
|
toNotify.trigger('style'); // just fire the event
|
|
}
|
|
return this; // chaining
|
|
},
|
|
|
|
// get the specified css property as a rendered value (i.e. on-screen value)
|
|
// or get the whole rendered style if no property specified (NB doesn't allow setting)
|
|
renderedCss: function( property ){
|
|
var cy = this.cy();
|
|
if( !cy.styleEnabled() ){ return this; }
|
|
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
var renstyle = ele.cy().style().getRenderedStyle( ele );
|
|
|
|
if( property === undefined ){
|
|
return renstyle;
|
|
} else {
|
|
return renstyle[ property ];
|
|
}
|
|
}
|
|
},
|
|
|
|
// read the calculated css style of the element or override the style (via a bypass)
|
|
css: function( name, value ){
|
|
var cy = this.cy();
|
|
|
|
if( !cy.styleEnabled() ){ return this; }
|
|
|
|
var updateTransitions = false;
|
|
var style = cy.style();
|
|
|
|
if( $$.is.plainObject(name) ){ // then extend the bypass
|
|
var props = name;
|
|
style.applyBypass( this, props, updateTransitions );
|
|
|
|
var updatedCompounds = this.updateCompoundBounds();
|
|
var toNotify = updatedCompounds.length > 0 ? this.add( updatedCompounds ) : this;
|
|
toNotify.rtrigger('style'); // let the renderer know we've updated style
|
|
|
|
} else if( $$.is.string(name) ){
|
|
|
|
if( value === undefined ){ // then get the property from the style
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
return ele._private.style[ name ].strValue;
|
|
} else { // empty collection => can't get any value
|
|
return;
|
|
}
|
|
|
|
} else { // then set the bypass with the property value
|
|
style.applyBypass( this, name, value, updateTransitions );
|
|
|
|
var updatedCompounds = this.updateCompoundBounds();
|
|
var toNotify = updatedCompounds.length > 0 ? this.add( updatedCompounds ) : this;
|
|
toNotify.rtrigger('style'); // let the renderer know we've updated style
|
|
}
|
|
|
|
} else if( name === undefined ){
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
return style.getRawStyle( ele );
|
|
} else { // empty collection => can't get any value
|
|
return;
|
|
}
|
|
}
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
removeCss: function( names ){
|
|
var cy = this.cy();
|
|
|
|
if( !cy.styleEnabled() ){ return this; }
|
|
|
|
var updateTransitions = false;
|
|
var style = cy.style();
|
|
var eles = this;
|
|
|
|
if( names === undefined ){
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
|
|
style.removeAllBypasses( ele, updateTransitions );
|
|
}
|
|
} else {
|
|
names = names.split(/\s+/);
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
|
|
style.removeBypasses( ele, names, updateTransitions );
|
|
}
|
|
}
|
|
|
|
var updatedCompounds = this.updateCompoundBounds();
|
|
var toNotify = updatedCompounds.length > 0 ? this.add( updatedCompounds ) : this;
|
|
toNotify.rtrigger('style'); // let the renderer know we've updated style
|
|
|
|
return this; // chaining
|
|
},
|
|
|
|
show: function(){
|
|
this.css('display', 'element');
|
|
return this; // chaining
|
|
},
|
|
|
|
hide: function(){
|
|
this.css('display', 'none');
|
|
return this; // chaining
|
|
},
|
|
|
|
visible: function(){
|
|
var cy = this.cy();
|
|
if( !cy.styleEnabled() ){ return true; }
|
|
|
|
var ele = this[0];
|
|
var hasCompoundNodes = cy.hasCompoundNodes();
|
|
|
|
if( ele ){
|
|
var style = ele._private.style;
|
|
|
|
if(
|
|
style['visibility'].value !== 'visible'
|
|
|| style['display'].value !== 'element'
|
|
){
|
|
return false;
|
|
}
|
|
|
|
if( ele._private.group === 'nodes' ){
|
|
if( !hasCompoundNodes ){ return true; }
|
|
|
|
var parents = ele._private.data.parent ? ele.parents() : null;
|
|
|
|
if( parents ){
|
|
for( var i = 0; i < parents.length; i++ ){
|
|
var parent = parents[i];
|
|
var pStyle = parent._private.style;
|
|
var pVis = pStyle['visibility'].value;
|
|
var pDis = pStyle['display'].value;
|
|
|
|
if( pVis !== 'visible' || pDis !== 'element' ){
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
var src = ele._private.source;
|
|
var tgt = ele._private.target;
|
|
|
|
return src.visible() && tgt.visible();
|
|
}
|
|
|
|
}
|
|
},
|
|
|
|
hidden: function(){
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
return !ele.visible();
|
|
}
|
|
},
|
|
|
|
effectiveOpacity: function(){
|
|
var cy = this.cy();
|
|
if( !cy.styleEnabled() ){ return 1; }
|
|
|
|
var hasCompoundNodes = cy.hasCompoundNodes();
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
var _p = ele._private;
|
|
var parentOpacity = _p.style.opacity.value;
|
|
|
|
if( !hasCompoundNodes ){ return parentOpacity; }
|
|
|
|
var parents = !_p.data.parent ? null : ele.parents();
|
|
|
|
if( parents ){
|
|
for( var i = 0; i < parents.length; i++ ){
|
|
var parent = parents[i];
|
|
var opacity = parent._private.style.opacity.value;
|
|
|
|
parentOpacity = opacity * parentOpacity;
|
|
}
|
|
}
|
|
|
|
return parentOpacity;
|
|
}
|
|
},
|
|
|
|
transparent: function(){
|
|
var cy = this.cy();
|
|
if( !cy.styleEnabled() ){ return false; }
|
|
|
|
var ele = this[0];
|
|
var hasCompoundNodes = ele.cy().hasCompoundNodes();
|
|
|
|
if( ele ){
|
|
if( !hasCompoundNodes ){
|
|
return ele._private.style.opacity.value === 0;
|
|
} else {
|
|
return ele.effectiveOpacity() === 0;
|
|
}
|
|
}
|
|
},
|
|
|
|
isFullAutoParent: function(){
|
|
var cy = this.cy();
|
|
if( !cy.styleEnabled() ){ return false; }
|
|
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
var autoW = ele._private.style['width'].value === 'auto';
|
|
var autoH = ele._private.style['height'].value === 'auto';
|
|
|
|
return ele.isParent() && autoW && autoH;
|
|
}
|
|
},
|
|
|
|
backgrounding: function(){
|
|
var cy = this.cy();
|
|
if( !cy.styleEnabled() ){ return false; }
|
|
|
|
var ele = this[0];
|
|
|
|
return ele._private.backgrounding ? true : false;
|
|
}
|
|
|
|
});
|
|
|
|
|
|
$$.elesfn.bypass = $$.elesfn.style = $$.elesfn.css;
|
|
$$.elesfn.renderedStyle = $$.elesfn.renderedCss;
|
|
$$.elesfn.removeBypass = $$.elesfn.removeStyle = $$.elesfn.removeCss;
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// Collection functions that toggle a boolean value
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
function defineSwitchFunction(params){
|
|
return function(){
|
|
var args = arguments;
|
|
var changedEles = [];
|
|
|
|
// e.g. cy.nodes().select( data, handler )
|
|
if( args.length === 2 ){
|
|
var data = args[0];
|
|
var handler = args[1];
|
|
this.bind( params.event, data, handler );
|
|
}
|
|
|
|
// e.g. cy.nodes().select( handler )
|
|
else if( args.length === 1 ){
|
|
var handler = args[0];
|
|
this.bind( params.event, handler );
|
|
}
|
|
|
|
// e.g. cy.nodes().select()
|
|
else if( args.length === 0 ){
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
var able = !params.ableField || ele._private[params.ableField];
|
|
var changed = ele._private[params.field] != params.value;
|
|
|
|
if( params.overrideAble ){
|
|
var overrideAble = params.overrideAble(ele);
|
|
|
|
if( overrideAble !== undefined ){
|
|
able = overrideAble;
|
|
|
|
if( !overrideAble ){ return this; } // to save cycles assume not able for all on override
|
|
}
|
|
}
|
|
|
|
if( able ){
|
|
ele._private[params.field] = params.value;
|
|
|
|
if( changed ){
|
|
changedEles.push( ele );
|
|
}
|
|
}
|
|
}
|
|
|
|
var changedColl = $$.Collection( this.cy(), changedEles );
|
|
changedColl.updateStyle(); // change of state => possible change of style
|
|
changedColl.trigger( params.event );
|
|
}
|
|
|
|
return this;
|
|
};
|
|
}
|
|
|
|
function defineSwitchSet( params ){
|
|
$$.elesfn[ params.field ] = function(){
|
|
var ele = this[0];
|
|
|
|
if( ele ){
|
|
if( params.overrideField ){
|
|
var val = params.overrideField(ele);
|
|
|
|
if( val !== undefined ){
|
|
return val;
|
|
}
|
|
}
|
|
|
|
return ele._private[ params.field ];
|
|
}
|
|
};
|
|
|
|
$$.elesfn[ params.on ] = defineSwitchFunction({
|
|
event: params.on,
|
|
field: params.field,
|
|
ableField: params.ableField,
|
|
overrideAble: params.overrideAble,
|
|
value: true
|
|
});
|
|
|
|
$$.elesfn[ params.off ] = defineSwitchFunction({
|
|
event: params.off,
|
|
field: params.field,
|
|
ableField: params.ableField,
|
|
overrideAble: params.overrideAble,
|
|
value: false
|
|
});
|
|
}
|
|
|
|
defineSwitchSet({
|
|
field: 'locked',
|
|
overrideField: function(ele){
|
|
return ele.cy().autolock() ? true : undefined;
|
|
},
|
|
on: 'lock',
|
|
off: 'unlock'
|
|
});
|
|
|
|
defineSwitchSet({
|
|
field: 'grabbable',
|
|
overrideField: function(ele){
|
|
return ele.cy().autoungrabify() ? false : undefined;
|
|
},
|
|
on: 'grabify',
|
|
off: 'ungrabify'
|
|
});
|
|
|
|
defineSwitchSet({
|
|
field: 'selected',
|
|
ableField: 'selectable',
|
|
overrideAble: function(ele){
|
|
return ele.cy().autounselectify() ? false : undefined;
|
|
},
|
|
on: 'select',
|
|
off: 'unselect'
|
|
});
|
|
|
|
defineSwitchSet({
|
|
field: 'selectable',
|
|
overrideField: function(ele){
|
|
return ele.cy().autounselectify() ? false : undefined;
|
|
},
|
|
on: 'selectify',
|
|
off: 'unselectify'
|
|
});
|
|
|
|
$$.elesfn.deselect = $$.elesfn.unselect;
|
|
|
|
$$.elesfn.grabbed = function(){
|
|
var ele = this[0];
|
|
if( ele ){
|
|
return ele._private.grabbed;
|
|
}
|
|
};
|
|
|
|
defineSwitchSet({
|
|
field: 'active',
|
|
on: 'activate',
|
|
off: 'unactivate'
|
|
});
|
|
|
|
$$.elesfn.inactive = function(){
|
|
var ele = this[0];
|
|
if( ele ){
|
|
return !ele._private.active;
|
|
}
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// DAG functions
|
|
//////////////////////////
|
|
|
|
$$.fn.eles({
|
|
// get the root nodes in the DAG
|
|
roots: function( selector ){
|
|
var eles = this;
|
|
var roots = [];
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
if( !ele.isNode() ){
|
|
continue;
|
|
}
|
|
|
|
var hasEdgesPointingIn = ele.connectedEdges(function(){
|
|
return this.data('target') === ele.id() && this.data('source') !== ele.id();
|
|
}).length > 0;
|
|
|
|
if( !hasEdgesPointingIn ){
|
|
roots.push( ele );
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( this._private.cy, roots, { unique: true } ).filter( selector );
|
|
},
|
|
|
|
// get the leaf nodes in the DAG
|
|
leaves: function( selector ){
|
|
var eles = this;
|
|
var leaves = [];
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
if( !ele.isNode() ){
|
|
continue;
|
|
}
|
|
|
|
var hasEdgesPointingOut = ele.connectedEdges(function(){
|
|
return this.data('source') === ele.id() && this.data('target') !== ele.id();
|
|
}).length > 0;
|
|
|
|
if( !hasEdgesPointingOut ){
|
|
leaves.push( ele );
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( this._private.cy, leaves, { unique: true } ).filter( selector );
|
|
},
|
|
|
|
// normally called children in graph theory
|
|
// these nodes =edges=> outgoing nodes
|
|
outgoers: function( selector ){
|
|
var eles = this;
|
|
var oEles = [];
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
var eleId = ele.id();
|
|
|
|
if( !ele.isNode() ){ continue; }
|
|
|
|
var edges = ele._private.edges;
|
|
for( var j = 0; j < edges.length; j++ ){
|
|
var edge = edges[j];
|
|
var srcId = edge._private.data.source;
|
|
var tgtId = edge._private.data.target;
|
|
|
|
if( srcId === eleId && tgtId !== eleId ){
|
|
oEles.push( edge );
|
|
oEles.push( edge.target()[0] );
|
|
}
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( this._private.cy, oEles, { unique: true } ).filter( selector );
|
|
},
|
|
|
|
// aka DAG descendants
|
|
successors: function( selector ){
|
|
var eles = this;
|
|
var sEles = [];
|
|
var sElesIds = {};
|
|
|
|
for(;;){
|
|
var outgoers = eles.outgoers();
|
|
|
|
if( outgoers.length === 0 ){ break; } // done if no outgoers left
|
|
|
|
var newOutgoers = false;
|
|
for( var i = 0; i < outgoers.length; i++ ){
|
|
var outgoer = outgoers[i];
|
|
var outgoerId = outgoer.id();
|
|
|
|
if( !sElesIds[ outgoerId ] ){
|
|
sElesIds[ outgoerId ] = true;
|
|
sEles.push( outgoer );
|
|
newOutgoers = true;
|
|
}
|
|
}
|
|
|
|
if( !newOutgoers ){ break; } // done if touched all outgoers already
|
|
|
|
eles = outgoers;
|
|
}
|
|
|
|
return new $$.Collection( this._private.cy, sEles, { unique: true } ).filter( selector );
|
|
},
|
|
|
|
// normally called parents in graph theory
|
|
// these nodes <=edges= incoming nodes
|
|
incomers: function( selector ){
|
|
var eles = this;
|
|
var oEles = [];
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
var eleId = ele.id();
|
|
|
|
if( !ele.isNode() ){ continue; }
|
|
|
|
var edges = ele._private.edges;
|
|
for( var j = 0; j < edges.length; j++ ){
|
|
var edge = edges[j];
|
|
var srcId = edge._private.data.source;
|
|
var tgtId = edge._private.data.target;
|
|
|
|
if( tgtId === eleId && srcId !== eleId ){
|
|
oEles.push( edge );
|
|
oEles.push( edge.source()[0] );
|
|
}
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( this._private.cy, oEles, { unique: true } ).filter( selector );
|
|
},
|
|
|
|
// aka DAG ancestors
|
|
predecessors: function( selector ){
|
|
var eles = this;
|
|
var pEles = [];
|
|
var pElesIds = {};
|
|
|
|
for(;;){
|
|
var incomers = eles.incomers();
|
|
|
|
if( incomers.length === 0 ){ break; } // done if no incomers left
|
|
|
|
var newIncomers = false;
|
|
for( var i = 0; i < incomers.length; i++ ){
|
|
var incomer = incomers[i];
|
|
var incomerId = incomer.id();
|
|
|
|
if( !pElesIds[ incomerId ] ){
|
|
pElesIds[ incomerId ] = true;
|
|
pEles.push( incomer );
|
|
newIncomers = true;
|
|
}
|
|
}
|
|
|
|
if( !newIncomers ){ break; } // done if touched all incomers already
|
|
|
|
eles = incomers;
|
|
}
|
|
|
|
return new $$.Collection( this._private.cy, pEles, { unique: true } ).filter( selector );
|
|
}
|
|
});
|
|
|
|
|
|
// Neighbourhood functions
|
|
//////////////////////////
|
|
|
|
$$.fn.eles({
|
|
neighborhood: function(selector){
|
|
var elements = [];
|
|
var cy = this._private.cy;
|
|
var nodes = this.nodes();
|
|
|
|
for( var i = 0; i < nodes.length; i++ ){ // for all nodes
|
|
var node = nodes[i];
|
|
var connectedEdges = node.connectedEdges();
|
|
|
|
// for each connected edge, add the edge and the other node
|
|
for( var j = 0; j < connectedEdges.length; j++ ){
|
|
var edge = connectedEdges[j];
|
|
var otherNode = edge.connectedNodes().not(node);
|
|
|
|
// need check in case of loop
|
|
if( otherNode.length > 0 ){
|
|
elements.push( otherNode[0] ); // add node 1 hop away
|
|
}
|
|
|
|
// add connected edge
|
|
elements.push( edge[0] );
|
|
}
|
|
|
|
}
|
|
|
|
return ( new $$.Collection( cy, elements, { unique: true } ) ).filter( selector );
|
|
},
|
|
|
|
closedNeighborhood: function(selector){
|
|
return this.neighborhood().add( this ).filter( selector );
|
|
},
|
|
|
|
openNeighborhood: function(selector){
|
|
return this.neighborhood( selector );
|
|
}
|
|
});
|
|
|
|
// aliases
|
|
$$.elesfn.neighbourhood = $$.elesfn.neighborhood;
|
|
$$.elesfn.closedNeighbourhood = $$.elesfn.closedNeighborhood;
|
|
$$.elesfn.openNeighbourhood = $$.elesfn.openNeighborhood;
|
|
|
|
|
|
// Edge functions
|
|
/////////////////
|
|
|
|
$$.fn.eles({
|
|
source: function( selector ){
|
|
var ele = this[0];
|
|
var src;
|
|
|
|
if( ele ){
|
|
src = ele._private.source;
|
|
}
|
|
|
|
return src && selector ? src.filter( selector ) : src;
|
|
},
|
|
|
|
target: function( selector ){
|
|
var ele = this[0];
|
|
var tgt;
|
|
|
|
if( ele ){
|
|
tgt = ele._private.target;
|
|
}
|
|
|
|
return tgt && selector ? tgt.filter( selector ) : tgt;
|
|
},
|
|
|
|
sources: defineSourceFunction({
|
|
attr: 'source'
|
|
}),
|
|
|
|
targets: defineSourceFunction({
|
|
attr: 'target'
|
|
})
|
|
});
|
|
|
|
function defineSourceFunction( params ){
|
|
return function( selector ){
|
|
var sources = [];
|
|
var cy = this._private.cy;
|
|
|
|
for( var i = 0; i < this.length; i++ ){
|
|
var ele = this[i];
|
|
var src = ele._private[ params.attr ];
|
|
|
|
if( src ){
|
|
sources.push( src );
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( cy, sources, { unique: true } ).filter( selector );
|
|
};
|
|
}
|
|
|
|
$$.fn.eles({
|
|
edgesWith: defineEdgesWithFunction(),
|
|
|
|
edgesTo: defineEdgesWithFunction({
|
|
thisIs: 'source'
|
|
})
|
|
});
|
|
|
|
function defineEdgesWithFunction( params ){
|
|
|
|
return function(otherNodes){
|
|
var elements = [];
|
|
var cy = this._private.cy;
|
|
var p = params || {};
|
|
|
|
// get elements if a selector is specified
|
|
if( $$.is.string(otherNodes) ){
|
|
otherNodes = cy.$( otherNodes );
|
|
}
|
|
|
|
var thisIds = this._private.ids;
|
|
var otherIds = otherNodes._private.ids;
|
|
|
|
for( var h = 0; h < otherNodes.length; h++ ){
|
|
var edges = otherNodes[h]._private.edges;
|
|
|
|
for( var i = 0; i < edges.length; i++ ){
|
|
var edge = edges[i];
|
|
var foundId;
|
|
var edgeData = edge._private.data;
|
|
var thisToOther = thisIds[ edgeData.source ] && otherIds[ edgeData.target ];
|
|
var otherToThis = otherIds[ edgeData.source ] && thisIds[ edgeData.target ];
|
|
var edgeConnectsThisAndOther = thisToOther || otherToThis;
|
|
|
|
if( !edgeConnectsThisAndOther ){ continue; }
|
|
|
|
if( p.thisIs ){
|
|
if( p.thisIs === 'source' && !thisToOther ){ continue; }
|
|
|
|
if( p.thisIs === 'target' && !otherToThis ){ continue; }
|
|
}
|
|
|
|
elements.push( edge );
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( cy, elements, { unique: true } );
|
|
};
|
|
}
|
|
|
|
$$.fn.eles({
|
|
connectedEdges: function( selector ){
|
|
var retEles = [];
|
|
var cy = this._private.cy;
|
|
|
|
var eles = this;
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var node = eles[i];
|
|
if( !node.isNode() ){ continue; }
|
|
|
|
var edges = node._private.edges;
|
|
|
|
for( var j = 0; j < edges.length; j++ ){
|
|
var edge = edges[j];
|
|
retEles.push( edge );
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( cy, retEles, { unique: true } ).filter( selector );
|
|
},
|
|
|
|
connectedNodes: function( selector ){
|
|
var retEles = [];
|
|
var cy = this._private.cy;
|
|
|
|
var eles = this;
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var edge = eles[i];
|
|
if( !edge.isEdge() ){ continue; }
|
|
|
|
retEles.push( edge.source()[0] );
|
|
retEles.push( edge.target()[0] );
|
|
}
|
|
|
|
return new $$.Collection( cy, retEles, { unique: true } ).filter( selector );
|
|
},
|
|
|
|
parallelEdges: defineParallelEdgesFunction(),
|
|
|
|
codirectedEdges: defineParallelEdgesFunction({
|
|
codirected: true
|
|
})
|
|
});
|
|
|
|
function defineParallelEdgesFunction(params){
|
|
var defaults = {
|
|
codirected: false
|
|
};
|
|
params = $$.util.extend({}, defaults, params);
|
|
|
|
return function( selector ){
|
|
var cy = this._private.cy;
|
|
var elements = [];
|
|
var edges = this.edges();
|
|
var p = params;
|
|
|
|
// look at all the edges in the collection
|
|
for( var i = 0; i < edges.length; i++ ){
|
|
var edge1 = edges[i];
|
|
var src1 = edge1.source()[0];
|
|
var srcid1 = src1.id();
|
|
var tgt1 = edge1.target()[0];
|
|
var tgtid1 = tgt1.id();
|
|
var srcEdges1 = src1._private.edges;
|
|
|
|
// look at edges connected to the src node of this edge
|
|
for( var j = 0; j < srcEdges1.length; j++ ){
|
|
var edge2 = srcEdges1[j];
|
|
var edge2data = edge2._private.data;
|
|
var tgtid2 = edge2data.target;
|
|
var srcid2 = edge2data.source;
|
|
|
|
var codirected = tgtid2 === tgtid1 && srcid2 === srcid1;
|
|
var oppdirected = srcid1 === tgtid2 && tgtid1 === srcid2;
|
|
|
|
if( (p.codirected && codirected) || (!p.codirected && (codirected || oppdirected)) ){
|
|
elements.push( edge2 );
|
|
}
|
|
}
|
|
}
|
|
|
|
return new $$.Collection( cy, elements, { unique: true } ).filter( selector );
|
|
};
|
|
|
|
}
|
|
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
$$.fn.eles({
|
|
|
|
fit: function(){},
|
|
center: function(){}
|
|
|
|
});
|
|
|
|
})( cytoscape );
|
|
|
|
;(function ($$) {
|
|
"use strict";
|
|
|
|
/* Min and Max heap predefaults */
|
|
|
|
$$.Minheap = function (cy, eles, valueFn) {
|
|
return new $$.Heap(cy, eles, $$.Heap.minHeapComparator, valueFn);
|
|
};
|
|
|
|
$$.Maxheap = function (cy, eles, valueFn) {
|
|
return new $$.Heap(cy, eles, $$.Heap.maxHeapComparator, valueFn);
|
|
};
|
|
|
|
$$.Heap = function (cy, eles, comparator, valueFn) {
|
|
if (typeof comparator === "undefined" || typeof eles === "undefined") {
|
|
return;
|
|
}
|
|
|
|
if (typeof valueFn === "undefined") {
|
|
valueFn = $$.Heap.idFn;
|
|
}
|
|
|
|
var sourceHeap = [],
|
|
pointers = {},
|
|
elements = [],
|
|
i = 0,
|
|
id,
|
|
heap,
|
|
elesLen;
|
|
|
|
eles = this.getArgumentAsCollection(eles, cy);
|
|
elesLen = eles.length;
|
|
|
|
for (i = 0; i < elesLen; i += 1) {
|
|
sourceHeap.push(valueFn.call(cy, eles[i], i, eles));
|
|
|
|
id = eles[i].id();
|
|
|
|
if (pointers.hasOwnProperty(id)) {
|
|
throw "ERROR: Multiple items with the same id found: " + id;
|
|
}
|
|
|
|
pointers[id] = i;
|
|
elements.push(id);
|
|
}
|
|
|
|
this._private = {
|
|
cy: cy,
|
|
heap: sourceHeap,
|
|
pointers: pointers,
|
|
elements: elements,
|
|
comparator: comparator,
|
|
extractor: valueFn,
|
|
length: elesLen
|
|
};
|
|
|
|
for (i = Math.floor(elesLen / 2); i >= 0; i -= 1) {
|
|
heap = this.heapify(i);
|
|
}
|
|
|
|
return heap;
|
|
};
|
|
|
|
/* static methods */
|
|
$$.Heap.idFn = function (node) {
|
|
return node.id();
|
|
};
|
|
|
|
$$.Heap.minHeapComparator = function (a, b) {
|
|
return a >= b;
|
|
};
|
|
|
|
$$.Heap.maxHeapComparator = function (a, b) {
|
|
return a <= b;
|
|
};
|
|
|
|
$$.fn.heap = function( fnMap, options ){
|
|
for( var name in fnMap ){
|
|
var fn = fnMap[name];
|
|
$$.Heap.prototype[ name ] = fn;
|
|
}
|
|
};
|
|
|
|
$$.heapfn = $$.Heap.prototype; // short alias
|
|
|
|
/* object methods */
|
|
$$.heapfn.size = function () {
|
|
return this._private.length;
|
|
};
|
|
|
|
$$.heapfn.getArgumentAsCollection = function (eles, cy) {
|
|
var result;
|
|
if(typeof cy === "undefined") {
|
|
cy = this._private.cy;
|
|
}
|
|
|
|
if ($$.is.elementOrCollection(eles)) {
|
|
result = eles;
|
|
|
|
} else {
|
|
var resultArray = [],
|
|
sourceEles = [].concat.apply([], [eles]);
|
|
|
|
for (var i = 0; i < sourceEles.length; i++) {
|
|
var id = sourceEles[i],
|
|
ele = cy.getElementById(id);
|
|
|
|
if(ele.length > 0) {
|
|
resultArray.push(ele);
|
|
}
|
|
}
|
|
|
|
result = new $$.Collection(cy, resultArray);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
$$.heapfn.isHeap = function () {
|
|
var array = this._private.heap,
|
|
arrlen = array.length,
|
|
i,
|
|
left,
|
|
right,
|
|
lCheck,
|
|
rCheck,
|
|
comparator = this._private.comparator;
|
|
|
|
for (i = 0; i < arrlen; i += 1) {
|
|
left = 2 * i + 1;
|
|
right = left + 1;
|
|
lCheck = left < arrlen ? comparator(array[left], array[i]) : true;
|
|
rCheck = right < arrlen ? comparator(array[right], array[i]) : true;
|
|
|
|
if (!lCheck || !rCheck) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
$$.heapfn.heapSwap = function (i, j) {
|
|
var heap = this._private.heap,
|
|
pointers = this._private.pointers,
|
|
elements = this._private.elements,
|
|
swapValue = heap[i],
|
|
swapElems = elements[i],
|
|
idI = elements[i],
|
|
idJ = elements[j];
|
|
|
|
heap[i] = heap[j];
|
|
elements[i] = elements[j];
|
|
|
|
pointers[idI] = j;
|
|
pointers[idJ] = i;
|
|
|
|
heap[j] = swapValue;
|
|
elements[j] = swapElems;
|
|
};
|
|
|
|
$$.heapfn.heapify = function (i, rootToLeaf) {
|
|
var treeLen = 0,
|
|
condHeap = false,
|
|
array,
|
|
current,
|
|
left,
|
|
right,
|
|
best,
|
|
comparator,
|
|
parent;
|
|
|
|
if (typeof rootToLeaf === "undefined") {
|
|
rootToLeaf = true;
|
|
}
|
|
|
|
array = this._private.heap;
|
|
treeLen = array.length;
|
|
comparator = this._private.comparator;
|
|
current = i;
|
|
|
|
while (!condHeap) {
|
|
|
|
if (rootToLeaf) {
|
|
left = 2 * current + 1;
|
|
right = left + 1;
|
|
best = current;
|
|
|
|
if (left < treeLen && !comparator(array[left], array[best])) {
|
|
best = left;
|
|
}
|
|
|
|
if (right < treeLen && !comparator(array[right], array[best])) {
|
|
best = right;
|
|
}
|
|
|
|
condHeap = best === current;
|
|
|
|
if (!condHeap) {
|
|
this.heapSwap(best, current);
|
|
current = best;
|
|
}
|
|
|
|
} else {
|
|
parent = Math.floor((current - 1) / 2);
|
|
best = current;
|
|
condHeap = parent < 0 || comparator(array[best], array[parent]);
|
|
|
|
if (!condHeap) {
|
|
this.heapSwap(best, parent);
|
|
current = parent;
|
|
}
|
|
}
|
|
|
|
} // while
|
|
};
|
|
|
|
/* collectionOrElement */
|
|
$$.heapfn.insert = function (eles) {
|
|
var elements = this.getArgumentAsCollection(eles),
|
|
elsize = elements.length,
|
|
element,
|
|
elindex,
|
|
elvalue,
|
|
elid,
|
|
i;
|
|
|
|
for (i = 0; i < elsize; i += 1) {
|
|
element = elements[i];
|
|
elindex = this._private.heap.length;
|
|
elvalue = this._private.extractor(element);
|
|
elid = element.id();
|
|
|
|
if (this._private.pointers.hasOwnProperty(elid)) {
|
|
throw "ERROR: Multiple items with the same id found: " + elid;
|
|
}
|
|
|
|
this._private.heap.push(elvalue);
|
|
this._private.elements.push(elid);
|
|
this._private.pointers[elid] = elindex;
|
|
this.heapify(elindex, false);
|
|
}
|
|
|
|
this._private.length = this._private.heap.length;
|
|
};
|
|
|
|
$$.heapfn.getValueById = function (elementId) {
|
|
if (this._private.pointers.hasOwnProperty(elementId)) {
|
|
var elementIndex = this._private.pointers[elementId];
|
|
|
|
return this._private.heap[elementIndex];
|
|
}
|
|
};
|
|
|
|
$$.heapfn.contains = function (eles) {
|
|
var elements = this.getArgumentAsCollection(eles);
|
|
|
|
for (var i = 0; i < elements.length; i += 1) {
|
|
var elementId = elements[i].id();
|
|
|
|
if(!this._private.pointers.hasOwnProperty(elementId)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
$$.heapfn.top = function () {
|
|
if (this._private.length > 0) {
|
|
|
|
return {
|
|
value: this._private.heap[0],
|
|
id: this._private.elements[0]
|
|
};
|
|
|
|
}
|
|
};
|
|
|
|
$$.heapfn.pop = function () {
|
|
if (this._private.length > 0) {
|
|
var top = this.top(),
|
|
lastIndex = this._private.length - 1,
|
|
removeCandidate,
|
|
removeValue,
|
|
remId;
|
|
|
|
this.heapSwap(0, lastIndex);
|
|
|
|
removeCandidate = this._private.elements[lastIndex];
|
|
removeValue = this._private.heap[lastIndex];
|
|
remId = removeCandidate;
|
|
|
|
this._private.heap.pop();
|
|
this._private.elements.pop();
|
|
this._private.length = this._private.heap.length;
|
|
this._private.pointers[remId] = undefined;
|
|
|
|
this.heapify(0);
|
|
return top;
|
|
}
|
|
};
|
|
|
|
$$.heapfn.findDirectionHeapify = function (index) {
|
|
var parent = Math.floor((index - 1) / 2),
|
|
array = this._private.heap,
|
|
condHeap = parent < 0 || this._private.comparator(array[index], array[parent]);
|
|
|
|
this.heapify(index, condHeap);
|
|
};
|
|
|
|
/* edit is a new value or function */
|
|
// only values in heap are updated. elements themselves are not!
|
|
$$.heapfn.edit = function (eles, edit) {
|
|
var elements = this.getArgumentAsCollection(eles);
|
|
|
|
for (var i = 0; i < elements.length; i += 1) {
|
|
var elementId = elements[i].id(),
|
|
elementIndex = this._private.pointers[elementId],
|
|
elementValue = this._private.heap[elementIndex];
|
|
|
|
if ($$.is.number(edit)) {
|
|
this._private.heap[elementIndex] = edit;
|
|
|
|
} else if ($$.is.fn(edit)) {
|
|
this._private.heap[elementIndex] = edit.call(this._private.cy, elementValue, elementIndex);
|
|
}
|
|
|
|
this.findDirectionHeapify(elementIndex);
|
|
}
|
|
};
|
|
|
|
$$.heapfn.remove = function (eles) {
|
|
var elements = this.getArgumentAsCollection(eles);
|
|
|
|
for (var i = 0; i < elements.length; i += 1) {
|
|
var elementId = elements[i].id(),
|
|
elementIndex = this._private.pointers[elementId],
|
|
lastIndex = this._private.length - 1,
|
|
removeCandidate,
|
|
removeValue,
|
|
remId;
|
|
|
|
if (elementIndex !== lastIndex) {
|
|
this.heapSwap(elementIndex, lastIndex);
|
|
}
|
|
|
|
removeCandidate = this._private.elements[lastIndex];
|
|
removeValue = this._private.heap[lastIndex];
|
|
remId = removeCandidate;
|
|
|
|
this._private.heap.pop();
|
|
this._private.elements.pop();
|
|
this._private.length = this._private.heap.length;
|
|
this._private.pointers[remId] = undefined;
|
|
|
|
this.findDirectionHeapify(elementIndex);
|
|
}
|
|
|
|
return removeValue;
|
|
};
|
|
|
|
})(cytoscape);
|
|
|
|
/*
|
|
The canvas renderer was written by Yue Dong.
|
|
|
|
Modifications tracked on Github.
|
|
*/
|
|
|
|
(function($$) { 'use strict';
|
|
|
|
CanvasRenderer.CANVAS_LAYERS = 3;
|
|
//
|
|
CanvasRenderer.SELECT_BOX = 0;
|
|
CanvasRenderer.DRAG = 1;
|
|
CanvasRenderer.NODE = 2;
|
|
|
|
CanvasRenderer.BUFFER_COUNT = 3;
|
|
//
|
|
CanvasRenderer.TEXTURE_BUFFER = 0;
|
|
CanvasRenderer.MOTIONBLUR_BUFFER_NODE = 1;
|
|
CanvasRenderer.MOTIONBLUR_BUFFER_DRAG = 2;
|
|
|
|
function CanvasRenderer(options) {
|
|
|
|
this.options = options;
|
|
|
|
this.data = {
|
|
|
|
select: [undefined, undefined, undefined, undefined, 0], // Coordinates for selection box, plus enabled flag
|
|
renderer: this, cy: options.cy, container: options.cy.container(),
|
|
|
|
canvases: new Array(CanvasRenderer.CANVAS_LAYERS),
|
|
contexts: new Array(CanvasRenderer.CANVAS_LAYERS),
|
|
canvasNeedsRedraw: new Array(CanvasRenderer.CANVAS_LAYERS),
|
|
|
|
bufferCanvases: new Array(CanvasRenderer.BUFFER_COUNT),
|
|
bufferContexts: new Array(CanvasRenderer.CANVAS_LAYERS)
|
|
|
|
};
|
|
|
|
//--Pointer-related data
|
|
this.hoverData = {down: null, last: null,
|
|
downTime: null, triggerMode: null,
|
|
dragging: false,
|
|
initialPan: [null, null], capture: false};
|
|
|
|
this.timeoutData = {panTimeout: null};
|
|
|
|
this.dragData = {possibleDragElements: []};
|
|
|
|
this.touchData = {start: null, capture: false,
|
|
// These 3 fields related to tap, taphold events
|
|
startPosition: [null, null, null, null, null, null],
|
|
singleTouchStartTime: null,
|
|
singleTouchMoved: true,
|
|
|
|
|
|
now: [null, null, null, null, null, null],
|
|
earlier: [null, null, null, null, null, null] };
|
|
//--
|
|
|
|
//--Wheel-related data
|
|
this.zoomData = {freeToZoom: false, lastPointerX: null};
|
|
//--
|
|
|
|
this.redraws = 0;
|
|
this.showFps = options.showFps;
|
|
|
|
this.bindings = [];
|
|
|
|
this.data.canvasContainer = document.createElement('div');
|
|
var containerStyle = this.data.canvasContainer.style;
|
|
containerStyle.position = 'absolute';
|
|
containerStyle.zIndex = '0';
|
|
containerStyle.overflow = 'hidden';
|
|
|
|
this.data.container.appendChild( this.data.canvasContainer );
|
|
|
|
for (var i = 0; i < CanvasRenderer.CANVAS_LAYERS; i++) {
|
|
this.data.canvases[i] = document.createElement('canvas');
|
|
this.data.contexts[i] = this.data.canvases[i].getContext('2d');
|
|
this.data.canvases[i].style.position = 'absolute';
|
|
this.data.canvases[i].setAttribute('data-id', 'layer' + i);
|
|
this.data.canvases[i].style.zIndex = String(CanvasRenderer.CANVAS_LAYERS - i);
|
|
this.data.canvasContainer.appendChild(this.data.canvases[i]);
|
|
|
|
this.data.canvasNeedsRedraw[i] = false;
|
|
}
|
|
this.data.topCanvas = this.data.canvases[0];
|
|
|
|
this.data.canvases[CanvasRenderer.NODE].setAttribute('data-id', 'layer' + CanvasRenderer.NODE + '-node');
|
|
this.data.canvases[CanvasRenderer.SELECT_BOX].setAttribute('data-id', 'layer' + CanvasRenderer.SELECT_BOX + '-selectbox');
|
|
this.data.canvases[CanvasRenderer.DRAG].setAttribute('data-id', 'layer' + CanvasRenderer.DRAG + '-drag');
|
|
|
|
for (var i = 0; i < CanvasRenderer.BUFFER_COUNT; i++) {
|
|
this.data.bufferCanvases[i] = document.createElement('canvas');
|
|
this.data.bufferContexts[i] = this.data.bufferCanvases[i].getContext('2d');
|
|
this.data.bufferCanvases[i].style.position = 'absolute';
|
|
this.data.bufferCanvases[i].setAttribute('data-id', 'buffer' + i);
|
|
this.data.bufferCanvases[i].style.zIndex = String(-i - 1);
|
|
this.data.bufferCanvases[i].style.visibility = 'hidden';
|
|
//this.data.canvasContainer.appendChild(this.data.bufferCanvases[i]);
|
|
}
|
|
|
|
this.hideEdgesOnViewport = options.hideEdgesOnViewport;
|
|
this.hideLabelsOnViewport = options.hideLabelsOnViewport;
|
|
this.textureOnViewport = options.textureOnViewport;
|
|
this.wheelSensitivity = options.wheelSensitivity;
|
|
this.motionBlurEnabled = options.motionBlur; // on by default
|
|
this.forcedPixelRatio = options.pixelRatio;
|
|
this.motionBlur = true; // for initial kick off
|
|
this.motionBlurOpacity = options.motionBlurOpacity;
|
|
this.motionBlurTransparency = 1 - this.motionBlurOpacity;
|
|
this.motionBlurPxRatio = 1;
|
|
this.mbPxRBlurry = 1; //0.8;
|
|
this.minMbLowQualFrames = 4;
|
|
this.fullQualityMb = false;
|
|
this.clearedForMotionBlur = [];
|
|
this.desktopTapThreshold = options.desktopTapThreshold;
|
|
this.desktopTapThreshold2 = options.desktopTapThreshold * options.desktopTapThreshold;
|
|
this.touchTapThreshold = options.touchTapThreshold;
|
|
this.touchTapThreshold2 = options.touchTapThreshold * options.touchTapThreshold;
|
|
this.tapholdDuration = 500;
|
|
|
|
this.load();
|
|
}
|
|
|
|
CanvasRenderer.panOrBoxSelectDelay = 400;
|
|
|
|
// whether to use Path2D caching for drawing
|
|
var pathsImpld = typeof Path2D !== 'undefined';
|
|
CanvasRenderer.usePaths = function(){
|
|
return pathsImpld;
|
|
};
|
|
|
|
CanvasRenderer.prototype.notify = function(params) {
|
|
var types;
|
|
|
|
if( $$.is.array( params.type ) ){
|
|
types = params.type;
|
|
|
|
} else {
|
|
types = [ params.type ];
|
|
}
|
|
|
|
for( var i = 0; i < types.length; i++ ){
|
|
var type = types[i];
|
|
|
|
switch( type ){
|
|
case 'destroy':
|
|
this.destroy();
|
|
return;
|
|
|
|
case 'add':
|
|
case 'remove':
|
|
case 'load':
|
|
this.updateNodesCache();
|
|
this.updateEdgesCache();
|
|
break;
|
|
|
|
case 'viewport':
|
|
this.data.canvasNeedsRedraw[CanvasRenderer.SELECT_BOX] = true;
|
|
break;
|
|
|
|
case 'style':
|
|
this.updateCachedZSortedEles();
|
|
break;
|
|
}
|
|
|
|
if( type === 'load' || type === 'resize' ){
|
|
this.invalidateContainerClientCoordsCache();
|
|
this.matchCanvasSize(this.data.container);
|
|
}
|
|
} // for
|
|
|
|
this.data.canvasNeedsRedraw[CanvasRenderer.NODE] = true;
|
|
this.data.canvasNeedsRedraw[CanvasRenderer.DRAG] = true;
|
|
|
|
this.redraw();
|
|
};
|
|
|
|
CanvasRenderer.prototype.destroy = function(){
|
|
this.destroyed = true;
|
|
|
|
for( var i = 0; i < this.bindings.length; i++ ){
|
|
var binding = this.bindings[i];
|
|
var b = binding;
|
|
|
|
b.target.removeEventListener(b.event, b.handler, b.useCapture);
|
|
}
|
|
|
|
if( this.removeObserver ){
|
|
this.removeObserver.disconnect();
|
|
}
|
|
|
|
if( this.labelCalcDiv ){
|
|
try{
|
|
document.body.removeChild(this.labelCalcDiv);
|
|
} catch(e){
|
|
// ie10 issue #1014
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// copy the math functions into the renderer prototype
|
|
// unfortunately these functions are used interspersed t/o the code
|
|
// and this makes sure things work just in case a ref was missed in refactoring
|
|
// TODO remove this eventually
|
|
for( var fnName in $$.math ){
|
|
CanvasRenderer.prototype[ fnName ] = $$.math[ fnName ];
|
|
}
|
|
|
|
|
|
$$('renderer', 'canvas', CanvasRenderer);
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var rendFunc = CanvasRenderer.prototype;
|
|
var arrowShapes = CanvasRenderer.arrowShapes = {};
|
|
|
|
CanvasRenderer.arrowShapeHeight = 0.3;
|
|
|
|
// Contract for arrow shapes:
|
|
// 0, 0 is arrow tip
|
|
// (0, 1) is direction towards node
|
|
// (1, 0) is right
|
|
//
|
|
// functional api:
|
|
// collide: check x, y in shape
|
|
// roughCollide: called before collide, no false negatives
|
|
// draw: draw
|
|
// spacing: dist(arrowTip, nodeBoundary)
|
|
// gap: dist(edgeTip, nodeBoundary), edgeTip may != arrowTip
|
|
|
|
var bbCollide = function(x, y, centerX, centerY, width, height, direction, padding){
|
|
var x1 = centerX - width/2;
|
|
var x2 = centerX + width/2;
|
|
var y1 = centerY - height/2;
|
|
var y2 = centerY + height/2;
|
|
|
|
return (x1 <= x && x <= x2) && (y1 <= y && y <= y2);
|
|
};
|
|
|
|
var transform = function(x, y, size, angle, translation){
|
|
angle = -angle; // b/c of notation used in arrow draw fn
|
|
|
|
var xRotated = x * Math.cos(angle) - y * Math.sin(angle);
|
|
var yRotated = x * Math.sin(angle) + y * Math.cos(angle);
|
|
|
|
var xScaled = xRotated * size;
|
|
var yScaled = yRotated * size;
|
|
|
|
var xTranslated = xScaled + translation.x;
|
|
var yTranslated = yScaled + translation.y;
|
|
|
|
return {
|
|
x: xTranslated,
|
|
y: yTranslated
|
|
};
|
|
};
|
|
|
|
arrowShapes['arrow'] = {
|
|
_points: [
|
|
-0.15, -0.3,
|
|
0, 0,
|
|
0.15, -0.3
|
|
],
|
|
|
|
collide: function(x, y, centerX, centerY, width, height, direction, padding) {
|
|
var points = arrowShapes['arrow']._points;
|
|
|
|
// console.log("collide(): " + direction);
|
|
|
|
return $$.math.pointInsidePolygon(
|
|
x, y, points, centerX, centerY, width, height, direction, padding);
|
|
},
|
|
|
|
roughCollide: bbCollide,
|
|
|
|
draw: function(context, size, angle, translation) {
|
|
var points = arrowShapes['arrow']._points;
|
|
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
var pt = transform( points[i * 2], points[i * 2 + 1], size, angle, translation );
|
|
|
|
context.lineTo(pt.x, pt.y);
|
|
}
|
|
|
|
},
|
|
|
|
spacing: function(edge) {
|
|
return 0;
|
|
},
|
|
|
|
gap: function(edge) {
|
|
return edge._private.style['width'].pxValue * 2;
|
|
}
|
|
};
|
|
|
|
arrowShapes['triangle'] = arrowShapes['arrow'];
|
|
|
|
arrowShapes['triangle-backcurve'] = {
|
|
_ctrlPt: [ 0, -0.15 ],
|
|
|
|
collide: function(x, y, centerX, centerY, width, height, direction, padding) {
|
|
var points = arrowShapes['triangle']._points;
|
|
|
|
// console.log("collide(): " + direction);
|
|
|
|
return $$.math.pointInsidePolygon(
|
|
x, y, points, centerX, centerY, width, height, direction, padding);
|
|
},
|
|
|
|
roughCollide: bbCollide,
|
|
|
|
draw: function(context, size, angle, translation) {
|
|
var points = arrowShapes['triangle']._points;
|
|
var firstPt;
|
|
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
var pt = transform( points[i * 2], points[i * 2 + 1], size, angle, translation );
|
|
|
|
if( i === 0 ){
|
|
firstPt = pt;
|
|
}
|
|
|
|
context.lineTo(pt.x, pt.y);
|
|
}
|
|
|
|
var ctrlPt = this._ctrlPt;
|
|
var ctrlPtTrans = transform( ctrlPt[0], ctrlPt[1], size, angle, translation );
|
|
|
|
context.quadraticCurveTo( ctrlPtTrans.x, ctrlPtTrans.y, firstPt.x, firstPt.y );
|
|
},
|
|
|
|
spacing: function(edge) {
|
|
return 0;
|
|
},
|
|
|
|
gap: function(edge) {
|
|
return edge._private.style['width'].pxValue * 2;
|
|
}
|
|
};
|
|
|
|
|
|
arrowShapes['triangle-tee'] = {
|
|
_points: [
|
|
-0.15, -0.3,
|
|
0, 0,
|
|
0.15, -0.3,
|
|
-0.15, -0.3
|
|
],
|
|
|
|
_pointsTee: [
|
|
-0.15, -0.4,
|
|
-0.15, -0.5,
|
|
0.15, -0.5,
|
|
0.15, -0.4
|
|
],
|
|
|
|
collide: function(x, y, centerX, centerY, width, height, direction, padding) {
|
|
var triPts = arrowShapes['triangle-tee']._points;
|
|
var teePts = arrowShapes['triangle-tee']._pointsTee;
|
|
|
|
var inside = $$.math.pointInsidePolygon(x, y, teePts, centerX, centerY, width, height, direction, padding)
|
|
|| $$.math.pointInsidePolygon(x, y, triPts, centerX, centerY, width, height, direction, padding);
|
|
|
|
return inside;
|
|
},
|
|
|
|
roughCollide: bbCollide,
|
|
|
|
draw: function(context, size, angle, translation) {
|
|
var triPts = arrowShapes['triangle-tee']._points;
|
|
for (var i = 0; i < triPts.length / 2; i++){
|
|
var pt = transform( triPts[ i * 2 ], triPts[ i * 2 + 1 ], size, angle, translation );
|
|
|
|
context.lineTo( pt.x, pt.y );
|
|
}
|
|
|
|
var teePts = arrowShapes['triangle-tee']._pointsTee;
|
|
var firstTeePt = transform( teePts[0], teePts[1], size, angle, translation );
|
|
context.moveTo( firstTeePt.x, firstTeePt.y );
|
|
|
|
for (var i = 0; i < teePts.length / 2; i++){
|
|
var pt = transform( teePts[ i * 2 ], teePts[ i * 2 + 1 ], size, angle, translation );
|
|
|
|
context.lineTo( pt.x, pt.y );
|
|
}
|
|
},
|
|
|
|
spacing: function(edge) {
|
|
return 0;
|
|
},
|
|
|
|
gap: function(edge) {
|
|
return edge._private.style['width'].pxValue * 2;
|
|
}
|
|
};
|
|
|
|
arrowShapes['half-triangle-overshot'] = {
|
|
_points: [
|
|
0, -0.25,
|
|
-0.5, -0.25,
|
|
0.5, 0.25
|
|
],
|
|
|
|
leavePathOpen: true,
|
|
matchEdgeWidth: true,
|
|
|
|
collide: function(x, y, centerX, centerY, width, height, direction, padding) {
|
|
var points = this._points;
|
|
|
|
// console.log("collide(): " + direction);
|
|
|
|
return $$.math.pointInsidePolygon(
|
|
x, y, points, centerX, centerY, width, height, direction, padding);
|
|
},
|
|
|
|
roughCollide: bbCollide,
|
|
|
|
draw: function(context, size, angle, translation) {
|
|
var points = this._points;
|
|
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
var pt = transform( points[i * 2], points[i * 2 + 1], size, angle, translation );
|
|
|
|
context.lineTo(pt.x, pt.y);
|
|
}
|
|
},
|
|
|
|
spacing: function(edge) {
|
|
return 0;
|
|
},
|
|
|
|
gap: function(edge) {
|
|
return edge._private.style['width'].pxValue * 2;
|
|
}
|
|
};
|
|
|
|
arrowShapes['none'] = {
|
|
collide: function(x, y, centerX, centerY, width, height, direction, padding) {
|
|
return false;
|
|
},
|
|
|
|
roughCollide: function(x, y, centerX, centerY, width, height, direction, padding) {
|
|
return false;
|
|
},
|
|
|
|
draw: function(context) {
|
|
},
|
|
|
|
spacing: function(edge) {
|
|
return 0;
|
|
},
|
|
|
|
gap: function(edge) {
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
arrowShapes['circle'] = {
|
|
_baseRadius: 0.15,
|
|
|
|
collide: function(x, y, centerX, centerY, width, height, direction, padding) {
|
|
// Transform x, y to get non-rotated ellipse
|
|
|
|
if (width != height) {
|
|
var aspectRatio = (height + padding) / (width + padding);
|
|
y /= aspectRatio;
|
|
centerY /= aspectRatio;
|
|
|
|
return (Math.pow(centerX - x, 2)
|
|
+ Math.pow(centerY - y, 2) <= Math.pow((width + padding)
|
|
* arrowShapes['circle']._baseRadius, 2));
|
|
} else {
|
|
return (Math.pow(centerX - x, 2)
|
|
+ Math.pow(centerY - y, 2) <= Math.pow((width + padding)
|
|
* arrowShapes['circle']._baseRadius, 2));
|
|
}
|
|
},
|
|
|
|
roughCollide: bbCollide,
|
|
|
|
draw: function(context, size, angle, translation) {
|
|
context.arc(translation.x, translation.y, arrowShapes['circle']._baseRadius * size, 0, Math.PI * 2, false);
|
|
},
|
|
|
|
spacing: function(edge) {
|
|
return rendFunc.getArrowWidth(edge._private.style['width'].pxValue)
|
|
* arrowShapes['circle']._baseRadius;
|
|
},
|
|
|
|
gap: function(edge) {
|
|
return edge._private.style['width'].pxValue * 2;
|
|
}
|
|
};
|
|
|
|
arrowShapes['inhibitor'] = {
|
|
_points: [
|
|
-0.25, 0,
|
|
-0.25, -0.1,
|
|
0.25, -0.1,
|
|
0.25, 0
|
|
],
|
|
|
|
collide: function(x, y, centerX, centerY, width, height, direction, padding) {
|
|
var points = arrowShapes['inhibitor']._points;
|
|
|
|
return $$.math.pointInsidePolygon(
|
|
x, y, points, centerX, centerY, width, height, direction, padding);
|
|
},
|
|
|
|
roughCollide: bbCollide,
|
|
|
|
draw: function(context, size, angle, translation) {
|
|
var points = arrowShapes['inhibitor']._points;
|
|
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
var pt = transform( points[i * 2], points[i * 2 + 1], size, angle, translation );
|
|
|
|
context.lineTo(pt.x, pt.y);
|
|
}
|
|
},
|
|
|
|
spacing: function(edge) {
|
|
return 1;
|
|
},
|
|
|
|
gap: function(edge) {
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
arrowShapes['tee'] = arrowShapes['inhibitor'];
|
|
|
|
arrowShapes['square'] = {
|
|
_points: [
|
|
-0.15, 0.00,
|
|
0.15, 0.00,
|
|
0.15, -0.3,
|
|
-0.15, -0.3
|
|
],
|
|
|
|
collide: function(x, y, centerX, centerY, width, height, direction, padding) {
|
|
var points = arrowShapes['square']._points;
|
|
|
|
return $$.math.pointInsidePolygon(
|
|
x, y, points, centerX, centerY, width, height, direction, padding);
|
|
},
|
|
|
|
roughCollide: bbCollide,
|
|
|
|
draw: function(context, size, angle, translation) {
|
|
var points = arrowShapes['square']._points;
|
|
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
var pt = transform( points[i * 2], points[i * 2 + 1], size, angle, translation );
|
|
|
|
context.lineTo(pt.x, pt.y);
|
|
}
|
|
},
|
|
|
|
spacing: function(edge) {
|
|
return 0;
|
|
},
|
|
|
|
gap: function(edge) {
|
|
return edge._private.style['width'].pxValue * 2;
|
|
}
|
|
};
|
|
|
|
arrowShapes['diamond'] = {
|
|
_points: [
|
|
-0.15, -0.15,
|
|
0, -0.3,
|
|
0.15, -0.15,
|
|
0, 0
|
|
],
|
|
|
|
collide: function(x, y, centerX, centerY, width, height, direction, padding) {
|
|
var points = arrowShapes['diamond']._points;
|
|
|
|
return $$.math.pointInsidePolygon(
|
|
x, y, points, centerX, centerY, width, height, direction, padding);
|
|
},
|
|
|
|
roughCollide: bbCollide,
|
|
|
|
draw: function(context, size, angle, translation) {
|
|
var points = arrowShapes['diamond']._points;
|
|
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
var pt = transform( points[i * 2], points[i * 2 + 1], size, angle, translation );
|
|
|
|
context.lineTo(pt.x, pt.y);
|
|
}
|
|
},
|
|
|
|
spacing: function(edge) {
|
|
return 0;
|
|
},
|
|
|
|
gap: function(edge) {
|
|
return edge._private.style['width'].pxValue;
|
|
}
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var CRp = CanvasRenderer.prototype;
|
|
|
|
CRp.getCachedNodes = function() {
|
|
var data = this.data; var cy = this.data.cy;
|
|
|
|
if (data.cache == null) {
|
|
data.cache = {};
|
|
}
|
|
|
|
if (data.cache.cachedNodes == null) {
|
|
data.cache.cachedNodes = cy.nodes();
|
|
}
|
|
|
|
return data.cache.cachedNodes;
|
|
};
|
|
|
|
CRp.updateNodesCache = function() {
|
|
var data = this.data; var cy = this.data.cy;
|
|
|
|
if (data.cache == null) {
|
|
data.cache = {};
|
|
}
|
|
|
|
data.cache.cachedNodes = cy.nodes();
|
|
};
|
|
|
|
CRp.getCachedEdges = function() {
|
|
var data = this.data; var cy = this.data.cy;
|
|
|
|
if (data.cache == null) {
|
|
data.cache = {};
|
|
}
|
|
|
|
if (data.cache.cachedEdges == null) {
|
|
data.cache.cachedEdges = cy.edges();
|
|
}
|
|
|
|
return data.cache.cachedEdges;
|
|
};
|
|
|
|
CRp.updateEdgesCache = function() {
|
|
var data = this.data; var cy = this.data.cy;
|
|
|
|
if (data.cache == null) {
|
|
data.cache = {};
|
|
}
|
|
|
|
data.cache.cachedEdges = cy.edges();
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var CRp = CanvasRenderer.prototype;
|
|
|
|
// Project mouse
|
|
CRp.projectIntoViewport = function(clientX, clientY) {
|
|
var offsets = this.findContainerClientCoords();
|
|
var offsetLeft = offsets[0];
|
|
var offsetTop = offsets[1];
|
|
|
|
var x = clientX - offsetLeft;
|
|
var y = clientY - offsetTop;
|
|
|
|
x -= this.data.cy.pan().x; y -= this.data.cy.pan().y; x /= this.data.cy.zoom(); y /= this.data.cy.zoom();
|
|
return [x, y];
|
|
};
|
|
|
|
CRp.findContainerClientCoords = function() {
|
|
var container = this.data.container;
|
|
|
|
var bb = this.containerBB = this.containerBB || container.getBoundingClientRect();
|
|
|
|
return [bb.left, bb.top, bb.right - bb.left, bb.bottom - bb.top];
|
|
};
|
|
|
|
CRp.invalidateContainerClientCoordsCache = function(){
|
|
this.containerBB = null;
|
|
};
|
|
|
|
// Find nearest element
|
|
CRp.findNearestElement = function(x, y, visibleElementsOnly, isTouch){
|
|
var self = this;
|
|
var eles = this.getCachedZSortedEles();
|
|
var near = [];
|
|
var zoom = this.data.cy.zoom();
|
|
var hasCompounds = this.data.cy.hasCompoundNodes();
|
|
var edgeThreshold = (isTouch ? 24 : 8) / zoom;
|
|
var nodeThreshold = (isTouch ? 8 : 2) / zoom;
|
|
|
|
function checkNode(node){
|
|
var width = node.outerWidth() + 2*nodeThreshold;
|
|
var height = node.outerHeight() + 2*nodeThreshold;
|
|
var hw = width/2;
|
|
var hh = height/2;
|
|
var pos = node._private.position;
|
|
|
|
if(
|
|
pos.x - hw <= x && x <= pos.x + hw // bb check x
|
|
&&
|
|
pos.y - hh <= y && y <= pos.y + hh // bb check y
|
|
){
|
|
var visible = !visibleElementsOnly || ( node.visible() && !node.transparent() );
|
|
|
|
// exit early if invisible edge and must be visible
|
|
if( visibleElementsOnly && !visible ){
|
|
return;
|
|
}
|
|
|
|
var shape = CanvasRenderer.nodeShapes[ self.getNodeShape(node) ];
|
|
var borderWO = node._private.style['border-width'].pxValue / 2;
|
|
|
|
if(
|
|
shape.checkPoint(x, y, 0, width, height, pos.x, pos.y)
|
|
){
|
|
near.push( node );
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
function checkEdge(edge){
|
|
var rs = edge._private.rscratch;
|
|
var style = edge._private.style;
|
|
var width = style['width'].pxValue/2 + edgeThreshold; // more like a distance radius from centre
|
|
var widthSq = width * width;
|
|
var width2 = width * 2;
|
|
var src = edge._private.source;
|
|
var tgt = edge._private.target;
|
|
var inEdgeBB = false;
|
|
var sqDist;
|
|
|
|
// exit early if invisible edge and must be visible
|
|
var passedVisibilityCheck;
|
|
var passesVisibilityCheck = function(){
|
|
if( passedVisibilityCheck !== undefined ){
|
|
return passedVisibilityCheck;
|
|
}
|
|
|
|
if( !visibleElementsOnly ){
|
|
passedVisibilityCheck = true;
|
|
return true;
|
|
}
|
|
|
|
var visible = edge.visible() && !edge.transparent();
|
|
if( visible ){
|
|
passedVisibilityCheck = true;
|
|
return true;
|
|
}
|
|
|
|
passedVisibilityCheck = false;
|
|
return false;
|
|
};
|
|
|
|
if (rs.edgeType === 'self' || rs.edgeType === 'compound') {
|
|
if(
|
|
(
|
|
(inEdgeBB = $$.math.inBezierVicinity(x, y, rs.startX, rs.startY, rs.cp2ax, rs.cp2ay, rs.selfEdgeMidX, rs.selfEdgeMidY, widthSq))
|
|
&& passesVisibilityCheck() &&
|
|
( widthSq > (sqDist = $$.math.sqDistanceToQuadraticBezier(x, y, rs.startX, rs.startY, rs.cp2ax, rs.cp2ay, rs.selfEdgeMidX, rs.selfEdgeMidY)) )
|
|
)
|
|
||
|
|
(
|
|
(inEdgeBB = $$.math.inBezierVicinity(x, y, rs.selfEdgeMidX, rs.selfEdgeMidY, rs.cp2cx, rs.cp2cy, rs.endX, rs.endY, widthSq))
|
|
&& passesVisibilityCheck() &&
|
|
( widthSq > (sqDist = $$.math.sqDistanceToQuadraticBezier(x, y, rs.selfEdgeMidX, rs.selfEdgeMidY, rs.cp2cx, rs.cp2cy, rs.endX, rs.endY)) )
|
|
)
|
|
){
|
|
near.push( edge );
|
|
}
|
|
|
|
} else if (rs.edgeType === 'haystack') {
|
|
var radius = style['haystack-radius'].value;
|
|
var halfRadius = radius/2; // b/c have to half width/height
|
|
|
|
var tgtPos = tgt._private.position;
|
|
var tgtW = tgt.width();
|
|
var tgtH = tgt.height();
|
|
var srcPos = src._private.position;
|
|
var srcW = src.width();
|
|
var srcH = src.height();
|
|
|
|
var startX = srcPos.x + rs.source.x * srcW * halfRadius;
|
|
var startY = srcPos.y + rs.source.y * srcH * halfRadius;
|
|
var endX = tgtPos.x + rs.target.x * tgtW * halfRadius;
|
|
var endY = tgtPos.y + rs.target.y * tgtH * halfRadius;
|
|
|
|
if(
|
|
(inEdgeBB = $$.math.inLineVicinity(x, y, startX, startY, endX, endY, width2))
|
|
&& passesVisibilityCheck() &&
|
|
widthSq > ( sqDist = $$.math.sqDistanceToFiniteLine( x, y, startX, startY, endX, endY ) )
|
|
){
|
|
near.push( edge );
|
|
}
|
|
|
|
} else if (rs.edgeType === 'straight') {
|
|
if(
|
|
(inEdgeBB = $$.math.inLineVicinity(x, y, rs.startX, rs.startY, rs.endX, rs.endY, width2))
|
|
&& passesVisibilityCheck() &&
|
|
widthSq > ( sqDist = $$.math.sqDistanceToFiniteLine(x, y, rs.startX, rs.startY, rs.endX, rs.endY) )
|
|
){
|
|
near.push( edge );
|
|
}
|
|
|
|
} else if (rs.edgeType === 'bezier') {
|
|
if(
|
|
(inEdgeBB = $$.math.inBezierVicinity(x, y, rs.startX, rs.startY, rs.cp2x, rs.cp2y, rs.endX, rs.endY, widthSq))
|
|
&& passesVisibilityCheck() &&
|
|
(widthSq > (sqDist = $$.math.sqDistanceToQuadraticBezier(x, y, rs.startX, rs.startY, rs.cp2x, rs.cp2y, rs.endX, rs.endY)) )
|
|
){
|
|
near.push( edge );
|
|
}
|
|
}
|
|
|
|
// if we're close to the edge but didn't hit it, maybe we hit its arrows
|
|
if( inEdgeBB && passesVisibilityCheck() && near.length === 0 || near[near.length - 1] !== edge ){
|
|
var srcShape = CanvasRenderer.arrowShapes[ style['source-arrow-shape'].value ];
|
|
var tgtShape = CanvasRenderer.arrowShapes[ style['target-arrow-shape'].value ];
|
|
|
|
var src = src || edge._private.source;
|
|
var tgt = tgt || edge._private.target;
|
|
|
|
var tgtPos = tgt._private.position;
|
|
var srcPos = src._private.position;
|
|
|
|
var srcArW = self.getArrowWidth( style['width'].pxValue );
|
|
var srcArH = self.getArrowHeight( style['width'].pxValue );
|
|
|
|
var tgtArW = srcArW;
|
|
var tgtArH = srcArH;
|
|
|
|
if(
|
|
(
|
|
srcShape.roughCollide(x, y, rs.arrowStartX, rs.arrowStartY, srcArW, srcArH, [rs.arrowStartX - srcPos.x, rs.arrowStartY - srcPos.y], edgeThreshold)
|
|
&&
|
|
srcShape.collide(x, y, rs.arrowStartX, rs.arrowStartY, srcArW, srcArH, [rs.arrowStartX - srcPos.x, rs.arrowStartY - srcPos.y], edgeThreshold)
|
|
)
|
|
||
|
|
(
|
|
tgtShape.roughCollide(x, y, rs.arrowEndX, rs.arrowEndY, tgtArW, tgtArH, [rs.arrowEndX - tgtPos.x, rs.arrowEndY - tgtPos.y], edgeThreshold)
|
|
&&
|
|
tgtShape.collide(x, y, rs.arrowEndX, rs.arrowEndY, tgtArW, tgtArH, [rs.arrowEndX - tgtPos.x, rs.arrowEndY - tgtPos.y], edgeThreshold)
|
|
)
|
|
){
|
|
near.push( edge );
|
|
}
|
|
}
|
|
|
|
// for compound graphs, hitting edge may actually want a connected node instead (b/c edge may have greater z-index precedence)
|
|
if( hasCompounds && near.length > 0 && near[ near.length - 1 ] === edge ){
|
|
checkNode( src );
|
|
checkNode( tgt );
|
|
}
|
|
}
|
|
|
|
for( var i = eles.length - 1; i >= 0; i-- ){ // reverse order for precedence
|
|
var ele = eles[i];
|
|
|
|
if( near.length > 0 ){ break; } // since we check in z-order, first found is top and best result => exit early
|
|
|
|
if( ele._private.group === 'nodes' ){
|
|
checkNode( eles[i] );
|
|
|
|
} else { // then edge
|
|
checkEdge( eles[i] );
|
|
}
|
|
|
|
}
|
|
|
|
|
|
if( near.length > 0 ){
|
|
return near[ near.length - 1 ];
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// 'Give me everything from this box'
|
|
CRp.getAllInBox = function(x1, y1, x2, y2) {
|
|
var nodes = this.getCachedNodes();
|
|
var edges = this.getCachedEdges();
|
|
var box = [];
|
|
|
|
var x1c = Math.min(x1, x2);
|
|
var x2c = Math.max(x1, x2);
|
|
var y1c = Math.min(y1, y2);
|
|
var y2c = Math.max(y1, y2);
|
|
|
|
x1 = x1c;
|
|
x2 = x2c;
|
|
y1 = y1c;
|
|
y2 = y2c;
|
|
|
|
var heur;
|
|
|
|
for ( var i = 0; i < nodes.length; i++ ){
|
|
var pos = nodes[i]._private.position;
|
|
var nShape = this.getNodeShape(nodes[i]);
|
|
var w = this.getNodeWidth(nodes[i]);
|
|
var h = this.getNodeHeight(nodes[i]);
|
|
var border = nodes[i]._private.style['border-width'].pxValue / 2;
|
|
var shapeObj = CanvasRenderer.nodeShapes[ nShape ];
|
|
|
|
if ( shapeObj.intersectBox(x1, y1, x2, y2, w, h, pos.x, pos.y, border) ){
|
|
box.push(nodes[i]);
|
|
}
|
|
}
|
|
|
|
for ( var i = 0; i < edges.length; i++ ){
|
|
var rs = edges[i]._private.rscratch;
|
|
|
|
if (edges[i]._private.rscratch.edgeType == 'self') {
|
|
if ((heur = $$.math.boxInBezierVicinity(x1, y1, x2, y2,
|
|
rs.startX, rs.startY,
|
|
rs.cp2ax, rs.cp2ay,
|
|
rs.endX, rs.endY, edges[i]._private.style['width'].pxValue))
|
|
&&
|
|
(heur == 2 || (heur == 1 && $$.math.checkBezierInBox(x1, y1, x2, y2,
|
|
rs.startX, rs.startY,
|
|
rs.cp2ax, rs.cp2ay,
|
|
rs.endX, rs.endY, edges[i]._private.style['width'].pxValue)))
|
|
||
|
|
(heur = $$.math.boxInBezierVicinity(x1, y1, x2, y2,
|
|
rs.startX, rs.startY,
|
|
rs.cp2cx, rs.cp2cy,
|
|
rs.endX, rs.endY, edges[i]._private.style['width'].pxValue))
|
|
&&
|
|
(heur == 2 || (heur == 1 && $$.math.checkBezierInBox(x1, y1, x2, y2,
|
|
rs.startX, rs.startY,
|
|
rs.cp2cx, rs.cp2cy,
|
|
rs.endX, rs.endY, edges[i]._private.style['width'].pxValue)))
|
|
)
|
|
{ box.push(edges[i]); }
|
|
}
|
|
|
|
if (rs.edgeType == 'bezier' &&
|
|
(heur = $$.math.boxInBezierVicinity(x1, y1, x2, y2,
|
|
rs.startX, rs.startY,
|
|
rs.cp2x, rs.cp2y,
|
|
rs.endX, rs.endY, edges[i]._private.style['width'].pxValue))
|
|
&&
|
|
(heur == 2 || (heur == 1 && $$.math.checkBezierInBox(x1, y1, x2, y2,
|
|
rs.startX, rs.startY,
|
|
rs.cp2x, rs.cp2y,
|
|
rs.endX, rs.endY, edges[i]._private.style['width'].pxValue))))
|
|
{ box.push(edges[i]); }
|
|
|
|
if (rs.edgeType == 'straight' &&
|
|
(heur = $$.math.boxInBezierVicinity(x1, y1, x2, y2,
|
|
rs.startX, rs.startY,
|
|
rs.startX * 0.5 + rs.endX * 0.5,
|
|
rs.startY * 0.5 + rs.endY * 0.5,
|
|
rs.endX, rs.endY, edges[i]._private.style['width'].pxValue))
|
|
&& /* console.log('test', heur) == undefined && */
|
|
(heur == 2 || (heur == 1 && $$.math.checkStraightEdgeInBox(x1, y1, x2, y2,
|
|
rs.startX, rs.startY,
|
|
rs.endX, rs.endY, edges[i]._private.style['width'].pxValue))))
|
|
{ box.push(edges[i]); }
|
|
|
|
|
|
if (rs.edgeType == 'haystack'){
|
|
var tgt = edges[i].target()[0];
|
|
var tgtPos = tgt.position();
|
|
var src = edges[i].source()[0];
|
|
var srcPos = src.position();
|
|
|
|
var startX = srcPos.x + rs.source.x;
|
|
var startY = srcPos.y + rs.source.y;
|
|
var endX = tgtPos.x + rs.target.x;
|
|
var endY = tgtPos.y + rs.target.y;
|
|
|
|
var startInBox = (x1 <= startX && startX <= x2) && (y1 <= startY && startY <= y2);
|
|
var endInBox = (x1 <= endX && endX <= x2) && (y1 <= endY && endY <= y2);
|
|
|
|
if( startInBox && endInBox ){
|
|
box.push( edges[i] );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return box;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the width of the given node. If the width is set to auto,
|
|
* returns the value of the autoWidth field.
|
|
*
|
|
* @param node a node
|
|
* @return {number} width of the node
|
|
*/
|
|
CRp.getNodeWidth = function(node)
|
|
{
|
|
return node.width();
|
|
};
|
|
|
|
/**
|
|
* Returns the height of the given node. If the height is set to auto,
|
|
* returns the value of the autoHeight field.
|
|
*
|
|
* @param node a node
|
|
* @return {number} width of the node
|
|
*/
|
|
CRp.getNodeHeight = function(node)
|
|
{
|
|
return node.height();
|
|
};
|
|
|
|
/**
|
|
* Returns the shape of the given node. If the height or width of the given node
|
|
* is set to auto, the node is considered to be a compound.
|
|
*
|
|
* @param node a node
|
|
* @return {String} shape of the node
|
|
*/
|
|
CRp.getNodeShape = function(node)
|
|
{
|
|
// TODO only allow rectangle for a compound node?
|
|
// if (node._private.style['width'].value == 'auto' ||
|
|
// node._private.style['height'].value == 'auto')
|
|
// {
|
|
// return 'rectangle';
|
|
// }
|
|
|
|
var shape = node._private.style['shape'].value;
|
|
|
|
if( node.isParent() ){
|
|
if( shape === 'rectangle' || shape === 'roundrectangle' ){
|
|
return shape;
|
|
} else {
|
|
return 'rectangle';
|
|
}
|
|
}
|
|
|
|
return shape;
|
|
};
|
|
|
|
|
|
CRp.getNodePadding = function(node)
|
|
{
|
|
var left = node._private.style['padding-left'].pxValue;
|
|
var right = node._private.style['padding-right'].pxValue;
|
|
var top = node._private.style['padding-top'].pxValue;
|
|
var bottom = node._private.style['padding-bottom'].pxValue;
|
|
|
|
if (isNaN(left))
|
|
{
|
|
left = 0;
|
|
}
|
|
|
|
if (isNaN(right))
|
|
{
|
|
right = 0;
|
|
}
|
|
|
|
if (isNaN(top))
|
|
{
|
|
top = 0;
|
|
}
|
|
|
|
if (isNaN(bottom))
|
|
{
|
|
bottom = 0;
|
|
}
|
|
|
|
return {left : left,
|
|
right : right,
|
|
top : top,
|
|
bottom : bottom};
|
|
};
|
|
|
|
CRp.zOrderSort = $$.Collection.zIndexSort;
|
|
|
|
CRp.updateCachedZSortedEles = function(){
|
|
this.getCachedZSortedEles( true );
|
|
};
|
|
|
|
CRp.getCachedZSortedEles = function( forceRecalc ){
|
|
var lastNodes = this.lastZOrderCachedNodes;
|
|
var lastEdges = this.lastZOrderCachedEdges;
|
|
var nodes = this.getCachedNodes();
|
|
var edges = this.getCachedEdges();
|
|
var eles = [];
|
|
|
|
if( forceRecalc || !lastNodes || !lastEdges || lastNodes !== nodes || lastEdges !== edges ){
|
|
//console.time('cachezorder')
|
|
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var n = nodes[i];
|
|
|
|
if( n.animated() || (n.visible() && !n.transparent()) ){
|
|
eles.push( n );
|
|
}
|
|
}
|
|
|
|
for( var i = 0; i < edges.length; i++ ){
|
|
var e = edges[i];
|
|
|
|
if( e.animated() || (e.visible() && !e.transparent()) ){
|
|
eles.push( e );
|
|
}
|
|
}
|
|
|
|
eles.sort( this.zOrderSort );
|
|
this.cachedZSortedEles = eles;
|
|
//console.log('make cache')
|
|
|
|
//console.timeEnd('cachezorder')
|
|
} else {
|
|
eles = this.cachedZSortedEles;
|
|
//console.log('read cache')
|
|
}
|
|
|
|
this.lastZOrderCachedNodes = nodes;
|
|
this.lastZOrderCachedEdges = edges;
|
|
|
|
return eles;
|
|
};
|
|
|
|
CRp.projectBezier = function(edge){
|
|
var qbezierAt = $$.math.qbezierAt;
|
|
var rs = edge._private.rscratch;
|
|
var bpts = edge._private.rstyle.bezierPts = [];
|
|
|
|
function pushBezierPts(pts){
|
|
bpts.push({
|
|
x: qbezierAt( pts[0], pts[2], pts[4], 0.05 ),
|
|
y: qbezierAt( pts[1], pts[3], pts[5], 0.05 )
|
|
});
|
|
|
|
bpts.push({
|
|
x: qbezierAt( pts[0], pts[2], pts[4], 0.25 ),
|
|
y: qbezierAt( pts[1], pts[3], pts[5], 0.25 )
|
|
});
|
|
|
|
bpts.push({
|
|
x: qbezierAt( pts[0], pts[2], pts[4], 0.4 ),
|
|
y: qbezierAt( pts[1], pts[3], pts[5], 0.4 )
|
|
});
|
|
|
|
var mid = {
|
|
x: qbezierAt( pts[0], pts[2], pts[4], 0.5 ),
|
|
y: qbezierAt( pts[1], pts[3], pts[5], 0.5 )
|
|
};
|
|
|
|
bpts.push( mid );
|
|
|
|
if( rs.edgeType === 'self' || rs.edgeType === 'compound' ){
|
|
rs.midX = rs.selfEdgeMidX;
|
|
rs.midY = rs.selfEdgeMidY;
|
|
} else {
|
|
rs.midX = mid.x;
|
|
rs.midY = mid.y;
|
|
}
|
|
|
|
bpts.push({
|
|
x: qbezierAt( pts[0], pts[2], pts[4], 0.6 ),
|
|
y: qbezierAt( pts[1], pts[3], pts[5], 0.6 )
|
|
});
|
|
|
|
bpts.push({
|
|
x: qbezierAt( pts[0], pts[2], pts[4], 0.75 ),
|
|
y: qbezierAt( pts[1], pts[3], pts[5], 0.75 )
|
|
});
|
|
|
|
bpts.push({
|
|
x: qbezierAt( pts[0], pts[2], pts[4], 0.95 ),
|
|
y: qbezierAt( pts[1], pts[3], pts[5], 0.95 )
|
|
});
|
|
}
|
|
|
|
if( rs.edgeType === 'self' ){
|
|
pushBezierPts( [rs.startX, rs.startY, rs.cp2ax, rs.cp2ay, rs.selfEdgeMidX, rs.selfEdgeMidY] );
|
|
pushBezierPts( [rs.selfEdgeMidX, rs.selfEdgeMidY, rs.cp2cx, rs.cp2cy, rs.endX, rs.endY] );
|
|
} else if( rs.edgeType === 'bezier' ){
|
|
pushBezierPts( [rs.startX, rs.startY, rs.cp2x, rs.cp2y, rs.endX, rs.endY] );
|
|
}
|
|
};
|
|
|
|
CRp.recalculateNodeLabelProjection = function( node ){
|
|
var content = node._private.style['content'].strValue;
|
|
if( !content || content.match(/^\s+$/) ){ return; }
|
|
|
|
var textX, textY;
|
|
var nodeWidth = node.outerWidth();
|
|
var nodeHeight = node.outerHeight();
|
|
var nodePos = node._private.position;
|
|
var textHalign = node._private.style['text-halign'].strValue;
|
|
var textValign = node._private.style['text-valign'].strValue;
|
|
var rs = node._private.rscratch;
|
|
var rstyle = node._private.rstyle;
|
|
|
|
switch( textHalign ){
|
|
case 'left':
|
|
textX = nodePos.x - nodeWidth / 2;
|
|
break;
|
|
|
|
case 'right':
|
|
textX = nodePos.x + nodeWidth / 2;
|
|
break;
|
|
|
|
default: // e.g. center
|
|
textX = nodePos.x;
|
|
}
|
|
|
|
switch( textValign ){
|
|
case 'top':
|
|
textY = nodePos.y - nodeHeight / 2;
|
|
break;
|
|
|
|
case 'bottom':
|
|
textY = nodePos.y + nodeHeight / 2;
|
|
break;
|
|
|
|
default: // e.g. middle
|
|
textY = nodePos.y;
|
|
}
|
|
|
|
rs.labelX = textX;
|
|
rs.labelY = textY;
|
|
rstyle.labelX = textX;
|
|
rstyle.labelY = textY;
|
|
|
|
this.applyLabelDimensions( node );
|
|
};
|
|
|
|
CRp.recalculateEdgeLabelProjection = function( edge ){
|
|
var content = edge._private.style['content'].strValue;
|
|
if( !content || content.match(/^\s+$/) ){ return; }
|
|
|
|
var textX, textY;
|
|
var edgeCenterX, edgeCenterY;
|
|
var _p = edge._private;
|
|
var rs = _p.rscratch;
|
|
//var style = _p.style;
|
|
var rstyle = _p.rstyle;
|
|
|
|
if (rs.edgeType == 'self') {
|
|
edgeCenterX = rs.selfEdgeMidX;
|
|
edgeCenterY = rs.selfEdgeMidY;
|
|
} else if (rs.edgeType == 'straight') {
|
|
edgeCenterX = (rs.startX + rs.endX) / 2;
|
|
edgeCenterY = (rs.startY + rs.endY) / 2;
|
|
} else if (rs.edgeType == 'bezier') {
|
|
edgeCenterX = $$.math.qbezierAt( rs.startX, rs.cp2x, rs.endX, 0.5 );
|
|
edgeCenterY = $$.math.qbezierAt( rs.startY, rs.cp2y, rs.endY, 0.5 );
|
|
} else if (rs.edgeType == 'haystack') {
|
|
// var src = _p.source;
|
|
// var tgt = _p.target;
|
|
// var srcPos = src._private.position;
|
|
// var tgtPos = tgt._private.position;
|
|
var pts = rs.haystackPts;
|
|
|
|
edgeCenterX = ( pts[0] + pts[2] )/2;
|
|
edgeCenterY = ( pts[1] + pts[3] )/2;
|
|
}
|
|
|
|
textX = edgeCenterX;
|
|
textY = edgeCenterY;
|
|
|
|
// add center point to style so bounding box calculations can use it
|
|
rs.labelX = textX;
|
|
rs.labelY = textY;
|
|
rstyle.labelX = textX;
|
|
rstyle.labelY = textY;
|
|
|
|
this.applyLabelDimensions( edge );
|
|
};
|
|
|
|
CRp.applyLabelDimensions = function( ele ){
|
|
var rs = ele._private.rscratch;
|
|
var rstyle = ele._private.rstyle;
|
|
|
|
var text = this.getLabelText( ele );
|
|
var labelDims = this.calculateLabelDimensions( ele, text );
|
|
|
|
rstyle.labelWidth = labelDims.width;
|
|
rs.labelWidth = labelDims.width;
|
|
|
|
rstyle.labelHeight = labelDims.height;
|
|
rs.labelHeight = labelDims.height;
|
|
};
|
|
|
|
CRp.getLabelText = function( ele ){
|
|
var style = ele._private.style;
|
|
var text = ele._private.style['content'].strValue;
|
|
var textTransform = style['text-transform'].value;
|
|
var rscratch = ele._private.rscratch;
|
|
|
|
if (textTransform == 'none') {
|
|
} else if (textTransform == 'uppercase') {
|
|
text = text.toUpperCase();
|
|
} else if (textTransform == 'lowercase') {
|
|
text = text.toLowerCase();
|
|
}
|
|
|
|
if( style['text-wrap'].value === 'wrap' ){
|
|
//console.log('wrap');
|
|
|
|
// save recalc if the label is the same as before
|
|
if( rscratch.labelWrapKey === rscratch.labelKey ){
|
|
// console.log('wrap cache hit');
|
|
return rscratch.labelWrapCachedText;
|
|
}
|
|
// console.log('wrap cache miss');
|
|
|
|
var lines = text.split('\n');
|
|
var maxW = style['text-max-width'].pxValue;
|
|
var wrappedLines = [];
|
|
|
|
for( var l = 0; l < lines.length; l++ ){
|
|
var line = lines[l];
|
|
var lineDims = this.calculateLabelDimensions( ele, line, 'line=' + line );
|
|
var lineW = lineDims.width;
|
|
|
|
if( lineW > maxW ){ // line is too long
|
|
var words = line.split(/\s+/); // NB: assume collapsed whitespace into single space
|
|
var subline = '';
|
|
|
|
for( var w = 0; w < words.length; w++ ){
|
|
var word = words[w];
|
|
var testLine = subline.length === 0 ? word : subline + ' ' + word;
|
|
var testDims = this.calculateLabelDimensions( ele, testLine, 'testLine=' + testLine );
|
|
var testW = testDims.width;
|
|
|
|
if( testW <= maxW ){ // word fits on current line
|
|
subline += word + ' ';
|
|
} else { // word starts new line
|
|
wrappedLines.push( subline );
|
|
subline = word + ' ';
|
|
}
|
|
}
|
|
|
|
// if there's remaining text, put it in a wrapped line
|
|
if( !subline.match(/^\s+$/) ){
|
|
wrappedLines.push( subline );
|
|
}
|
|
} else { // line is already short enough
|
|
wrappedLines.push( line );
|
|
}
|
|
} // for
|
|
|
|
rscratch.labelWrapCachedLines = wrappedLines;
|
|
rscratch.labelWrapCachedText = text = wrappedLines.join('\n');
|
|
rscratch.labelWrapKey = rscratch.labelKey;
|
|
|
|
// console.log(text)
|
|
} // if wrap
|
|
|
|
return text;
|
|
};
|
|
|
|
CRp.calculateLabelDimensions = function( ele, text, extraKey ){
|
|
var r = this;
|
|
var style = ele._private.style;
|
|
var fStyle = style['font-style'].strValue;
|
|
var size = style['font-size'].pxValue + 'px';
|
|
var family = style['font-family'].strValue;
|
|
// var variant = style['font-variant'].strValue;
|
|
var weight = style['font-weight'].strValue;
|
|
|
|
var cacheKey = ele._private.labelKey;
|
|
|
|
if( extraKey ){
|
|
cacheKey += '$@$' + extraKey;
|
|
}
|
|
|
|
var cache = r.labelDimCache || (r.labelDimCache = {});
|
|
|
|
if( cache[cacheKey] ){
|
|
return cache[cacheKey];
|
|
}
|
|
|
|
var div = this.labelCalcDiv;
|
|
|
|
if( !div ){
|
|
div = this.labelCalcDiv = document.createElement('div');
|
|
document.body.appendChild( div );
|
|
}
|
|
|
|
var ds = div.style;
|
|
|
|
// from ele style
|
|
ds.fontFamily = family;
|
|
ds.fontStyle = fStyle;
|
|
ds.fontSize = size;
|
|
// ds.fontVariant = variant;
|
|
ds.fontWeight = weight;
|
|
|
|
// forced style
|
|
ds.position = 'absolute';
|
|
ds.left = '-9999px';
|
|
ds.top = '-9999px';
|
|
ds.zIndex = '-1';
|
|
ds.visibility = 'hidden';
|
|
ds.pointerEvents = 'none';
|
|
ds.padding = '0';
|
|
ds.lineHeight = '1';
|
|
|
|
if( style['text-wrap'].value === 'wrap' ){
|
|
ds.whiteSpace = 'pre'; // so newlines are taken into account
|
|
} else {
|
|
ds.whiteSpace = 'normal';
|
|
}
|
|
|
|
// put label content in div
|
|
div.textContent = text;
|
|
|
|
cache[cacheKey] = {
|
|
width: div.clientWidth,
|
|
height: div.clientHeight
|
|
};
|
|
|
|
return cache[cacheKey];
|
|
};
|
|
|
|
CRp.recalculateRenderedStyle = function( eles ){
|
|
var edges = [];
|
|
var nodes = [];
|
|
var handledEdge = {};
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
var _p = ele._private;
|
|
var style = _p.style;
|
|
var rs = _p.rscratch;
|
|
var rstyle = _p.rstyle;
|
|
var id = _p.data.id;
|
|
var bbStyleSame = rs.boundingBoxKey != null && _p.boundingBoxKey === rs.boundingBoxKey;
|
|
var labelStyleSame = rs.labelKey != null && _p.labelKey === rs.labelKey;
|
|
var styleSame = bbStyleSame && labelStyleSame;
|
|
|
|
if( ele._private.group === 'nodes' ){
|
|
var pos = _p.position;
|
|
var posSame = rstyle.nodeX != null && rstyle.nodeY != null && pos.x === rstyle.nodeX && pos.y === rstyle.nodeY;
|
|
var wSame = rstyle.nodeW != null && rstyle.nodeW === style['width'].pxValue;
|
|
var hSame = rstyle.nodeH != null && rstyle.nodeH === style['height'].pxValue;
|
|
|
|
if( !posSame || !styleSame || !wSame || !hSame ){
|
|
nodes.push( ele );
|
|
}
|
|
|
|
rstyle.nodeX = pos.x;
|
|
rstyle.nodeY = pos.y;
|
|
rstyle.nodeW = style['width'].pxValue;
|
|
rstyle.nodeH = style['height'].pxValue;
|
|
} else { // edges
|
|
|
|
var srcPos = ele._private.source._private.position;
|
|
var tgtPos = ele._private.target._private.position;
|
|
var srcSame = rstyle.srcX != null && rstyle.srcY != null && srcPos.x === rstyle.srcX && srcPos.y === rstyle.srcY;
|
|
var tgtSame = rstyle.tgtX != null && rstyle.tgtY != null && tgtPos.x === rstyle.tgtX && tgtPos.y === rstyle.tgtY;
|
|
var positionsSame = srcSame && tgtSame;
|
|
|
|
if( !positionsSame || !styleSame ){
|
|
var curveType = _p.style['curve-style'].value;
|
|
|
|
if( curveType === 'bezier' ){
|
|
if( !handledEdge[ id ] ){
|
|
edges.push( ele );
|
|
handledEdge[ id ] = true;
|
|
|
|
var parallelEdges = ele.parallelEdges();
|
|
for( var i = 0; i < parallelEdges.length; i++ ){
|
|
var pEdge = parallelEdges[i];
|
|
var pId = pEdge._private.data.id;
|
|
|
|
if( !handledEdge[ pId ] ){
|
|
edges.push( pEdge );
|
|
handledEdge[ pId ] = true;
|
|
}
|
|
|
|
}
|
|
}
|
|
} else {
|
|
edges.push( ele );
|
|
}
|
|
} // if positions diff
|
|
|
|
// update rstyle positions
|
|
rstyle.srcX = srcPos.x;
|
|
rstyle.srcY = srcPos.y;
|
|
rstyle.tgtX = tgtPos.x;
|
|
rstyle.tgtY = tgtPos.y;
|
|
|
|
} // if edges
|
|
|
|
rs.boundingBoxKey = _p.boundingBoxKey;
|
|
rs.labelKey = _p.labelKey;
|
|
}
|
|
|
|
this.recalculateEdgeProjections( edges );
|
|
this.recalculateLabelProjections( nodes, edges );
|
|
};
|
|
|
|
CRp.recalculateLabelProjections = function( nodes, edges ){
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
this.recalculateNodeLabelProjection( nodes[i] );
|
|
}
|
|
|
|
for( var i = 0; i < edges.length; i++ ){
|
|
this.recalculateEdgeLabelProjection( edges[i] );
|
|
}
|
|
};
|
|
|
|
CRp.recalculateEdgeProjections = function( edges ){
|
|
this.findEdgeControlPoints( edges );
|
|
};
|
|
|
|
|
|
// Find edge control points
|
|
CRp.findEdgeControlPoints = function(edges) {
|
|
if( !edges || edges.length === 0 ){ return; }
|
|
|
|
var cy = this.data.cy;
|
|
var hasCompounds = cy.hasCompoundNodes();
|
|
var hashTable = {};
|
|
var pairIds = [];
|
|
var haystackEdges = [];
|
|
|
|
// create a table of edge (src, tgt) => list of edges between them
|
|
var pairId;
|
|
for (var i = 0; i < edges.length; i++){
|
|
var edge = edges[i];
|
|
var style = edge._private.style;
|
|
var edgeIsUnbundled = style['curve-style'].value === 'unbundled-bezier';
|
|
|
|
// ignore edges who are not to be displayed
|
|
// they shouldn't take up space
|
|
if( style.display.value === 'none' ){
|
|
continue;
|
|
}
|
|
|
|
if( style['curve-style'].value === 'haystack' ){
|
|
haystackEdges.push( edge );
|
|
continue;
|
|
}
|
|
|
|
var srcId = edge._private.data.source;
|
|
var tgtId = edge._private.data.target;
|
|
|
|
pairId = srcId > tgtId ?
|
|
tgtId + '-' + srcId :
|
|
srcId + '-' + tgtId ;
|
|
|
|
if( edgeIsUnbundled ){
|
|
pairId = 'unbundled' + edge._private.data.id;
|
|
}
|
|
|
|
if (hashTable[pairId] == null) {
|
|
hashTable[pairId] = [];
|
|
pairIds.push( pairId );
|
|
}
|
|
|
|
hashTable[pairId].push( edge );
|
|
|
|
if( edgeIsUnbundled ){
|
|
hashTable[pairId].hasUnbundled = true;
|
|
}
|
|
}
|
|
|
|
var src, tgt, srcPos, tgtPos, srcW, srcH, tgtW, tgtH, srcShape, tgtShape, srcBorder, tgtBorder;
|
|
var vectorNormInverse;
|
|
var badBezier;
|
|
|
|
// for each pair (src, tgt), create the ctrl pts
|
|
// Nested for loop is OK; total number of iterations for both loops = edgeCount
|
|
for (var p = 0; p < pairIds.length; p++) {
|
|
pairId = pairIds[p];
|
|
var pairEdges = hashTable[pairId];
|
|
|
|
// for each pair id, the edges should be sorted by index
|
|
pairEdges.sort(function(edge1, edge2){
|
|
return edge1._private.index - edge2._private.index;
|
|
});
|
|
|
|
src = pairEdges[0]._private.source;
|
|
tgt = pairEdges[0]._private.target;
|
|
|
|
// make sure src/tgt distinction is consistent
|
|
// (src/tgt in this case are just for ctrlpts and don't actually have to be true src/tgt)
|
|
if( src._private.data.id > tgt._private.data.id ){
|
|
var temp = src;
|
|
src = tgt;
|
|
tgt = temp;
|
|
}
|
|
|
|
srcPos = src._private.position;
|
|
tgtPos = tgt._private.position;
|
|
|
|
srcW = this.getNodeWidth(src);
|
|
srcH = this.getNodeHeight(src);
|
|
|
|
tgtW = this.getNodeWidth(tgt);
|
|
tgtH = this.getNodeHeight(tgt);
|
|
|
|
srcShape = CanvasRenderer.nodeShapes[ this.getNodeShape(src) ];
|
|
tgtShape = CanvasRenderer.nodeShapes[ this.getNodeShape(tgt) ];
|
|
|
|
srcBorder = src._private.style['border-width'].pxValue;
|
|
tgtBorder = tgt._private.style['border-width'].pxValue;
|
|
|
|
badBezier = false;
|
|
|
|
|
|
if( (pairEdges.length > 1 && src !== tgt) || pairEdges.hasUnbundled ){
|
|
|
|
// pt outside src shape to calc distance/displacement from src to tgt
|
|
var srcOutside = srcShape.intersectLine(
|
|
srcPos.x,
|
|
srcPos.y,
|
|
srcW,
|
|
srcH,
|
|
tgtPos.x,
|
|
tgtPos.y,
|
|
srcBorder / 2
|
|
);
|
|
|
|
// pt outside tgt shape to calc distance/displacement from src to tgt
|
|
var tgtOutside = tgtShape.intersectLine(
|
|
tgtPos.x,
|
|
tgtPos.y,
|
|
tgtW,
|
|
tgtH,
|
|
srcPos.x,
|
|
srcPos.y,
|
|
tgtBorder / 2
|
|
);
|
|
|
|
var midptSrcPts = {
|
|
x1: srcOutside[0],
|
|
x2: tgtOutside[0],
|
|
y1: srcOutside[1],
|
|
y2: tgtOutside[1]
|
|
};
|
|
|
|
var dy = ( tgtOutside[1] - srcOutside[1] );
|
|
var dx = ( tgtOutside[0] - srcOutside[0] );
|
|
var l = Math.sqrt( dx*dx + dy*dy );
|
|
|
|
var vector = {
|
|
x: dx,
|
|
y: dy
|
|
};
|
|
|
|
var vectorNorm = {
|
|
x: vector.x/l,
|
|
y: vector.y/l
|
|
};
|
|
vectorNormInverse = {
|
|
x: -vectorNorm.y,
|
|
y: vectorNorm.x
|
|
};
|
|
|
|
// if src intersection is inside tgt or tgt intersection is inside src, then no ctrl pts to draw
|
|
if(
|
|
tgtShape.checkPoint( srcOutside[0], srcOutside[1], tgtBorder/2, tgtW, tgtH, tgtPos.x, tgtPos.y ) ||
|
|
srcShape.checkPoint( tgtOutside[0], tgtOutside[1], srcBorder/2, srcW, srcH, srcPos.x, srcPos.y )
|
|
){
|
|
vectorNormInverse = {};
|
|
badBezier = true;
|
|
}
|
|
|
|
}
|
|
|
|
var edge;
|
|
var rs;
|
|
|
|
for (var i = 0; i < pairEdges.length; i++) {
|
|
edge = pairEdges[i];
|
|
rs = edge._private.rscratch;
|
|
|
|
var edgeIndex1 = rs.lastEdgeIndex;
|
|
var edgeIndex2 = i;
|
|
|
|
var numEdges1 = rs.lastNumEdges;
|
|
var numEdges2 = pairEdges.length;
|
|
|
|
var eStyle = edge._private.style;
|
|
var stepSize = eStyle['control-point-step-size'].pxValue;
|
|
var stepDist = eStyle['control-point-distance'] !== undefined ? eStyle['control-point-distance'].pxValue : undefined;
|
|
var stepWeight = eStyle['control-point-weight'].value;
|
|
var edgeIsUnbundled = eStyle['curve-style'].value === 'unbundled-bezier';
|
|
|
|
var swappedDirection = edge._private.source !== src;
|
|
|
|
if( swappedDirection && edgeIsUnbundled ){
|
|
stepDist *= -1;
|
|
}
|
|
|
|
var srcX1 = rs.lastSrcCtlPtX;
|
|
var srcX2 = srcPos.x;
|
|
var srcY1 = rs.lastSrcCtlPtY;
|
|
var srcY2 = srcPos.y;
|
|
var srcW1 = rs.lastSrcCtlPtW;
|
|
var srcW2 = src.outerWidth();
|
|
var srcH1 = rs.lastSrcCtlPtH;
|
|
var srcH2 = src.outerHeight();
|
|
|
|
var tgtX1 = rs.lastTgtCtlPtX;
|
|
var tgtX2 = tgtPos.x;
|
|
var tgtY1 = rs.lastTgtCtlPtY;
|
|
var tgtY2 = tgtPos.y;
|
|
var tgtW1 = rs.lastTgtCtlPtW;
|
|
var tgtW2 = tgt.outerWidth();
|
|
var tgtH1 = rs.lastTgtCtlPtH;
|
|
var tgtH2 = tgt.outerHeight();
|
|
|
|
var width1 = rs.lastW;
|
|
var width2 = eStyle['control-point-step-size'].pxValue;
|
|
|
|
if( badBezier ){
|
|
rs.badBezier = true;
|
|
} else {
|
|
rs.badBezier = false;
|
|
}
|
|
|
|
if( srcX1 === srcX2 && srcY1 === srcY2 && srcW1 === srcW2 && srcH1 === srcH2
|
|
&& tgtX1 === tgtX2 && tgtY1 === tgtY2 && tgtW1 === tgtW2 && tgtH1 === tgtH2
|
|
&& width1 === width2
|
|
&& ((edgeIndex1 === edgeIndex2 && numEdges1 === numEdges2) || edgeIsUnbundled) ){
|
|
// console.log('edge ctrl pt cache HIT')
|
|
continue; // then the control points haven't changed and we can skip calculating them
|
|
} else {
|
|
rs.lastSrcCtlPtX = srcX2;
|
|
rs.lastSrcCtlPtY = srcY2;
|
|
rs.lastSrcCtlPtW = srcW2;
|
|
rs.lastSrcCtlPtH = srcH2;
|
|
rs.lastTgtCtlPtX = tgtX2;
|
|
rs.lastTgtCtlPtY = tgtY2;
|
|
rs.lastTgtCtlPtW = tgtW2;
|
|
rs.lastTgtCtlPtH = tgtH2;
|
|
rs.lastEdgeIndex = edgeIndex2;
|
|
rs.lastNumEdges = numEdges2;
|
|
rs.lastWidth = width2;
|
|
// console.log('edge ctrl pt cache MISS')
|
|
}
|
|
|
|
// Self-edge
|
|
if ( src === tgt ) {
|
|
|
|
rs.edgeType = 'self';
|
|
|
|
var j = i;
|
|
var loopDist = stepSize;
|
|
|
|
if( edgeIsUnbundled ){
|
|
j = 0;
|
|
loopDist = stepDist;
|
|
}
|
|
|
|
// New -- fix for large nodes
|
|
rs.cp2ax = srcPos.x;
|
|
rs.cp2ay = srcPos.y - (1 + Math.pow(srcH, 1.12) / 100) * loopDist * (j / 3 + 1);
|
|
|
|
rs.cp2cx = srcPos.x - (1 + Math.pow(srcW, 1.12) / 100) * loopDist * (j / 3 + 1);
|
|
rs.cp2cy = srcPos.y;
|
|
|
|
rs.selfEdgeMidX = (rs.cp2ax + rs.cp2cx) / 2.0;
|
|
rs.selfEdgeMidY = (rs.cp2ay + rs.cp2cy) / 2.0;
|
|
|
|
// Compound edge
|
|
} else if(
|
|
hasCompounds &&
|
|
( src.isParent() || src.isChild() || tgt.isParent() || tgt.isChild() ) &&
|
|
( src.parents().anySame(tgt) || tgt.parents().anySame(src) )
|
|
){
|
|
|
|
rs.edgeType = 'compound';
|
|
|
|
// because the line approximation doesn't apply for compound beziers
|
|
// (loop/self edges are already elided b/c of cheap src==tgt check)
|
|
rs.badBezier = false;
|
|
|
|
var j = i;
|
|
var loopDist = stepSize;
|
|
|
|
if( edgeIsUnbundled ){
|
|
j = 0;
|
|
loopDist = stepDist;
|
|
}
|
|
|
|
|
|
var loopW = 50;
|
|
|
|
var loopaPos = {
|
|
x: srcPos.x - srcW/2,
|
|
y: srcPos.y - srcH/2
|
|
};
|
|
|
|
var loopbPos = {
|
|
x: tgtPos.x - tgtW/2,
|
|
y: tgtPos.y - tgtH/2
|
|
};
|
|
|
|
var minCompoundStretch = 1;
|
|
|
|
rs.cp2ax = loopaPos.x;
|
|
rs.compoundStretchA = Math.max( minCompoundStretch, Math.log(srcW * 0.01) ); // avoids cases with impossible beziers
|
|
rs.cp2ay = loopaPos.y - (1 + Math.pow(loopW, 1.12) / 100) * loopDist * (j / 3 + 1) * rs.compoundStretchA;
|
|
|
|
rs.compoundStretchB = Math.max( minCompoundStretch, Math.log(tgtW * 0.01) ); // avoids cases with impossible beziers
|
|
rs.cp2cx = loopbPos.x - (1 + Math.pow(loopW, 1.12) / 100) * loopDist * (j / 3 + 1) * rs.compoundStretchB;
|
|
rs.cp2cy = loopbPos.y;
|
|
|
|
rs.selfEdgeMidX = (rs.cp2ax + rs.cp2cx) / 2.0;
|
|
rs.selfEdgeMidY = (rs.cp2ay + rs.cp2cy) / 2.0;
|
|
|
|
// Straight edge
|
|
} else if (pairEdges.length % 2 === 1
|
|
&& i === Math.floor(pairEdges.length / 2)
|
|
&& !edgeIsUnbundled ) {
|
|
|
|
rs.edgeType = 'straight';
|
|
|
|
// Bezier edge
|
|
} else {
|
|
var normStepDist = (0.5 - pairEdges.length / 2 + i) * stepSize;
|
|
var manStepDist;
|
|
var sign = $$.math.signum( normStepDist );
|
|
|
|
if( edgeIsUnbundled ){
|
|
manStepDist = stepDist;
|
|
} else {
|
|
manStepDist = stepDist !== undefined ? sign * stepDist : undefined;
|
|
}
|
|
|
|
var distanceFromMidpoint = manStepDist !== undefined ? manStepDist : normStepDist;
|
|
|
|
var w1 = (1 - stepWeight);
|
|
var w2 = stepWeight;
|
|
|
|
if( swappedDirection ){
|
|
w1 = stepWeight;
|
|
w2 = (1 - stepWeight);
|
|
}
|
|
|
|
var adjustedMidpt = {
|
|
x: midptSrcPts.x1 * w1 + midptSrcPts.x2 * w2,
|
|
y: midptSrcPts.y1 * w1 + midptSrcPts.y2 * w2
|
|
};
|
|
|
|
rs.edgeType = 'bezier';
|
|
|
|
rs.cp2x = adjustedMidpt.x + vectorNormInverse.x * distanceFromMidpoint;
|
|
rs.cp2y = adjustedMidpt.y + vectorNormInverse.y * distanceFromMidpoint;
|
|
|
|
// console.log(edge, midPointX, displacementX, distanceFromMidpoint);
|
|
}
|
|
|
|
// find endpts for edge
|
|
this.findEndpoints( edge );
|
|
|
|
var badStart = !$$.is.number( rs.startX ) || !$$.is.number( rs.startY );
|
|
var badAStart = !$$.is.number( rs.arrowStartX ) || !$$.is.number( rs.arrowStartY );
|
|
var badEnd = !$$.is.number( rs.endX ) || !$$.is.number( rs.endY );
|
|
var badAEnd = !$$.is.number( rs.arrowEndX ) || !$$.is.number( rs.arrowEndY );
|
|
|
|
var minCpADistFactor = 3;
|
|
var arrowW = this.getArrowWidth( edge._private.style['width'].pxValue ) * CanvasRenderer.arrowShapeHeight;
|
|
var minCpADist = minCpADistFactor * arrowW;
|
|
var startACpDist = $$.math.distance( { x: rs.cp2x, y: rs.cp2y }, { x: rs.startX, y: rs.startY } );
|
|
var closeStartACp = startACpDist < minCpADist;
|
|
var endACpDist = $$.math.distance( { x: rs.cp2x, y: rs.cp2y }, { x: rs.endX, y: rs.endY } );
|
|
var closeEndACp = endACpDist < minCpADist;
|
|
|
|
if( rs.edgeType === 'bezier' ){
|
|
var overlapping = false;
|
|
|
|
if( badStart || badAStart || closeStartACp ){
|
|
overlapping = true;
|
|
|
|
// project control point along line from src centre to outside the src shape
|
|
// (otherwise intersection will yield nothing)
|
|
var cpD = { // delta
|
|
x: rs.cp2x - srcPos.x,
|
|
y: rs.cp2y - srcPos.y
|
|
};
|
|
var cpL = Math.sqrt( cpD.x*cpD.x + cpD.y*cpD.y ); // length of line
|
|
var cpM = { // normalised delta
|
|
x: cpD.x / cpL,
|
|
y: cpD.y / cpL
|
|
};
|
|
var radius = Math.max(srcW, srcH);
|
|
var cpProj = { // *2 radius guarantees outside shape
|
|
x: rs.cp2x + cpM.x * 2 * radius,
|
|
y: rs.cp2y + cpM.y * 2 * radius
|
|
};
|
|
|
|
var srcCtrlPtIntn = srcShape.intersectLine(
|
|
srcPos.x,
|
|
srcPos.y,
|
|
srcW,
|
|
srcH,
|
|
cpProj.x,
|
|
cpProj.y,
|
|
srcBorder / 2
|
|
);
|
|
|
|
if( closeStartACp ){
|
|
rs.cp2x = rs.cp2x + cpM.x * (minCpADist - startACpDist);
|
|
rs.cp2y = rs.cp2y + cpM.y * (minCpADist - startACpDist);
|
|
} else {
|
|
rs.cp2x = srcCtrlPtIntn[0] + cpM.x * minCpADist;
|
|
rs.cp2y = srcCtrlPtIntn[1] + cpM.y * minCpADist;
|
|
}
|
|
}
|
|
|
|
if( badEnd || badAEnd || closeEndACp ){
|
|
overlapping = true;
|
|
|
|
// project control point along line from tgt centre to outside the tgt shape
|
|
// (otherwise intersection will yield nothing)
|
|
var cpD = { // delta
|
|
x: rs.cp2x - tgtPos.x,
|
|
y: rs.cp2y - tgtPos.y
|
|
};
|
|
var cpL = Math.sqrt( cpD.x*cpD.x + cpD.y*cpD.y ); // length of line
|
|
var cpM = { // normalised delta
|
|
x: cpD.x / cpL,
|
|
y: cpD.y / cpL
|
|
};
|
|
var radius = Math.max(srcW, srcH);
|
|
var cpProj = { // *2 radius guarantees outside shape
|
|
x: rs.cp2x + cpM.x * 2 * radius,
|
|
y: rs.cp2y + cpM.y * 2 * radius
|
|
};
|
|
|
|
var tgtCtrlPtIntn = tgtShape.intersectLine(
|
|
tgtPos.x,
|
|
tgtPos.y,
|
|
tgtW,
|
|
tgtH,
|
|
cpProj.x,
|
|
cpProj.y,
|
|
tgtBorder / 2
|
|
);
|
|
|
|
if( closeEndACp ){
|
|
rs.cp2x = rs.cp2x + cpM.x * (minCpADist - endACpDist);
|
|
rs.cp2y = rs.cp2y + cpM.y * (minCpADist - endACpDist);
|
|
} else {
|
|
rs.cp2x = tgtCtrlPtIntn[0] + cpM.x * minCpADist;
|
|
rs.cp2y = tgtCtrlPtIntn[1] + cpM.y * minCpADist;
|
|
}
|
|
|
|
}
|
|
|
|
if( overlapping ){
|
|
// recalc endpts
|
|
this.findEndpoints( edge );
|
|
}
|
|
} else if( rs.edgeType === 'straight' ){
|
|
rs.midX = ( srcX2 + tgtX2 )/2;
|
|
rs.midY = ( srcY2 + tgtY2 )/2;
|
|
}
|
|
|
|
// project the edge into rstyle
|
|
this.projectBezier( edge );
|
|
this.recalculateEdgeLabelProjection( edge );
|
|
|
|
}
|
|
}
|
|
|
|
for( var i = 0; i < haystackEdges.length; i++ ){
|
|
var edge = haystackEdges[i];
|
|
var _p = edge._private;
|
|
var rscratch = _p.rscratch;
|
|
var rs = rscratch;
|
|
|
|
if( !rscratch.haystack ){
|
|
var angle = Math.random() * 2 * Math.PI;
|
|
|
|
rscratch.source = {
|
|
x: Math.cos(angle),
|
|
y: Math.sin(angle)
|
|
};
|
|
|
|
var angle = Math.random() * 2 * Math.PI;
|
|
|
|
rscratch.target = {
|
|
x: Math.cos(angle),
|
|
y: Math.sin(angle)
|
|
};
|
|
|
|
}
|
|
|
|
var src = _p.source;
|
|
var tgt = _p.target;
|
|
var srcPos = src._private.position;
|
|
var tgtPos = tgt._private.position;
|
|
var srcW = src.width();
|
|
var tgtW = tgt.width();
|
|
var srcH = src.height();
|
|
var tgtH = tgt.height();
|
|
var radius = style['haystack-radius'].value;
|
|
var halfRadius = radius/2; // b/c have to half width/height
|
|
|
|
rs.haystackPts = [
|
|
rs.source.x * srcW * halfRadius + srcPos.x,
|
|
rs.source.y * srcH * halfRadius + srcPos.y,
|
|
rs.target.x * tgtW * halfRadius + tgtPos.x,
|
|
rs.target.y * tgtH * halfRadius + tgtPos.y
|
|
];
|
|
|
|
// always override as haystack in case set to different type previously
|
|
rscratch.edgeType = 'haystack';
|
|
rscratch.haystack = true;
|
|
|
|
this.recalculateEdgeLabelProjection( edge );
|
|
}
|
|
|
|
return hashTable;
|
|
};
|
|
|
|
CRp.findEndpoints = function(edge) {
|
|
var intersect;
|
|
|
|
var source = edge.source()[0];
|
|
var target = edge.target()[0];
|
|
|
|
var tgtArShape = edge._private.style['target-arrow-shape'].value;
|
|
var srcArShape = edge._private.style['source-arrow-shape'].value;
|
|
|
|
var tgtBorderW = target._private.style['border-width'].pxValue;
|
|
var srcBorderW = source._private.style['border-width'].pxValue;
|
|
|
|
var rs = edge._private.rscratch;
|
|
|
|
if (rs.edgeType == 'self' || rs.edgeType == 'compound') {
|
|
|
|
var cp = [rs.cp2cx, rs.cp2cy];
|
|
|
|
intersect = CanvasRenderer.nodeShapes[this.getNodeShape(target)].intersectLine(
|
|
target._private.position.x,
|
|
target._private.position.y,
|
|
this.getNodeWidth(target),
|
|
this.getNodeHeight(target),
|
|
cp[0],
|
|
cp[1],
|
|
tgtBorderW / 2
|
|
);
|
|
|
|
var arrowEnd = $$.math.shortenIntersection(intersect, cp,
|
|
CanvasRenderer.arrowShapes[tgtArShape].spacing(edge));
|
|
var edgeEnd = $$.math.shortenIntersection(intersect, cp,
|
|
CanvasRenderer.arrowShapes[tgtArShape].gap(edge));
|
|
|
|
rs.endX = edgeEnd[0];
|
|
rs.endY = edgeEnd[1];
|
|
|
|
rs.arrowEndX = arrowEnd[0];
|
|
rs.arrowEndY = arrowEnd[1];
|
|
|
|
var cp = [rs.cp2ax, rs.cp2ay];
|
|
|
|
intersect = CanvasRenderer.nodeShapes[this.getNodeShape(source)].intersectLine(
|
|
source._private.position.x,
|
|
source._private.position.y,
|
|
this.getNodeWidth(source),
|
|
this.getNodeHeight(source),
|
|
cp[0], //halfPointX,
|
|
cp[1], //halfPointY
|
|
srcBorderW / 2
|
|
);
|
|
|
|
var arrowStart = $$.math.shortenIntersection(intersect, cp,
|
|
CanvasRenderer.arrowShapes[srcArShape].spacing(edge));
|
|
var edgeStart = $$.math.shortenIntersection(intersect, cp,
|
|
CanvasRenderer.arrowShapes[srcArShape].gap(edge));
|
|
|
|
rs.startX = edgeStart[0];
|
|
rs.startY = edgeStart[1];
|
|
|
|
|
|
rs.arrowStartX = arrowStart[0];
|
|
rs.arrowStartY = arrowStart[1];
|
|
|
|
} else if (rs.edgeType == 'straight') {
|
|
|
|
intersect = CanvasRenderer.nodeShapes[this.getNodeShape(target)].intersectLine(
|
|
target._private.position.x,
|
|
target._private.position.y,
|
|
this.getNodeWidth(target),
|
|
this.getNodeHeight(target),
|
|
source.position().x,
|
|
source.position().y,
|
|
tgtBorderW / 2);
|
|
|
|
if (intersect.length === 0) {
|
|
rs.noArrowPlacement = true;
|
|
// return;
|
|
} else {
|
|
rs.noArrowPlacement = false;
|
|
}
|
|
|
|
var arrowEnd = $$.math.shortenIntersection(intersect,
|
|
[source.position().x, source.position().y],
|
|
CanvasRenderer.arrowShapes[tgtArShape].spacing(edge));
|
|
var edgeEnd = $$.math.shortenIntersection(intersect,
|
|
[source.position().x, source.position().y],
|
|
CanvasRenderer.arrowShapes[tgtArShape].gap(edge));
|
|
|
|
rs.endX = edgeEnd[0];
|
|
rs.endY = edgeEnd[1];
|
|
|
|
rs.arrowEndX = arrowEnd[0];
|
|
rs.arrowEndY = arrowEnd[1];
|
|
|
|
intersect = CanvasRenderer.nodeShapes[this.getNodeShape(source)].intersectLine(
|
|
source._private.position.x,
|
|
source._private.position.y,
|
|
this.getNodeWidth(source),
|
|
this.getNodeHeight(source),
|
|
target.position().x,
|
|
target.position().y,
|
|
srcBorderW / 2);
|
|
|
|
if (intersect.length === 0) {
|
|
rs.noArrowPlacement = true;
|
|
// return;
|
|
} else {
|
|
rs.noArrowPlacement = false;
|
|
}
|
|
|
|
/*
|
|
console.log("1: "
|
|
+ CanvasRenderer.arrowShapes[srcArShape],
|
|
srcArShape);
|
|
*/
|
|
var arrowStart = $$.math.shortenIntersection(intersect,
|
|
[target.position().x, target.position().y],
|
|
CanvasRenderer.arrowShapes[srcArShape].spacing(edge));
|
|
var edgeStart = $$.math.shortenIntersection(intersect,
|
|
[target.position().x, target.position().y],
|
|
CanvasRenderer.arrowShapes[srcArShape].gap(edge));
|
|
|
|
rs.startX = edgeStart[0];
|
|
rs.startY = edgeStart[1];
|
|
|
|
rs.arrowStartX = arrowStart[0];
|
|
rs.arrowStartY = arrowStart[1];
|
|
|
|
if( !$$.is.number(rs.startX) || !$$.is.number(rs.startY) || !$$.is.number(rs.endX) || !$$.is.number(rs.endY) ){
|
|
rs.badLine = true;
|
|
} else {
|
|
rs.badLine = false;
|
|
}
|
|
|
|
} else if (rs.edgeType == 'bezier') {
|
|
// if( window.badArrow) debugger;
|
|
var cp = [rs.cp2x, rs.cp2y];
|
|
|
|
intersect = CanvasRenderer.nodeShapes[
|
|
this.getNodeShape(target)].intersectLine(
|
|
target._private.position.x,
|
|
target._private.position.y,
|
|
this.getNodeWidth(target),
|
|
this.getNodeHeight(target),
|
|
cp[0], //halfPointX,
|
|
cp[1], //halfPointY
|
|
tgtBorderW / 2
|
|
);
|
|
|
|
/*
|
|
console.log("2: "
|
|
+ CanvasRenderer.arrowShapes[srcArShape],
|
|
srcArShape);
|
|
*/
|
|
var arrowEnd = $$.math.shortenIntersection(intersect, cp,
|
|
CanvasRenderer.arrowShapes[tgtArShape].spacing(edge));
|
|
var edgeEnd = $$.math.shortenIntersection(intersect, cp,
|
|
CanvasRenderer.arrowShapes[tgtArShape].gap(edge));
|
|
|
|
rs.endX = edgeEnd[0];
|
|
rs.endY = edgeEnd[1];
|
|
|
|
rs.arrowEndX = arrowEnd[0];
|
|
rs.arrowEndY = arrowEnd[1];
|
|
|
|
intersect = CanvasRenderer.nodeShapes[
|
|
this.getNodeShape(source)].intersectLine(
|
|
source._private.position.x,
|
|
source._private.position.y,
|
|
this.getNodeWidth(source),
|
|
this.getNodeHeight(source),
|
|
cp[0], //halfPointX,
|
|
cp[1], //halfPointY
|
|
srcBorderW / 2
|
|
);
|
|
|
|
var arrowStart = $$.math.shortenIntersection(
|
|
intersect,
|
|
cp,
|
|
CanvasRenderer.arrowShapes[srcArShape].spacing(edge)
|
|
);
|
|
var edgeStart = $$.math.shortenIntersection(
|
|
intersect,
|
|
cp,
|
|
CanvasRenderer.arrowShapes[srcArShape].gap(edge)
|
|
);
|
|
|
|
rs.startX = edgeStart[0];
|
|
rs.startY = edgeStart[1];
|
|
|
|
rs.arrowStartX = arrowStart[0];
|
|
rs.arrowStartY = arrowStart[1];
|
|
|
|
// if( isNaN(rs.startX) || isNaN(rs.startY) ){
|
|
// debugger;
|
|
// }
|
|
|
|
} else if (rs.isArcEdge) {
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Find adjacent edges
|
|
CRp.findEdges = function(nodeSet) {
|
|
|
|
var edges = this.getCachedEdges();
|
|
|
|
var hashTable = {};
|
|
var adjacentEdges = [];
|
|
|
|
for (var i = 0; i < nodeSet.length; i++) {
|
|
hashTable[nodeSet[i]._private.data.id] = nodeSet[i];
|
|
}
|
|
|
|
for (var i = 0; i < edges.length; i++) {
|
|
if (hashTable[edges[i]._private.data.source]
|
|
|| hashTable[edges[i]._private.data.target]) {
|
|
|
|
adjacentEdges.push(edges[i]);
|
|
}
|
|
}
|
|
|
|
return adjacentEdges;
|
|
};
|
|
|
|
CRp.getArrowWidth = CRp.getArrowHeight = function(edgeWidth) {
|
|
var cache = this.arrowWidthCache = this.arrowWidthCache || {};
|
|
|
|
var cachedVal = cache[edgeWidth];
|
|
if( cachedVal ){
|
|
return cachedVal;
|
|
}
|
|
|
|
cachedVal = Math.max(Math.pow(edgeWidth * 13.37, 0.9), 29);
|
|
cache[edgeWidth] = cachedVal;
|
|
|
|
return cachedVal;
|
|
};
|
|
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var CRp = CanvasRenderer.prototype;
|
|
|
|
// Draw edge
|
|
CRp.drawEdge = function(context, edge, drawOverlayInstead) {
|
|
var rs = edge._private.rscratch;
|
|
var usePaths = CanvasRenderer.usePaths();
|
|
|
|
// if bezier ctrl pts can not be calculated, then die
|
|
if( rs.badBezier || ( (rs.edgeType === 'bezier' || rs.edgeType === 'straight') && isNaN(rs.startX)) ){ // extra isNaN() for safari 7.1 b/c it mangles ctrlpt calcs
|
|
return;
|
|
}
|
|
|
|
var style = edge._private.style;
|
|
|
|
// Edge line width
|
|
if (style['width'].pxValue <= 0) {
|
|
return;
|
|
}
|
|
|
|
var overlayPadding = style['overlay-padding'].pxValue;
|
|
var overlayOpacity = style['overlay-opacity'].value;
|
|
var overlayColor = style['overlay-color'].value;
|
|
|
|
// Edge color & opacity
|
|
if( drawOverlayInstead ){
|
|
|
|
if( overlayOpacity === 0 ){ // exit early if no overlay
|
|
return;
|
|
}
|
|
|
|
this.strokeStyle(context, overlayColor[0], overlayColor[1], overlayColor[2], overlayOpacity);
|
|
context.lineCap = 'round';
|
|
|
|
if( edge._private.rscratch.edgeType == 'self' && !usePaths ){
|
|
context.lineCap = 'butt';
|
|
}
|
|
|
|
} else {
|
|
var lineColor = style['line-color'].value;
|
|
|
|
this.strokeStyle(context, lineColor[0], lineColor[1], lineColor[2], style.opacity.value);
|
|
|
|
context.lineCap = 'butt';
|
|
}
|
|
|
|
var startNode, endNode, source, target;
|
|
source = startNode = edge._private.source;
|
|
target = endNode = edge._private.target;
|
|
|
|
// var targetPos = target._private.position;
|
|
// var targetW = target.width();
|
|
// var targetH = target.height();
|
|
// var sourcePos = source._private.position;
|
|
// var sourceW = source.width();
|
|
// var sourceH = source.height();
|
|
|
|
|
|
var edgeWidth = style['width'].pxValue + (drawOverlayInstead ? 2 * overlayPadding : 0);
|
|
var lineStyle = drawOverlayInstead ? 'solid' : style['line-style'].value;
|
|
context.lineWidth = edgeWidth;
|
|
|
|
var shadowBlur = style['shadow-blur'].pxValue;
|
|
var shadowOpacity = style['shadow-opacity'].value;
|
|
var shadowColor = style['shadow-color'].value;
|
|
var shadowOffsetX = style['shadow-offset-x'].pxValue;
|
|
var shadowOffsetY = style['shadow-offset-y'].pxValue;
|
|
|
|
this.shadowStyle(context, shadowColor, drawOverlayInstead ? 0 : shadowOpacity, shadowBlur, shadowOffsetX, shadowOffsetY);
|
|
|
|
// if( rs.edgeType !== 'haystack' ){
|
|
// this.findEndpoints(edge);
|
|
// }
|
|
|
|
if( rs.edgeType === 'haystack' ){
|
|
// var radius = style['haystack-radius'].value;
|
|
// var halfRadius = radius/2; // b/c have to half width/height
|
|
|
|
this.drawStyledEdge(
|
|
edge,
|
|
context,
|
|
rs.haystackPts,
|
|
lineStyle,
|
|
edgeWidth
|
|
);
|
|
} else if (rs.edgeType === 'self' || rs.edgeType === 'compound') {
|
|
|
|
var details = edge._private.rscratch;
|
|
var points = [details.startX, details.startY, details.cp2ax,
|
|
details.cp2ay, details.selfEdgeMidX, details.selfEdgeMidY,
|
|
details.selfEdgeMidX, details.selfEdgeMidY,
|
|
details.cp2cx, details.cp2cy, details.endX, details.endY];
|
|
|
|
this.drawStyledEdge(edge, context, points, lineStyle, edgeWidth);
|
|
|
|
} else if (rs.edgeType === 'straight') {
|
|
|
|
var nodeDirectionX = endNode._private.position.x - startNode._private.position.x;
|
|
var nodeDirectionY = endNode._private.position.y - startNode._private.position.y;
|
|
|
|
var edgeDirectionX = rs.endX - rs.startX;
|
|
var edgeDirectionY = rs.endY - rs.startY;
|
|
|
|
if (nodeDirectionX * edgeDirectionX
|
|
+ nodeDirectionY * edgeDirectionY < 0) {
|
|
|
|
rs.straightEdgeTooShort = true;
|
|
} else {
|
|
|
|
var details = rs;
|
|
this.drawStyledEdge(edge, context, [details.startX, details.startY,
|
|
details.endX, details.endY],
|
|
lineStyle,
|
|
edgeWidth);
|
|
|
|
rs.straightEdgeTooShort = false;
|
|
}
|
|
} else {
|
|
|
|
var details = rs;
|
|
|
|
this.drawStyledEdge(edge, context, [details.startX, details.startY,
|
|
details.cp2x, details.cp2y, details.endX, details.endY],
|
|
lineStyle,
|
|
edgeWidth);
|
|
|
|
}
|
|
|
|
if( rs.edgeType === 'haystack' ){
|
|
this.drawArrowheads(context, edge, drawOverlayInstead);
|
|
} else if ( rs.noArrowPlacement !== true && rs.startX !== undefined ){
|
|
this.drawArrowheads(context, edge, drawOverlayInstead);
|
|
}
|
|
|
|
this.shadowStyle(context, 'transparent', 0); // reset for next guy
|
|
|
|
};
|
|
|
|
|
|
CRp.drawStyledEdge = function(
|
|
edge, context, pts, type, width) {
|
|
|
|
// 3 points given -> assume Bezier
|
|
// 2 -> assume straight
|
|
|
|
var rs = edge._private.rscratch;
|
|
var canvasCxt = context;
|
|
var path;
|
|
var pathCacheHit = false;
|
|
var usePaths = CanvasRenderer.usePaths();
|
|
|
|
|
|
if( usePaths ){
|
|
|
|
var pathCacheKey = pts;
|
|
var keyLengthMatches = rs.pathCacheKey && pathCacheKey.length === rs.pathCacheKey.length;
|
|
var keyMatches = keyLengthMatches;
|
|
|
|
for( var i = 0; keyMatches && i < pathCacheKey.length; i++ ){
|
|
if( rs.pathCacheKey[i] !== pathCacheKey[i] ){
|
|
keyMatches = false;
|
|
}
|
|
}
|
|
|
|
if( keyMatches ){
|
|
path = context = rs.pathCache;
|
|
pathCacheHit = true;
|
|
} else {
|
|
path = context = new Path2D();
|
|
rs.pathCacheKey = pathCacheKey;
|
|
rs.pathCache = path;
|
|
}
|
|
|
|
}
|
|
|
|
if( canvasCxt.setLineDash ){ // for very outofdate browsers
|
|
switch( type ){
|
|
case 'dotted':
|
|
canvasCxt.setLineDash([ 1, 1 ]);
|
|
break;
|
|
|
|
case 'dashed':
|
|
canvasCxt.setLineDash([ 6, 3 ]);
|
|
break;
|
|
|
|
case 'solid':
|
|
canvasCxt.setLineDash([ ]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !pathCacheHit ){
|
|
if( context.beginPath ){ context.beginPath(); }
|
|
context.moveTo(pts[0], pts[1]);
|
|
|
|
if( pts.length === 6 && !rs.badBezier ){ // bezier
|
|
context.quadraticCurveTo(pts[2], pts[3], pts[4], pts[5]);
|
|
} else if( pts.length === 12 && !rs.badBezier ){ // double bezier loop
|
|
context.quadraticCurveTo(pts[2], pts[3], pts[4], pts[5]);
|
|
context.quadraticCurveTo(pts[8], pts[9], pts[10], pts[11]);
|
|
} else if( pts.length === 4 && !rs.badLine ){ // line
|
|
context.lineTo(pts[2], pts[3]);
|
|
}
|
|
}
|
|
|
|
context = canvasCxt;
|
|
if( usePaths ){
|
|
context.stroke( path );
|
|
} else {
|
|
context.stroke();
|
|
}
|
|
|
|
// reset any line dashes
|
|
if( context.setLineDash ){ // for very outofdate browsers
|
|
context.setLineDash([ ]);
|
|
}
|
|
|
|
};
|
|
|
|
CRp.drawArrowheads = function(context, edge, drawOverlayInstead) {
|
|
if( drawOverlayInstead ){ return; } // don't do anything for overlays
|
|
|
|
var rs = edge._private.rscratch;
|
|
var self = this;
|
|
var isHaystack = rs.edgeType === 'haystack';
|
|
|
|
// Displacement gives direction for arrowhead orientation
|
|
var dispX, dispY;
|
|
var startX, startY, endX, endY;
|
|
|
|
var srcPos = edge.source().position();
|
|
var tgtPos = edge.target().position();
|
|
|
|
if( isHaystack ){
|
|
startX = rs.haystackPts[0];
|
|
startY = rs.haystackPts[1];
|
|
endX = rs.haystackPts[2];
|
|
endY = rs.haystackPts[3];
|
|
} else {
|
|
startX = rs.arrowStartX;
|
|
startY = rs.arrowStartY;
|
|
endX = rs.arrowEndX;
|
|
endY = rs.arrowEndY;
|
|
}
|
|
|
|
var style = edge._private.style;
|
|
|
|
function drawArrowhead( prefix, x, y, dispX, dispY ){
|
|
var arrowShape = style[prefix + '-arrow-shape'].value;
|
|
|
|
if( arrowShape === 'none' ){
|
|
return;
|
|
}
|
|
|
|
var gco = context.globalCompositeOperation;
|
|
|
|
var arrowClearFill = style[prefix + '-arrow-fill'].value === 'hollow' ? 'both' : 'filled';
|
|
var arrowFill = style[prefix + '-arrow-fill'].value;
|
|
|
|
if( arrowShape === 'half-triangle-overshot' ){
|
|
arrowFill = 'hollow';
|
|
arrowClearFill = 'hollow';
|
|
}
|
|
|
|
if( style.opacity.value !== 1 || arrowFill === 'hollow' ){ // then extra clear is needed
|
|
context.globalCompositeOperation = 'destination-out';
|
|
|
|
self.fillStyle(context, 255, 255, 255, 1);
|
|
self.strokeStyle(context, 255, 255, 255, 1);
|
|
|
|
self.drawArrowShape( edge, prefix, context,
|
|
arrowClearFill, style['width'].pxValue, style[prefix + '-arrow-shape'].value,
|
|
x, y, dispX, dispY
|
|
);
|
|
|
|
context.globalCompositeOperation = gco;
|
|
} // otherwise, the opaque arrow clears it for free :)
|
|
|
|
var color = style[prefix + '-arrow-color'].value;
|
|
self.fillStyle(context, color[0], color[1], color[2], style.opacity.value);
|
|
self.strokeStyle(context, color[0], color[1], color[2], style.opacity.value);
|
|
|
|
self.drawArrowShape( edge, prefix, context,
|
|
arrowFill, style['width'].pxValue, style[prefix + '-arrow-shape'].value,
|
|
x, y, dispX, dispY
|
|
);
|
|
}
|
|
|
|
dispX = startX - srcPos.x;
|
|
dispY = startY - srcPos.y;
|
|
|
|
if( !isHaystack && !isNaN(startX) && !isNaN(startY) && !isNaN(dispX) && !isNaN(dispY) ){
|
|
drawArrowhead( 'source', startX, startY, dispX, dispY );
|
|
|
|
} else {
|
|
// window.badArrow = true;
|
|
// debugger;
|
|
}
|
|
|
|
var midX = rs.midX;
|
|
var midY = rs.midY;
|
|
|
|
if( isHaystack ){
|
|
midX = ( startX + endX )/2;
|
|
midY = ( startY + endY )/2;
|
|
}
|
|
|
|
dispX = startX - endX;
|
|
dispY = startY - endY;
|
|
|
|
if( rs.edgeType === 'self' ){
|
|
dispX = 1;
|
|
dispY = -1;
|
|
}
|
|
|
|
if( !isNaN(midX) && !isNaN(midY) ){
|
|
drawArrowhead( 'mid-target', midX, midY, dispX, dispY );
|
|
}
|
|
|
|
dispX *= -1;
|
|
dispY *= -1;
|
|
|
|
if( !isNaN(midX) && !isNaN(midY) ){
|
|
drawArrowhead( 'mid-source', midX, midY, dispX, dispY );
|
|
}
|
|
|
|
dispX = endX - tgtPos.x;
|
|
dispY = endY - tgtPos.y;
|
|
|
|
if( !isHaystack && !isNaN(endX) && !isNaN(endY) && !isNaN(dispX) && !isNaN(dispY) ){
|
|
drawArrowhead( 'target', endX, endY, dispX, dispY );
|
|
}
|
|
};
|
|
|
|
// Draw arrowshape
|
|
CRp.drawArrowShape = function(edge, arrowType, context, fill, edgeWidth, shape, x, y, dispX, dispY) {
|
|
var usePaths = CanvasRenderer.usePaths();
|
|
var rs = edge._private.rscratch;
|
|
var pathCacheHit = false;
|
|
var path;
|
|
var canvasContext = context;
|
|
var translation = { x: x, y: y };
|
|
|
|
// Negative of the angle
|
|
var angle = Math.asin(dispY / (Math.sqrt(dispX * dispX + dispY * dispY)));
|
|
|
|
if (dispX < 0) {
|
|
angle = angle + Math.PI / 2;
|
|
} else {
|
|
angle = - (Math.PI / 2 + angle);
|
|
}
|
|
|
|
var size = this.getArrowWidth( edgeWidth );
|
|
var shapeImpl = CanvasRenderer.arrowShapes[shape];
|
|
|
|
// context.translate(x, y);
|
|
|
|
if( usePaths ){
|
|
var pathCacheKey = size + '$' + shape + '$' + angle + '$' + x + '$' + y;
|
|
rs.arrowPathCacheKey = rs.arrowPathCacheKey || {};
|
|
rs.arrowPathCache = rs.arrowPathCache || {};
|
|
|
|
var alreadyCached = rs.arrowPathCacheKey[arrowType] === pathCacheKey;
|
|
if( alreadyCached ){
|
|
path = context = rs.arrowPathCache[arrowType];
|
|
pathCacheHit = true;
|
|
} else {
|
|
path = context = new Path2D();
|
|
rs.arrowPathCacheKey[arrowType] = pathCacheKey;
|
|
rs.arrowPathCache[arrowType] = path;
|
|
}
|
|
}
|
|
|
|
if( context.beginPath ){ context.beginPath(); }
|
|
|
|
if( !pathCacheHit ){
|
|
shapeImpl.draw(context, size, angle, translation);
|
|
}
|
|
|
|
if( !shapeImpl.leavePathOpen && context.closePath ){
|
|
context.closePath();
|
|
}
|
|
|
|
context = canvasContext;
|
|
|
|
if( fill === 'filled' || fill === 'both' ){
|
|
if( usePaths ){
|
|
context.fill( path );
|
|
} else {
|
|
context.fill();
|
|
}
|
|
}
|
|
|
|
if( fill === 'hollow' || fill === 'both' ){
|
|
context.lineWidth = ( shapeImpl.matchEdgeWidth ? edgeWidth : 1 );
|
|
context.lineJoin = 'miter';
|
|
|
|
if( usePaths ){
|
|
context.stroke( path );
|
|
} else {
|
|
context.stroke();
|
|
}
|
|
|
|
}
|
|
|
|
// context.translate(-x, -y);
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var CRp = CanvasRenderer.prototype;
|
|
|
|
CRp.getCachedImage = function(url, onLoad) {
|
|
var r = this;
|
|
var imageCache = r.imageCache = r.imageCache || {};
|
|
|
|
if( imageCache[url] && imageCache[url].image ){
|
|
return imageCache[url].image;
|
|
}
|
|
|
|
var cache = imageCache[url] = imageCache[url] || {};
|
|
|
|
var image = cache.image = new Image();
|
|
image.addEventListener('load', onLoad);
|
|
image.src = url;
|
|
|
|
return image;
|
|
};
|
|
|
|
CRp.safeDrawImage = function( context, img, ix, iy, iw, ih, x, y, w, h ){
|
|
var r = this;
|
|
|
|
try {
|
|
context.drawImage( img, ix, iy, iw, ih, x, y, w, h );
|
|
} catch(e){
|
|
r.data.canvasNeedsRedraw[CanvasRenderer.NODE] = true;
|
|
r.data.canvasNeedsRedraw[CanvasRenderer.DRAG] = true;
|
|
|
|
r.drawingImage = true;
|
|
|
|
r.redraw();
|
|
}
|
|
};
|
|
|
|
CRp.drawInscribedImage = function(context, img, node) {
|
|
var r = this;
|
|
var nodeX = node._private.position.x;
|
|
var nodeY = node._private.position.y;
|
|
var style = node._private.style;
|
|
var fit = style['background-fit'].value;
|
|
var xPos = style['background-position-x'];
|
|
var yPos = style['background-position-y'];
|
|
var repeat = style['background-repeat'].value;
|
|
var nodeW = node.width();
|
|
var nodeH = node.height();
|
|
var rs = node._private.rscratch;
|
|
var clip = style['background-clip'].value;
|
|
var shouldClip = clip === 'node';
|
|
var imgOpacity = style['background-image-opacity'].value;
|
|
|
|
var w = img.width;
|
|
var h = img.height;
|
|
|
|
if( w === 0 || h === 0 ){
|
|
return; // no point in drawing empty image (and chrome is broken in this case)
|
|
}
|
|
|
|
var bgW = style['background-width'];
|
|
if( bgW.value !== 'auto' ){
|
|
if( bgW.units === '%' ){
|
|
w = bgW.value/100 * nodeW;
|
|
} else {
|
|
w = bgW.pxValue;
|
|
}
|
|
}
|
|
|
|
var bgH = style['background-height'];
|
|
if( bgH.value !== 'auto' ){
|
|
if( bgH.units === '%' ){
|
|
h = bgH.value/100 * nodeH;
|
|
} else {
|
|
h = bgH.pxValue;
|
|
}
|
|
}
|
|
|
|
if( w === 0 || h === 0 ){
|
|
return; // no point in drawing empty image (and chrome is broken in this case)
|
|
}
|
|
|
|
if( fit === 'contain' ){
|
|
var scale = Math.min( nodeW/w, nodeH/h );
|
|
|
|
w *= scale;
|
|
h *= scale;
|
|
|
|
} else if( fit === 'cover' ){
|
|
var scale = Math.max( nodeW/w, nodeH/h );
|
|
|
|
w *= scale;
|
|
h *= scale;
|
|
}
|
|
|
|
var x = (nodeX - nodeW/2); // left
|
|
if( xPos.units === '%' ){
|
|
x += (nodeW - w) * xPos.value/100;
|
|
} else {
|
|
x += xPos.pxValue;
|
|
}
|
|
|
|
var y = (nodeY - nodeH/2); // top
|
|
if( yPos.units === '%' ){
|
|
y += (nodeH - h) * yPos.value/100;
|
|
} else {
|
|
y += yPos.pxValue;
|
|
}
|
|
|
|
if( rs.pathCache ){
|
|
x -= nodeX;
|
|
y -= nodeY;
|
|
|
|
nodeX = 0;
|
|
nodeY = 0;
|
|
}
|
|
|
|
var gAlpha = context.globalAlpha;
|
|
|
|
context.globalAlpha = imgOpacity;
|
|
|
|
if( repeat === 'no-repeat' ){
|
|
|
|
if( shouldClip ){
|
|
context.save();
|
|
|
|
if( rs.pathCache ){
|
|
context.clip( rs.pathCache );
|
|
} else {
|
|
CanvasRenderer.nodeShapes[r.getNodeShape(node)].drawPath(
|
|
context,
|
|
nodeX, nodeY,
|
|
nodeW, nodeH);
|
|
|
|
context.clip();
|
|
}
|
|
}
|
|
|
|
// context.drawImage( img, 0, 0, img.width, img.height, x, y, w, h );
|
|
r.safeDrawImage( context, img, 0, 0, img.width, img.height, x, y, w, h );
|
|
|
|
if( shouldClip ){
|
|
context.restore();
|
|
}
|
|
} else {
|
|
var pattern = context.createPattern( img, repeat );
|
|
context.fillStyle = pattern;
|
|
|
|
CanvasRenderer.nodeShapes[r.getNodeShape(node)].drawPath(
|
|
context,
|
|
nodeX, nodeY,
|
|
nodeW, nodeH);
|
|
|
|
context.translate(x, y);
|
|
context.fill();
|
|
context.translate(-x, -y);
|
|
}
|
|
|
|
context.globalAlpha = gAlpha;
|
|
|
|
};
|
|
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var CRp = CanvasRenderer.prototype;
|
|
|
|
// Draw edge text
|
|
CRp.drawEdgeText = function(context, edge) {
|
|
var text = edge._private.style['content'].strValue;
|
|
|
|
if( !text || text.match(/^\s+$/) ){
|
|
return;
|
|
}
|
|
|
|
if( this.hideEdgesOnViewport && (this.dragData.didDrag || this.pinching || this.hoverData.dragging || this.data.wheel || this.swipePanning) ){ return; } // save cycles on pinching
|
|
|
|
var computedSize = edge._private.style['font-size'].pxValue * edge.cy().zoom();
|
|
var minSize = edge._private.style['min-zoomed-font-size'].pxValue;
|
|
|
|
if( computedSize < minSize ){
|
|
return;
|
|
}
|
|
|
|
// Calculate text draw position
|
|
|
|
context.textAlign = 'center';
|
|
context.textBaseline = 'middle';
|
|
|
|
var rs = edge._private.rscratch;
|
|
if( !$$.is.number( rs.labelX ) || !$$.is.number( rs.labelY ) ){ return; } // no pos => label can't be rendered
|
|
|
|
var style = edge._private.style;
|
|
var autorotate = style['edge-text-rotation'].strValue === 'autorotate';
|
|
var theta, dx, dy;
|
|
|
|
if( autorotate ){
|
|
switch( rs.edgeType ){
|
|
case 'haystack':
|
|
dx = rs.haystackPts[2] - rs.haystackPts[0];
|
|
dy = rs.haystackPts[3] - rs.haystackPts[1];
|
|
break;
|
|
default:
|
|
dx = rs.endX - rs.startX;
|
|
dy = rs.endY - rs.startY;
|
|
}
|
|
|
|
theta = Math.atan( dy / dx );
|
|
|
|
context.translate(rs.labelX, rs.labelY);
|
|
context.rotate(theta);
|
|
|
|
this.drawText(context, edge, 0, 0);
|
|
|
|
context.rotate(-theta);
|
|
context.translate(-rs.labelX, -rs.labelY);
|
|
} else {
|
|
this.drawText(context, edge, rs.labelX, rs.labelY);
|
|
}
|
|
|
|
};
|
|
|
|
// Draw node text
|
|
CRp.drawNodeText = function(context, node) {
|
|
var text = node._private.style['content'].strValue;
|
|
|
|
if ( !text || text.match(/^\s+$/) ) {
|
|
return;
|
|
}
|
|
|
|
var computedSize = node._private.style['font-size'].pxValue * node.cy().zoom();
|
|
var minSize = node._private.style['min-zoomed-font-size'].pxValue;
|
|
|
|
if( computedSize < minSize ){
|
|
return;
|
|
}
|
|
|
|
// this.recalculateNodeLabelProjection( node );
|
|
|
|
var textHalign = node._private.style['text-halign'].strValue;
|
|
var textValign = node._private.style['text-valign'].strValue;
|
|
var rs = node._private.rscratch;
|
|
if( !$$.is.number( rs.labelX ) || !$$.is.number( rs.labelY ) ){ return; } // no pos => label can't be rendered
|
|
|
|
switch( textHalign ){
|
|
case 'left':
|
|
context.textAlign = 'right';
|
|
break;
|
|
|
|
case 'right':
|
|
context.textAlign = 'left';
|
|
break;
|
|
|
|
default: // e.g. center
|
|
context.textAlign = 'center';
|
|
}
|
|
|
|
switch( textValign ){
|
|
case 'top':
|
|
context.textBaseline = 'bottom';
|
|
break;
|
|
|
|
case 'bottom':
|
|
context.textBaseline = 'top';
|
|
break;
|
|
|
|
default: // e.g. center
|
|
context.textBaseline = 'middle';
|
|
}
|
|
|
|
this.drawText(context, node, rs.labelX, rs.labelY);
|
|
};
|
|
|
|
CRp.getFontCache = function(context){
|
|
var cache;
|
|
|
|
this.fontCaches = this.fontCaches || [];
|
|
|
|
for( var i = 0; i < this.fontCaches.length; i++ ){
|
|
cache = this.fontCaches[i];
|
|
|
|
if( cache.context === context ){
|
|
return cache;
|
|
}
|
|
}
|
|
|
|
cache = {
|
|
context: context
|
|
};
|
|
this.fontCaches.push(cache);
|
|
|
|
return cache;
|
|
};
|
|
|
|
// set up canvas context with font
|
|
// returns transformed text string
|
|
CRp.setupTextStyle = function( context, element ){
|
|
// Font style
|
|
var parentOpacity = element.effectiveOpacity();
|
|
var style = element._private.style;
|
|
var labelStyle = style['font-style'].strValue;
|
|
var labelSize = style['font-size'].pxValue + 'px';
|
|
var labelFamily = style['font-family'].strValue;
|
|
var labelWeight = style['font-weight'].strValue;
|
|
var opacity = style['text-opacity'].value * style['opacity'].value * parentOpacity;
|
|
var outlineOpacity = style['text-outline-opacity'].value * opacity;
|
|
var color = style['color'].value;
|
|
var outlineColor = style['text-outline-color'].value;
|
|
var shadowBlur = style['text-shadow-blur'].pxValue;
|
|
var shadowOpacity = style['text-shadow-opacity'].value;
|
|
var shadowColor = style['text-shadow-color'].value;
|
|
var shadowOffsetX = style['text-shadow-offset-x'].pxValue;
|
|
var shadowOffsetY = style['text-shadow-offset-y'].pxValue;
|
|
|
|
var fontCacheKey = element._private.fontKey;
|
|
var cache = this.getFontCache(context);
|
|
|
|
if( cache.key !== fontCacheKey ){
|
|
context.font = labelStyle + ' ' + labelWeight + ' ' + labelSize + ' ' + labelFamily;
|
|
|
|
cache.key = fontCacheKey;
|
|
}
|
|
|
|
var text = this.getLabelText( element );
|
|
|
|
// Calculate text draw position based on text alignment
|
|
|
|
// so text outlines aren't jagged
|
|
context.lineJoin = 'round';
|
|
|
|
this.fillStyle(context, color[0], color[1], color[2], opacity);
|
|
|
|
this.strokeStyle(context, outlineColor[0], outlineColor[1], outlineColor[2], outlineOpacity);
|
|
|
|
this.shadowStyle(context, shadowColor, shadowOpacity, shadowBlur, shadowOffsetX, shadowOffsetY);
|
|
|
|
return text;
|
|
};
|
|
|
|
function roundRect(ctx, x, y, width, height, radius) {
|
|
var radius = radius || 5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + radius, y);
|
|
ctx.lineTo(x + width - radius, y);
|
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
ctx.lineTo(x + width, y + height - radius);
|
|
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
ctx.lineTo(x + radius, y + height);
|
|
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
ctx.lineTo(x, y + radius);
|
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
|
|
// Draw text
|
|
CRp.drawText = function(context, element, textX, textY) {
|
|
var _p = element._private;
|
|
var style = _p.style;
|
|
var rstyle = _p.rstyle;
|
|
var rscratch = _p.rscratch;
|
|
var parentOpacity = element.effectiveOpacity();
|
|
if( parentOpacity === 0 || style['text-opacity'].value === 0){ return; }
|
|
|
|
var text = this.setupTextStyle( context, element );
|
|
var halign = style['text-halign'].value;
|
|
var valign = style['text-valign'].value;
|
|
|
|
if( element.isEdge() ){
|
|
halign = 'center';
|
|
valign = 'center';
|
|
}
|
|
|
|
if ( text != null && !isNaN(textX) && !isNaN(textY)) {
|
|
var backgroundOpacity = style['text-background-opacity'].value;
|
|
var borderOpacity = style['text-border-opacity'].value;
|
|
var textBorderWidth = style['text-border-width'].pxValue;
|
|
|
|
if( backgroundOpacity > 0 || (textBorderWidth > 0 && borderOpacity > 0) ){
|
|
var margin = 4 + textBorderWidth/2;
|
|
|
|
if (element.isNode()) {
|
|
//Move textX, textY to include the background margins
|
|
if (valign === 'top') {
|
|
textY -= margin;
|
|
} else if (valign === 'bottom') {
|
|
textY += margin;
|
|
}
|
|
if (halign === 'left') {
|
|
textX -= margin;
|
|
} else if (halign === 'right') {
|
|
textX += margin;
|
|
}
|
|
}
|
|
|
|
var bgWidth = rstyle.labelWidth;
|
|
var bgHeight = rstyle.labelHeight;
|
|
var bgX = textX;
|
|
|
|
if (halign) {
|
|
if (halign == 'center') {
|
|
bgX = bgX - bgWidth / 2;
|
|
} else if (halign == 'left') {
|
|
bgX = bgX- bgWidth;
|
|
}
|
|
}
|
|
|
|
var bgY = textY;
|
|
|
|
if (element.isNode()) {
|
|
if (valign == 'top') {
|
|
bgY = bgY - bgHeight;
|
|
} else if (valign == 'center') {
|
|
bgY = bgY- bgHeight / 2;
|
|
}
|
|
} else {
|
|
bgY = bgY - bgHeight / 2;
|
|
}
|
|
|
|
if (style['edge-text-rotation'].strValue === 'autorotate') {
|
|
textY = 0;
|
|
bgWidth += 4;
|
|
bgX = textX - bgWidth / 2;
|
|
bgY = textY - bgHeight / 2;
|
|
} else {
|
|
// Adjust with border width & margin
|
|
bgX -= margin;
|
|
bgY -= margin;
|
|
bgHeight += margin*2;
|
|
bgWidth += margin*2;
|
|
}
|
|
|
|
if( backgroundOpacity > 0 ){
|
|
var textFill = context.fillStyle;
|
|
var textBackgroundColor = style['text-background-color'].value;
|
|
|
|
context.fillStyle = 'rgba(' + textBackgroundColor[0] + ',' + textBackgroundColor[1] + ',' + textBackgroundColor[2] + ',' + backgroundOpacity * parentOpacity + ')';
|
|
var styleShape = style['text-background-shape'].strValue;
|
|
if (styleShape == 'roundrectangle') {
|
|
roundRect(context, bgX, bgY, bgWidth, bgHeight, 2);
|
|
} else {
|
|
context.fillRect(bgX,bgY,bgWidth,bgHeight);
|
|
}
|
|
context.fillStyle = textFill;
|
|
}
|
|
|
|
if( textBorderWidth > 0 && borderOpacity > 0 ){
|
|
var textStroke = context.strokeStyle;
|
|
var textLineWidth = context.lineWidth;
|
|
var textBorderColor = style['text-border-color'].value;
|
|
var textBorderStyle = style['text-border-style'].value;
|
|
|
|
context.strokeStyle = 'rgba(' + textBorderColor[0] + ',' + textBorderColor[1] + ',' + textBorderColor[2] + ',' + borderOpacity * parentOpacity + ')';
|
|
context.lineWidth = textBorderWidth;
|
|
|
|
if( context.setLineDash ){ // for very outofdate browsers
|
|
switch( textBorderStyle ){
|
|
case 'dotted':
|
|
context.setLineDash([ 1, 1 ]);
|
|
break;
|
|
case 'dashed':
|
|
context.setLineDash([ 4, 2 ]);
|
|
break;
|
|
case 'double':
|
|
context.lineWidth = textBorderWidth/4; // 50% reserved for white between the two borders
|
|
context.setLineDash([ ]);
|
|
break;
|
|
case 'solid':
|
|
context.setLineDash([ ]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
context.strokeRect(bgX,bgY,bgWidth,bgHeight);
|
|
|
|
if( textBorderStyle === 'double' ){
|
|
var whiteWidth = textBorderWidth/2;
|
|
|
|
context.strokeRect(bgX+whiteWidth,bgY+whiteWidth,bgWidth-whiteWidth*2,bgHeight-whiteWidth*2);
|
|
}
|
|
|
|
if( context.setLineDash ){ // for very outofdate browsers
|
|
context.setLineDash([ ]);
|
|
}
|
|
context.lineWidth = textLineWidth;
|
|
context.strokeStyle = textStroke;
|
|
}
|
|
|
|
}
|
|
|
|
var lineWidth = 2 * style['text-outline-width'].pxValue; // *2 b/c the stroke is drawn centred on the middle
|
|
|
|
if( lineWidth > 0 ){
|
|
context.lineWidth = lineWidth;
|
|
}
|
|
|
|
if( style['text-wrap'].value === 'wrap' ){ //console.log('draw wrap');
|
|
var lines = rscratch.labelWrapCachedLines;
|
|
var lineHeight = rstyle.labelHeight / lines.length;
|
|
|
|
//console.log('lines', lines);
|
|
|
|
switch( valign ){
|
|
case 'top':
|
|
textY -= (lines.length - 1) * lineHeight;
|
|
break;
|
|
|
|
case 'bottom':
|
|
// nothing required
|
|
break;
|
|
|
|
default:
|
|
case 'center':
|
|
textY -= (lines.length - 1) * lineHeight / 2;
|
|
}
|
|
|
|
for( var l = 0; l < lines.length; l++ ){
|
|
if( lineWidth > 0 ){
|
|
context.strokeText( lines[l], textX, textY );
|
|
}
|
|
|
|
context.fillText( lines[l], textX, textY );
|
|
|
|
textY += lineHeight;
|
|
}
|
|
|
|
// var fontSize = style['font-size'].pxValue;
|
|
// wrapText(context, text, textX, textY, style['text-max-width'].pxValue, fontSize + 1);
|
|
} else {
|
|
if( lineWidth > 0 ){
|
|
context.strokeText( text, textX, textY );
|
|
}
|
|
|
|
context.fillText( text, textX, textY );
|
|
}
|
|
|
|
|
|
this.shadowStyle(context, 'transparent', 0); // reset for next guy
|
|
}
|
|
};
|
|
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var CRp = CanvasRenderer.prototype;
|
|
|
|
// Draw node
|
|
CRp.drawNode = function(context, node, drawOverlayInstead) {
|
|
|
|
var r = this;
|
|
var nodeWidth, nodeHeight;
|
|
var style = node._private.style;
|
|
var rs = node._private.rscratch;
|
|
var _p = node._private;
|
|
var pos = _p.position;
|
|
|
|
if( pos.x === undefined || pos.y === undefined ){
|
|
return; // can't draw node with undefined position
|
|
}
|
|
|
|
var usePaths = CanvasRenderer.usePaths();
|
|
var canvasContext = context;
|
|
var path;
|
|
var pathCacheHit = false;
|
|
|
|
var overlayPadding = style['overlay-padding'].pxValue;
|
|
var overlayOpacity = style['overlay-opacity'].value;
|
|
var overlayColor = style['overlay-color'].value;
|
|
|
|
if( drawOverlayInstead && overlayOpacity === 0 ){ // exit early if drawing overlay but none to draw
|
|
return;
|
|
}
|
|
|
|
var parentOpacity = node.effectiveOpacity();
|
|
if( parentOpacity === 0 ){ return; }
|
|
|
|
nodeWidth = this.getNodeWidth(node);
|
|
nodeHeight = this.getNodeHeight(node);
|
|
|
|
context.lineWidth = style['border-width'].pxValue;
|
|
|
|
if( drawOverlayInstead === undefined || !drawOverlayInstead ){
|
|
|
|
var url = style['background-image'].value[2] ||
|
|
style['background-image'].value[1];
|
|
var image;
|
|
|
|
if (url !== undefined) {
|
|
|
|
// get image, and if not loaded then ask to redraw when later loaded
|
|
image = this.getCachedImage(url, function(){
|
|
r.data.canvasNeedsRedraw[CanvasRenderer.NODE] = true;
|
|
r.data.canvasNeedsRedraw[CanvasRenderer.DRAG] = true;
|
|
|
|
r.drawingImage = true;
|
|
|
|
r.redraw();
|
|
});
|
|
|
|
var prevBging = _p.backgrounding;
|
|
_p.backgrounding = !image.complete;
|
|
|
|
if( prevBging !== _p.backgrounding ){ // update style b/c :backgrounding state changed
|
|
node.updateStyle( false );
|
|
}
|
|
}
|
|
|
|
// Node color & opacity
|
|
|
|
var bgColor = style['background-color'].value;
|
|
var borderColor = style['border-color'].value;
|
|
var borderStyle = style['border-style'].value;
|
|
|
|
this.fillStyle(context, bgColor[0], bgColor[1], bgColor[2], style['background-opacity'].value * parentOpacity);
|
|
|
|
this.strokeStyle(context, borderColor[0], borderColor[1], borderColor[2], style['border-opacity'].value * parentOpacity);
|
|
|
|
var shadowBlur = style['shadow-blur'].pxValue;
|
|
var shadowOpacity = style['shadow-opacity'].value;
|
|
var shadowColor = style['shadow-color'].value;
|
|
var shadowOffsetX = style['shadow-offset-x'].pxValue;
|
|
var shadowOffsetY = style['shadow-offset-y'].pxValue;
|
|
|
|
this.shadowStyle(context, shadowColor, shadowOpacity, shadowBlur, shadowOffsetX, shadowOffsetY);
|
|
|
|
context.lineJoin = 'miter'; // so borders are square with the node shape
|
|
|
|
if( context.setLineDash ){ // for very outofdate browsers
|
|
switch( borderStyle ){
|
|
case 'dotted':
|
|
context.setLineDash([ 1, 1 ]);
|
|
break;
|
|
|
|
case 'dashed':
|
|
context.setLineDash([ 4, 2 ]);
|
|
break;
|
|
|
|
case 'solid':
|
|
case 'double':
|
|
context.setLineDash([ ]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
var styleShape = style['shape'].strValue;
|
|
|
|
if( usePaths ){
|
|
var pathCacheKey = styleShape + '$' + nodeWidth +'$' + nodeHeight;
|
|
|
|
context.translate( pos.x, pos.y );
|
|
|
|
if( rs.pathCacheKey === pathCacheKey ){
|
|
path = context = rs.pathCache;
|
|
pathCacheHit = true;
|
|
} else {
|
|
path = context = new Path2D();
|
|
rs.pathCacheKey = pathCacheKey;
|
|
rs.pathCache = path;
|
|
}
|
|
}
|
|
|
|
if( !pathCacheHit ){
|
|
|
|
var npos = pos;
|
|
|
|
if( usePaths ){
|
|
npos = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
}
|
|
|
|
CanvasRenderer.nodeShapes[this.getNodeShape(node)].drawPath(
|
|
context,
|
|
npos.x,
|
|
npos.y,
|
|
nodeWidth,
|
|
nodeHeight);
|
|
}
|
|
|
|
context = canvasContext;
|
|
|
|
if( usePaths ){
|
|
context.fill( path );
|
|
} else {
|
|
context.fill();
|
|
}
|
|
|
|
this.shadowStyle(context, 'transparent', 0); // reset for next guy
|
|
|
|
if (url !== undefined) {
|
|
if( image.complete ){
|
|
this.drawInscribedImage(context, image, node);
|
|
}
|
|
}
|
|
|
|
var darkness = style['background-blacken'].value;
|
|
var borderWidth = style['border-width'].pxValue;
|
|
|
|
if( this.hasPie(node) ){
|
|
this.drawPie( context, node, parentOpacity );
|
|
|
|
// redraw path for blacken and border
|
|
if( darkness !== 0 || borderWidth !== 0 ){
|
|
|
|
if( !usePaths ){
|
|
CanvasRenderer.nodeShapes[this.getNodeShape(node)].drawPath(
|
|
context,
|
|
pos.x,
|
|
pos.y,
|
|
nodeWidth,
|
|
nodeHeight);
|
|
}
|
|
}
|
|
}
|
|
|
|
if( darkness > 0 ){
|
|
this.fillStyle(context, 0, 0, 0, darkness);
|
|
|
|
if( usePaths ){
|
|
context.fill( path );
|
|
} else {
|
|
context.fill();
|
|
}
|
|
|
|
} else if( darkness < 0 ){
|
|
this.fillStyle(context, 255, 255, 255, -darkness);
|
|
|
|
if( usePaths ){
|
|
context.fill( path );
|
|
} else {
|
|
context.fill();
|
|
}
|
|
}
|
|
|
|
// Border width, draw border
|
|
if (borderWidth > 0) {
|
|
|
|
if( usePaths ){
|
|
context.stroke( path );
|
|
} else {
|
|
context.stroke();
|
|
}
|
|
|
|
if( borderStyle === 'double' ){
|
|
context.lineWidth = style['border-width'].pxValue/3;
|
|
|
|
var gco = context.globalCompositeOperation;
|
|
context.globalCompositeOperation = 'destination-out';
|
|
|
|
if( usePaths ){
|
|
context.stroke( path );
|
|
} else {
|
|
context.stroke();
|
|
}
|
|
|
|
context.globalCompositeOperation = gco;
|
|
}
|
|
|
|
}
|
|
|
|
if( usePaths ){
|
|
context.translate( -pos.x, -pos.y );
|
|
}
|
|
|
|
// reset in case we changed the border style
|
|
if( context.setLineDash ){ // for very outofdate browsers
|
|
context.setLineDash([ ]);
|
|
}
|
|
|
|
// draw the overlay
|
|
} else {
|
|
|
|
if( overlayOpacity > 0 ){
|
|
this.fillStyle(context, overlayColor[0], overlayColor[1], overlayColor[2], overlayOpacity);
|
|
|
|
CanvasRenderer.nodeShapes['roundrectangle'].drawPath(
|
|
context,
|
|
node._private.position.x,
|
|
node._private.position.y,
|
|
nodeWidth + overlayPadding * 2,
|
|
nodeHeight + overlayPadding * 2
|
|
);
|
|
|
|
context.fill();
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
// does the node have at least one pie piece?
|
|
CRp.hasPie = function(node){
|
|
node = node[0]; // ensure ele ref
|
|
|
|
return node._private.hasPie;
|
|
};
|
|
|
|
CRp.drawPie = function( context, node, nodeOpacity ){
|
|
node = node[0]; // ensure ele ref
|
|
|
|
var _p = node._private;
|
|
var style = _p.style;
|
|
var pieSize = style['pie-size'];
|
|
var nodeW = this.getNodeWidth( node );
|
|
var nodeH = this.getNodeHeight( node );
|
|
var x = _p.position.x;
|
|
var y = _p.position.y;
|
|
var radius = Math.min( nodeW, nodeH ) / 2; // must fit in node
|
|
var lastPercent = 0; // what % to continue drawing pie slices from on [0, 1]
|
|
var usePaths = CanvasRenderer.usePaths();
|
|
|
|
if( usePaths ){
|
|
x = 0;
|
|
y = 0;
|
|
}
|
|
|
|
if( pieSize.units === '%' ){
|
|
radius = radius * pieSize.value / 100;
|
|
} else if( pieSize.pxValue !== undefined ){
|
|
radius = pieSize.pxValue / 2;
|
|
}
|
|
|
|
for( var i = 1; i <= $$.style.pieBackgroundN; i++ ){ // 1..N
|
|
var size = style['pie-' + i + '-background-size'].value;
|
|
var color = style['pie-' + i + '-background-color'].value;
|
|
var opacity = style['pie-' + i + '-background-opacity'].value * nodeOpacity;
|
|
var percent = size / 100; // map integer range [0, 100] to [0, 1]
|
|
|
|
// percent can't push beyond 1
|
|
if( percent + lastPercent > 1 ){
|
|
percent = 1 - lastPercent;
|
|
}
|
|
|
|
var angleStart = 1.5 * Math.PI + 2 * Math.PI * lastPercent; // start at 12 o'clock and go clockwise
|
|
var angleDelta = 2 * Math.PI * percent;
|
|
var angleEnd = angleStart + angleDelta;
|
|
|
|
// ignore if
|
|
// - zero size
|
|
// - we're already beyond the full circle
|
|
// - adding the current slice would go beyond the full circle
|
|
if( size === 0 || lastPercent >= 1 || lastPercent + percent > 1 ){
|
|
continue;
|
|
}
|
|
|
|
context.beginPath();
|
|
context.moveTo(x, y);
|
|
context.arc( x, y, radius, angleStart, angleEnd );
|
|
context.closePath();
|
|
|
|
this.fillStyle(context, color[0], color[1], color[2], opacity);
|
|
|
|
context.fill();
|
|
|
|
lastPercent += percent;
|
|
}
|
|
|
|
};
|
|
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var CR = CanvasRenderer;
|
|
var CRp = CanvasRenderer.prototype;
|
|
|
|
// var isFirefox = typeof InstallTrigger !== 'undefined';
|
|
|
|
CRp.getPixelRatio = function(){
|
|
var context = this.data.contexts[0];
|
|
|
|
if( this.forcedPixelRatio != null ){
|
|
return this.forcedPixelRatio;
|
|
}
|
|
|
|
var backingStore = context.backingStorePixelRatio ||
|
|
context.webkitBackingStorePixelRatio ||
|
|
context.mozBackingStorePixelRatio ||
|
|
context.msBackingStorePixelRatio ||
|
|
context.oBackingStorePixelRatio ||
|
|
context.backingStorePixelRatio || 1;
|
|
|
|
//console.log(window.devicePixelRatio, backingStore);
|
|
|
|
// if( isFirefox ){ // because ff can't scale canvas properly
|
|
// return 1;
|
|
// }
|
|
|
|
return (window.devicePixelRatio || 1) / backingStore;
|
|
};
|
|
|
|
CRp.paintCache = function(context){
|
|
var caches = this.paintCaches = this.paintCaches || [];
|
|
var needToCreateCache = true;
|
|
var cache;
|
|
|
|
for(var i = 0; i < caches.length; i++ ){
|
|
cache = caches[i];
|
|
|
|
if( cache.context === context ){
|
|
needToCreateCache = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( needToCreateCache ){
|
|
cache = {
|
|
context: context
|
|
};
|
|
caches.push( cache );
|
|
}
|
|
|
|
return cache;
|
|
};
|
|
|
|
CRp.fillStyle = function(context, r, g, b, a){
|
|
context.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
|
|
|
|
// turn off for now, seems context does its own caching
|
|
|
|
// var cache = this.paintCache(context);
|
|
|
|
// var fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
|
|
|
|
// if( cache.fillStyle !== fillStyle ){
|
|
// context.fillStyle = cache.fillStyle = fillStyle;
|
|
// }
|
|
};
|
|
|
|
CRp.strokeStyle = function(context, r, g, b, a){
|
|
context.strokeStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
|
|
|
|
// turn off for now, seems context does its own caching
|
|
|
|
// var cache = this.paintCache(context);
|
|
|
|
// var strokeStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
|
|
|
|
// if( cache.strokeStyle !== strokeStyle ){
|
|
// context.strokeStyle = cache.strokeStyle = strokeStyle;
|
|
// }
|
|
};
|
|
|
|
CRp.shadowStyle = function(context, color, opacity, blur, offsetX, offsetY){
|
|
var zoom = this.data.cy.zoom();
|
|
|
|
var cache = this.paintCache(context);
|
|
|
|
// don't make expensive changes to the shadow style if it's not used
|
|
if( cache.shadowOpacity === 0 && opacity === 0 ){
|
|
return;
|
|
}
|
|
|
|
cache.shadowOpacity = opacity;
|
|
|
|
if (opacity > 0) {
|
|
context.shadowBlur = blur * zoom;
|
|
context.shadowColor = "rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + opacity + ")";
|
|
context.shadowOffsetX = offsetX * zoom;
|
|
context.shadowOffsetY = offsetY * zoom;
|
|
} else {
|
|
context.shadowBlur = 0;
|
|
context.shadowColor = "transparent";
|
|
}
|
|
};
|
|
|
|
// Resize canvas
|
|
CRp.matchCanvasSize = function(container) {
|
|
var data = this.data;
|
|
var width = container.clientWidth;
|
|
var height = container.clientHeight;
|
|
var pixelRatio = this.getPixelRatio();
|
|
var mbPxRatio = this.motionBlurPxRatio;
|
|
|
|
if(
|
|
container === this.data.bufferCanvases[CR.MOTIONBLUR_BUFFER_NODE] ||
|
|
container === this.data.bufferCanvases[CR.MOTIONBLUR_BUFFER_DRAG]
|
|
){
|
|
pixelRatio = mbPxRatio;
|
|
}
|
|
|
|
var canvasWidth = width * pixelRatio;
|
|
var canvasHeight = height * pixelRatio;
|
|
var canvas;
|
|
|
|
if( canvasWidth === this.canvasWidth && canvasHeight === this.canvasHeight ){
|
|
return; // save cycles if same
|
|
}
|
|
|
|
this.fontCaches = null; // resizing resets the style
|
|
|
|
var canvasContainer = data.canvasContainer;
|
|
canvasContainer.style.width = width + 'px';
|
|
canvasContainer.style.height = height + 'px';
|
|
|
|
for (var i = 0; i < CanvasRenderer.CANVAS_LAYERS; i++) {
|
|
|
|
canvas = data.canvases[i];
|
|
|
|
if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
|
|
|
|
canvas.width = canvasWidth;
|
|
canvas.height = canvasHeight;
|
|
|
|
canvas.style.width = width + 'px';
|
|
canvas.style.height = height + 'px';
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < CanvasRenderer.BUFFER_COUNT; i++) {
|
|
|
|
canvas = data.bufferCanvases[i];
|
|
|
|
if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
|
|
|
|
canvas.width = canvasWidth;
|
|
canvas.height = canvasHeight;
|
|
|
|
canvas.style.width = width + 'px';
|
|
canvas.style.height = height + 'px';
|
|
}
|
|
}
|
|
|
|
this.textureMult = 1;
|
|
if( pixelRatio <= 1 ){
|
|
canvas = data.bufferCanvases[ CanvasRenderer.TEXTURE_BUFFER ];
|
|
|
|
this.textureMult = 2;
|
|
canvas.width = canvasWidth * this.textureMult;
|
|
canvas.height = canvasHeight * this.textureMult;
|
|
}
|
|
|
|
this.canvasWidth = canvasWidth;
|
|
this.canvasHeight = canvasHeight;
|
|
|
|
};
|
|
|
|
CRp.renderTo = function( cxt, zoom, pan, pxRatio ){
|
|
this.redraw({
|
|
forcedContext: cxt,
|
|
forcedZoom: zoom,
|
|
forcedPan: pan,
|
|
drawAllLayers: true,
|
|
forcedPxRatio: pxRatio
|
|
});
|
|
};
|
|
|
|
CRp.timeToRender = function(){
|
|
return this.redrawTotalTime / this.redrawCount;
|
|
};
|
|
|
|
CanvasRenderer.minRedrawLimit = 1000/60; // people can't see much better than 60fps
|
|
CanvasRenderer.maxRedrawLimit = 1000; // don't cap max b/c it's more important to be responsive than smooth
|
|
CanvasRenderer.motionBlurDelay = 100;
|
|
|
|
// Redraw frame
|
|
CRp.redraw = function( options ) {
|
|
options = options || {};
|
|
|
|
// console.log('redraw()');
|
|
|
|
var forcedContext = options.forcedContext;
|
|
var drawAllLayers = options.drawAllLayers;
|
|
var drawOnlyNodeLayer = options.drawOnlyNodeLayer;
|
|
var forcedZoom = options.forcedZoom;
|
|
var forcedPan = options.forcedPan;
|
|
var r = this;
|
|
var pixelRatio = options.forcedPxRatio === undefined ? this.getPixelRatio() : options.forcedPxRatio;
|
|
var cy = r.data.cy; var data = r.data;
|
|
var needDraw = data.canvasNeedsRedraw;
|
|
var textureDraw = r.textureOnViewport && !forcedContext && (r.pinching || r.hoverData.dragging || r.swipePanning || r.data.wheelZooming);
|
|
var motionBlur = options.motionBlur !== undefined ? options.motionBlur : r.motionBlur;
|
|
var mbPxRatio = r.motionBlurPxRatio;
|
|
var hasCompoundNodes = cy.hasCompoundNodes();
|
|
var inNodeDragGesture = r.hoverData.draggingEles;
|
|
var inBoxSelection = r.hoverData.selecting || r.touchData.selecting ? true : false;
|
|
motionBlur = motionBlur && !forcedContext && r.motionBlurEnabled && !inBoxSelection;
|
|
var motionBlurFadeEffect = motionBlur;
|
|
|
|
// console.log('textureDraw?', textureDraw);
|
|
|
|
|
|
if( !forcedContext && r.motionBlurTimeout ){
|
|
clearTimeout( r.motionBlurTimeout );
|
|
}
|
|
|
|
if( !forcedContext && this.redrawTimeout ){
|
|
clearTimeout( this.redrawTimeout );
|
|
}
|
|
this.redrawTimeout = null;
|
|
|
|
if( this.averageRedrawTime === undefined ){ this.averageRedrawTime = 0; }
|
|
|
|
var minRedrawLimit = CanvasRenderer.minRedrawLimit;
|
|
var maxRedrawLimit = CanvasRenderer.maxRedrawLimit;
|
|
|
|
var redrawLimit = this.averageRedrawTime; // estimate the ideal redraw limit based on how fast we can draw
|
|
redrawLimit = minRedrawLimit > redrawLimit ? minRedrawLimit : redrawLimit;
|
|
redrawLimit = redrawLimit < maxRedrawLimit ? redrawLimit : maxRedrawLimit;
|
|
|
|
//console.log('--\nideal: %i; effective: %i', this.averageRedrawTime, redrawLimit);
|
|
|
|
if( this.lastDrawTime === undefined ){ this.lastDrawTime = 0; }
|
|
|
|
var nowTime = Date.now();
|
|
var timeElapsed = nowTime - this.lastDrawTime;
|
|
var callAfterLimit = timeElapsed >= redrawLimit;
|
|
|
|
if( !forcedContext && !r.clearingMotionBlur ){
|
|
if( !callAfterLimit || this.currentlyDrawing ){
|
|
// console.log('-- skip', redrawLimit);
|
|
|
|
// we have new things to draw but we're busy, so try again when possibly free
|
|
this.redrawTimeout = setTimeout(function(){
|
|
r.redraw();
|
|
}, redrawLimit);
|
|
return;
|
|
}
|
|
|
|
this.lastDrawTime = nowTime;
|
|
this.currentlyDrawing = true;
|
|
}
|
|
|
|
if( motionBlur ){
|
|
if( r.mbFrames == null ){
|
|
r.mbFrames = 0;
|
|
}
|
|
|
|
if( !r.drawingImage ){ // image loading frames don't count towards motion blur blurry frames
|
|
r.mbFrames++;
|
|
}
|
|
|
|
if( r.mbFrames < 3 ){ // need several frames before even high quality motionblur
|
|
motionBlurFadeEffect = false;
|
|
}
|
|
|
|
// go to lower quality blurry frames when several m/b frames have been rendered (avoids flashing)
|
|
if( r.mbFrames > r.minMbLowQualFrames ){
|
|
//r.fullQualityMb = false;
|
|
r.motionBlurPxRatio = r.mbPxRBlurry;
|
|
}
|
|
}
|
|
|
|
// console.log('mb: %s, N: %s, q: %s', motionBlur, r.mbFrames, r.motionBlurPxRatio);
|
|
|
|
if( r.clearingMotionBlur ){
|
|
//r.fullQualityMb = true; // TODO enable when doesn't cause scaled flashing issue
|
|
|
|
r.motionBlurPxRatio = 1;
|
|
}
|
|
|
|
|
|
var startTime = Date.now();
|
|
|
|
// console.log('-- redraw --')
|
|
|
|
function drawToContext(){
|
|
// startTime = Date.now();
|
|
// console.profile('draw' + startTime)
|
|
|
|
// b/c drawToContext() may be async w.r.t. redraw(), keep track of last texture frame
|
|
// because a rogue async texture frame would clear needDraw
|
|
if( r.textureDrawLastFrame && !textureDraw ){
|
|
needDraw[CR.NODE] = true;
|
|
needDraw[CR.SELECT_BOX] = true;
|
|
}
|
|
|
|
// console.log('drawToContext()');
|
|
// console.log( 'needDraw', needDraw[CR.NODE], needDraw[CR.DRAG], needDraw[CR.SELECT_BOX] );
|
|
|
|
var edges = r.getCachedEdges();
|
|
var coreStyle = cy.style()._private.coreStyle;
|
|
|
|
var zoom = cy.zoom();
|
|
var effectiveZoom = forcedZoom !== undefined ? forcedZoom : zoom;
|
|
var pan = cy.pan();
|
|
var effectivePan = {
|
|
x: pan.x,
|
|
y: pan.y
|
|
};
|
|
|
|
var vp = {
|
|
zoom: zoom,
|
|
pan: {
|
|
x: pan.x,
|
|
y: pan.y
|
|
}
|
|
};
|
|
var prevVp = r.prevViewport;
|
|
var viewportIsDiff = prevVp === undefined || vp.zoom !== prevVp.zoom || vp.pan.x !== prevVp.pan.x || vp.pan.y !== prevVp.pan.y;
|
|
|
|
// we want the low quality motionblur only when the viewport is being manipulated etc (where it's not noticed)
|
|
if( !viewportIsDiff && !(inNodeDragGesture && !hasCompoundNodes) ){
|
|
r.motionBlurPxRatio = 1;
|
|
}
|
|
|
|
if( forcedPan ){
|
|
effectivePan = forcedPan;
|
|
}
|
|
|
|
// apply pixel ratio
|
|
|
|
effectiveZoom *= pixelRatio;
|
|
effectivePan.x *= pixelRatio;
|
|
effectivePan.y *= pixelRatio;
|
|
|
|
var eles = {
|
|
drag: {
|
|
nodes: [],
|
|
edges: [],
|
|
eles: []
|
|
},
|
|
nondrag: {
|
|
nodes: [],
|
|
edges: [],
|
|
eles: []
|
|
}
|
|
};
|
|
|
|
function mbclear( context, x, y, w, h ){
|
|
var gco = context.globalCompositeOperation;
|
|
|
|
context.globalCompositeOperation = 'destination-out';
|
|
r.fillStyle( context, 255, 255, 255, r.motionBlurTransparency );
|
|
context.fillRect(x, y, w, h);
|
|
|
|
context.globalCompositeOperation = gco;
|
|
}
|
|
|
|
function setContextTransform(context, clear){
|
|
var ePan, eZoom, w, h;
|
|
|
|
if( /*!r.fullQualityMb &&*/ !r.clearingMotionBlur && (context === data.bufferContexts[CR.MOTIONBLUR_BUFFER_NODE] || context === data.bufferContexts[CR.MOTIONBLUR_BUFFER_DRAG]) ){
|
|
ePan = {
|
|
x: pan.x * mbPxRatio,
|
|
y: pan.y * mbPxRatio
|
|
};
|
|
|
|
eZoom = zoom * mbPxRatio;
|
|
|
|
w = r.canvasWidth * mbPxRatio;
|
|
h = r.canvasHeight * mbPxRatio;
|
|
} else {
|
|
ePan = effectivePan;
|
|
eZoom = effectiveZoom;
|
|
|
|
w = r.canvasWidth;
|
|
h = r.canvasHeight;
|
|
}
|
|
|
|
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
|
|
if( clear === 'motionBlur' ){
|
|
mbclear(context, 0, 0, w, h);
|
|
} else if( !forcedContext && (clear === undefined || clear) ){
|
|
context.clearRect(0, 0, w, h);
|
|
}
|
|
|
|
if( !drawAllLayers ){
|
|
context.translate( ePan.x, ePan.y );
|
|
context.scale( eZoom, eZoom );
|
|
}
|
|
if( forcedPan ){
|
|
context.translate( forcedPan.x, forcedPan.y );
|
|
}
|
|
if( forcedZoom ){
|
|
context.scale( forcedZoom, forcedZoom );
|
|
}
|
|
}
|
|
|
|
if( !textureDraw ){
|
|
r.textureDrawLastFrame = false;
|
|
}
|
|
|
|
if( textureDraw ){
|
|
// console.log('textureDraw')
|
|
|
|
r.textureDrawLastFrame = true;
|
|
|
|
var bb;
|
|
|
|
if( !r.textureCache ){
|
|
r.textureCache = {};
|
|
|
|
bb = r.textureCache.bb = cy.elements().boundingBox();
|
|
|
|
r.textureCache.texture = r.data.bufferCanvases[ CanvasRenderer.TEXTURE_BUFFER ];
|
|
|
|
var cxt = r.data.bufferContexts[ CanvasRenderer.TEXTURE_BUFFER ];
|
|
|
|
cxt.setTransform(1, 0, 0, 1, 0, 0);
|
|
cxt.clearRect(0, 0, r.canvasWidth * r.textureMult, r.canvasHeight * r.textureMult);
|
|
|
|
r.redraw({
|
|
forcedContext: cxt,
|
|
drawOnlyNodeLayer: true,
|
|
forcedPxRatio: pixelRatio * r.textureMult
|
|
});
|
|
|
|
var vp = r.textureCache.viewport = {
|
|
zoom: cy.zoom(),
|
|
pan: cy.pan(),
|
|
width: r.canvasWidth,
|
|
height: r.canvasHeight
|
|
};
|
|
|
|
vp.mpan = {
|
|
x: (0 - vp.pan.x)/vp.zoom,
|
|
y: (0 - vp.pan.y)/vp.zoom
|
|
};
|
|
}
|
|
|
|
needDraw[CR.DRAG] = false;
|
|
needDraw[CR.NODE] = false;
|
|
|
|
var context = data.contexts[CR.NODE];
|
|
|
|
var texture = r.textureCache.texture;
|
|
var vp = r.textureCache.viewport;
|
|
bb = r.textureCache.bb;
|
|
|
|
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
|
|
if( motionBlur ){
|
|
mbclear(context, 0, 0, vp.width, vp.height);
|
|
} else {
|
|
context.clearRect(0, 0, vp.width, vp.height);
|
|
}
|
|
|
|
var outsideBgColor = coreStyle['outside-texture-bg-color'].value;
|
|
var outsideBgOpacity = coreStyle['outside-texture-bg-opacity'].value;
|
|
r.fillStyle( context, outsideBgColor[0], outsideBgColor[1], outsideBgColor[2], outsideBgOpacity );
|
|
context.fillRect( 0, 0, vp.width, vp.height );
|
|
|
|
var zoom = cy.zoom();
|
|
|
|
setContextTransform( context, false );
|
|
|
|
context.clearRect( vp.mpan.x, vp.mpan.y, vp.width/vp.zoom/pixelRatio, vp.height/vp.zoom/pixelRatio );
|
|
context.drawImage( texture, vp.mpan.x, vp.mpan.y, vp.width/vp.zoom/pixelRatio, vp.height/vp.zoom/pixelRatio );
|
|
|
|
} else if( r.textureOnViewport && !forcedContext ){ // clear the cache since we don't need it
|
|
r.textureCache = null;
|
|
}
|
|
|
|
var vpManip = (r.pinching || r.hoverData.dragging || r.swipePanning || r.data.wheelZooming || r.hoverData.draggingEles);
|
|
var hideEdges = r.hideEdgesOnViewport && vpManip;
|
|
var hideLabels = r.hideLabelsOnViewport && vpManip;
|
|
|
|
if (needDraw[CR.DRAG] || needDraw[CR.NODE] || drawAllLayers || drawOnlyNodeLayer) {
|
|
//NB : VERY EXPENSIVE
|
|
|
|
if( hideEdges ){
|
|
} else {
|
|
r.findEdgeControlPoints(edges);
|
|
}
|
|
|
|
var zEles = r.getCachedZSortedEles();
|
|
var extent = cy.extent();
|
|
|
|
for (var i = 0; i < zEles.length; i++) {
|
|
var ele = zEles[i];
|
|
var list;
|
|
var bb = forcedContext ? null : ele.boundingBox();
|
|
var insideExtent = forcedContext ? true : $$.math.boundingBoxesIntersect( extent, bb );
|
|
|
|
if( !insideExtent ){ continue; } // no need to render
|
|
|
|
if ( ele._private.rscratch.inDragLayer ) {
|
|
list = eles.drag;
|
|
} else {
|
|
list = eles.nondrag;
|
|
}
|
|
|
|
list.eles.push( ele );
|
|
}
|
|
|
|
}
|
|
|
|
|
|
function drawElements( list, context ){
|
|
var eles = list.eles;
|
|
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
var ele = eles[i];
|
|
|
|
if( ele.isNode() ){
|
|
r.drawNode(context, ele);
|
|
|
|
if( !hideLabels ){
|
|
r.drawNodeText(context, ele);
|
|
}
|
|
|
|
r.drawNode(context, ele, true);
|
|
} else if( !hideEdges ) {
|
|
r.drawEdge(context, ele);
|
|
|
|
if( !hideLabels ){
|
|
r.drawEdgeText(context, ele);
|
|
}
|
|
|
|
r.drawEdge(context, ele, true);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var needMbClear = [];
|
|
|
|
needMbClear[CR.NODE] = !needDraw[CR.NODE] && motionBlur && !r.clearedForMotionBlur[CR.NODE] || r.clearingMotionBlur;
|
|
if( needMbClear[CR.NODE] ){ r.clearedForMotionBlur[CR.NODE] = true; }
|
|
|
|
needMbClear[CR.DRAG] = !needDraw[CR.DRAG] && motionBlur && !r.clearedForMotionBlur[CR.DRAG] || r.clearingMotionBlur;
|
|
if( needMbClear[CR.DRAG] ){ r.clearedForMotionBlur[CR.DRAG] = true; }
|
|
|
|
// console.log('--');
|
|
|
|
// if( needDraw[CR.DRAG] && motionBlur && needDraw[CR.NODE] && inNodeDragGesture ){
|
|
// console.log('NODE blurclean');
|
|
//
|
|
// var context = data.contexts[CR.NODE];
|
|
//
|
|
// setContextTransform( context, true );
|
|
// drawElements(eles.nondrag, context);
|
|
//
|
|
// needDraw[CR.NODE] = false;
|
|
// needMbClear[CR.NODE] = false;
|
|
//
|
|
// } else
|
|
if( needDraw[CR.NODE] || drawAllLayers || drawOnlyNodeLayer || needMbClear[CR.NODE] ){
|
|
// console.log('NODE', needDraw[CR.NODE], needMbClear[CR.NODE]);
|
|
|
|
var useBuffer = motionBlur && !needMbClear[CR.NODE] && mbPxRatio !== 1;
|
|
var context = forcedContext || ( useBuffer ? r.data.bufferContexts[ CR.MOTIONBLUR_BUFFER_NODE ] : data.contexts[CR.NODE] );
|
|
var clear = motionBlur && !useBuffer ? 'motionBlur' : undefined;
|
|
|
|
// if( needDraw[CR.DRAG] && needDraw[CR.NODE] ){
|
|
// clear = true;
|
|
// }
|
|
|
|
setContextTransform( context, clear );
|
|
drawElements(eles.nondrag, context);
|
|
|
|
if( !drawAllLayers && !motionBlur ){
|
|
needDraw[CR.NODE] = false;
|
|
}
|
|
}
|
|
|
|
if ( !drawOnlyNodeLayer && (needDraw[CR.DRAG] || drawAllLayers || needMbClear[CR.DRAG]) ) {
|
|
// console.log('DRAG');
|
|
|
|
var useBuffer = motionBlur && !needMbClear[CR.DRAG] && mbPxRatio !== 1;
|
|
var context = forcedContext || ( useBuffer ? r.data.bufferContexts[ CR.MOTIONBLUR_BUFFER_DRAG ] : data.contexts[CR.DRAG] );
|
|
|
|
setContextTransform( context, motionBlur && !useBuffer ? 'motionBlur' : undefined );
|
|
drawElements(eles.drag, context);
|
|
|
|
if( !drawAllLayers && !motionBlur ){
|
|
needDraw[CR.DRAG] = false;
|
|
}
|
|
}
|
|
|
|
if( r.showFps || (!drawOnlyNodeLayer && (needDraw[CR.SELECT_BOX] && !drawAllLayers)) ) {
|
|
// console.log('redrawing selection box');
|
|
|
|
var context = forcedContext || data.contexts[CR.SELECT_BOX];
|
|
|
|
setContextTransform( context );
|
|
|
|
if( data.select[4] == 1 && ( r.hoverData.selecting || r.touchData.selecting ) ){
|
|
var zoom = data.cy.zoom();
|
|
var borderWidth = coreStyle['selection-box-border-width'].value / zoom;
|
|
|
|
context.lineWidth = borderWidth;
|
|
context.fillStyle = "rgba("
|
|
+ coreStyle['selection-box-color'].value[0] + ","
|
|
+ coreStyle['selection-box-color'].value[1] + ","
|
|
+ coreStyle['selection-box-color'].value[2] + ","
|
|
+ coreStyle['selection-box-opacity'].value + ")";
|
|
|
|
context.fillRect(
|
|
data.select[0],
|
|
data.select[1],
|
|
data.select[2] - data.select[0],
|
|
data.select[3] - data.select[1]);
|
|
|
|
if (borderWidth > 0) {
|
|
context.strokeStyle = "rgba("
|
|
+ coreStyle['selection-box-border-color'].value[0] + ","
|
|
+ coreStyle['selection-box-border-color'].value[1] + ","
|
|
+ coreStyle['selection-box-border-color'].value[2] + ","
|
|
+ coreStyle['selection-box-opacity'].value + ")";
|
|
|
|
context.strokeRect(
|
|
data.select[0],
|
|
data.select[1],
|
|
data.select[2] - data.select[0],
|
|
data.select[3] - data.select[1]);
|
|
}
|
|
}
|
|
|
|
if( data.bgActivePosistion && !r.hoverData.selecting ){
|
|
var zoom = data.cy.zoom();
|
|
var pos = data.bgActivePosistion;
|
|
|
|
context.fillStyle = "rgba("
|
|
+ coreStyle['active-bg-color'].value[0] + ","
|
|
+ coreStyle['active-bg-color'].value[1] + ","
|
|
+ coreStyle['active-bg-color'].value[2] + ","
|
|
+ coreStyle['active-bg-opacity'].value + ")";
|
|
|
|
context.beginPath();
|
|
context.arc(pos.x, pos.y, coreStyle['active-bg-size'].pxValue / zoom, 0, 2 * Math.PI);
|
|
context.fill();
|
|
}
|
|
|
|
var timeToRender = r.averageRedrawTime;
|
|
if( r.showFps && timeToRender ){
|
|
timeToRender = Math.round( timeToRender );
|
|
var fps = Math.round(1000/timeToRender);
|
|
|
|
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
|
|
//context.font = '20px helvetica';
|
|
context.fillStyle = 'rgba(255, 0, 0, 0.75)';
|
|
context.strokeStyle = 'rgba(255, 0, 0, 0.75)';
|
|
context.lineWidth = 1;
|
|
context.fillText( '1 frame = ' + timeToRender + ' ms = ' + fps + ' fps', 0, 20);
|
|
|
|
var maxFps = 60;
|
|
context.strokeRect(0, 30, 250, 20);
|
|
context.fillRect(0, 30, 250 * Math.min(fps/maxFps, 1), 20);
|
|
}
|
|
|
|
if( !drawAllLayers ){
|
|
needDraw[CR.SELECT_BOX] = false;
|
|
}
|
|
}
|
|
|
|
// motionblur: blit rendered blurry frames
|
|
if( motionBlur && mbPxRatio !== 1 ){
|
|
var cxtNode = data.contexts[CR.NODE];
|
|
var txtNode = r.data.bufferCanvases[ CR.MOTIONBLUR_BUFFER_NODE ];
|
|
|
|
var cxtDrag = data.contexts[CR.DRAG];
|
|
var txtDrag = r.data.bufferCanvases[ CR.MOTIONBLUR_BUFFER_DRAG ];
|
|
|
|
var drawMotionBlur = function( cxt, txt, needClear ){
|
|
cxt.setTransform(1, 0, 0, 1, 0, 0);
|
|
|
|
if( needClear || !motionBlurFadeEffect ){
|
|
cxt.clearRect( 0, 0, r.canvasWidth, r.canvasHeight );
|
|
} else {
|
|
mbclear( cxt, 0, 0, r.canvasWidth, r.canvasHeight );
|
|
}
|
|
|
|
var pxr = /*r.fullQualityMb ? 1 :*/ mbPxRatio;
|
|
|
|
cxt.drawImage(
|
|
txt, // img
|
|
0, 0, // sx, sy
|
|
r.canvasWidth * pxr, r.canvasHeight * pxr, // sw, sh
|
|
0, 0, // x, y
|
|
r.canvasWidth, r.canvasHeight // w, h
|
|
);
|
|
};
|
|
|
|
if( needDraw[CR.NODE] || needMbClear[CR.NODE] ){
|
|
// console.log('mb NODE', needMbClear[CR.NODE]);
|
|
|
|
drawMotionBlur( cxtNode, txtNode, needMbClear[CR.NODE] );
|
|
needDraw[CR.NODE] = false;
|
|
}
|
|
|
|
if( needDraw[CR.DRAG] || needMbClear[CR.DRAG] ){
|
|
// console.log('mb DRAG');
|
|
|
|
drawMotionBlur( cxtDrag, txtDrag, needMbClear[CR.DRAG] );
|
|
needDraw[CR.DRAG] = false;
|
|
//needMbClear[CR.NODE] = true;
|
|
}
|
|
}
|
|
|
|
|
|
var endTime = Date.now();
|
|
|
|
if( r.averageRedrawTime === undefined ){
|
|
r.averageRedrawTime = endTime - startTime;
|
|
}
|
|
|
|
if( r.redrawCount === undefined ){
|
|
r.redrawCount = 0;
|
|
}
|
|
|
|
r.redrawCount++;
|
|
|
|
if( r.redrawTotalTime === undefined ){
|
|
r.redrawTotalTime = 0;
|
|
}
|
|
|
|
r.redrawTotalTime += endTime - startTime;
|
|
r.lastRedrawTime = endTime - startTime;
|
|
|
|
// use a weighted average with a bias from the previous average so we don't spike so easily
|
|
r.averageRedrawTime = r.averageRedrawTime/2 + (endTime - startTime)/2;
|
|
//console.log('actual: %i, average: %i', endTime - startTime, this.averageRedrawTime);
|
|
|
|
r.currentlyDrawing = false;
|
|
|
|
r.prevViewport = vp;
|
|
|
|
// console.profileEnd('draw' + startTime)
|
|
|
|
if( r.clearingMotionBlur ){
|
|
r.clearingMotionBlur = false;
|
|
r.motionBlurCleared = true;
|
|
r.motionBlur = true;
|
|
}
|
|
|
|
if( motionBlur ){
|
|
r.motionBlurTimeout = setTimeout(function(){
|
|
r.motionBlurTimeout = null;
|
|
// console.log('mb CLEAR');
|
|
|
|
r.clearedForMotionBlur[CR.NODE] = false;
|
|
r.clearedForMotionBlur[CR.DRAG] = false;
|
|
r.motionBlur = false;
|
|
r.clearingMotionBlur = !textureDraw;
|
|
r.mbFrames = 0;
|
|
|
|
needDraw[CR.NODE] = true;
|
|
needDraw[CR.DRAG] = true;
|
|
|
|
r.redraw();
|
|
}, CanvasRenderer.motionBlurDelay);
|
|
}
|
|
|
|
r.drawingImage = false;
|
|
|
|
} // draw to context
|
|
|
|
if( !forcedContext ){
|
|
$$.util.requestAnimationFrame(drawToContext); // makes direct renders to screen a bit more responsive
|
|
} else {
|
|
drawToContext();
|
|
}
|
|
|
|
if( !forcedContext && !r.initrender ){
|
|
r.initrender = true;
|
|
cy.trigger('initrender');
|
|
}
|
|
|
|
if( !forcedContext ){
|
|
cy.triggerOnRender();
|
|
}
|
|
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var CRp = CanvasRenderer.prototype;
|
|
|
|
// @O Polygon drawing
|
|
CRp.drawPolygonPath = function(
|
|
context, x, y, width, height, points) {
|
|
|
|
var halfW = width / 2;
|
|
var halfH = height / 2;
|
|
|
|
if( context.beginPath ){ context.beginPath(); }
|
|
|
|
context.moveTo( x + halfW * points[0], y + halfH * points[1] );
|
|
|
|
for (var i = 1; i < points.length / 2; i++) {
|
|
context.lineTo( x + halfW * points[i * 2], y + halfH * points[i * 2 + 1] );
|
|
}
|
|
|
|
context.closePath();
|
|
};
|
|
|
|
CRp.drawPolygon = function(
|
|
context, x, y, width, height, points) {
|
|
|
|
// Draw path
|
|
this.drawPolygonPath(context, x, y, width, height, points);
|
|
|
|
// Fill path
|
|
context.fill();
|
|
};
|
|
|
|
// Round rectangle drawing
|
|
CRp.drawRoundRectanglePath = function(
|
|
context, x, y, width, height, radius) {
|
|
|
|
var halfWidth = width / 2;
|
|
var halfHeight = height / 2;
|
|
var cornerRadius = $$.math.getRoundRectangleRadius(width, height);
|
|
|
|
if( context.beginPath ){ context.beginPath(); }
|
|
|
|
// Start at top middle
|
|
context.moveTo(x, y - halfHeight);
|
|
// Arc from middle top to right side
|
|
context.arcTo(x + halfWidth, y - halfHeight, x + halfWidth, y, cornerRadius);
|
|
// Arc from right side to bottom
|
|
context.arcTo(x + halfWidth, y + halfHeight, x, y + halfHeight, cornerRadius);
|
|
// Arc from bottom to left side
|
|
context.arcTo(x - halfWidth, y + halfHeight, x - halfWidth, y, cornerRadius);
|
|
// Arc from left side to topBorder
|
|
context.arcTo(x - halfWidth, y - halfHeight, x, y - halfHeight, cornerRadius);
|
|
// Join line
|
|
context.lineTo(x, y - halfHeight);
|
|
|
|
|
|
context.closePath();
|
|
};
|
|
|
|
CRp.drawRoundRectangle = function(
|
|
context, x, y, width, height, radius) {
|
|
|
|
this.drawRoundRectanglePath(context, x, y, width, height, radius);
|
|
|
|
context.fill();
|
|
};
|
|
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var CRp = CanvasRenderer.prototype;
|
|
|
|
CRp.createBuffer = function(w, h) {
|
|
var buffer = document.createElement('canvas');
|
|
buffer.width = w;
|
|
buffer.height = h;
|
|
|
|
return [buffer, buffer.getContext('2d')];
|
|
};
|
|
|
|
CRp.bufferCanvasImage = function( options ){
|
|
var data = this.data;
|
|
var cy = data.cy;
|
|
var bb = cy.elements().boundingBox();
|
|
var width = options.full ? Math.ceil(bb.w) : this.data.container.clientWidth;
|
|
var height = options.full ? Math.ceil(bb.h) : this.data.container.clientHeight;
|
|
var scale = 1;
|
|
|
|
if( options.scale !== undefined ){
|
|
width *= options.scale;
|
|
height *= options.scale;
|
|
|
|
scale = options.scale;
|
|
} else if( $$.is.number(options.maxWidth) || $$.is.number(options.maxHeight) ){
|
|
var maxScaleW = Infinity;
|
|
var maxScaleH = Infinity;
|
|
|
|
if( $$.is.number(options.maxWidth) ){
|
|
maxScaleW = scale * options.maxWidth / width;
|
|
}
|
|
|
|
if( $$.is.number(options.maxHeight) ){
|
|
maxScaleH = scale * options.maxHeight / height;
|
|
}
|
|
|
|
scale = Math.min( maxScaleW, maxScaleH );
|
|
|
|
width *= scale;
|
|
height *= scale;
|
|
}
|
|
|
|
var buffCanvas = document.createElement('canvas');
|
|
|
|
buffCanvas.width = width;
|
|
buffCanvas.height = height;
|
|
|
|
buffCanvas.style.width = width + 'px';
|
|
buffCanvas.style.height = height + 'px';
|
|
|
|
var buffCxt = buffCanvas.getContext('2d');
|
|
|
|
// Rasterize the layers, but only if container has nonzero size
|
|
if (width > 0 && height > 0) {
|
|
|
|
buffCxt.clearRect( 0, 0, width, height );
|
|
|
|
if( options.bg ){
|
|
buffCxt.fillStyle = options.bg;
|
|
buffCxt.rect( 0, 0, width, height );
|
|
buffCxt.fill();
|
|
}
|
|
|
|
buffCxt.globalCompositeOperation = 'source-over';
|
|
|
|
if( options.full ){ // draw the full bounds of the graph
|
|
this.redraw({
|
|
forcedContext: buffCxt,
|
|
drawAllLayers: true,
|
|
forcedZoom: scale,
|
|
forcedPan: { x: -bb.x1*scale, y: -bb.y1*scale },
|
|
forcedPxRatio: 1
|
|
});
|
|
} else { // draw the current view
|
|
var cyPan = cy.pan();
|
|
var pan = {
|
|
x: cyPan.x * scale,
|
|
y: cyPan.y * scale
|
|
};
|
|
var zoom = cy.zoom() * scale;
|
|
|
|
this.redraw({
|
|
forcedContext: buffCxt,
|
|
drawAllLayers: true,
|
|
forcedZoom: zoom,
|
|
forcedPan: pan,
|
|
forcedPxRatio: 1
|
|
});
|
|
}
|
|
}
|
|
|
|
return buffCanvas;
|
|
};
|
|
|
|
CRp.png = function( options ){
|
|
return this.bufferCanvasImage( options ).toDataURL('image/png');
|
|
};
|
|
|
|
CRp.jpg = function( options ){
|
|
return this.bufferCanvasImage( options ).toDataURL('image/jpeg');
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var CR = CanvasRenderer;
|
|
var CRp = CR.prototype;
|
|
|
|
CRp.registerBinding = function(target, event, handler, useCapture){
|
|
this.bindings.push({
|
|
target: target,
|
|
event: event,
|
|
handler: handler,
|
|
useCapture: useCapture
|
|
});
|
|
|
|
target.addEventListener(event, handler, useCapture);
|
|
};
|
|
|
|
CRp.nodeIsDraggable = function(node) {
|
|
if (node._private.style['opacity'].value !== 0
|
|
&& node._private.style['visibility'].value == 'visible'
|
|
&& node._private.style['display'].value == 'element'
|
|
&& !node.locked()
|
|
&& node.grabbable() ) {
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
CRp.load = function() {
|
|
var r = this;
|
|
|
|
var getDragListIds = function(opts){
|
|
var listHasId;
|
|
|
|
if( opts.addToList && r.data.cy.hasCompoundNodes() ){ // only needed for compound graphs
|
|
if( !opts.addToList.hasId ){ // build ids lookup if doesn't already exist
|
|
opts.addToList.hasId = {};
|
|
|
|
for( var i = 0; i < opts.addToList.length; i++ ){
|
|
var ele = opts.addToList[i];
|
|
|
|
opts.addToList.hasId[ ele.id() ] = true;
|
|
}
|
|
}
|
|
|
|
listHasId = opts.addToList.hasId;
|
|
}
|
|
|
|
return listHasId || {};
|
|
};
|
|
|
|
// helper function to determine which child nodes and inner edges
|
|
// of a compound node to be dragged as well as the grabbed and selected nodes
|
|
var addDescendantsToDrag = function(node, opts){
|
|
if( !node._private.cy.hasCompoundNodes() ){
|
|
return;
|
|
}
|
|
|
|
if( opts.inDragLayer == null && opts.addToList == null ){ return; } // nothing to do
|
|
|
|
var listHasId = getDragListIds( opts );
|
|
|
|
var innerNodes = node.descendants();
|
|
|
|
// TODO do not drag hidden children & children of hidden children?
|
|
for( var i = 0; i < innerNodes.size(); i++ ){
|
|
var iNode = innerNodes[i];
|
|
var _p = iNode._private;
|
|
|
|
if( opts.inDragLayer ){
|
|
_p.rscratch.inDragLayer = true;
|
|
}
|
|
|
|
if( opts.addToList && !listHasId[ iNode.id() ] ){
|
|
opts.addToList.push( iNode );
|
|
listHasId[ iNode.id() ] = true;
|
|
|
|
_p.grabbed = true;
|
|
}
|
|
|
|
var edges = _p.edges;
|
|
for( var j = 0; opts.inDragLayer && j < edges.length; j++ ){
|
|
edges[j]._private.rscratch.inDragLayer = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
// adds the given nodes, and its edges to the drag layer
|
|
var addNodeToDrag = function(node, opts){
|
|
|
|
var _p = node._private;
|
|
var listHasId = getDragListIds( opts );
|
|
|
|
if( opts.inDragLayer ){
|
|
_p.rscratch.inDragLayer = true;
|
|
}
|
|
|
|
if( opts.addToList && !listHasId[ node.id() ] ){
|
|
opts.addToList.push( node );
|
|
listHasId[ node.id() ] = true;
|
|
|
|
_p.grabbed = true;
|
|
}
|
|
|
|
var edges = _p.edges;
|
|
for( var i = 0; opts.inDragLayer && i < edges.length; i++ ){
|
|
edges[i]._private.rscratch.inDragLayer = true;
|
|
}
|
|
|
|
addDescendantsToDrag( node, opts ); // always add to drag
|
|
|
|
// also add nodes and edges related to the topmost ancestor
|
|
updateAncestorsInDragLayer( node, {
|
|
inDragLayer: opts.inDragLayer
|
|
} );
|
|
};
|
|
|
|
var freeDraggedElements = function( draggedElements ){
|
|
if( !draggedElements ){ return; }
|
|
|
|
for (var i=0; i < draggedElements.length; i++) {
|
|
|
|
var dEi_p = draggedElements[i]._private;
|
|
|
|
if(dEi_p.group === 'nodes') {
|
|
dEi_p.rscratch.inDragLayer = false;
|
|
dEi_p.grabbed = false;
|
|
|
|
var sEdges = dEi_p.edges;
|
|
for( var j = 0; j < sEdges.length; j++ ){ sEdges[j]._private.rscratch.inDragLayer = false; }
|
|
|
|
// for compound nodes, also remove related nodes and edges from the drag layer
|
|
updateAncestorsInDragLayer(draggedElements[i], { inDragLayer: false });
|
|
|
|
} else if( dEi_p.group === 'edges' ){
|
|
dEi_p.rscratch.inDragLayer = false;
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
// helper function to determine which ancestor nodes and edges should go
|
|
// to the drag layer (or should be removed from drag layer).
|
|
var updateAncestorsInDragLayer = function(node, opts) {
|
|
|
|
if( opts.inDragLayer == null && opts.addToList == null ){ return; } // nothing to do
|
|
|
|
// find top-level parent
|
|
var parent = node;
|
|
|
|
if( !node._private.cy.hasCompoundNodes() ){
|
|
return;
|
|
}
|
|
|
|
while( parent.parent().nonempty() ){
|
|
parent = parent.parent()[0];
|
|
}
|
|
|
|
// no parent node: no nodes to add to the drag layer
|
|
if( parent == node ){
|
|
return;
|
|
}
|
|
|
|
var nodes = parent.descendants()
|
|
.merge( parent )
|
|
.unmerge( node )
|
|
.unmerge( node.descendants() )
|
|
;
|
|
|
|
var edges = nodes.connectedEdges();
|
|
|
|
var listHasId = getDragListIds( opts );
|
|
|
|
for( var i = 0; i < nodes.size(); i++ ){
|
|
if( opts.inDragLayer !== undefined ){
|
|
nodes[i]._private.rscratch.inDragLayer = opts.inDragLayer;
|
|
}
|
|
|
|
if( opts.addToList && !listHasId[ nodes[i].id() ] ){
|
|
opts.addToList.push( nodes[i] );
|
|
listHasId[ nodes[i].id() ] = true;
|
|
|
|
nodes[i]._private.grabbed = true;
|
|
}
|
|
}
|
|
|
|
for( var j = 0; opts.inDragLayer !== undefined && j < edges.length; j++ ) {
|
|
edges[j]._private.rscratch.inDragLayer = opts.inDragLayer;
|
|
}
|
|
};
|
|
|
|
if( typeof MutationObserver !== 'undefined' ){
|
|
r.removeObserver = new MutationObserver(function( mutns ){
|
|
for( var i = 0; i < mutns.length; i++ ){
|
|
var mutn = mutns[i];
|
|
var rNodes = mutn.removedNodes;
|
|
|
|
if( rNodes ){ for( var j = 0; j < rNodes.length; j++ ){
|
|
var rNode = rNodes[j];
|
|
|
|
if( rNode === r.data.container ){
|
|
r.destroy();
|
|
break;
|
|
}
|
|
} }
|
|
}
|
|
});
|
|
|
|
r.removeObserver.observe( r.data.container.parentNode, { childList: true } );
|
|
} else {
|
|
r.registerBinding(r.data.container, 'DOMNodeRemoved', function(e){
|
|
r.destroy();
|
|
});
|
|
}
|
|
|
|
|
|
|
|
// auto resize
|
|
r.registerBinding(window, 'resize', $$.util.debounce( function(e) {
|
|
r.invalidateContainerClientCoordsCache();
|
|
|
|
r.matchCanvasSize(r.data.container);
|
|
r.data.canvasNeedsRedraw[CR.NODE] = true;
|
|
r.redraw();
|
|
}, 100 ) );
|
|
|
|
var invalCtnrBBOnScroll = function(domEle){
|
|
r.registerBinding(domEle, 'scroll', function(e){
|
|
r.invalidateContainerClientCoordsCache();
|
|
} );
|
|
};
|
|
|
|
var bbCtnr = r.data.cy.container();
|
|
|
|
for( ;; ){
|
|
|
|
invalCtnrBBOnScroll( bbCtnr );
|
|
|
|
if( bbCtnr.parentNode ){
|
|
bbCtnr = bbCtnr.parentNode;
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
// stop right click menu from appearing on cy
|
|
r.registerBinding(r.data.container, 'contextmenu', function(e){
|
|
e.preventDefault();
|
|
});
|
|
|
|
var inBoxSelection = function(){
|
|
return r.data.select[4] !== 0;
|
|
};
|
|
|
|
// Primary key
|
|
r.registerBinding(r.data.container, 'mousedown', function(e) {
|
|
e.preventDefault();
|
|
r.hoverData.capture = true;
|
|
r.hoverData.which = e.which;
|
|
|
|
var cy = r.data.cy;
|
|
var pos = r.projectIntoViewport(e.clientX, e.clientY);
|
|
var select = r.data.select;
|
|
var near = r.findNearestElement(pos[0], pos[1], true, false);
|
|
var draggedElements = r.dragData.possibleDragElements;
|
|
|
|
r.hoverData.mdownPos = pos;
|
|
|
|
var needsRedraw = r.data.canvasNeedsRedraw;
|
|
|
|
var checkForTaphold = function(){
|
|
r.hoverData.tapholdCancelled = false;
|
|
|
|
clearTimeout( r.hoverData.tapholdTimeout );
|
|
|
|
r.hoverData.tapholdTimeout = setTimeout(function(){
|
|
|
|
if( r.hoverData.tapholdCancelled ){
|
|
return;
|
|
} else {
|
|
var ele = r.hoverData.down;
|
|
|
|
if( ele ){
|
|
ele.trigger( new $$.Event(e, {
|
|
type: 'taphold',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) );
|
|
} else {
|
|
cy.trigger( new $$.Event(e, {
|
|
type: 'taphold',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) );
|
|
}
|
|
}
|
|
|
|
}, r.tapholdDuration);
|
|
};
|
|
|
|
// Right click button
|
|
if( e.which == 3 ){
|
|
|
|
r.hoverData.cxtStarted = true;
|
|
|
|
var cxtEvt = new $$.Event(e, {
|
|
type: 'cxttapstart',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
});
|
|
|
|
if( near ){
|
|
near.activate();
|
|
near.trigger( cxtEvt );
|
|
|
|
r.hoverData.down = near;
|
|
} else {
|
|
cy.trigger( cxtEvt );
|
|
}
|
|
|
|
r.hoverData.downTime = (new Date()).getTime();
|
|
r.hoverData.cxtDragged = false;
|
|
|
|
// Primary button
|
|
} else if (e.which == 1) {
|
|
|
|
if( near ){
|
|
near.activate();
|
|
}
|
|
|
|
// Element dragging
|
|
{
|
|
// If something is under the cursor and it is draggable, prepare to grab it
|
|
if (near != null) {
|
|
|
|
if( r.nodeIsDraggable(near) ){
|
|
|
|
var grabEvent = new $$.Event(e, {
|
|
type: 'grab',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
});
|
|
|
|
if ( near.isNode() && !near.selected() ){
|
|
|
|
draggedElements = r.dragData.possibleDragElements = [];
|
|
addNodeToDrag( near, { addToList: draggedElements } );
|
|
|
|
near.trigger(grabEvent);
|
|
|
|
} else if ( near.isNode() && near.selected() ){
|
|
draggedElements = r.dragData.possibleDragElements = [ ];
|
|
|
|
var selectedNodes = cy.$(function(){ return this.isNode() && this.selected(); });
|
|
|
|
for( var i = 0; i < selectedNodes.length; i++ ){
|
|
|
|
// Only add this selected node to drag if it is draggable, eg. has nonzero opacity
|
|
if( r.nodeIsDraggable( selectedNodes[i] ) ){
|
|
addNodeToDrag( selectedNodes[i], { addToList: draggedElements } );
|
|
}
|
|
}
|
|
|
|
near.trigger( grabEvent );
|
|
}
|
|
|
|
needsRedraw[CR.NODE] = true;
|
|
needsRedraw[CR.DRAG] = true;
|
|
|
|
}
|
|
|
|
near
|
|
.trigger(new $$.Event(e, {
|
|
type: 'mousedown',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapstart',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmousedown',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
;
|
|
|
|
} else if (near == null) {
|
|
cy
|
|
.trigger(new $$.Event(e, {
|
|
type: 'mousedown',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapstart',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmousedown',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
;
|
|
}
|
|
|
|
r.hoverData.down = near;
|
|
r.hoverData.downTime = (new Date()).getTime();
|
|
|
|
}
|
|
|
|
// Selection box
|
|
if ( near == null || near.isEdge() ) {
|
|
select[4] = 1;
|
|
var timeUntilActive = Math.max( 0, CR.panOrBoxSelectDelay - (+new Date() - r.hoverData.downTime) );
|
|
|
|
clearTimeout( r.bgActiveTimeout );
|
|
|
|
if( cy.boxSelectionEnabled() || ( near && near.isEdge() ) ){
|
|
r.bgActiveTimeout = setTimeout(function(){
|
|
if( near ){
|
|
near.unactivate();
|
|
}
|
|
|
|
r.data.bgActivePosistion = {
|
|
x: pos[0],
|
|
y: pos[1]
|
|
};
|
|
|
|
r.hoverData.dragging = true;
|
|
|
|
//checkForTaphold();
|
|
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
r.redraw();
|
|
}, timeUntilActive);
|
|
} else {
|
|
r.data.bgActivePosistion = {
|
|
x: pos[0],
|
|
y: pos[1]
|
|
};
|
|
|
|
//r.hoverData.dragging = true;
|
|
|
|
//checkForTaphold();
|
|
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
r.redraw();
|
|
}
|
|
|
|
}
|
|
|
|
checkForTaphold();
|
|
|
|
}
|
|
|
|
// Initialize selection box coordinates
|
|
select[0] = select[2] = pos[0];
|
|
select[1] = select[3] = pos[1];
|
|
|
|
}, false);
|
|
|
|
r.registerBinding(window, 'mousemove', $$.util.throttle( function(e) {
|
|
var preventDefault = false;
|
|
var capture = r.hoverData.capture;
|
|
|
|
// save cycles if mouse events aren't to be captured
|
|
if ( !capture ){
|
|
var containerPageCoords = r.findContainerClientCoords();
|
|
|
|
if (e.clientX > containerPageCoords[0] && e.clientX < containerPageCoords[0] + r.canvasWidth
|
|
&& e.clientY > containerPageCoords[1] && e.clientY < containerPageCoords[1] + r.canvasHeight
|
|
) {
|
|
// inside container bounds so OK
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
var cyContainer = r.data.container;
|
|
var target = e.target;
|
|
var tParent = target.parentNode;
|
|
var containerIsTarget = false;
|
|
|
|
while( tParent ){
|
|
if( tParent === cyContainer ){
|
|
containerIsTarget = true;
|
|
break;
|
|
}
|
|
|
|
tParent = tParent.parentNode;
|
|
}
|
|
|
|
if( !containerIsTarget ){ return; } // if target is outisde cy container, then this event is not for us
|
|
}
|
|
|
|
var cy = r.data.cy;
|
|
var zoom = cy.zoom();
|
|
var pos = r.projectIntoViewport(e.clientX, e.clientY);
|
|
var select = r.data.select;
|
|
var needsRedraw = r.data.canvasNeedsRedraw;
|
|
|
|
var near = null;
|
|
if( !r.hoverData.draggingEles ){
|
|
near = r.findNearestElement(pos[0], pos[1], true, false);
|
|
}
|
|
var last = r.hoverData.last;
|
|
var down = r.hoverData.down;
|
|
|
|
var disp = [pos[0] - select[2], pos[1] - select[3]];
|
|
|
|
var draggedElements = r.dragData.possibleDragElements;
|
|
|
|
var dx = select[2] - select[0];
|
|
var dx2 = dx * dx;
|
|
var dy = select[3] - select[1];
|
|
var dy2 = dy * dy;
|
|
var dist2 = dx2 + dy2;
|
|
var rdist2 = dist2 * zoom * zoom;
|
|
|
|
r.hoverData.tapholdCancelled = true;
|
|
|
|
var updateDragDelta = function(){
|
|
var dragDelta = r.hoverData.dragDelta = r.hoverData.dragDelta || [];
|
|
|
|
if( dragDelta.length === 0 ){
|
|
dragDelta.push( disp[0] );
|
|
dragDelta.push( disp[1] );
|
|
} else {
|
|
dragDelta[0] += disp[0];
|
|
dragDelta[1] += disp[1];
|
|
}
|
|
};
|
|
|
|
|
|
preventDefault = true;
|
|
|
|
// Mousemove event
|
|
{
|
|
if (near != null) {
|
|
near
|
|
.trigger(new $$.Event(e, {
|
|
type: 'mousemove',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmousemove',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapdrag',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
;
|
|
|
|
} else if (near == null) {
|
|
cy
|
|
.trigger(new $$.Event(e, {
|
|
type: 'mousemove',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmousemove',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapdrag',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
;
|
|
}
|
|
|
|
}
|
|
|
|
// trigger context drag if rmouse down
|
|
if( r.hoverData.which === 3 ){
|
|
var cxtEvt = new $$.Event(e, {
|
|
type: 'cxtdrag',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
});
|
|
|
|
if( down ){
|
|
down.trigger( cxtEvt );
|
|
} else {
|
|
cy.trigger( cxtEvt );
|
|
}
|
|
|
|
r.hoverData.cxtDragged = true;
|
|
|
|
if( !r.hoverData.cxtOver || near !== r.hoverData.cxtOver ){
|
|
|
|
if( r.hoverData.cxtOver ){
|
|
r.hoverData.cxtOver.trigger( new $$.Event(e, {
|
|
type: 'cxtdragout',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) );
|
|
|
|
// console.log('cxtdragout ' + r.hoverData.cxtOver.id());
|
|
}
|
|
|
|
r.hoverData.cxtOver = near;
|
|
|
|
if( near ){
|
|
near.trigger( new $$.Event(e, {
|
|
type: 'cxtdragover',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) );
|
|
|
|
// console.log('cxtdragover ' + near.id());
|
|
}
|
|
|
|
}
|
|
|
|
// Check if we are drag panning the entire graph
|
|
} else if (r.hoverData.dragging) {
|
|
preventDefault = true;
|
|
|
|
if( cy.panningEnabled() && cy.userPanningEnabled() ){
|
|
var deltaP;
|
|
|
|
if( r.hoverData.justStartedPan ){
|
|
var mdPos = r.hoverData.mdownPos;
|
|
|
|
deltaP = {
|
|
x: ( pos[0] - mdPos[0] ) * zoom,
|
|
y: ( pos[1] - mdPos[1] ) * zoom
|
|
};
|
|
|
|
r.hoverData.justStartedPan = false;
|
|
|
|
} else {
|
|
deltaP = {
|
|
x: disp[0] * zoom,
|
|
y: disp[1] * zoom
|
|
};
|
|
|
|
}
|
|
|
|
cy.panBy( deltaP );
|
|
|
|
r.hoverData.dragged = true;
|
|
}
|
|
|
|
// Needs reproject due to pan changing viewport
|
|
pos = r.projectIntoViewport(e.clientX, e.clientY);
|
|
|
|
// Checks primary button down & out of time & mouse not moved much
|
|
} else if(
|
|
select[4] == 1 && (down == null || down.isEdge())
|
|
&& ( !cy.boxSelectionEnabled() || (+new Date() - r.hoverData.downTime >= CR.panOrBoxSelectDelay) )
|
|
//&& (Math.abs(select[3] - select[1]) + Math.abs(select[2] - select[0]) < 4)
|
|
&& !r.hoverData.selecting
|
|
&& rdist2 >= r.desktopTapThreshold2
|
|
&& cy.panningEnabled() && cy.userPanningEnabled()
|
|
){
|
|
r.hoverData.dragging = true;
|
|
r.hoverData.selecting = false;
|
|
r.hoverData.justStartedPan = true;
|
|
select[4] = 0;
|
|
|
|
} else {
|
|
// deactivate bg on box selection
|
|
if (cy.boxSelectionEnabled() && !r.hoverData.dragging && Math.pow(select[2] - select[0], 2) + Math.pow(select[3] - select[1], 2) > 7 && select[4]){
|
|
clearTimeout( r.bgActiveTimeout );
|
|
r.data.bgActivePosistion = undefined;
|
|
r.hoverData.selecting = true;
|
|
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
r.redraw();
|
|
}
|
|
|
|
if( down && down.isEdge() && down.active() ){ down.unactivate(); }
|
|
|
|
if (near != last) {
|
|
|
|
if (last) {
|
|
last.trigger( new $$.Event(e, {
|
|
type: 'mouseout',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) );
|
|
|
|
last.trigger( new $$.Event(e, {
|
|
type: 'tapdragout',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) );
|
|
}
|
|
|
|
if (near) {
|
|
near.trigger( new $$.Event(e, {
|
|
type: 'mouseover',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) );
|
|
|
|
near.trigger( new $$.Event(e, {
|
|
type: 'tapdragover',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) );
|
|
}
|
|
|
|
r.hoverData.last = near;
|
|
}
|
|
|
|
if( down && down.isNode() && r.nodeIsDraggable(down) ){
|
|
|
|
if( rdist2 >= r.desktopTapThreshold2 ){ // then drag
|
|
|
|
var justStartedDrag = !r.dragData.didDrag;
|
|
|
|
if( justStartedDrag ) {
|
|
needsRedraw[CR.NODE] = true;
|
|
}
|
|
|
|
r.dragData.didDrag = true; // indicate that we actually did drag the node
|
|
|
|
var toTrigger = [];
|
|
|
|
for( var i = 0; i < draggedElements.length; i++ ){
|
|
var dEle = draggedElements[i];
|
|
|
|
// now, add the elements to the drag layer if not done already
|
|
if( !r.hoverData.draggingEles ){
|
|
addNodeToDrag( dEle, { inDragLayer: true } );
|
|
}
|
|
|
|
// Locked nodes not draggable, as well as non-visible nodes
|
|
if( dEle.isNode() && r.nodeIsDraggable(dEle) && dEle.grabbed() ){
|
|
var dPos = dEle._private.position;
|
|
|
|
toTrigger.push( dEle );
|
|
|
|
if( $$.is.number(disp[0]) && $$.is.number(disp[1]) ){
|
|
dPos.x += disp[0];
|
|
dPos.y += disp[1];
|
|
|
|
if( justStartedDrag ){
|
|
var dragDelta = r.hoverData.dragDelta;
|
|
|
|
if( $$.is.number(dragDelta[0]) && $$.is.number(dragDelta[1]) ){
|
|
dPos.x += dragDelta[0];
|
|
dPos.y += dragDelta[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
r.hoverData.draggingEles = true;
|
|
|
|
var tcol = (new $$.Collection(cy, toTrigger));
|
|
|
|
tcol.updateCompoundBounds();
|
|
tcol.trigger('position drag');
|
|
|
|
needsRedraw[CR.DRAG] = true;
|
|
r.redraw();
|
|
|
|
} else { // otherwise save drag delta for when we actually start dragging so the relative grab pos is constant
|
|
updateDragDelta();
|
|
}
|
|
}
|
|
|
|
// prevent the dragging from triggering text selection on the page
|
|
preventDefault = true;
|
|
}
|
|
|
|
select[2] = pos[0]; select[3] = pos[1];
|
|
|
|
if( preventDefault ){
|
|
if(e.stopPropagation) e.stopPropagation();
|
|
if(e.preventDefault) e.preventDefault();
|
|
return false;
|
|
}
|
|
}, 1000/30, { trailing: true }), false);
|
|
|
|
r.registerBinding(window, 'mouseup', function(e) {
|
|
// console.log('--\nmouseup', e)
|
|
|
|
var capture = r.hoverData.capture;
|
|
if (!capture) { return; }
|
|
r.hoverData.capture = false;
|
|
|
|
var cy = r.data.cy; var pos = r.projectIntoViewport(e.clientX, e.clientY); var select = r.data.select;
|
|
var near = r.findNearestElement(pos[0], pos[1], true, false);
|
|
var draggedElements = r.dragData.possibleDragElements; var down = r.hoverData.down;
|
|
var shiftDown = e.shiftKey;
|
|
var needsRedraw = r.data.canvasNeedsRedraw;
|
|
|
|
if( r.data.bgActivePosistion ){
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
r.redraw();
|
|
}
|
|
|
|
r.hoverData.tapholdCancelled = true;
|
|
|
|
r.data.bgActivePosistion = undefined; // not active bg now
|
|
clearTimeout( r.bgActiveTimeout );
|
|
|
|
if( down ){
|
|
down.unactivate();
|
|
}
|
|
|
|
if( r.hoverData.which === 3 ){
|
|
var cxtEvt = new $$.Event(e, {
|
|
type: 'cxttapend',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
});
|
|
|
|
if( down ){
|
|
down.trigger( cxtEvt );
|
|
} else {
|
|
cy.trigger( cxtEvt );
|
|
}
|
|
|
|
if( !r.hoverData.cxtDragged ){
|
|
var cxtTap = new $$.Event(e, {
|
|
type: 'cxttap',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
});
|
|
|
|
if( down ){
|
|
down.trigger( cxtTap );
|
|
} else {
|
|
cy.trigger( cxtTap );
|
|
}
|
|
}
|
|
|
|
r.hoverData.cxtDragged = false;
|
|
r.hoverData.which = null;
|
|
|
|
// if not right mouse
|
|
} else {
|
|
|
|
// Deselect all elements if nothing is currently under the mouse cursor and we aren't dragging something
|
|
if ( (down == null) // not mousedown on node
|
|
&& !r.dragData.didDrag // didn't move the node around
|
|
//&& !(Math.pow(select[2] - select[0], 2) + Math.pow(select[3] - select[1], 2) > 7 && select[4]) // not box selection
|
|
&& !r.hoverData.dragged // didn't pan
|
|
) {
|
|
|
|
cy.$(function(){
|
|
return this.selected();
|
|
}).unselect();
|
|
|
|
if (draggedElements.length > 0) {
|
|
needsRedraw[CR.NODE] = true;
|
|
}
|
|
|
|
r.dragData.possibleDragElements = draggedElements = [];
|
|
}
|
|
|
|
|
|
// Mouseup event
|
|
{
|
|
// console.log('trigger mouseup et al');
|
|
|
|
if (near != null) {
|
|
near
|
|
.trigger(new $$.Event(e, {
|
|
type: 'mouseup',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapend',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmouseup',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
;
|
|
} else if (near == null) {
|
|
cy
|
|
.trigger(new $$.Event(e, {
|
|
type: 'mouseup',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapend',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmouseup',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}))
|
|
;
|
|
}
|
|
}
|
|
|
|
// Click event
|
|
{
|
|
// console.log('trigger click et al');
|
|
|
|
if(
|
|
//Math.pow(select[2] - select[0], 2) + Math.pow(select[3] - select[1], 2) === 0
|
|
!r.dragData.didDrag // didn't move a node around
|
|
&& !r.hoverData.dragged // didn't pan
|
|
){
|
|
if (near != null) {
|
|
near
|
|
.trigger( new $$.Event(e, {
|
|
type: 'click',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) )
|
|
.trigger( new $$.Event(e, {
|
|
type: 'tap',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) )
|
|
.trigger( new $$.Event(e, {
|
|
type: 'vclick',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) )
|
|
;
|
|
} else if (near == null) {
|
|
cy
|
|
.trigger( new $$.Event(e, {
|
|
type: 'click',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) )
|
|
.trigger( new $$.Event(e, {
|
|
type: 'tap',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) )
|
|
.trigger( new $$.Event(e, {
|
|
type: 'vclick',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}) )
|
|
;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Single selection
|
|
if (near == down && !r.dragData.didDrag) {
|
|
if (near != null && near._private.selectable) {
|
|
|
|
// console.log('single selection')
|
|
|
|
if( r.hoverData.dragging ){
|
|
// if panning, don't change selection state
|
|
} else if( cy.selectionType() === 'additive' || shiftDown ){
|
|
if( near.selected() ){
|
|
near.unselect();
|
|
} else {
|
|
near.select();
|
|
}
|
|
} else {
|
|
if( !shiftDown ){
|
|
cy.$(':selected').unmerge( near ).unselect();
|
|
near.select();
|
|
}
|
|
}
|
|
|
|
needsRedraw[CR.NODE] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( r.hoverData.selecting && cy.boxSelectionEnabled() && Math.pow(select[2] - select[0], 2) + Math.pow(select[3] - select[1], 2) > 7 && select[4] ) {
|
|
var newlySelected = [];
|
|
var box = r.getAllInBox( select[0], select[1], select[2], select[3] );
|
|
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
if( box.length > 0 ) {
|
|
needsRedraw[CR.NODE] = true;
|
|
}
|
|
|
|
for( var i = 0; i < box.length; i++ ){
|
|
if( box[i]._private.selectable ){
|
|
newlySelected.push( box[i] );
|
|
}
|
|
}
|
|
|
|
var newlySelCol = new $$.Collection( cy, newlySelected );
|
|
|
|
if( cy.selectionType() === 'additive' ){
|
|
newlySelCol.select();
|
|
} else {
|
|
if( !shiftDown ){
|
|
cy.$(':selected').unmerge( newlySelCol ).unselect();
|
|
}
|
|
|
|
newlySelCol.select();
|
|
}
|
|
|
|
// always need redraw in case eles unselectable
|
|
r.redraw();
|
|
|
|
}
|
|
|
|
// Cancel drag pan
|
|
if( r.hoverData.dragging ){
|
|
r.hoverData.dragging = false;
|
|
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
needsRedraw[CR.NODE] = true;
|
|
|
|
r.redraw();
|
|
}
|
|
|
|
if (!select[4]) {
|
|
// console.log('free at end', draggedElements)
|
|
|
|
needsRedraw[CR.DRAG] = true;
|
|
needsRedraw[CR.NODE] = true;
|
|
|
|
freeDraggedElements( draggedElements );
|
|
|
|
if( down ){ down.trigger('free'); }
|
|
|
|
// draggedElements = r.dragData.possibleDragElements = [];
|
|
|
|
}
|
|
|
|
} // else not right mouse
|
|
|
|
select[4] = 0; r.hoverData.down = null;
|
|
|
|
//r.data.canvasNeedsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
// console.log('mu', pos[0], pos[1]);
|
|
// console.log('ss', select);
|
|
|
|
r.hoverData.cxtStarted = false;
|
|
r.hoverData.draggingEles = false;
|
|
r.hoverData.selecting = false;
|
|
r.dragData.didDrag = false;
|
|
r.hoverData.dragged = false;
|
|
r.hoverData.dragDelta = [];
|
|
|
|
}, false);
|
|
|
|
var wheelHandler = function(e) {
|
|
if( r.scrollingPage ){ return; } // while scrolling, ignore wheel-to-zoom
|
|
|
|
var cy = r.data.cy;
|
|
var pos = r.projectIntoViewport(e.clientX, e.clientY);
|
|
var rpos = [pos[0] * cy.zoom() + cy.pan().x,
|
|
pos[1] * cy.zoom() + cy.pan().y];
|
|
|
|
if( r.hoverData.draggingEles || r.hoverData.dragging || r.hoverData.cxtStarted || inBoxSelection() ){ // if pan dragging or cxt dragging, wheel movements make no zoom
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if( cy.panningEnabled() && cy.userPanningEnabled() && cy.zoomingEnabled() && cy.userZoomingEnabled() ){
|
|
e.preventDefault();
|
|
|
|
r.data.wheelZooming = true;
|
|
clearTimeout( r.data.wheelTimeout );
|
|
r.data.wheelTimeout = setTimeout(function(){
|
|
r.data.wheelZooming = false;
|
|
|
|
r.data.canvasNeedsRedraw[CR.NODE] = true;
|
|
r.redraw();
|
|
}, 150);
|
|
|
|
var diff = e.deltaY / -250 || e.wheelDeltaY / 1000 || e.wheelDelta / 1000;
|
|
diff = diff * r.wheelSensitivity;
|
|
|
|
var needsWheelFix = e.deltaMode === 1;
|
|
if( needsWheelFix ){ // fixes slow wheel events on ff/linux and ff/windows
|
|
diff *= 33;
|
|
}
|
|
|
|
cy.zoom({
|
|
level: cy.zoom() * Math.pow(10, diff),
|
|
renderedPosition: { x: rpos[0], y: rpos[1] }
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
// Functions to help with whether mouse wheel should trigger zooming
|
|
// --
|
|
r.registerBinding(r.data.container, 'wheel', wheelHandler, true);
|
|
|
|
// disable nonstandard wheel events
|
|
// r.registerBinding(r.data.container, 'mousewheel', wheelHandler, true);
|
|
// r.registerBinding(r.data.container, 'DOMMouseScroll', wheelHandler, true);
|
|
// r.registerBinding(r.data.container, 'MozMousePixelScroll', wheelHandler, true); // older firefox
|
|
|
|
r.registerBinding(window, 'scroll', function(e){
|
|
r.scrollingPage = true;
|
|
|
|
clearTimeout( r.scrollingPageTimeout );
|
|
r.scrollingPageTimeout = setTimeout(function(){
|
|
r.scrollingPage = false;
|
|
}, 250);
|
|
}, true);
|
|
|
|
// Functions to help with handling mouseout/mouseover on the Cytoscape container
|
|
// Handle mouseout on Cytoscape container
|
|
r.registerBinding(r.data.container, 'mouseout', function(e) {
|
|
var pos = r.projectIntoViewport(e.clientX, e.clientY);
|
|
|
|
r.data.cy.trigger(new $$.Event(e, {
|
|
type: 'mouseout',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}));
|
|
}, false);
|
|
|
|
r.registerBinding(r.data.container, 'mouseover', function(e) {
|
|
var pos = r.projectIntoViewport(e.clientX, e.clientY);
|
|
|
|
r.data.cy.trigger(new $$.Event(e, {
|
|
type: 'mouseover',
|
|
cyPosition: { x: pos[0], y: pos[1] }
|
|
}));
|
|
}, false);
|
|
|
|
var f1x1, f1y1, f2x1, f2y1; // starting points for pinch-to-zoom
|
|
var distance1, distance1Sq; // initial distance between finger 1 and finger 2 for pinch-to-zoom
|
|
var center1, modelCenter1; // center point on start pinch to zoom
|
|
var offsetLeft, offsetTop;
|
|
var containerWidth, containerHeight;
|
|
var twoFingersStartInside;
|
|
|
|
var distance = function(x1, y1, x2, y2){
|
|
return Math.sqrt( (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1) );
|
|
};
|
|
|
|
var distanceSq = function(x1, y1, x2, y2){
|
|
return (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1);
|
|
};
|
|
|
|
r.registerBinding(r.data.container, 'touchstart', function(e) {
|
|
|
|
clearTimeout( this.threeFingerSelectTimeout );
|
|
|
|
if( e.target !== r.data.link ){
|
|
e.preventDefault();
|
|
}
|
|
|
|
r.touchData.capture = true;
|
|
r.data.bgActivePosistion = undefined;
|
|
|
|
var cy = r.data.cy;
|
|
var nodes = r.getCachedNodes();
|
|
var edges = r.getCachedEdges();
|
|
var now = r.touchData.now;
|
|
var earlier = r.touchData.earlier;
|
|
var needsRedraw = r.data.canvasNeedsRedraw;
|
|
|
|
if (e.touches[0]) { var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY); now[0] = pos[0]; now[1] = pos[1]; }
|
|
if (e.touches[1]) { var pos = r.projectIntoViewport(e.touches[1].clientX, e.touches[1].clientY); now[2] = pos[0]; now[3] = pos[1]; }
|
|
if (e.touches[2]) { var pos = r.projectIntoViewport(e.touches[2].clientX, e.touches[2].clientY); now[4] = pos[0]; now[5] = pos[1]; }
|
|
|
|
|
|
// record starting points for pinch-to-zoom
|
|
if( e.touches[1] ){
|
|
|
|
// anything in the set of dragged eles should be released
|
|
var release = function( eles ){
|
|
for( var i = 0; i < eles.length; i++ ){
|
|
eles[i]._private.grabbed = false;
|
|
eles[i]._private.rscratch.inDragLayer = false;
|
|
if( eles[i].active() ){ eles[i].unactivate(); }
|
|
}
|
|
};
|
|
release(nodes);
|
|
release(edges);
|
|
|
|
var offsets = r.findContainerClientCoords();
|
|
offsetLeft = offsets[0];
|
|
offsetTop = offsets[1];
|
|
containerWidth = offsets[2];
|
|
containerHeight = offsets[3];
|
|
|
|
f1x1 = e.touches[0].clientX - offsetLeft;
|
|
f1y1 = e.touches[0].clientY - offsetTop;
|
|
|
|
f2x1 = e.touches[1].clientX - offsetLeft;
|
|
f2y1 = e.touches[1].clientY - offsetTop;
|
|
|
|
twoFingersStartInside =
|
|
0 <= f1x1 && f1x1 <= containerWidth
|
|
&& 0 <= f2x1 && f2x1 <= containerWidth
|
|
&& 0 <= f1y1 && f1y1 <= containerHeight
|
|
&& 0 <= f2y1 && f2y1 <= containerHeight
|
|
;
|
|
|
|
var pan = cy.pan();
|
|
var zoom = cy.zoom();
|
|
|
|
distance1 = distance( f1x1, f1y1, f2x1, f2y1 );
|
|
distance1Sq = distanceSq( f1x1, f1y1, f2x1, f2y1 );
|
|
center1 = [ (f1x1 + f2x1)/2, (f1y1 + f2y1)/2 ];
|
|
modelCenter1 = [
|
|
(center1[0] - pan.x) / zoom,
|
|
(center1[1] - pan.y) / zoom
|
|
];
|
|
|
|
// consider context tap
|
|
var cxtDistThreshold = 200;
|
|
var cxtDistThresholdSq = cxtDistThreshold * cxtDistThreshold;
|
|
if( distance1Sq < cxtDistThresholdSq && !e.touches[2] ){
|
|
|
|
var near1 = r.findNearestElement(now[0], now[1], true, true);
|
|
var near2 = r.findNearestElement(now[2], now[3], true, true);
|
|
|
|
//console.log(distance1)
|
|
|
|
if( near1 && near1.isNode() ){
|
|
near1.activate().trigger( new $$.Event(e, {
|
|
type: 'cxttapstart',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
r.touchData.start = near1;
|
|
|
|
} else if( near2 && near2.isNode() ){
|
|
near2.activate().trigger( new $$.Event(e, {
|
|
type: 'cxttapstart',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
r.touchData.start = near2;
|
|
|
|
} else {
|
|
cy.trigger( new $$.Event(e, {
|
|
type: 'cxttapstart',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
r.touchData.start = null;
|
|
}
|
|
|
|
if( r.touchData.start ){ r.touchData.start._private.grabbed = false; }
|
|
r.touchData.cxt = true;
|
|
r.touchData.cxtDragged = false;
|
|
r.data.bgActivePosistion = undefined;
|
|
|
|
//console.log('cxttapstart')
|
|
|
|
r.redraw();
|
|
return;
|
|
|
|
}
|
|
|
|
// console.log(center1);
|
|
// console.log('touchstart ptz');
|
|
// console.log(offsetLeft, offsetTop);
|
|
// console.log(f1x1, f1y1);
|
|
// console.log(f2x1, f2y1);
|
|
// console.log(distance1);
|
|
// console.log(center1);
|
|
}
|
|
|
|
// console.log('another tapstart')
|
|
|
|
|
|
if (e.touches[2]) {
|
|
|
|
} else if (e.touches[1]) {
|
|
|
|
} else if (e.touches[0]) {
|
|
var near = r.findNearestElement(now[0], now[1], true, true);
|
|
|
|
if (near != null) {
|
|
near.activate();
|
|
|
|
r.touchData.start = near;
|
|
|
|
if( near.isNode() && r.nodeIsDraggable(near) ){
|
|
|
|
var draggedEles = r.dragData.touchDragEles = [];
|
|
|
|
needsRedraw[CR.NODE] = true;
|
|
needsRedraw[CR.DRAG] = true;
|
|
|
|
if( near.selected() ){
|
|
// reset drag elements, since near will be added again
|
|
|
|
var selectedNodes = cy.$(function(){
|
|
return this.isNode() && this.selected();
|
|
});
|
|
|
|
for( var k = 0; k < selectedNodes.length; k++ ){
|
|
var selectedNode = selectedNodes[k];
|
|
|
|
if( r.nodeIsDraggable(selectedNode) ){
|
|
addNodeToDrag( selectedNode, { addToList: draggedEles } );
|
|
}
|
|
}
|
|
} else {
|
|
addNodeToDrag( near, { addToList: draggedEles } );
|
|
}
|
|
|
|
near.trigger( new $$.Event(e, {
|
|
type: 'grab',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
}
|
|
|
|
near
|
|
.trigger(new $$.Event(e, {
|
|
type: 'touchstart',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapstart',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmousdown',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
;
|
|
} if (near == null) {
|
|
cy
|
|
.trigger(new $$.Event(e, {
|
|
type: 'touchstart',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapstart',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmousedown',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
;
|
|
|
|
r.data.bgActivePosistion = {
|
|
x: pos[0],
|
|
y: pos[1]
|
|
};
|
|
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
r.redraw();
|
|
}
|
|
|
|
|
|
// Tap, taphold
|
|
// -----
|
|
|
|
for (var i=0; i<now.length; i++) {
|
|
earlier[i] = now[i];
|
|
r.touchData.startPosition[i] = now[i];
|
|
}
|
|
|
|
r.touchData.singleTouchMoved = false;
|
|
r.touchData.singleTouchStartTime = +new Date();
|
|
|
|
clearTimeout( r.touchData.tapholdTimeout );
|
|
r.touchData.tapholdTimeout = setTimeout(function() {
|
|
if(
|
|
r.touchData.singleTouchMoved === false
|
|
&& !r.pinching // if pinching, then taphold unselect shouldn't take effect
|
|
|
|
// This time double constraint prevents multiple quick taps
|
|
// followed by a taphold triggering multiple taphold events
|
|
//&& Date.now() - r.touchData.singleTouchStartTime > 250
|
|
){
|
|
if (r.touchData.start) {
|
|
r.touchData.start.trigger( new $$.Event(e, {
|
|
type: 'taphold',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
} else {
|
|
r.data.cy.trigger( new $$.Event(e, {
|
|
type: 'taphold',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
|
|
cy.$(':selected').unselect();
|
|
}
|
|
|
|
// console.log('taphold');
|
|
}
|
|
}, r.tapholdDuration);
|
|
}
|
|
|
|
//r.redraw();
|
|
|
|
}, false);
|
|
|
|
// console.log = function(m){ $('#console').append('<div>'+m+'</div>'); };
|
|
|
|
r.registerBinding(window, 'touchmove', $$.util.throttle(function(e) {
|
|
|
|
var select = r.data.select;
|
|
var capture = r.touchData.capture; //if (!capture) { return; };
|
|
if( capture ){ e.preventDefault(); }
|
|
|
|
var cy = r.data.cy;
|
|
var now = r.touchData.now; var earlier = r.touchData.earlier;
|
|
var zoom = cy.zoom();
|
|
|
|
var needsRedraw = r.data.canvasNeedsRedraw;
|
|
|
|
if (e.touches[0]) { var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY); now[0] = pos[0]; now[1] = pos[1]; }
|
|
if (e.touches[1]) { var pos = r.projectIntoViewport(e.touches[1].clientX, e.touches[1].clientY); now[2] = pos[0]; now[3] = pos[1]; }
|
|
if (e.touches[2]) { var pos = r.projectIntoViewport(e.touches[2].clientX, e.touches[2].clientY); now[4] = pos[0]; now[5] = pos[1]; }
|
|
var disp = []; for (var j=0;j<now.length;j++) { disp[j] = now[j] - earlier[j]; }
|
|
|
|
var startPos = r.touchData.startPosition;
|
|
|
|
var dx = now[0] - startPos[0];
|
|
var dx2 = dx * dx;
|
|
var dy = now[1] - startPos[1];
|
|
var dy2 = dy * dy;
|
|
var dist2 = dx2 + dy2;
|
|
var rdist2 = dist2 * zoom * zoom;
|
|
|
|
if( capture && r.touchData.cxt ){
|
|
var f1x2 = e.touches[0].clientX - offsetLeft, f1y2 = e.touches[0].clientY - offsetTop;
|
|
var f2x2 = e.touches[1].clientX - offsetLeft, f2y2 = e.touches[1].clientY - offsetTop;
|
|
// var distance2 = distance( f1x2, f1y2, f2x2, f2y2 );
|
|
var distance2Sq = distanceSq( f1x2, f1y2, f2x2, f2y2 );
|
|
var factorSq = distance2Sq / distance1Sq;
|
|
|
|
var distThreshold = 150;
|
|
var distThresholdSq = distThreshold * distThreshold;
|
|
var factorThreshold = 1.5;
|
|
var factorThresholdSq = factorThreshold * factorThreshold;
|
|
|
|
//console.log(factor, distance2)
|
|
|
|
// cancel ctx gestures if the distance b/t the fingers increases
|
|
if( factorSq >= factorThresholdSq || distance2Sq >= distThresholdSq ){
|
|
r.touchData.cxt = false;
|
|
if( r.touchData.start ){ r.touchData.start.unactivate(); r.touchData.start = null; }
|
|
r.data.bgActivePosistion = undefined;
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
var cxtEvt = new $$.Event(e, {
|
|
type: 'cxttapend',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
});
|
|
if( r.touchData.start ){
|
|
r.touchData.start.trigger( cxtEvt );
|
|
} else {
|
|
cy.trigger( cxtEvt );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if( capture && r.touchData.cxt ){
|
|
var cxtEvt = new $$.Event(e, {
|
|
type: 'cxtdrag',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
});
|
|
r.data.bgActivePosistion = undefined;
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
if( r.touchData.start ){
|
|
r.touchData.start.trigger( cxtEvt );
|
|
} else {
|
|
cy.trigger( cxtEvt );
|
|
}
|
|
|
|
if( r.touchData.start ){ r.touchData.start._private.grabbed = false; }
|
|
r.touchData.cxtDragged = true;
|
|
|
|
//console.log('cxtdrag')
|
|
|
|
var near = r.findNearestElement(now[0], now[1], true, true);
|
|
|
|
if( !r.touchData.cxtOver || near !== r.touchData.cxtOver ){
|
|
|
|
if( r.touchData.cxtOver ){
|
|
r.touchData.cxtOver.trigger( new $$.Event(e, {
|
|
type: 'cxtdragout',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
|
|
// console.log('cxtdragout');
|
|
}
|
|
|
|
r.touchData.cxtOver = near;
|
|
|
|
if( near ){
|
|
near.trigger( new $$.Event(e, {
|
|
type: 'cxtdragover',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
|
|
// console.log('cxtdragover');
|
|
}
|
|
|
|
}
|
|
|
|
} else if( capture && e.touches[2] && cy.boxSelectionEnabled() ){
|
|
r.data.bgActivePosistion = undefined;
|
|
clearTimeout( this.threeFingerSelectTimeout );
|
|
this.lastThreeTouch = +new Date();
|
|
r.touchData.selecting = true;
|
|
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
if( !select || select.length === 0 || select[0] === undefined ){
|
|
select[0] = (now[0] + now[2] + now[4])/3;
|
|
select[1] = (now[1] + now[3] + now[5])/3;
|
|
select[2] = (now[0] + now[2] + now[4])/3 + 1;
|
|
select[3] = (now[1] + now[3] + now[5])/3 + 1;
|
|
} else {
|
|
select[2] = (now[0] + now[2] + now[4])/3;
|
|
select[3] = (now[1] + now[3] + now[5])/3;
|
|
}
|
|
|
|
select[4] = 1;
|
|
r.touchData.selecting = true;
|
|
|
|
r.redraw();
|
|
|
|
} else if ( capture && e.touches[1] && cy.zoomingEnabled() && cy.panningEnabled() && cy.userZoomingEnabled() && cy.userPanningEnabled() ) { // two fingers => pinch to zoom
|
|
r.data.bgActivePosistion = undefined;
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
var draggedEles = r.dragData.touchDragEles;
|
|
if( draggedEles ){
|
|
needsRedraw[CR.DRAG] = true;
|
|
|
|
for( var i = 0; i < draggedEles.length; i++ ){
|
|
draggedEles[i]._private.grabbed = false;
|
|
draggedEles[i]._private.rscratch.inDragLayer = false;
|
|
}
|
|
}
|
|
|
|
// console.log('touchmove ptz');
|
|
|
|
// (x2, y2) for fingers 1 and 2
|
|
var f1x2 = e.touches[0].clientX - offsetLeft, f1y2 = e.touches[0].clientY - offsetTop;
|
|
var f2x2 = e.touches[1].clientX - offsetLeft, f2y2 = e.touches[1].clientY - offsetTop;
|
|
|
|
// console.log( f1x2, f1y2 )
|
|
// console.log( f2x2, f2y2 )
|
|
|
|
var distance2 = distance( f1x2, f1y2, f2x2, f2y2 );
|
|
// var distance2Sq = distanceSq( f1x2, f1y2, f2x2, f2y2 );
|
|
// var factor = Math.sqrt( distance2Sq ) / Math.sqrt( distance1Sq );
|
|
var factor = distance2 / distance1;
|
|
|
|
// console.log(distance2)
|
|
// console.log(factor)
|
|
|
|
if( factor != 1 && twoFingersStartInside){
|
|
|
|
// console.log(factor)
|
|
// console.log(distance2 + ' / ' + distance1);
|
|
// console.log('--');
|
|
|
|
// delta finger1
|
|
var df1x = f1x2 - f1x1;
|
|
var df1y = f1y2 - f1y1;
|
|
|
|
// delta finger 2
|
|
var df2x = f2x2 - f2x1;
|
|
var df2y = f2y2 - f2y1;
|
|
|
|
// translation is the normalised vector of the two fingers movement
|
|
// i.e. so pinching cancels out and moving together pans
|
|
var tx = (df1x + df2x)/2;
|
|
var ty = (df1y + df2y)/2;
|
|
|
|
// adjust factor by the speed multiplier
|
|
// var speed = 1.5;
|
|
// if( factor > 1 ){
|
|
// factor = (factor - 1) * speed + 1;
|
|
// } else {
|
|
// factor = 1 - (1 - factor) * speed;
|
|
// }
|
|
|
|
// now calculate the zoom
|
|
var zoom1 = cy.zoom();
|
|
var zoom2 = zoom1 * factor;
|
|
var pan1 = cy.pan();
|
|
|
|
// the model center point converted to the current rendered pos
|
|
var ctrx = modelCenter1[0] * zoom1 + pan1.x;
|
|
var ctry = modelCenter1[1] * zoom1 + pan1.y;
|
|
|
|
var pan2 = {
|
|
x: -zoom2/zoom1 * (ctrx - pan1.x - tx) + ctrx,
|
|
y: -zoom2/zoom1 * (ctry - pan1.y - ty) + ctry
|
|
};
|
|
|
|
// console.log(pan2);
|
|
// console.log(zoom2);
|
|
|
|
// remove dragged eles
|
|
if( r.touchData.start ){
|
|
var draggedEles = r.dragData.touchDragEles;
|
|
|
|
if( draggedEles ){ for( var i = 0; i < draggedEles.length; i++ ){
|
|
var dEi_p = draggedEles[i]._private;
|
|
|
|
dEi_p.grabbed = false;
|
|
dEi_p.rscratch.inDragLayer = false;
|
|
} }
|
|
|
|
var start_p = r.touchData.start._private;
|
|
start_p.active = false;
|
|
start_p.grabbed = false;
|
|
start_p.rscratch.inDragLayer = false;
|
|
|
|
needsRedraw[CR.DRAG] = true;
|
|
|
|
r.touchData.start
|
|
.trigger('free')
|
|
.trigger('unactivate')
|
|
;
|
|
}
|
|
|
|
cy.viewport({
|
|
zoom: zoom2,
|
|
pan: pan2,
|
|
cancelOnFailedZoom: true
|
|
});
|
|
|
|
distance1 = distance2;
|
|
f1x1 = f1x2;
|
|
f1y1 = f1y2;
|
|
f2x1 = f2x2;
|
|
f2y1 = f2y2;
|
|
|
|
r.pinching = true;
|
|
}
|
|
|
|
// Re-project
|
|
if (e.touches[0]) { var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY); now[0] = pos[0]; now[1] = pos[1]; }
|
|
if (e.touches[1]) { var pos = r.projectIntoViewport(e.touches[1].clientX, e.touches[1].clientY); now[2] = pos[0]; now[3] = pos[1]; }
|
|
if (e.touches[2]) { var pos = r.projectIntoViewport(e.touches[2].clientX, e.touches[2].clientY); now[4] = pos[0]; now[5] = pos[1]; }
|
|
|
|
} else if (e.touches[0]) {
|
|
var start = r.touchData.start;
|
|
var last = r.touchData.last;
|
|
var near = near || r.findNearestElement(now[0], now[1], true, true);
|
|
|
|
if( start != null && start._private.group == 'nodes' && r.nodeIsDraggable(start) ){
|
|
|
|
if( rdist2 >= r.touchTapThreshold2 ){ // then dragging can happen
|
|
var draggedEles = r.dragData.touchDragEles;
|
|
|
|
for( var k = 0; k < draggedEles.length; k++ ){
|
|
var draggedEle = draggedEles[k];
|
|
|
|
if( r.nodeIsDraggable(draggedEle) && draggedEle.isNode() && draggedEle.grabbed() ){
|
|
r.dragData.didDrag = true;
|
|
var dPos = draggedEle._private.position;
|
|
var justStartedDrag = !r.hoverData.draggingEles;
|
|
|
|
if( $$.is.number(disp[0]) && $$.is.number(disp[1]) ){
|
|
dPos.x += disp[0];
|
|
dPos.y += disp[1];
|
|
}
|
|
|
|
if( justStartedDrag ){
|
|
addNodeToDrag( draggedEle, { inDragLayer: true } );
|
|
|
|
needsRedraw[CR.NODE] = true;
|
|
|
|
var dragDelta = r.touchData.dragDelta;
|
|
|
|
if( $$.is.number(dragDelta[0]) && $$.is.number(dragDelta[1]) ){
|
|
dPos.x += dragDelta[0];
|
|
dPos.y += dragDelta[1];
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
var tcol = new $$.Collection(cy, draggedEle);
|
|
|
|
tcol.updateCompoundBounds();
|
|
tcol.trigger('position drag');
|
|
|
|
r.hoverData.draggingEles = true;
|
|
|
|
needsRedraw[CR.DRAG] = true;
|
|
|
|
if(
|
|
r.touchData.startPosition[0] == earlier[0]
|
|
&& r.touchData.startPosition[1] == earlier[1]
|
|
){
|
|
|
|
needsRedraw[CR.NODE] = true;
|
|
}
|
|
|
|
r.redraw();
|
|
} else { // otherise keep track of drag delta for later
|
|
var dragDelta = r.touchData.dragDelta = r.touchData.dragDelta || [];
|
|
|
|
if( dragDelta.length === 0 ){
|
|
dragDelta.push( disp[0] );
|
|
dragDelta.push( disp[1] );
|
|
} else {
|
|
dragDelta[0] += disp[0];
|
|
dragDelta[1] += disp[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Touchmove event
|
|
{
|
|
|
|
if (start != null) {
|
|
start.trigger( new $$.Event(e, {
|
|
type: 'touchmove',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
|
|
start.trigger( new $$.Event(e, {
|
|
type: 'tapdrag',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
|
|
start.trigger( new $$.Event(e, {
|
|
type: 'vmousemove',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
}
|
|
|
|
if (start == null) {
|
|
|
|
if (near != null) {
|
|
near.trigger( new $$.Event(e, {
|
|
type: 'touchmove',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
|
|
near.trigger( new $$.Event(e, {
|
|
type: 'tapdrag',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
|
|
near.trigger( new $$.Event(e, {
|
|
type: 'vmousemove',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
}
|
|
|
|
if (near == null) {
|
|
cy.trigger( new $$.Event(e, {
|
|
type: 'touchmove',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
|
|
cy.trigger( new $$.Event(e, {
|
|
type: 'tapdrag',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
|
|
cy.trigger( new $$.Event(e, {
|
|
type: 'vmousemove',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}) );
|
|
}
|
|
}
|
|
|
|
if (near != last) {
|
|
if (last) { last.trigger(new $$.Event(e, { type: 'tapdragout', cyPosition: { x: now[0], y: now[1] } })); }
|
|
if (near) { near.trigger(new $$.Event(e, { type: 'tapdragover', cyPosition: { x: now[0], y: now[1] } })); }
|
|
}
|
|
|
|
r.touchData.last = near;
|
|
}
|
|
|
|
// Check to cancel taphold
|
|
for (var i=0;i<now.length;i++) {
|
|
if (now[i]
|
|
&& r.touchData.startPosition[i]
|
|
&& Math.abs(now[i] - r.touchData.startPosition[i]) > 4) {
|
|
|
|
r.touchData.singleTouchMoved = true;
|
|
}
|
|
}
|
|
|
|
if(
|
|
capture
|
|
&& ( start == null || start.isEdge() )
|
|
&& cy.panningEnabled() && cy.userPanningEnabled()
|
|
){
|
|
|
|
if( r.swipePanning ){
|
|
cy.panBy({
|
|
x: disp[0] * zoom,
|
|
y: disp[1] * zoom
|
|
});
|
|
|
|
} else if( rdist2 >= r.touchTapThreshold2 ){
|
|
r.swipePanning = true;
|
|
|
|
cy.panBy({
|
|
x: dx * zoom,
|
|
y: dy * zoom
|
|
});
|
|
}
|
|
|
|
if( start ){
|
|
start.unactivate();
|
|
|
|
if( !r.data.bgActivePosistion ){
|
|
r.data.bgActivePosistion = {
|
|
x: now[0],
|
|
y: now[1]
|
|
};
|
|
}
|
|
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
r.touchData.start = null;
|
|
}
|
|
|
|
// Re-project
|
|
var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY);
|
|
now[0] = pos[0]; now[1] = pos[1];
|
|
}
|
|
}
|
|
|
|
for (var j=0; j<now.length; j++) { earlier[j] = now[j]; }
|
|
//r.redraw();
|
|
|
|
}, 1000/30, { trailing: true }), false);
|
|
|
|
r.registerBinding(window, 'touchcancel', function(e) {
|
|
var start = r.touchData.start;
|
|
|
|
r.touchData.capture = false;
|
|
|
|
if( start ){
|
|
start.unactivate();
|
|
}
|
|
});
|
|
|
|
r.registerBinding(window, 'touchend', function(e) {
|
|
var start = r.touchData.start;
|
|
|
|
var capture = r.touchData.capture;
|
|
|
|
if( capture ){
|
|
r.touchData.capture = false;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
var select = r.data.select;
|
|
|
|
r.swipePanning = false;
|
|
r.hoverData.draggingEles = false;
|
|
|
|
var cy = r.data.cy;
|
|
var zoom = cy.zoom();
|
|
var now = r.touchData.now;
|
|
var earlier = r.touchData.earlier;
|
|
|
|
var needsRedraw = r.data.canvasNeedsRedraw;
|
|
|
|
if (e.touches[0]) { var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY); now[0] = pos[0]; now[1] = pos[1]; }
|
|
if (e.touches[1]) { var pos = r.projectIntoViewport(e.touches[1].clientX, e.touches[1].clientY); now[2] = pos[0]; now[3] = pos[1]; }
|
|
if (e.touches[2]) { var pos = r.projectIntoViewport(e.touches[2].clientX, e.touches[2].clientY); now[4] = pos[0]; now[5] = pos[1]; }
|
|
|
|
if( start ){
|
|
start.unactivate();
|
|
}
|
|
|
|
var ctxTapend;
|
|
if( r.touchData.cxt ){
|
|
ctxTapend = new $$.Event(e, {
|
|
type: 'cxttapend',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
});
|
|
|
|
if( start ){
|
|
start.trigger( ctxTapend );
|
|
} else {
|
|
cy.trigger( ctxTapend );
|
|
}
|
|
|
|
//console.log('cxttapend')
|
|
|
|
if( !r.touchData.cxtDragged ){
|
|
var ctxTap = new $$.Event(e, {
|
|
type: 'cxttap',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
});
|
|
|
|
if( start ){
|
|
start.trigger( ctxTap );
|
|
} else {
|
|
cy.trigger( ctxTap );
|
|
}
|
|
|
|
//console.log('cxttap')
|
|
}
|
|
|
|
if( r.touchData.start ){ r.touchData.start._private.grabbed = false; }
|
|
r.touchData.cxt = false;
|
|
r.touchData.start = null;
|
|
|
|
r.redraw();
|
|
return;
|
|
}
|
|
|
|
// no more box selection if we don't have three fingers
|
|
if( !e.touches[2] && cy.boxSelectionEnabled() && r.touchData.selecting ){
|
|
r.touchData.selecting = false;
|
|
clearTimeout( this.threeFingerSelectTimeout );
|
|
//this.threeFingerSelectTimeout = setTimeout(function(){
|
|
var newlySelected = [];
|
|
var box = r.getAllInBox( select[0], select[1], select[2], select[3] );
|
|
|
|
select[0] = undefined;
|
|
select[1] = undefined;
|
|
select[2] = undefined;
|
|
select[3] = undefined;
|
|
select[4] = 0;
|
|
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
// console.log(box);
|
|
for( var i = 0; i< box.length; i++ ) {
|
|
if( box[i]._private.selectable ){
|
|
newlySelected.push( box[i] );
|
|
}
|
|
}
|
|
|
|
var newlySelCol = new $$.Collection( cy, newlySelected );
|
|
|
|
if( cy.selectionType() === 'single' ){
|
|
cy.$(':selected').unmerge( newlySelCol ).unselect();
|
|
}
|
|
|
|
newlySelCol.select();
|
|
|
|
if( newlySelCol.length > 0 ) {
|
|
needsRedraw[CR.NODE] = true;
|
|
} else {
|
|
r.redraw();
|
|
}
|
|
|
|
//}, 100);
|
|
}
|
|
|
|
var updateStartStyle = false;
|
|
|
|
if( start != null ){
|
|
start._private.active = false;
|
|
updateStartStyle = true;
|
|
start.unactivate();
|
|
}
|
|
|
|
if (e.touches[2]) {
|
|
r.data.bgActivePosistion = undefined;
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
} else if (e.touches[1]) {
|
|
|
|
} else if (e.touches[0]) {
|
|
|
|
// Last touch released
|
|
} else if (!e.touches[0]) {
|
|
|
|
r.data.bgActivePosistion = undefined;
|
|
needsRedraw[CR.SELECT_BOX] = true;
|
|
|
|
var draggedEles = r.dragData.touchDragEles;
|
|
|
|
if (start != null ) {
|
|
|
|
var startWasGrabbed = start._private.grabbed;
|
|
|
|
freeDraggedElements( draggedEles );
|
|
|
|
needsRedraw[CR.DRAG] = true;
|
|
needsRedraw[CR.NODE] = true;
|
|
|
|
if( startWasGrabbed ){
|
|
start.trigger('free');
|
|
}
|
|
|
|
start
|
|
.trigger(new $$.Event(e, {
|
|
type: 'touchend',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapend',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmouseup',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
;
|
|
|
|
start.unactivate();
|
|
|
|
r.touchData.start = null;
|
|
|
|
} else {
|
|
var near = r.findNearestElement(now[0], now[1], true, true);
|
|
|
|
if (near != null) {
|
|
near
|
|
.trigger(new $$.Event(e, {
|
|
type: 'touchend',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapend',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmouseup',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
;
|
|
}
|
|
|
|
if (near == null) {
|
|
cy
|
|
.trigger(new $$.Event(e, {
|
|
type: 'touchend',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tapend',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vmouseup',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
;
|
|
}
|
|
}
|
|
|
|
var dx = r.touchData.startPosition[0] - now[0];
|
|
var dx2 = dx * dx;
|
|
var dy = r.touchData.startPosition[1] - now[1];
|
|
var dy2 = dy * dy;
|
|
var dist2 = dx2 + dy2;
|
|
var rdist2 = dist2 * zoom * zoom;
|
|
|
|
// Prepare to select the currently touched node, only if it hasn't been dragged past a certain distance
|
|
if (start != null
|
|
&& !r.dragData.didDrag // didn't drag nodes around
|
|
&& start._private.selectable
|
|
&& rdist2 < r.touchTapThreshold2
|
|
&& !r.pinching // pinch to zoom should not affect selection
|
|
) {
|
|
|
|
if( cy.selectionType() === 'single' ){
|
|
cy.$(':selected').unmerge( start ).unselect();
|
|
start.select();
|
|
} else {
|
|
if( start.selected() ){
|
|
start.unselect();
|
|
} else {
|
|
start.select();
|
|
}
|
|
}
|
|
|
|
updateStartStyle = true;
|
|
|
|
|
|
needsRedraw[CR.NODE] = true;
|
|
}
|
|
|
|
// Tap event, roughly same as mouse click event for touch
|
|
if ( r.touchData.singleTouchMoved === false ) {
|
|
|
|
if (start) {
|
|
start
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tap',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vclick',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
;
|
|
} else {
|
|
cy
|
|
.trigger(new $$.Event(e, {
|
|
type: 'tap',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
.trigger(new $$.Event(e, {
|
|
type: 'vclick',
|
|
cyPosition: { x: now[0], y: now[1] }
|
|
}))
|
|
;
|
|
}
|
|
|
|
// console.log('tap');
|
|
}
|
|
|
|
r.touchData.singleTouchMoved = true;
|
|
}
|
|
|
|
for( var j = 0; j < now.length; j++ ){ earlier[j] = now[j]; }
|
|
|
|
r.dragData.didDrag = false; // reset for next mousedown
|
|
|
|
if( e.touches.length === 0 ){
|
|
r.touchData.dragDelta = [];
|
|
}
|
|
|
|
if( updateStartStyle && start ){
|
|
start.updateStyle(false);
|
|
}
|
|
|
|
if( e.touches.length < 2 ){
|
|
r.pinching = false;
|
|
needsRedraw[CR.NODE] = true;
|
|
r.redraw();
|
|
}
|
|
|
|
//r.redraw();
|
|
|
|
}, false);
|
|
};
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var CanvasRenderer = $$('renderer', 'canvas');
|
|
var renderer = CanvasRenderer.prototype;
|
|
var usePaths = CanvasRenderer.usePaths();
|
|
|
|
// Node shape contract:
|
|
//
|
|
// draw: draw
|
|
// intersectLine: report intersection from x, y, to node center
|
|
// checkPoint: check x, y in node
|
|
|
|
var nodeShapes = CanvasRenderer.nodeShapes = {};
|
|
|
|
var sin0 = Math.sin(0);
|
|
var cos0 = Math.cos(0);
|
|
|
|
var sin = {};
|
|
var cos = {};
|
|
|
|
var ellipseStepSize = 0.1;
|
|
|
|
for (var i = 0 * Math.PI; i < 2 * Math.PI; i += ellipseStepSize ) {
|
|
sin[i] = Math.sin(i);
|
|
cos[i] = Math.cos(i);
|
|
}
|
|
|
|
nodeShapes['ellipse'] = {
|
|
draw: function(context, centerX, centerY, width, height) {
|
|
nodeShapes['ellipse'].drawPath(context, centerX, centerY, width, height);
|
|
context.fill();
|
|
},
|
|
|
|
drawPath: function(context, centerX, centerY, width, height) {
|
|
|
|
if( usePaths ){
|
|
if( context.beginPath ){ context.beginPath(); }
|
|
|
|
var xPos, yPos;
|
|
var rw = width/2;
|
|
var rh = height/2;
|
|
for (var i = 0 * Math.PI; i < 2 * Math.PI; i += ellipseStepSize ) {
|
|
xPos = centerX - (rw * sin[i]) * sin0 + (rw * cos[i]) * cos0;
|
|
yPos = centerY + (rh * cos[i]) * sin0 + (rh * sin[i]) * cos0;
|
|
|
|
if (i === 0) {
|
|
context.moveTo(xPos, yPos);
|
|
} else {
|
|
context.lineTo(xPos, yPos);
|
|
}
|
|
}
|
|
context.closePath();
|
|
|
|
} else {
|
|
|
|
if( context.beginPath ){ context.beginPath(); }
|
|
context.translate(centerX, centerY);
|
|
context.scale(width / 2, height / 2);
|
|
// At origin, radius 1, 0 to 2pi
|
|
context.arc(0, 0, 1, 0, Math.PI * 2 * 0.999, false); // *0.999 b/c chrome rendering bug on full circle
|
|
context.closePath();
|
|
|
|
context.scale(2/width, 2/height);
|
|
context.translate(-centerX, -centerY);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
intersectLine: function(nodeX, nodeY, width, height, x, y, padding) {
|
|
var intersect = $$.math.intersectLineEllipse(
|
|
x, y,
|
|
nodeX,
|
|
nodeY,
|
|
width / 2 + padding,
|
|
height / 2 + padding);
|
|
|
|
return intersect;
|
|
},
|
|
|
|
intersectBox: function(
|
|
x1, y1, x2, y2, width, height, centerX, centerY, padding) {
|
|
|
|
return $$.math.boxIntersectEllipse(
|
|
x1, y1, x2, y2, padding, width, height, centerX, centerY);
|
|
},
|
|
|
|
checkPoint: function(
|
|
x, y, padding, width, height, centerX, centerY) {
|
|
|
|
// console.log(arguments);
|
|
|
|
x -= centerX;
|
|
y -= centerY;
|
|
|
|
x /= (width / 2 + padding);
|
|
y /= (height / 2 + padding);
|
|
|
|
return (Math.pow(x, 2) + Math.pow(y, 2) <= 1);
|
|
}
|
|
};
|
|
|
|
function generatePolygon( name, points ){
|
|
nodeShapes[name] = {
|
|
points: points,
|
|
|
|
draw: function(context, centerX, centerY, width, height) {
|
|
renderer.drawPolygon(context,
|
|
centerX, centerY,
|
|
width, height,
|
|
nodeShapes[name].points);
|
|
},
|
|
|
|
drawPath: function(context, centerX, centerY, width, height) {
|
|
renderer.drawPolygonPath(context,
|
|
centerX, centerY,
|
|
width, height,
|
|
nodeShapes[name].points);
|
|
},
|
|
|
|
intersectLine: function(nodeX, nodeY, width, height, x, y, padding) {
|
|
return $$.math.polygonIntersectLine(
|
|
x, y,
|
|
nodeShapes[name].points,
|
|
nodeX,
|
|
nodeY,
|
|
width / 2, height / 2,
|
|
padding);
|
|
},
|
|
|
|
intersectBox: function(
|
|
x1, y1, x2, y2,
|
|
width, height, centerX,
|
|
centerY, padding) {
|
|
|
|
var points = nodeShapes[name].points;
|
|
|
|
return $$.math.boxIntersectPolygon(
|
|
x1, y1, x2, y2,
|
|
points, width, height, centerX,
|
|
centerY, [0, -1], padding);
|
|
},
|
|
|
|
checkPoint: function(
|
|
x, y, padding, width, height, centerX, centerY) {
|
|
|
|
return $$.math.pointInsidePolygon(x, y, nodeShapes[name].points,
|
|
centerX, centerY, width, height, [0, -1], padding);
|
|
}
|
|
};
|
|
}
|
|
|
|
generatePolygon( 'triangle', $$.math.generateUnitNgonPointsFitToSquare(3, 0) );
|
|
|
|
generatePolygon( 'square', $$.math.generateUnitNgonPointsFitToSquare(4, 0) );
|
|
nodeShapes['rectangle'] = nodeShapes['square'];
|
|
|
|
nodeShapes['roundrectangle'] = {
|
|
points: $$.math.generateUnitNgonPointsFitToSquare(4, 0),
|
|
|
|
draw: function(context, centerX, centerY, width, height) {
|
|
renderer.drawRoundRectangle(context,
|
|
centerX, centerY,
|
|
width, height,
|
|
10);
|
|
},
|
|
|
|
drawPath: function(context, centerX, centerY, width, height) {
|
|
renderer.drawRoundRectanglePath(context,
|
|
centerX, centerY,
|
|
width, height,
|
|
10);
|
|
},
|
|
|
|
intersectLine: function(nodeX, nodeY, width, height, x, y, padding) {
|
|
return $$.math.roundRectangleIntersectLine(
|
|
x, y,
|
|
nodeX,
|
|
nodeY,
|
|
width, height,
|
|
padding);
|
|
},
|
|
|
|
intersectBox: function(
|
|
x1, y1, x2, y2,
|
|
width, height, centerX,
|
|
centerY, padding) {
|
|
|
|
return $$.math.roundRectangleIntersectBox(
|
|
x1, y1, x2, y2,
|
|
width, height, centerX, centerY, padding);
|
|
},
|
|
|
|
// Looks like the width passed into this function is actually the total width / 2
|
|
checkPoint: function(
|
|
x, y, padding, width, height, centerX, centerY) {
|
|
|
|
var cornerRadius = $$.math.getRoundRectangleRadius(width, height);
|
|
|
|
// Check hBox
|
|
if ($$.math.pointInsidePolygon(x, y, nodeShapes['roundrectangle'].points,
|
|
centerX, centerY, width, height - 2 * cornerRadius, [0, -1], padding)) {
|
|
return true;
|
|
}
|
|
|
|
// Check vBox
|
|
if ($$.math.pointInsidePolygon(x, y, nodeShapes['roundrectangle'].points,
|
|
centerX, centerY, width - 2 * cornerRadius, height, [0, -1], padding)) {
|
|
return true;
|
|
}
|
|
|
|
var checkInEllipse = function(x, y, centerX, centerY, width, height, padding) {
|
|
x -= centerX;
|
|
y -= centerY;
|
|
|
|
x /= (width / 2 + padding);
|
|
y /= (height / 2 + padding);
|
|
|
|
return (Math.pow(x, 2) + Math.pow(y, 2) <= 1);
|
|
};
|
|
|
|
|
|
// Check top left quarter circle
|
|
if (checkInEllipse(x, y,
|
|
centerX - width / 2 + cornerRadius,
|
|
centerY - height / 2 + cornerRadius,
|
|
cornerRadius * 2, cornerRadius * 2, padding)) {
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
if (renderer.boxIntersectEllipse(x, y, x, y, padding,
|
|
cornerRadius * 2, cornerRadius * 2,
|
|
centerX - width + cornerRadius,
|
|
centerY - height + cornerRadius)) {
|
|
return true;
|
|
}
|
|
*/
|
|
|
|
// Check top right quarter circle
|
|
if (checkInEllipse(x, y,
|
|
centerX + width / 2 - cornerRadius,
|
|
centerY - height / 2 + cornerRadius,
|
|
cornerRadius * 2, cornerRadius * 2, padding)) {
|
|
|
|
return true;
|
|
}
|
|
|
|
// Check bottom right quarter circle
|
|
if (checkInEllipse(x, y,
|
|
centerX + width / 2 - cornerRadius,
|
|
centerY + height / 2 - cornerRadius,
|
|
cornerRadius * 2, cornerRadius * 2, padding)) {
|
|
|
|
return true;
|
|
}
|
|
|
|
// Check bottom left quarter circle
|
|
if (checkInEllipse(x, y,
|
|
centerX - width / 2 + cornerRadius,
|
|
centerY + height / 2 - cornerRadius,
|
|
cornerRadius * 2, cornerRadius * 2, padding)) {
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
generatePolygon( 'diamond', [
|
|
0, 1,
|
|
1, 0,
|
|
0, -1,
|
|
-1, 0
|
|
] );
|
|
|
|
generatePolygon( 'pentagon', $$.math.generateUnitNgonPointsFitToSquare(5, 0) );
|
|
|
|
generatePolygon( 'hexagon', $$.math.generateUnitNgonPointsFitToSquare(6, 0) );
|
|
|
|
generatePolygon( 'heptagon', $$.math.generateUnitNgonPointsFitToSquare(7, 0) );
|
|
|
|
generatePolygon( 'octagon', $$.math.generateUnitNgonPointsFitToSquare(8, 0) );
|
|
|
|
var star5Points = new Array(20);
|
|
{
|
|
var outerPoints = $$.math.generateUnitNgonPoints(5, 0);
|
|
var innerPoints = $$.math.generateUnitNgonPoints(5, Math.PI / 5);
|
|
|
|
// console.log(outerPoints);
|
|
// console.log(innerPoints);
|
|
|
|
// Outer radius is 1; inner radius of star is smaller
|
|
var innerRadius = 0.5 * (3 - Math.sqrt(5));
|
|
innerRadius *= 1.57;
|
|
|
|
for (var i=0;i<innerPoints.length/2;i++) {
|
|
innerPoints[i*2] *= innerRadius;
|
|
innerPoints[i*2+1] *= innerRadius;
|
|
}
|
|
|
|
for (var i=0;i<20/4;i++) {
|
|
star5Points[i*4] = outerPoints[i*2];
|
|
star5Points[i*4+1] = outerPoints[i*2+1];
|
|
|
|
star5Points[i*4+2] = innerPoints[i*2];
|
|
star5Points[i*4+3] = innerPoints[i*2+1];
|
|
}
|
|
|
|
// console.log(star5Points);
|
|
}
|
|
|
|
star5Points = $$.math.fitPolygonToSquare( star5Points );
|
|
|
|
generatePolygon( 'star', star5Points );
|
|
|
|
generatePolygon( 'vee', [
|
|
-1, -1,
|
|
0, -0.333,
|
|
1, -1,
|
|
0, 1
|
|
] );
|
|
|
|
generatePolygon( 'rhomboid', [
|
|
-1, -1,
|
|
0.333, -1,
|
|
1, 1,
|
|
-0.333, 1
|
|
] );
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var defaults = {
|
|
animate: true, // whether to show the layout as it's running
|
|
maxSimulationTime: 4000, // max length in ms to run the layout
|
|
fit: true, // on every layout reposition of nodes, fit the viewport
|
|
padding: 30, // padding around the simulation
|
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
ungrabifyWhileSimulating: false, // so you can't drag nodes during layout
|
|
|
|
// callbacks on layout events
|
|
ready: undefined, // callback on layoutready
|
|
stop: undefined, // callback on layoutstop
|
|
|
|
// forces used by arbor (use arbor default on undefined)
|
|
repulsion: undefined,
|
|
stiffness: undefined,
|
|
friction: undefined,
|
|
gravity: true,
|
|
fps: undefined,
|
|
precision: undefined,
|
|
|
|
// static numbers or functions that dynamically return what these
|
|
// values should be for each element
|
|
// e.g. nodeMass: function(n){ return n.data('weight') }
|
|
nodeMass: undefined,
|
|
edgeLength: undefined,
|
|
|
|
stepSize: 0.1, // smoothing of arbor bounding box
|
|
|
|
// function that returns true if the system is stable to indicate
|
|
// that the layout can be stopped
|
|
stableEnergy: function( energy ){
|
|
var e = energy;
|
|
return (e.max <= 0.5) || (e.mean <= 0.3);
|
|
},
|
|
|
|
// infinite layout options
|
|
infinite: false // overrides all other options for a forces-all-the-time mode
|
|
};
|
|
|
|
function ArborLayout(options){
|
|
this._private = {};
|
|
|
|
this._private.options = $$.util.extend({}, defaults, options);
|
|
}
|
|
|
|
ArborLayout.prototype.run = function(){
|
|
var layout = this;
|
|
var options = this._private.options;
|
|
|
|
$$.util.require('arbor', function(arbor){
|
|
|
|
var cy = options.cy;
|
|
var eles = options.eles;
|
|
var nodes = eles.nodes().not(':parent');
|
|
var edges = eles.edges();
|
|
var bb = $$.util.makeBoundingBox( options.boundingBox ? options.boundingBox : {
|
|
x1: 0, y1: 0, w: cy.width(), h: cy.height()
|
|
} );
|
|
var simUpdatingPos = false;
|
|
|
|
layout.trigger({ type: 'layoutstart', layout: layout });
|
|
|
|
// backward compatibility for old animation option
|
|
if( options.liveUpdate !== undefined ){
|
|
options.animate = options.liveUpdate;
|
|
}
|
|
|
|
// arbor doesn't work with just 1 node
|
|
if( eles.nodes().size() <= 1 ){
|
|
if( options.fit ){
|
|
cy.reset();
|
|
}
|
|
|
|
eles.nodes().position({
|
|
x: Math.round( (bb.x1 + bb.x2)/2 ),
|
|
y: Math.round( (bb.y1 + bb.y2)/2 )
|
|
});
|
|
|
|
layout.one('layoutready', options.ready);
|
|
layout.trigger({ type: 'layoutready', layout: layout });
|
|
|
|
layout.one('layoutstop', options.stop);
|
|
layout.trigger({ type: 'layoutstop', layout: layout });
|
|
|
|
return;
|
|
}
|
|
|
|
var sys = layout._private.system = arbor.ParticleSystem();
|
|
|
|
sys.parameters({
|
|
repulsion: options.repulsion,
|
|
stiffness: options.stiffness,
|
|
friction: options.friction,
|
|
gravity: options.gravity,
|
|
fps: options.fps,
|
|
dt: options.dt,
|
|
precision: options.precision
|
|
});
|
|
|
|
if( options.animate && options.fit ){
|
|
cy.fit( bb, options.padding );
|
|
}
|
|
|
|
var doneTime = 250;
|
|
var doneTimeout;
|
|
|
|
var ready = false;
|
|
|
|
var lastDraw = +new Date();
|
|
var sysRenderer = {
|
|
init: function(system){
|
|
},
|
|
redraw: function(){
|
|
var energy = sys.energy();
|
|
|
|
// if we're stable (according to the client), we're done
|
|
if( !options.infinite && options.stableEnergy != null && energy != null && energy.n > 0 && options.stableEnergy(energy) ){
|
|
layout.stop();
|
|
return;
|
|
}
|
|
|
|
if( !options.infinite && doneTime != Infinity ){
|
|
clearTimeout(doneTimeout);
|
|
doneTimeout = setTimeout(doneHandler, doneTime);
|
|
}
|
|
|
|
var movedNodes = cy.collection();
|
|
|
|
sys.eachNode(function(n, point){
|
|
var data = n.data;
|
|
var node = data.element;
|
|
|
|
if( node == null ){
|
|
return;
|
|
}
|
|
|
|
if( !node.locked() && !node.grabbed() ){
|
|
node.silentPosition({
|
|
x: bb.x1 + point.x,
|
|
y: bb.y1 + point.y
|
|
});
|
|
|
|
movedNodes.merge( node );
|
|
}
|
|
});
|
|
|
|
|
|
if( options.animate && movedNodes.length > 0 ){
|
|
simUpdatingPos = true;
|
|
|
|
movedNodes.rtrigger('position');
|
|
|
|
if( options.fit ){
|
|
cy.fit( options.padding );
|
|
}
|
|
|
|
lastDraw = +new Date();
|
|
simUpdatingPos = false;
|
|
}
|
|
|
|
|
|
if( !ready ){
|
|
ready = true;
|
|
layout.one('layoutready', options.ready);
|
|
layout.trigger({ type: 'layoutready', layout: layout });
|
|
}
|
|
}
|
|
|
|
};
|
|
sys.renderer = sysRenderer;
|
|
sys.screenSize( bb.w, bb.h );
|
|
sys.screenPadding( options.padding, options.padding, options.padding, options.padding );
|
|
sys.screenStep( options.stepSize );
|
|
|
|
function calculateValueForElement(element, value){
|
|
if( value == null ){
|
|
return undefined;
|
|
} else if( typeof value == typeof function(){} ){
|
|
return value.apply(element, [element._private.data, {
|
|
nodes: nodes.length,
|
|
edges: edges.length,
|
|
element: element
|
|
}]);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
var grabHandler;
|
|
nodes.on('grab free position', grabHandler = function(e){
|
|
if( simUpdatingPos ){ return; }
|
|
|
|
var pos = this.position();
|
|
var apos = sys.fromScreen( pos );
|
|
if( !apos ){ return; }
|
|
|
|
var p = arbor.Point(apos.x, apos.y);
|
|
var padding = options.padding;
|
|
|
|
if(
|
|
bb.x1 + padding <= pos.x && pos.x <= bb.x2 - padding &&
|
|
bb.y1 + padding <= pos.y && pos.y <= bb.y2 - padding
|
|
){
|
|
this.scratch().arbor.p = p;
|
|
}
|
|
|
|
switch( e.type ){
|
|
case 'grab':
|
|
this.scratch().arbor.fixed = true;
|
|
break;
|
|
case 'free':
|
|
this.scratch().arbor.fixed = false;
|
|
//this.scratch().arbor.tempMass = 1000;
|
|
break;
|
|
}
|
|
});
|
|
|
|
var lockHandler;
|
|
nodes.on('lock unlock', lockHandler = function(e){
|
|
node.scratch().arbor.fixed = node.locked();
|
|
});
|
|
|
|
var removeHandler;
|
|
eles.on('remove', removeHandler = function(e){ return; // TODO enable when layout add/remove api added
|
|
// var ele = this;
|
|
// var arborEle = ele.scratch().arbor;
|
|
|
|
// if( !arborEle ){ return; }
|
|
|
|
// if( ele.isNode() ){
|
|
// sys.pruneNode( arborEle );
|
|
// } else {
|
|
// sys.pruneEdge( arborEle );
|
|
// }
|
|
});
|
|
|
|
var addHandler;
|
|
cy.on('add', '*', addHandler = function(){ return; // TODO enable when layout add/remove api added
|
|
// var ele = this;
|
|
|
|
// if( ele.isNode() ){
|
|
// addNode( ele );
|
|
// } else {
|
|
// addEdge( ele );
|
|
// }
|
|
});
|
|
|
|
var resizeHandler;
|
|
cy.on('resize', resizeHandler = function(){
|
|
if( options.boundingBox == null && layout._private.system != null ){
|
|
var w = cy.width();
|
|
var h = cy.height();
|
|
|
|
sys.screenSize( w, h );
|
|
}
|
|
});
|
|
|
|
function addNode( node ){
|
|
if( node.isFullAutoParent() ){ return; } // they don't exist in the sim
|
|
|
|
var id = node._private.data.id;
|
|
var mass = calculateValueForElement(node, options.nodeMass);
|
|
var locked = node._private.locked;
|
|
var nPos = node.position();
|
|
|
|
var pos = sys.fromScreen({
|
|
x: nPos.x,
|
|
y: nPos.y
|
|
});
|
|
|
|
node.scratch().arbor = sys.addNode(id, {
|
|
element: node,
|
|
mass: mass,
|
|
fixed: locked,
|
|
x: locked && pos ? pos.x : undefined,
|
|
y: locked && pos ? pos.y : undefined
|
|
});
|
|
}
|
|
|
|
function addEdge( edge ){
|
|
var src = edge.source().id();
|
|
var tgt = edge.target().id();
|
|
var length = calculateValueForElement(edge, options.edgeLength);
|
|
|
|
edge.scratch().arbor = sys.addEdge(src, tgt, {
|
|
length: length
|
|
});
|
|
}
|
|
|
|
nodes.each(function(i, node){
|
|
addNode( node );
|
|
});
|
|
|
|
edges.each(function(i, edge){
|
|
addEdge( edge );
|
|
});
|
|
|
|
var grabbableNodes = nodes.filter(":grabbable");
|
|
// disable grabbing if so set
|
|
if( options.ungrabifyWhileSimulating ){
|
|
grabbableNodes.ungrabify();
|
|
}
|
|
|
|
var doneHandler = layout._private.doneHandler = function(){
|
|
layout._private.doneHandler = null;
|
|
|
|
if( !options.animate ){
|
|
if( options.fit ){
|
|
cy.reset();
|
|
}
|
|
|
|
nodes.rtrigger('position');
|
|
}
|
|
|
|
// unbind handlers
|
|
nodes.off('grab free position', grabHandler);
|
|
nodes.off('lock unlock', lockHandler);
|
|
eles.off('remove', removeHandler);
|
|
cy.off('add', '*', addHandler);
|
|
cy.off('resize', resizeHandler);
|
|
|
|
// enable back grabbing if so set
|
|
if( options.ungrabifyWhileSimulating ){
|
|
grabbableNodes.grabify();
|
|
}
|
|
|
|
layout.one('layoutstop', options.stop);
|
|
layout.trigger({ type: 'layoutstop', layout: layout });
|
|
};
|
|
|
|
sys.start();
|
|
if( !options.infinite && options.maxSimulationTime != null && options.maxSimulationTime > 0 && options.maxSimulationTime !== Infinity ){
|
|
setTimeout(function(){
|
|
layout.stop();
|
|
}, options.maxSimulationTime);
|
|
}
|
|
|
|
}); // require
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
|
|
ArborLayout.prototype.stop = function(){
|
|
if( this._private.system != null ){
|
|
this._private.system.stop();
|
|
}
|
|
|
|
if( this._private.doneHandler ){
|
|
this._private.doneHandler();
|
|
}
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
$$('layout', 'arbor', ArborLayout);
|
|
|
|
|
|
})(cytoscape);
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var defaults = {
|
|
fit: true, // whether to fit the viewport to the graph
|
|
directed: false, // whether the tree is directed downwards (or edges can point in any direction if false)
|
|
padding: 30, // padding on fit
|
|
circle: false, // put depths in concentric circles if true, put depths top down if false
|
|
spacingFactor: 1.75, // positive spacing factor, larger => more space between nodes (N.B. n/a if causes overlap)
|
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
|
|
roots: undefined, // the roots of the trees
|
|
maximalAdjustments: 0, // how many times to try to position the nodes in a maximal way (i.e. no backtracking)
|
|
animate: false, // whether to transition the node positions
|
|
animationDuration: 500, // duration of animation in ms if enabled
|
|
ready: undefined, // callback on layoutready
|
|
stop: undefined // callback on layoutstop
|
|
};
|
|
|
|
function BreadthFirstLayout( options ){
|
|
this.options = $$.util.extend({}, defaults, options);
|
|
}
|
|
|
|
BreadthFirstLayout.prototype.run = function(){
|
|
var params = this.options;
|
|
var options = params;
|
|
|
|
var cy = params.cy;
|
|
var eles = options.eles;
|
|
var nodes = eles.nodes().not(':parent');
|
|
var graph = eles;
|
|
|
|
var bb = $$.util.makeBoundingBox( options.boundingBox ? options.boundingBox : {
|
|
x1: 0, y1: 0, w: cy.width(), h: cy.height()
|
|
} );
|
|
|
|
var roots;
|
|
if( $$.is.elementOrCollection(options.roots) ){
|
|
roots = options.roots;
|
|
} else if( $$.is.array(options.roots) ){
|
|
var rootsArray = [];
|
|
|
|
for( var i = 0; i < options.roots.length; i++ ){
|
|
var id = options.roots[i];
|
|
var ele = cy.getElementById( id );
|
|
rootsArray.push( ele );
|
|
}
|
|
|
|
roots = new $$.Collection( cy, rootsArray );
|
|
} else if( $$.is.string(options.roots) ){
|
|
roots = cy.$( options.roots );
|
|
|
|
} else {
|
|
if( options.directed ){
|
|
roots = nodes.roots();
|
|
} else {
|
|
var components = [];
|
|
var unhandledNodes = nodes;
|
|
|
|
while( unhandledNodes.length > 0 ){
|
|
var currComp = cy.collection();
|
|
|
|
eles.bfs({
|
|
roots: unhandledNodes[0],
|
|
visit: function(i, depth, node, edge, pNode){
|
|
currComp = currComp.add( node );
|
|
},
|
|
directed: false
|
|
});
|
|
|
|
unhandledNodes = unhandledNodes.not( currComp );
|
|
components.push( currComp );
|
|
}
|
|
|
|
roots = cy.collection();
|
|
for( var i = 0; i < components.length; i++ ){
|
|
var comp = components[i];
|
|
var maxDegree = comp.maxDegree( false );
|
|
var compRoots = comp.filter(function(){
|
|
return this.degree(false) === maxDegree;
|
|
});
|
|
|
|
roots = roots.add( compRoots );
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
var depths = [];
|
|
var foundByBfs = {};
|
|
var id2depth = {};
|
|
var prevNode = {};
|
|
var prevEdge = {};
|
|
var successors = {};
|
|
|
|
// find the depths of the nodes
|
|
graph.bfs({
|
|
roots: roots,
|
|
directed: options.directed,
|
|
visit: function(i, depth, node, edge, pNode){
|
|
var ele = this[0];
|
|
var id = ele.id();
|
|
|
|
if( !depths[depth] ){
|
|
depths[depth] = [];
|
|
}
|
|
|
|
depths[depth].push( ele );
|
|
foundByBfs[ id ] = true;
|
|
id2depth[ id ] = depth;
|
|
prevNode[ id ] = pNode;
|
|
prevEdge[ id ] = edge;
|
|
|
|
if( pNode ){
|
|
var prevId = pNode.id();
|
|
var succ = successors[ prevId ] = successors[ prevId ] || [];
|
|
|
|
succ.push( node );
|
|
}
|
|
}
|
|
});
|
|
|
|
// check for nodes not found by bfs
|
|
var orphanNodes = [];
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var ele = nodes[i];
|
|
|
|
if( foundByBfs[ ele.id() ] ){
|
|
continue;
|
|
} else {
|
|
orphanNodes.push( ele );
|
|
}
|
|
}
|
|
|
|
// assign orphan nodes a depth from their neighborhood
|
|
var maxChecks = orphanNodes.length * 3;
|
|
var checks = 0;
|
|
while( orphanNodes.length !== 0 && checks < maxChecks ){
|
|
var node = orphanNodes.shift();
|
|
var neighbors = node.neighborhood().nodes();
|
|
var assignedDepth = false;
|
|
|
|
for( var i = 0; i < neighbors.length; i++ ){
|
|
var depth = id2depth[ neighbors[i].id() ];
|
|
|
|
if( depth !== undefined ){
|
|
depths[depth].push( node );
|
|
assignedDepth = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !assignedDepth ){
|
|
orphanNodes.push( node );
|
|
}
|
|
|
|
checks++;
|
|
}
|
|
|
|
// assign orphan nodes that are still left to the depth of their subgraph
|
|
while( orphanNodes.length !== 0 ){
|
|
var node = orphanNodes.shift();
|
|
//var subgraph = graph.bfs( node ).path;
|
|
var assignedDepth = false;
|
|
|
|
// for( var i = 0; i < subgraph.length; i++ ){
|
|
// var depth = id2depth[ subgraph[i].id() ];
|
|
|
|
// if( depth !== undefined ){
|
|
// depths[depth].push( node );
|
|
// assignedDepth = true;
|
|
// break;
|
|
// }
|
|
// }
|
|
|
|
if( !assignedDepth ){ // worst case if the graph really isn't tree friendly, then just dump it in 0
|
|
if( depths.length === 0 ){
|
|
depths.push([]);
|
|
}
|
|
|
|
depths[0].push( node );
|
|
}
|
|
}
|
|
|
|
// assign the nodes a depth and index
|
|
var assignDepthsToEles = function(){
|
|
for( var i = 0; i < depths.length; i++ ){
|
|
var eles = depths[i];
|
|
|
|
for( var j = 0; j < eles.length; j++ ){
|
|
var ele = eles[j];
|
|
|
|
ele._private.scratch.breadthfirst = {
|
|
depth: i,
|
|
index: j
|
|
};
|
|
}
|
|
}
|
|
};
|
|
assignDepthsToEles();
|
|
|
|
|
|
var intersectsDepth = function( node ){ // returns true if has edges pointing in from a higher depth
|
|
var edges = node.connectedEdges(function(){
|
|
return this.data('target') === node.id();
|
|
});
|
|
var thisInfo = node._private.scratch.breadthfirst;
|
|
var highestDepthOfOther = 0;
|
|
var highestOther;
|
|
for( var i = 0; i < edges.length; i++ ){
|
|
var edge = edges[i];
|
|
var otherNode = edge.source()[0];
|
|
var otherInfo = otherNode._private.scratch.breadthfirst;
|
|
|
|
if( thisInfo.depth <= otherInfo.depth && highestDepthOfOther < otherInfo.depth ){
|
|
highestDepthOfOther = otherInfo.depth;
|
|
highestOther = otherNode;
|
|
}
|
|
}
|
|
|
|
return highestOther;
|
|
};
|
|
|
|
// make maximal if so set by adjusting depths
|
|
for( var adj = 0; adj < options.maximalAdjustments; adj++ ){
|
|
|
|
var nDepths = depths.length;
|
|
var elesToMove = [];
|
|
for( var i = 0; i < nDepths; i++ ){
|
|
var depth = depths[i];
|
|
|
|
var nDepth = depth.length;
|
|
for( var j = 0; j < nDepth; j++ ){
|
|
var ele = depth[j];
|
|
var info = ele._private.scratch.breadthfirst;
|
|
var intEle = intersectsDepth(ele);
|
|
|
|
if( intEle ){
|
|
info.intEle = intEle;
|
|
elesToMove.push( ele );
|
|
}
|
|
}
|
|
}
|
|
|
|
for( var i = 0; i < elesToMove.length; i++ ){
|
|
var ele = elesToMove[i];
|
|
var info = ele._private.scratch.breadthfirst;
|
|
var intEle = info.intEle;
|
|
var intInfo = intEle._private.scratch.breadthfirst;
|
|
|
|
depths[ info.depth ].splice( info.index, 1 ); // remove from old depth & index
|
|
|
|
// add to end of new depth
|
|
var newDepth = intInfo.depth + 1;
|
|
while( newDepth > depths.length - 1 ){
|
|
depths.push([]);
|
|
}
|
|
depths[ newDepth ].push( ele );
|
|
|
|
info.depth = newDepth;
|
|
info.index = depths[newDepth].length - 1;
|
|
}
|
|
|
|
assignDepthsToEles();
|
|
}
|
|
|
|
// find min distance we need to leave between nodes
|
|
var minDistance = 0;
|
|
if( options.avoidOverlap ){
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var w = nodes[i].outerWidth();
|
|
var h = nodes[i].outerHeight();
|
|
|
|
minDistance = Math.max(minDistance, w, h);
|
|
}
|
|
minDistance *= options.spacingFactor; // just to have some nice spacing
|
|
}
|
|
|
|
// get the weighted percent for an element based on its connectivity to other levels
|
|
var cachedWeightedPercent = {};
|
|
var getWeightedPercent = function( ele ){
|
|
if( cachedWeightedPercent[ ele.id() ] ){
|
|
return cachedWeightedPercent[ ele.id() ];
|
|
}
|
|
|
|
var eleDepth = ele._private.scratch.breadthfirst.depth;
|
|
var neighbors = ele.neighborhood().nodes().not(':parent');
|
|
var percent = 0;
|
|
var samples = 0;
|
|
|
|
for( var i = 0; i < neighbors.length; i++ ){
|
|
var neighbor = neighbors[i];
|
|
var bf = neighbor._private.scratch.breadthfirst;
|
|
var index = bf.index;
|
|
var depth = bf.depth;
|
|
var nDepth = depths[depth].length;
|
|
|
|
if( eleDepth > depth || eleDepth === 0 ){ // only get influenced by elements above
|
|
percent += index / nDepth;
|
|
samples++;
|
|
}
|
|
}
|
|
|
|
samples = Math.max(1, samples);
|
|
percent = percent / samples;
|
|
|
|
if( samples === 0 ){ // so lone nodes have a "don't care" state in sorting
|
|
percent = undefined;
|
|
}
|
|
|
|
cachedWeightedPercent[ ele.id() ] = percent;
|
|
return percent;
|
|
};
|
|
|
|
|
|
// rearrange the indices in each depth level based on connectivity
|
|
|
|
var sortFn = function(a, b){
|
|
var apct = getWeightedPercent( a );
|
|
var bpct = getWeightedPercent( b );
|
|
|
|
return apct - bpct;
|
|
};
|
|
|
|
for( var times = 0; times < 3; times++ ){ // do it a few times b/c the depths are dynamic and we want a more stable result
|
|
|
|
for( var i = 0; i < depths.length; i++ ){
|
|
depths[i] = depths[i].sort( sortFn );
|
|
}
|
|
assignDepthsToEles(); // and update
|
|
|
|
}
|
|
|
|
var biggestDepthSize = 0;
|
|
for( var i = 0; i < depths.length; i++ ){
|
|
biggestDepthSize = Math.max( depths[i].length, biggestDepthSize );
|
|
}
|
|
|
|
var center = {
|
|
x: bb.x1 + bb.w/2,
|
|
y: bb.x1 + bb.h/2
|
|
};
|
|
|
|
var getPosition = function( ele, isBottomDepth ){
|
|
var info = ele._private.scratch.breadthfirst;
|
|
var depth = info.depth;
|
|
var index = info.index;
|
|
var depthSize = depths[depth].length;
|
|
|
|
var distanceX = Math.max( bb.w / (depthSize + 1), minDistance );
|
|
var distanceY = Math.max( bb.h / (depths.length + 1), minDistance );
|
|
var radiusStepSize = Math.min( bb.w / 2 / depths.length, bb.h / 2 / depths.length );
|
|
radiusStepSize = Math.max( radiusStepSize, minDistance );
|
|
|
|
if( !options.circle ){
|
|
|
|
var epos = {
|
|
x: center.x + (index + 1 - (depthSize + 1)/2) * distanceX,
|
|
y: (depth + 1) * distanceY
|
|
};
|
|
|
|
if( isBottomDepth ){
|
|
return epos;
|
|
}
|
|
|
|
// var succs = successors[ ele.id() ];
|
|
// if( succs ){
|
|
// epos.x = 0;
|
|
//
|
|
// for( var i = 0 ; i < succs.length; i++ ){
|
|
// var spos = pos[ succs[i].id() ];
|
|
//
|
|
// epos.x += spos.x;
|
|
// }
|
|
//
|
|
// epos.x /= succs.length;
|
|
// } else {
|
|
// //debugger;
|
|
// }
|
|
|
|
return epos;
|
|
|
|
} else {
|
|
if( options.circle ){
|
|
var radius = radiusStepSize * depth + radiusStepSize - (depths.length > 0 && depths[0].length <= 3 ? radiusStepSize/2 : 0);
|
|
var theta = 2 * Math.PI / depths[depth].length * index;
|
|
|
|
if( depth === 0 && depths[0].length === 1 ){
|
|
radius = 1;
|
|
}
|
|
|
|
return {
|
|
x: center.x + radius * Math.cos(theta),
|
|
y: center.y + radius * Math.sin(theta)
|
|
};
|
|
|
|
} else {
|
|
return {
|
|
x: center.x + (index + 1 - (depthSize + 1)/2) * distanceX,
|
|
y: (depth + 1) * distanceY
|
|
};
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
// get positions in reverse depth order
|
|
var pos = {};
|
|
for( var i = depths.length - 1; i >=0; i-- ){
|
|
var depth = depths[i];
|
|
|
|
for( var j = 0; j < depth.length; j++ ){
|
|
var node = depth[j];
|
|
|
|
pos[ node.id() ] = getPosition( node, i === depths.length - 1 );
|
|
}
|
|
}
|
|
|
|
nodes.layoutPositions(this, options, function(){
|
|
return pos[ this.id() ];
|
|
});
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
$$('layout', 'breadthfirst', BreadthFirstLayout);
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var defaults = {
|
|
fit: true, // whether to fit the viewport to the graph
|
|
padding: 30, // the padding on fit
|
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
avoidOverlap: true, // prevents node overlap, may overflow boundingBox and radius if not enough space
|
|
radius: undefined, // the radius of the circle
|
|
startAngle: 3/2 * Math.PI, // the position of the first node
|
|
counterclockwise: false, // whether the layout should go counterclockwise (true) or clockwise (false)
|
|
sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
|
|
animate: false, // whether to transition the node positions
|
|
animationDuration: 500, // duration of animation in ms if enabled
|
|
ready: undefined, // callback on layoutready
|
|
stop: undefined // callback on layoutstop
|
|
};
|
|
|
|
function CircleLayout( options ){
|
|
this.options = $$.util.extend({}, defaults, options);
|
|
}
|
|
|
|
CircleLayout.prototype.run = function(){
|
|
var params = this.options;
|
|
var options = params;
|
|
|
|
var cy = params.cy;
|
|
var eles = options.eles;
|
|
|
|
var nodes = eles.nodes().not(':parent');
|
|
|
|
if( options.sort ){
|
|
nodes = nodes.sort( options.sort );
|
|
}
|
|
|
|
var bb = $$.util.makeBoundingBox( options.boundingBox ? options.boundingBox : {
|
|
x1: 0, y1: 0, w: cy.width(), h: cy.height()
|
|
} );
|
|
|
|
var center = {
|
|
x: bb.x1 + bb.w/2,
|
|
y: bb.y1 + bb.h/2
|
|
};
|
|
|
|
var theta = options.startAngle;
|
|
var dTheta = 2 * Math.PI / nodes.length;
|
|
var r;
|
|
|
|
var minDistance = 0;
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var w = nodes[i].outerWidth();
|
|
var h = nodes[i].outerHeight();
|
|
|
|
minDistance = Math.max(minDistance, w, h);
|
|
}
|
|
|
|
if( $$.is.number(options.radius) ){
|
|
r = options.radius;
|
|
} else if( nodes.length <= 1 ){
|
|
r = 0;
|
|
} else {
|
|
r = Math.min( bb.h, bb.w )/2 - minDistance;
|
|
}
|
|
|
|
// calculate the radius
|
|
if( nodes.length > 1 && options.avoidOverlap ){ // but only if more than one node (can't overlap)
|
|
minDistance *= 1.75; // just to have some nice spacing
|
|
|
|
var dTheta = 2 * Math.PI / nodes.length;
|
|
var dcos = Math.cos(dTheta) - Math.cos(0);
|
|
var dsin = Math.sin(dTheta) - Math.sin(0);
|
|
var rMin = Math.sqrt( minDistance * minDistance / ( dcos*dcos + dsin*dsin ) ); // s.t. no nodes overlapping
|
|
r = Math.max( rMin, r );
|
|
}
|
|
|
|
var getPos = function( i, ele ){
|
|
var rx = r * Math.cos( theta );
|
|
var ry = r * Math.sin( theta );
|
|
var pos = {
|
|
x: center.x + rx,
|
|
y: center.y + ry
|
|
};
|
|
|
|
theta = options.counterclockwise ? theta - dTheta : theta + dTheta;
|
|
return pos;
|
|
};
|
|
|
|
nodes.layoutPositions( this, options, getPos );
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
$$('layout', 'circle', CircleLayout);
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// default layout options
|
|
var defaults = {
|
|
animate: true, // whether to show the layout as it's running
|
|
refresh: 1, // number of ticks per frame; higher is faster but more jerky
|
|
maxSimulationTime: 4000, // max length in ms to run the layout
|
|
ungrabifyWhileSimulating: false, // so you can't drag nodes during layout
|
|
fit: true, // on every layout reposition of nodes, fit the viewport
|
|
padding: 30, // padding around the simulation
|
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
|
|
// layout event callbacks
|
|
ready: function(){}, // on layoutready
|
|
stop: function(){}, // on layoutstop
|
|
|
|
// positioning options
|
|
randomize: false, // use random node positions at beginning of layout
|
|
avoidOverlap: true, // if true, prevents overlap of node bounding boxes
|
|
handleDisconnected: true, // if true, avoids disconnected components from overlapping
|
|
nodeSpacing: function( node ){ return 10; }, // extra spacing around nodes
|
|
flow: undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 }
|
|
alignment: undefined, // relative alignment constraints on nodes, e.g. function( node ){ return { x: 0, y: 1 } }
|
|
|
|
// different methods of specifying edge length
|
|
// each can be a constant numerical value or a function like `function( edge ){ return 2; }`
|
|
edgeLength: undefined, // sets edge length directly in simulation
|
|
edgeSymDiffLength: undefined, // symmetric diff edge length in simulation
|
|
edgeJaccardLength: undefined, // jaccard edge length in simulation
|
|
|
|
// iterations of cola algorithm; uses default values on undefined
|
|
unconstrIter: undefined, // unconstrained initial layout iterations
|
|
userConstIter: undefined, // initial layout iterations with user-specified constraints
|
|
allConstIter: undefined, // initial layout iterations with all constraints including non-overlap
|
|
|
|
// infinite layout options
|
|
infinite: false // overrides all other options for a forces-all-the-time mode
|
|
};
|
|
|
|
// constructor
|
|
// options : object containing layout options
|
|
function ColaLayout( options ){
|
|
this.options = $$.util.extend(true, {}, defaults, options);
|
|
}
|
|
|
|
// runs the layout
|
|
ColaLayout.prototype.run = function(){
|
|
var layout = this;
|
|
var options = this.options;
|
|
|
|
layout.manuallyStopped = false;
|
|
|
|
$$.util.require('cola', function(cola){
|
|
|
|
var cy = options.cy; // cy is automatically populated for us in the constructor
|
|
var eles = options.eles;
|
|
var nodes = eles.nodes();
|
|
var edges = eles.edges();
|
|
var ready = false;
|
|
|
|
var bb = $$.util.makeBoundingBox( options.boundingBox ? options.boundingBox : {
|
|
x1: 0, y1: 0, w: cy.width(), h: cy.height()
|
|
} );
|
|
|
|
var getOptVal = function( val, ele ){
|
|
if( $$.is.fn(val) ){
|
|
var fn = val;
|
|
return fn.apply( ele, [ ele ] );
|
|
} else {
|
|
return val;
|
|
}
|
|
};
|
|
|
|
var updateNodePositions = function(){
|
|
var x = { min: Infinity, max: -Infinity };
|
|
var y = { min: Infinity, max: -Infinity };
|
|
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var node = nodes[i];
|
|
var scratch = node._private.scratch.cola;
|
|
|
|
x.min = Math.min( x.min, scratch.x || 0 );
|
|
x.max = Math.max( x.max, scratch.x || 0 );
|
|
|
|
y.min = Math.min( y.min, scratch.y || 0 );
|
|
y.max = Math.max( y.max, scratch.y || 0 );
|
|
|
|
// update node dims
|
|
if( !scratch.updatedDims ){
|
|
var nbb = node.boundingBox();
|
|
var padding = getOptVal( options.nodeSpacing, node );
|
|
|
|
scratch.width = nbb.w + 2*padding;
|
|
scratch.height = nbb.h + 2*padding;
|
|
}
|
|
}
|
|
|
|
nodes.positions(function(i, node){
|
|
var scratch = node._private.scratch.cola;
|
|
var retPos;
|
|
|
|
if( !node.grabbed() && !node.isParent() ){
|
|
retPos = {
|
|
x: bb.x1 + scratch.x - x.min,
|
|
y: bb.y1 + scratch.y - y.min
|
|
};
|
|
|
|
if( !$$.is.number(retPos.x) || !$$.is.number(retPos.y) ){
|
|
retPos = undefined;
|
|
}
|
|
}
|
|
|
|
return retPos;
|
|
});
|
|
|
|
nodes.updateCompoundBounds(); // because the way this layout sets positions is buggy for some reason; ref #878
|
|
|
|
if( !ready ){
|
|
onReady();
|
|
ready = true;
|
|
}
|
|
|
|
if( options.fit ){
|
|
cy.fit( options.padding );
|
|
}
|
|
};
|
|
|
|
var onDone = function(){
|
|
if( options.ungrabifyWhileSimulating ){
|
|
grabbableNodes.grabify();
|
|
}
|
|
|
|
nodes.off('grab free position', grabHandler);
|
|
nodes.off('lock unlock', lockHandler);
|
|
|
|
// trigger layoutstop when the layout stops (e.g. finishes)
|
|
layout.one('layoutstop', options.stop);
|
|
layout.trigger({ type: 'layoutstop', layout: layout });
|
|
};
|
|
|
|
var onReady = function(){
|
|
// trigger layoutready when each node has had its position set at least once
|
|
layout.one('layoutready', options.ready);
|
|
layout.trigger({ type: 'layoutready', layout: layout });
|
|
};
|
|
|
|
var ticksPerFrame = options.refresh;
|
|
var tickSkip = 1; // frames until a tick; used to slow down sim for debugging
|
|
|
|
if( options.refresh < 0 ){
|
|
tickSkip = Math.abs( options.refresh );
|
|
ticksPerFrame = 1;
|
|
} else {
|
|
ticksPerFrame = Math.max( 1, ticksPerFrame ); // at least 1
|
|
}
|
|
|
|
var adaptor = layout.adaptor = cola.adaptor({
|
|
trigger: function( e ){ // on sim event
|
|
var TICK = cola.EventType ? cola.EventType.tick : null;
|
|
var END = cola.EventType ? cola.EventType.end : null;
|
|
|
|
switch( e.type ){
|
|
case 'tick':
|
|
case TICK:
|
|
if( options.animate ){
|
|
updateNodePositions();
|
|
}
|
|
break;
|
|
|
|
case 'end':
|
|
case END:
|
|
updateNodePositions();
|
|
if( !options.infinite ){ onDone(); }
|
|
break;
|
|
}
|
|
},
|
|
|
|
kick: function(){ // kick off the simulation
|
|
var skip = 0;
|
|
|
|
var inftick = function(){
|
|
if( layout.manuallyStopped ){
|
|
onDone();
|
|
|
|
return true;
|
|
}
|
|
|
|
var ret = adaptor.tick();
|
|
|
|
if( ret && options.infinite ){ // resume layout if done
|
|
adaptor.resume(); // resume => new kick
|
|
}
|
|
|
|
return ret; // allow regular finish b/c of new kick
|
|
};
|
|
|
|
var multitick = function(){ // multiple ticks in a row
|
|
var ret;
|
|
|
|
// skip ticks to slow down layout for debugging
|
|
// var thisSkip = skip;
|
|
// skip = (skip + 1) % tickSkip;
|
|
// if( thisSkip !== 0 ){
|
|
// return false;
|
|
// }
|
|
|
|
for( var i = 0; i < ticksPerFrame && !ret; i++ ){
|
|
ret = ret || inftick(); // pick up true ret vals => sim done
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
if( options.animate ){
|
|
var frame = function(){
|
|
if( multitick() ){ return; }
|
|
|
|
$$.util.requestAnimationFrame( frame );
|
|
};
|
|
|
|
$$.util.requestAnimationFrame( frame );
|
|
} else {
|
|
while( !inftick() ){}
|
|
}
|
|
},
|
|
|
|
on: function( type, listener ){}, // dummy; not needed
|
|
|
|
drag: function(){} // not needed for our case
|
|
});
|
|
layout.adaptor = adaptor;
|
|
|
|
// if set no grabbing during layout
|
|
var grabbableNodes = nodes.filter(':grabbable');
|
|
if( options.ungrabifyWhileSimulating ){
|
|
grabbableNodes.ungrabify();
|
|
}
|
|
|
|
// handle node dragging
|
|
var grabHandler;
|
|
nodes.on('grab free position', grabHandler = function(e){
|
|
var node = this;
|
|
var scrCola = node._private.scratch.cola;
|
|
var pos = node._private.position;
|
|
|
|
if( node.grabbed() ){
|
|
scrCola.x = pos.x - bb.x1;
|
|
scrCola.y = pos.y - bb.y1;
|
|
|
|
adaptor.dragstart( scrCola );
|
|
} else if( $$.is.number(scrCola.x) && $$.is.number(scrCola.y) ){
|
|
pos.x = scrCola.x + bb.x1;
|
|
pos.y = scrCola.y + bb.y1;
|
|
}
|
|
|
|
switch( e.type ){
|
|
case 'grab':
|
|
adaptor.dragstart( scrCola );
|
|
adaptor.resume();
|
|
break;
|
|
case 'free':
|
|
adaptor.dragend( scrCola );
|
|
break;
|
|
}
|
|
|
|
});
|
|
|
|
var lockHandler;
|
|
nodes.on('lock unlock', lockHandler = function(e){
|
|
var node = this;
|
|
var scrCola = node._private.scratch.cola;
|
|
|
|
if( node.locked() ){
|
|
adaptor.dragstart( scrCola );
|
|
} else {
|
|
adaptor.dragend( scrCola );
|
|
}
|
|
});
|
|
|
|
var nonparentNodes = nodes.stdFilter(function( node ){
|
|
return !node.isParent();
|
|
});
|
|
|
|
// add nodes to cola
|
|
adaptor.nodes( nonparentNodes.map(function( node, i ){
|
|
var padding = getOptVal( options.nodeSpacing, node );
|
|
var pos = node.position();
|
|
var nbb = node.boundingBox();
|
|
|
|
var struct = node._private.scratch.cola = {
|
|
x: options.randomize || pos.x === undefined ? Math.round( Math.random() * bb.w ) : pos.x,
|
|
y: options.randomize || pos.y === undefined ? Math.round( Math.random() * bb.h ) : pos.y,
|
|
width: nbb.w + 2*padding,
|
|
height: nbb.h + 2*padding,
|
|
index: i
|
|
};
|
|
|
|
return struct;
|
|
}) );
|
|
|
|
if( options.alignment ){ // then set alignment constraints
|
|
|
|
var offsetsX = [];
|
|
var offsetsY = [];
|
|
|
|
nonparentNodes.forEach(function( node ){
|
|
var align = getOptVal( options.alignment, node );
|
|
var scrCola = node._private.scratch.cola;
|
|
var index = scrCola.index;
|
|
|
|
if( !align ){ return; }
|
|
|
|
if( align.x != null ){
|
|
offsetsX.push({
|
|
node: index,
|
|
offset: align.x
|
|
});
|
|
}
|
|
|
|
if( align.y != null ){
|
|
offsetsY.push({
|
|
node: index,
|
|
offset: align.y
|
|
});
|
|
}
|
|
});
|
|
|
|
// add alignment constraints on nodes
|
|
var constraints = [];
|
|
|
|
if( offsetsX.length > 0 ){
|
|
constraints.push({
|
|
type: 'alignment',
|
|
axis: 'x',
|
|
offsets: offsetsX
|
|
});
|
|
}
|
|
|
|
if( offsetsY.length > 0 ){
|
|
constraints.push({
|
|
type: 'alignment',
|
|
axis: 'y',
|
|
offsets: offsetsY
|
|
});
|
|
}
|
|
|
|
adaptor.constraints( constraints );
|
|
|
|
}
|
|
|
|
// add compound nodes to cola
|
|
adaptor.groups( nodes.stdFilter(function( node ){
|
|
return node.isParent();
|
|
}).map(function( node, i ){ // add basic group incl leaf nodes
|
|
var style = node._private.style;
|
|
|
|
var optPadding = getOptVal( options.nodeSpacing, node );
|
|
|
|
var pleft = style['padding-left'].pxValue + optPadding;
|
|
var pright = style['padding-right'].pxValue + optPadding;
|
|
var ptop = style['padding-top'].pxValue + optPadding;
|
|
var pbottom = style['padding-bottom'].pxValue + optPadding;
|
|
|
|
node._private.scratch.cola = {
|
|
index: i,
|
|
|
|
padding: Math.max( pleft, pright, ptop, pbottom ),
|
|
|
|
leaves: node.descendants().stdFilter(function( child ){
|
|
return !child.isParent();
|
|
}).map(function( child ){
|
|
return child[0]._private.scratch.cola.index;
|
|
})
|
|
};
|
|
|
|
return node;
|
|
}).map(function( node ){ // add subgroups
|
|
node._private.scratch.cola.groups = node.descendants().stdFilter(function( child ){
|
|
return child.isParent();
|
|
}).map(function( child ){
|
|
return child._private.scratch.cola.index;
|
|
});
|
|
|
|
return node._private.scratch.cola;
|
|
}) );
|
|
|
|
// get the edge length setting mechanism
|
|
var length;
|
|
var lengthFnName;
|
|
if( options.edgeLength != null ){
|
|
length = options.edgeLength;
|
|
lengthFnName = 'linkDistance';
|
|
} else if( options.edgeSymDiffLength != null ){
|
|
length = options.edgeSymDiffLength;
|
|
lengthFnName = 'symmetricDiffLinkLengths';
|
|
} else if( options.edgeJaccardLength != null ){
|
|
length = options.edgeJaccardLength;
|
|
lengthFnName = 'jaccardLinkLengths';
|
|
} else {
|
|
length = 100;
|
|
lengthFnName = 'linkDistance';
|
|
}
|
|
|
|
var lengthGetter = function( link ){
|
|
return link.calcLength;
|
|
};
|
|
|
|
// add the edges to cola
|
|
adaptor.links( edges.stdFilter(function( edge ){
|
|
return !edge.source().isParent() && !edge.target().isParent();
|
|
}).map(function( edge, i ){
|
|
var c = edge._private.scratch.cola = {
|
|
source: edge.source()[0]._private.scratch.cola.index,
|
|
target: edge.target()[0]._private.scratch.cola.index
|
|
};
|
|
|
|
if( length != null ){
|
|
c.calcLength = getOptVal( length, edge );
|
|
}
|
|
|
|
return c;
|
|
}) );
|
|
|
|
adaptor.size([ bb.w, bb.h ]);
|
|
|
|
if( length != null ){
|
|
adaptor[ lengthFnName ]( lengthGetter );
|
|
}
|
|
|
|
// set the flow of cola
|
|
if( options.flow ){
|
|
var flow;
|
|
var defAxis = 'y';
|
|
var defMinSep = 50;
|
|
|
|
if( $$.is.string(options.flow) ){
|
|
flow = {
|
|
axis: options.flow,
|
|
minSeparation: defMinSep
|
|
};
|
|
} else if( $$.is.number(options.flow) ){
|
|
flow = {
|
|
axis: defAxis,
|
|
minSeparation: options.flow
|
|
};
|
|
} else if( $$.is.plainObject(options.flow) ){
|
|
flow = options.flow;
|
|
|
|
flow.axis = flow.axis || defAxis;
|
|
flow.minSeparation = flow.minSeparation != null ? flow.minSeparation : defMinSep;
|
|
} else { // e.g. options.flow: true
|
|
flow = {
|
|
axis: defAxis,
|
|
minSeparation: defMinSep
|
|
};
|
|
}
|
|
|
|
adaptor.flowLayout( flow.axis , flow.minSeparation );
|
|
}
|
|
|
|
layout.trigger({ type: 'layoutstart', layout: layout });
|
|
|
|
adaptor
|
|
.avoidOverlaps( options.avoidOverlap )
|
|
.handleDisconnected( options.handleDisconnected )
|
|
.start( options.unconstrIter, options.userConstIter, options.allConstIter)
|
|
;
|
|
|
|
if( !options.infinite ){
|
|
setTimeout(function(){
|
|
if( !layout.manuallyStopped ){
|
|
adaptor.stop();
|
|
}
|
|
}, options.maxSimulationTime);
|
|
}
|
|
|
|
}); // require
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
// called on continuous layouts to stop them before they finish
|
|
ColaLayout.prototype.stop = function(){
|
|
if( this.adaptor ){
|
|
this.manuallyStopped = true;
|
|
this.adaptor.stop();
|
|
}
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
// register the layout
|
|
$$('layout', 'cola', ColaLayout);
|
|
|
|
})(cytoscape);
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var defaults = {
|
|
fit: true, // whether to fit the viewport to the graph
|
|
padding: 30, // the padding on fit
|
|
startAngle: 3/2 * Math.PI, // the position of the first node
|
|
counterclockwise: false, // whether the layout should go counterclockwise/anticlockwise (true) or clockwise (false)
|
|
minNodeSpacing: 10, // min spacing between outside of nodes (used for radius adjustment)
|
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
|
|
height: undefined, // height of layout area (overrides container height)
|
|
width: undefined, // width of layout area (overrides container width)
|
|
concentric: function(node){ // returns numeric value for each node, placing higher nodes in levels towards the centre
|
|
return node.degree();
|
|
},
|
|
levelWidth: function(nodes){ // the variation of concentric values in each level
|
|
return nodes.maxDegree() / 4;
|
|
},
|
|
animate: false, // whether to transition the node positions
|
|
animationDuration: 500, // duration of animation in ms if enabled
|
|
ready: undefined, // callback on layoutready
|
|
stop: undefined // callback on layoutstop
|
|
};
|
|
|
|
function ConcentricLayout( options ){
|
|
this.options = $$.util.extend({}, defaults, options);
|
|
}
|
|
|
|
ConcentricLayout.prototype.run = function(){
|
|
var params = this.options;
|
|
var options = params;
|
|
|
|
var cy = params.cy;
|
|
|
|
var eles = options.eles;
|
|
var nodes = eles.nodes().not(':parent');
|
|
|
|
var bb = $$.util.makeBoundingBox( options.boundingBox ? options.boundingBox : {
|
|
x1: 0, y1: 0, w: cy.width(), h: cy.height()
|
|
} );
|
|
|
|
var center = {
|
|
x: bb.x1 + bb.w/2,
|
|
y: bb.y1 + bb.h/2
|
|
};
|
|
|
|
var nodeValues = []; // { node, value }
|
|
var theta = options.startAngle;
|
|
var maxNodeSize = 0;
|
|
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var node = nodes[i];
|
|
var value;
|
|
|
|
// calculate the node value
|
|
value = options.concentric.apply(node, [ node ]);
|
|
nodeValues.push({
|
|
value: value,
|
|
node: node
|
|
});
|
|
|
|
// for style mapping
|
|
node._private.scratch.concentric = value;
|
|
}
|
|
|
|
// in case we used the `concentric` in style
|
|
nodes.updateStyle();
|
|
|
|
// calculate max size now based on potentially updated mappers
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var node = nodes[i];
|
|
|
|
maxNodeSize = Math.max( maxNodeSize, node.outerWidth(), node.outerHeight() );
|
|
}
|
|
|
|
// sort node values in descreasing order
|
|
nodeValues.sort(function(a, b){
|
|
return b.value - a.value;
|
|
});
|
|
|
|
var levelWidth = options.levelWidth( nodes );
|
|
|
|
// put the values into levels
|
|
var levels = [ [] ];
|
|
var currentLevel = levels[0];
|
|
for( var i = 0; i < nodeValues.length; i++ ){
|
|
var val = nodeValues[i];
|
|
|
|
if( currentLevel.length > 0 ){
|
|
var diff = Math.abs( currentLevel[0].value - val.value );
|
|
|
|
if( diff >= levelWidth ){
|
|
currentLevel = [];
|
|
levels.push( currentLevel );
|
|
}
|
|
}
|
|
|
|
currentLevel.push( val );
|
|
}
|
|
|
|
// create positions from levels
|
|
|
|
var pos = {}; // id => position
|
|
var r = 0;
|
|
var minDist = maxNodeSize + options.minNodeSpacing; // min dist between nodes
|
|
|
|
if( !options.avoidOverlap ){ // then strictly constrain to bb
|
|
var firstLvlHasMulti = levels.length > 0 && levels[0].length > 1;
|
|
var maxR = ( Math.min(bb.w, bb.h) / 2 - minDist );
|
|
var rStep = maxR / ( levels.length + firstLvlHasMulti ? 1 : 0 );
|
|
|
|
minDist = Math.min( minDist, rStep );
|
|
}
|
|
|
|
for( var i = 0; i < levels.length; i++ ){
|
|
var level = levels[i];
|
|
var dTheta = 2 * Math.PI / level.length;
|
|
|
|
// calculate the radius
|
|
if( level.length > 1 && options.avoidOverlap ){ // but only if more than one node (can't overlap)
|
|
var dcos = Math.cos(dTheta) - Math.cos(0);
|
|
var dsin = Math.sin(dTheta) - Math.sin(0);
|
|
var rMin = Math.sqrt( minDist * minDist / ( dcos*dcos + dsin*dsin ) ); // s.t. no nodes overlapping
|
|
r = Math.max( rMin, r );
|
|
}
|
|
|
|
for( var j = 0; j < level.length; j++ ){
|
|
var val = level[j];
|
|
var theta = options.startAngle + (options.counterclockwise ? -1 : 1) * dTheta * j;
|
|
|
|
var p = {
|
|
x: center.x + r * Math.cos(theta),
|
|
y: center.y + r * Math.sin(theta)
|
|
};
|
|
|
|
pos[ val.node.id() ] = p;
|
|
}
|
|
|
|
r += minDist;
|
|
|
|
}
|
|
|
|
// position the nodes
|
|
nodes.layoutPositions(this, options, function(){
|
|
var id = this.id();
|
|
|
|
return pos[id];
|
|
});
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
$$('layout', 'concentric', ConcentricLayout);
|
|
|
|
})( cytoscape );
|
|
|
|
/*
|
|
The CoSE layout was written by Gerardo Huck.
|
|
|
|
Modifications tracked on Github.
|
|
*/
|
|
|
|
;(function($$) { 'use strict';
|
|
|
|
var DEBUG;
|
|
|
|
/**
|
|
* @brief : default layout options
|
|
*/
|
|
var defaults = {
|
|
// Called on `layoutready`
|
|
ready : function() {},
|
|
|
|
// Called on `layoutstop`
|
|
stop : function() {},
|
|
|
|
// Whether to animate while running the layout
|
|
animate : true,
|
|
|
|
// Number of iterations between consecutive screen positions update (0 -> only updated on the end)
|
|
refresh : 4,
|
|
|
|
// Whether to fit the network view after when done
|
|
fit : true,
|
|
|
|
// Padding on fit
|
|
padding : 30,
|
|
|
|
// Constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
boundingBox : undefined,
|
|
|
|
// Whether to randomize node positions on the beginning
|
|
randomize : true,
|
|
|
|
// Whether to use the JS console to print debug messages
|
|
debug : false,
|
|
|
|
// Node repulsion (non overlapping) multiplier
|
|
nodeRepulsion : 400000,
|
|
|
|
// Node repulsion (overlapping) multiplier
|
|
nodeOverlap : 10,
|
|
|
|
// Ideal edge (non nested) length
|
|
idealEdgeLength : 10,
|
|
|
|
// Divisor to compute edge forces
|
|
edgeElasticity : 100,
|
|
|
|
// Nesting factor (multiplier) to compute ideal edge length for nested edges
|
|
nestingFactor : 5,
|
|
|
|
// Gravity force (constant)
|
|
gravity : 250,
|
|
|
|
// Maximum number of iterations to perform
|
|
numIter : 100,
|
|
|
|
// Initial temperature (maximum node displacement)
|
|
initialTemp : 200,
|
|
|
|
// Cooling factor (how the temperature is reduced between consecutive iterations
|
|
coolingFactor : 0.95,
|
|
|
|
// Lower temperature threshold (below this point the layout will end)
|
|
minTemp : 1.0
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : constructor
|
|
* @arg options : object containing layout options
|
|
*/
|
|
function CoseLayout(options) {
|
|
this.options = $$.util.extend({}, defaults, options);
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief : runs the layout
|
|
*/
|
|
CoseLayout.prototype.run = function() {
|
|
var options = this.options;
|
|
var cy = options.cy;
|
|
var layout = this;
|
|
|
|
layout.stopped = false;
|
|
|
|
layout.trigger({ type: 'layoutstart', layout: layout });
|
|
|
|
// Set DEBUG - Global variable
|
|
if (true === options.debug) {
|
|
DEBUG = true;
|
|
} else {
|
|
DEBUG = false;
|
|
}
|
|
|
|
// Get start time
|
|
var startTime = new Date();
|
|
|
|
// Initialize layout info
|
|
var layoutInfo = createLayoutInfo(cy, layout, options);
|
|
|
|
// Show LayoutInfo contents if debugging
|
|
if (DEBUG) {
|
|
printLayoutInfo(layoutInfo);
|
|
}
|
|
|
|
// If required, randomize node positions
|
|
if (true === options.randomize) {
|
|
randomizePositions(layoutInfo, cy);
|
|
}
|
|
|
|
updatePositions(layoutInfo, cy, options);
|
|
|
|
var mainLoop = function(i){
|
|
if( layout.stopped ){
|
|
// logDebug("Layout manually stopped. Stopping computation in step " + i);
|
|
return false;
|
|
}
|
|
|
|
// Do one step in the phisical simulation
|
|
step(layoutInfo, cy, options, i);
|
|
|
|
// Update temperature
|
|
layoutInfo.temperature = layoutInfo.temperature * options.coolingFactor;
|
|
// logDebug("New temperature: " + layoutInfo.temperature);
|
|
|
|
if (layoutInfo.temperature < options.minTemp) {
|
|
// logDebug("Temperature drop below minimum threshold. Stopping computation in step " + i);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
var done = function(){
|
|
refreshPositions(layoutInfo, cy, options);
|
|
|
|
// Fit the graph if necessary
|
|
if (true === options.fit) {
|
|
cy.fit( options.padding );
|
|
}
|
|
|
|
// Get end time
|
|
var endTime = new Date();
|
|
|
|
console.info('Layout took ' + (endTime - startTime) + ' ms');
|
|
|
|
// Layout has finished
|
|
layout.one('layoutstop', options.stop);
|
|
layout.trigger({ type: 'layoutstop', layout: layout });
|
|
};
|
|
|
|
if( options.animate ){
|
|
var i = 0;
|
|
var frame = function(){
|
|
|
|
var f = 0;
|
|
var loopRet;
|
|
while( f < options.refresh && i < options.numIter ){
|
|
var loopRet = mainLoop(i);
|
|
if( loopRet === false ){ break; }
|
|
|
|
f++;
|
|
i++;
|
|
}
|
|
|
|
refreshPositions(layoutInfo, cy, options);
|
|
if( options.fit ){
|
|
cy.fit( options.padding );
|
|
}
|
|
|
|
if ( loopRet !== false && i + 1 < options.numIter ) {
|
|
$$.util.requestAnimationFrame( frame );
|
|
} else {
|
|
done();
|
|
}
|
|
};
|
|
|
|
$$.util.requestAnimationFrame( frame );
|
|
} else {
|
|
for (var i = 0; i < options.numIter; i++) {
|
|
if( mainLoop(i) === false ){ break; }
|
|
}
|
|
|
|
done();
|
|
}
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : called on continuous layouts to stop them before they finish
|
|
*/
|
|
CoseLayout.prototype.stop = function(){
|
|
this.stopped = true;
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Creates an object which is contains all the data
|
|
* used in the layout process
|
|
* @arg cy : cytoscape.js object
|
|
* @return : layoutInfo object initialized
|
|
*/
|
|
var createLayoutInfo = function(cy, layout, options) {
|
|
// Shortcut
|
|
var edges = options.eles.edges();
|
|
var nodes = options.eles.nodes();
|
|
|
|
var layoutInfo = {
|
|
layout : layout,
|
|
layoutNodes : [],
|
|
idToIndex : {},
|
|
nodeSize : nodes.size(),
|
|
graphSet : [],
|
|
indexToGraph : [],
|
|
layoutEdges : [],
|
|
edgeSize : edges.size(),
|
|
temperature : options.initialTemp,
|
|
clientWidth : cy.width(),
|
|
clientHeight : cy.width(),
|
|
boundingBox : $$.util.makeBoundingBox( options.boundingBox ? options.boundingBox : {
|
|
x1: 0, y1: 0, w: cy.width(), h: cy.height()
|
|
} )
|
|
};
|
|
|
|
// Iterate over all nodes, creating layout nodes
|
|
for (var i = 0; i < layoutInfo.nodeSize; i++) {
|
|
var tempNode = {};
|
|
tempNode.id = nodes[i].data('id');
|
|
tempNode.parentId = nodes[i].data('parent');
|
|
tempNode.children = [];
|
|
tempNode.positionX = nodes[i].position('x');
|
|
tempNode.positionY = nodes[i].position('y');
|
|
tempNode.offsetX = 0;
|
|
tempNode.offsetY = 0;
|
|
tempNode.height = nodes[i].height();
|
|
tempNode.width = nodes[i].width();
|
|
tempNode.maxX = tempNode.positionX + tempNode.width / 2;
|
|
tempNode.minX = tempNode.positionX - tempNode.width / 2;
|
|
tempNode.maxY = tempNode.positionY + tempNode.height / 2;
|
|
tempNode.minY = tempNode.positionY - tempNode.height / 2;
|
|
tempNode.padLeft = nodes[i]._private.style['padding-left'].pxValue;
|
|
tempNode.padRight = nodes[i]._private.style['padding-right'].pxValue;
|
|
tempNode.padTop = nodes[i]._private.style['padding-top'].pxValue;
|
|
tempNode.padBottom = nodes[i]._private.style['padding-bottom'].pxValue;
|
|
|
|
// Add new node
|
|
layoutInfo.layoutNodes.push(tempNode);
|
|
// Add entry to id-index map
|
|
layoutInfo.idToIndex[tempNode.id] = i;
|
|
}
|
|
|
|
// Inline implementation of a queue, used for traversing the graph in BFS order
|
|
var queue = [];
|
|
var start = 0; // Points to the start the queue
|
|
var end = -1; // Points to the end of the queue
|
|
|
|
var tempGraph = [];
|
|
|
|
// Second pass to add child information and
|
|
// initialize queue for hierarchical traversal
|
|
for (var i = 0; i < layoutInfo.nodeSize; i++) {
|
|
var n = layoutInfo.layoutNodes[i];
|
|
var p_id = n.parentId;
|
|
// Check if node n has a parent node
|
|
if (null != p_id) {
|
|
// Add node Id to parent's list of children
|
|
layoutInfo.layoutNodes[layoutInfo.idToIndex[p_id]].children.push(n.id);
|
|
} else {
|
|
// If a node doesn't have a parent, then it's in the root graph
|
|
queue[++end] = n.id;
|
|
tempGraph.push(n.id);
|
|
}
|
|
}
|
|
|
|
// Add root graph to graphSet
|
|
layoutInfo.graphSet.push(tempGraph);
|
|
|
|
// Traverse the graph, level by level,
|
|
while (start <= end) {
|
|
// Get the node to visit and remove it from queue
|
|
var node_id = queue[start++];
|
|
var node_ix = layoutInfo.idToIndex[node_id];
|
|
var node = layoutInfo.layoutNodes[node_ix];
|
|
var children = node.children;
|
|
if (children.length > 0) {
|
|
// Add children nodes as a new graph to graph set
|
|
layoutInfo.graphSet.push(children);
|
|
// Add children to que queue to be visited
|
|
for (var i = 0; i < children.length; i++) {
|
|
queue[++end] = children[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create indexToGraph map
|
|
for (var i = 0; i < layoutInfo.graphSet.length; i++) {
|
|
var graph = layoutInfo.graphSet[i];
|
|
for (var j = 0; j < graph.length; j++) {
|
|
var index = layoutInfo.idToIndex[graph[j]];
|
|
layoutInfo.indexToGraph[index] = i;
|
|
}
|
|
}
|
|
|
|
// Iterate over all edges, creating Layout Edges
|
|
for (var i = 0; i < layoutInfo.edgeSize; i++) {
|
|
var e = edges[i];
|
|
var tempEdge = {};
|
|
tempEdge.id = e.data('id');
|
|
tempEdge.sourceId = e.data('source');
|
|
tempEdge.targetId = e.data('target');
|
|
|
|
// Compute ideal length
|
|
var idealLength = options.idealEdgeLength;
|
|
|
|
// Check if it's an inter graph edge
|
|
var sourceIx = layoutInfo.idToIndex[tempEdge.sourceId];
|
|
var targetIx = layoutInfo.idToIndex[tempEdge.targetId];
|
|
var sourceGraph = layoutInfo.indexToGraph[sourceIx];
|
|
var targetGraph = layoutInfo.indexToGraph[targetIx];
|
|
|
|
if (sourceGraph != targetGraph) {
|
|
// Find lowest common graph ancestor
|
|
var lca = findLCA(tempEdge.sourceId, tempEdge.targetId, layoutInfo);
|
|
|
|
// Compute sum of node depths, relative to lca graph
|
|
var lcaGraph = layoutInfo.graphSet[lca];
|
|
var depth = 0;
|
|
|
|
// Source depth
|
|
var tempNode = layoutInfo.layoutNodes[sourceIx];
|
|
while (-1 === $.inArray(tempNode.id, lcaGraph)) {
|
|
tempNode = layoutInfo.layoutNodes[layoutInfo.idToIndex[tempNode.parentId]];
|
|
depth++;
|
|
}
|
|
|
|
// Target depth
|
|
tempNode = layoutInfo.layoutNodes[targetIx];
|
|
while (-1 === $.inArray(tempNode.id, lcaGraph)) {
|
|
tempNode = layoutInfo.layoutNodes[layoutInfo.idToIndex[tempNode.parentId]];
|
|
depth++;
|
|
}
|
|
|
|
// logDebug('LCA of nodes ' + tempEdge.sourceId + ' and ' + tempEdge.targetId +
|
|
// ". Index: " + lca + " Contents: " + lcaGraph.toString() +
|
|
// ". Depth: " + depth);
|
|
|
|
// Update idealLength
|
|
idealLength *= depth * options.nestingFactor;
|
|
}
|
|
|
|
tempEdge.idealLength = idealLength;
|
|
|
|
layoutInfo.layoutEdges.push(tempEdge);
|
|
}
|
|
|
|
// Finally, return layoutInfo object
|
|
return layoutInfo;
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : This function finds the index of the lowest common
|
|
* graph ancestor between 2 nodes in the subtree
|
|
* (from the graph hierarchy induced tree) whose
|
|
* root is graphIx
|
|
*
|
|
* @arg node1: node1's ID
|
|
* @arg node2: node2's ID
|
|
* @arg layoutInfo: layoutInfo object
|
|
*
|
|
*/
|
|
var findLCA = function(node1, node2, layoutInfo) {
|
|
// Find their common ancester, starting from the root graph
|
|
var res = findLCA_aux(node1, node2, 0, layoutInfo);
|
|
if (2 > res.count) {
|
|
// If aux function couldn't find the common ancester,
|
|
// then it is the root graph
|
|
return 0;
|
|
} else {
|
|
return res.graph;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Auxiliary function used for LCA computation
|
|
*
|
|
* @arg node1 : node1's ID
|
|
* @arg node2 : node2's ID
|
|
* @arg graphIx : subgraph index
|
|
* @arg layoutInfo : layoutInfo object
|
|
*
|
|
* @return : object of the form {count: X, graph: Y}, where:
|
|
* X is the number of ancesters (max: 2) found in
|
|
* graphIx (and it's subgraphs),
|
|
* Y is the graph index of the lowest graph containing
|
|
* all X nodes
|
|
*/
|
|
var findLCA_aux = function(node1, node2, graphIx, layoutInfo) {
|
|
var graph = layoutInfo.graphSet[graphIx];
|
|
// If both nodes belongs to graphIx
|
|
if (-1 < $.inArray(node1, graph) && -1 < $.inArray(node2, graph)) {
|
|
return {count:2, graph:graphIx};
|
|
}
|
|
|
|
// Make recursive calls for all subgraphs
|
|
var c = 0;
|
|
for (var i = 0; i < graph.length; i++) {
|
|
var nodeId = graph[i];
|
|
var nodeIx = layoutInfo.idToIndex[nodeId];
|
|
var children = layoutInfo.layoutNodes[nodeIx].children;
|
|
|
|
// If the node has no child, skip it
|
|
if (0 === children.length) {
|
|
continue;
|
|
}
|
|
|
|
var childGraphIx = layoutInfo.indexToGraph[layoutInfo.idToIndex[children[0]]];
|
|
var result = findLCA_aux(node1, node2, childGraphIx, layoutInfo);
|
|
if (0 === result.count) {
|
|
// Neither node1 nor node2 are present in this subgraph
|
|
continue;
|
|
} else if (1 === result.count) {
|
|
// One of (node1, node2) is present in this subgraph
|
|
c++;
|
|
if (2 === c) {
|
|
// We've already found both nodes, no need to keep searching
|
|
break;
|
|
}
|
|
} else {
|
|
// Both nodes are present in this subgraph
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return {count:c, graph:graphIx};
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief: printsLayoutInfo into js console
|
|
* Only used for debbuging
|
|
*/
|
|
var printLayoutInfo = function(layoutInfo) {
|
|
if (!DEBUG) {
|
|
return;
|
|
}
|
|
console.debug("layoutNodes:");
|
|
for (var i = 0; i < layoutInfo.nodeSize; i++) {
|
|
var n = layoutInfo.layoutNodes[i];
|
|
var s =
|
|
"\nindex: " + i +
|
|
"\nId: " + n.id +
|
|
"\nChildren: " + n.children.toString() +
|
|
"\nparentId: " + n.parentId +
|
|
"\npositionX: " + n.positionX +
|
|
"\npositionY: " + n.positionY +
|
|
"\nOffsetX: " + n.offsetX +
|
|
"\nOffsetY: " + n.offsetY +
|
|
"\npadLeft: " + n.padLeft +
|
|
"\npadRight: " + n.padRight +
|
|
"\npadTop: " + n.padTop +
|
|
"\npadBottom: " + n.padBottom;
|
|
|
|
console.debug(s);
|
|
}
|
|
|
|
console.debug('idToIndex');
|
|
for (var i in layoutInfo.idToIndex) {
|
|
console.debug("Id: " + i + "\nIndex: " + layoutInfo.idToIndex[i]);
|
|
}
|
|
|
|
console.debug('Graph Set');
|
|
var set = layoutInfo.graphSet;
|
|
for (var i = 0; i < set.length; i ++) {
|
|
console.debug("Set : " + i + ": " + set[i].toString());
|
|
}
|
|
|
|
var s = 'IndexToGraph';
|
|
for (var i = 0; i < layoutInfo.indexToGraph.length; i ++) {
|
|
s += "\nIndex : " + i + " Graph: "+ layoutInfo.indexToGraph[i];
|
|
}
|
|
console.debug(s);
|
|
|
|
s = 'Layout Edges';
|
|
for (var i = 0; i < layoutInfo.layoutEdges.length; i++) {
|
|
var e = layoutInfo.layoutEdges[i];
|
|
s += "\nEdge Index: " + i + " ID: " + e.id +
|
|
" SouceID: " + e.sourceId + " TargetId: " + e.targetId +
|
|
" Ideal Length: " + e.idealLength;
|
|
}
|
|
console.debug(s);
|
|
|
|
s = "nodeSize: " + layoutInfo.nodeSize;
|
|
s += "\nedgeSize: " + layoutInfo.edgeSize;
|
|
s += "\ntemperature: " + layoutInfo.temperature;
|
|
console.debug(s);
|
|
|
|
return;
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Randomizes the position of all nodes
|
|
*/
|
|
var randomizePositions = function(layoutInfo, cy) {
|
|
var width = layoutInfo.clientWidth;
|
|
var height = layoutInfo.clientHeight;
|
|
|
|
for (var i = 0; i < layoutInfo.nodeSize; i++) {
|
|
var n = layoutInfo.layoutNodes[i];
|
|
// No need to randomize compound nodes
|
|
if (true || 0 === n.children.length) {
|
|
n.positionX = Math.random() * width;
|
|
n.positionY = Math.random() * height;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Updates the positions of nodes in the network
|
|
* @arg layoutInfo : LayoutInfo object
|
|
* @arg cy : Cytoscape object
|
|
* @arg options : Layout options
|
|
*/
|
|
var refreshPositions = function(layoutInfo, cy, options) {
|
|
// var s = 'Refreshing positions';
|
|
// logDebug(s);
|
|
|
|
var layout = layoutInfo.layout;
|
|
var nodes = options.eles.nodes();
|
|
var bb = layoutInfo.boundingBox;
|
|
var coseBB = { x1: Infinity, x2: -Infinity, y1: Infinity, y2: -Infinity };
|
|
|
|
if( options.boundingBox ){
|
|
nodes.forEach(function( node ){
|
|
var lnode = layoutInfo.layoutNodes[layoutInfo.idToIndex[node.data('id')]];
|
|
|
|
coseBB.x1 = Math.min( coseBB.x1, lnode.positionX );
|
|
coseBB.x2 = Math.max( coseBB.x2, lnode.positionX );
|
|
|
|
coseBB.y1 = Math.min( coseBB.y1, lnode.positionY );
|
|
coseBB.y2 = Math.max( coseBB.y2, lnode.positionY );
|
|
});
|
|
|
|
coseBB.w = coseBB.x2 - coseBB.x1;
|
|
coseBB.h = coseBB.y2 - coseBB.y1;
|
|
}
|
|
|
|
nodes.positions(function(i, ele) {
|
|
var lnode = layoutInfo.layoutNodes[layoutInfo.idToIndex[ele.data('id')]];
|
|
// s = "Node: " + lnode.id + ". Refreshed position: (" +
|
|
// lnode.positionX + ", " + lnode.positionY + ").";
|
|
// logDebug(s);
|
|
|
|
if( options.boundingBox ){ // then add extra bounding box constraint
|
|
var pctX = (lnode.positionX - coseBB.x1) / coseBB.w;
|
|
var pctY = (lnode.positionY - coseBB.y1) / coseBB.h;
|
|
|
|
return {
|
|
x: bb.x1 + pctX * bb.w,
|
|
y: bb.y1 + pctY * bb.h
|
|
};
|
|
} else {
|
|
return {
|
|
x: lnode.positionX,
|
|
y: lnode.positionY
|
|
};
|
|
}
|
|
});
|
|
|
|
// Trigger layoutReady only on first call
|
|
if (true !== layoutInfo.ready) {
|
|
// s = 'Triggering layoutready';
|
|
// logDebug(s);
|
|
layoutInfo.ready = true;
|
|
layout.one('layoutready', options.ready);
|
|
layout.trigger({ type: 'layoutready', layout: this });
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Performs one iteration of the physical simulation
|
|
* @arg layoutInfo : LayoutInfo object already initialized
|
|
* @arg cy : Cytoscape object
|
|
* @arg options : Layout options
|
|
*/
|
|
var step = function(layoutInfo, cy, options, step) {
|
|
// var s = "\n\n###############################";
|
|
// s += "\nSTEP: " + step;
|
|
// s += "\n###############################\n";
|
|
// logDebug(s);
|
|
|
|
// Calculate node repulsions
|
|
calculateNodeForces(layoutInfo, cy, options);
|
|
// Calculate edge forces
|
|
calculateEdgeForces(layoutInfo, cy, options);
|
|
// Calculate gravity forces
|
|
calculateGravityForces(layoutInfo, cy, options);
|
|
// Propagate forces from parent to child
|
|
propagateForces(layoutInfo, cy, options);
|
|
// Update positions based on calculated forces
|
|
updatePositions(layoutInfo, cy, options);
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Computes the node repulsion forces
|
|
*/
|
|
var calculateNodeForces = function(layoutInfo, cy, options) {
|
|
// Go through each of the graphs in graphSet
|
|
// Nodes only repel each other if they belong to the same graph
|
|
// var s = 'calculateNodeForces';
|
|
// logDebug(s);
|
|
for (var i = 0; i < layoutInfo.graphSet.length; i ++) {
|
|
var graph = layoutInfo.graphSet[i];
|
|
var numNodes = graph.length;
|
|
|
|
// s = "Set: " + graph.toString();
|
|
// logDebug(s);
|
|
|
|
// Now get all the pairs of nodes
|
|
// Only get each pair once, (A, B) = (B, A)
|
|
for (var j = 0; j < numNodes; j++) {
|
|
var node1 = layoutInfo.layoutNodes[layoutInfo.idToIndex[graph[j]]];
|
|
for (var k = j + 1; k < numNodes; k++) {
|
|
var node2 = layoutInfo.layoutNodes[layoutInfo.idToIndex[graph[k]]];
|
|
nodeRepulsion(node1, node2, layoutInfo, cy, options);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Compute the node repulsion forces between a pair of nodes
|
|
*/
|
|
var nodeRepulsion = function(node1, node2, layoutInfo, cy, options) {
|
|
// var s = "Node repulsion. Node1: " + node1.id + " Node2: " + node2.id;
|
|
|
|
// Get direction of line connecting both node centers
|
|
var directionX = node2.positionX - node1.positionX;
|
|
var directionY = node2.positionY - node1.positionY;
|
|
// s += "\ndirectionX: " + directionX + ", directionY: " + directionY;
|
|
|
|
// If both centers are the same, apply a random force
|
|
if (0 === directionX && 0 === directionY) {
|
|
// s += "\nNodes have the same position.";
|
|
return; // TODO
|
|
}
|
|
|
|
var overlap = nodesOverlap(node1, node2, directionX, directionY);
|
|
|
|
if (overlap > 0) {
|
|
// s += "\nNodes DO overlap.";
|
|
// s += "\nOverlap: " + overlap;
|
|
// If nodes overlap, repulsion force is proportional
|
|
// to the overlap
|
|
var force = options.nodeOverlap * overlap;
|
|
|
|
// Compute the module and components of the force vector
|
|
var distance = Math.sqrt(directionX * directionX + directionY * directionY);
|
|
// s += "\nDistance: " + distance;
|
|
var forceX = force * directionX / distance;
|
|
var forceY = force * directionY / distance;
|
|
|
|
} else {
|
|
// s += "\nNodes do NOT overlap.";
|
|
// If there's no overlap, force is inversely proportional
|
|
// to squared distance
|
|
|
|
// Get clipping points for both nodes
|
|
var point1 = findClippingPoint(node1, directionX, directionY);
|
|
var point2 = findClippingPoint(node2, -1 * directionX, -1 * directionY);
|
|
|
|
// Use clipping points to compute distance
|
|
var distanceX = point2.x - point1.x;
|
|
var distanceY = point2.y - point1.y;
|
|
var distanceSqr = distanceX * distanceX + distanceY * distanceY;
|
|
var distance = Math.sqrt(distanceSqr);
|
|
// s += "\nDistance: " + distance;
|
|
|
|
// Compute the module and components of the force vector
|
|
var force = options.nodeRepulsion / distanceSqr;
|
|
var forceX = force * distanceX / distance;
|
|
var forceY = force * distanceY / distance;
|
|
}
|
|
|
|
// Apply force
|
|
node1.offsetX -= forceX;
|
|
node1.offsetY -= forceY;
|
|
node2.offsetX += forceX;
|
|
node2.offsetY += forceY;
|
|
|
|
// s += "\nForceX: " + forceX + " ForceY: " + forceY;
|
|
// logDebug(s);
|
|
|
|
return;
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Finds the point in which an edge (direction dX, dY) intersects
|
|
* the rectangular bounding box of it's source/target node
|
|
*/
|
|
var findClippingPoint = function(node, dX, dY) {
|
|
|
|
// Shorcuts
|
|
var X = node.positionX;
|
|
var Y = node.positionY;
|
|
var H = node.height;
|
|
var W = node.width;
|
|
var dirSlope = dY / dX;
|
|
var nodeSlope = H / W;
|
|
|
|
// var s = 'Computing clipping point of node ' + node.id +
|
|
// " . Height: " + H + ", Width: " + W +
|
|
// "\nDirection " + dX + ", " + dY;
|
|
//
|
|
// Compute intersection
|
|
var res = {};
|
|
do {
|
|
// Case: Vertical direction (up)
|
|
if (0 === dX && 0 < dY) {
|
|
res.x = X;
|
|
// s += "\nUp direction";
|
|
res.y = Y + H / 2;
|
|
break;
|
|
}
|
|
|
|
// Case: Vertical direction (down)
|
|
if (0 === dX && 0 > dY) {
|
|
res.x = X;
|
|
res.y = Y + H / 2;
|
|
// s += "\nDown direction";
|
|
break;
|
|
}
|
|
|
|
// Case: Intersects the right border
|
|
if (0 < dX &&
|
|
-1 * nodeSlope <= dirSlope &&
|
|
dirSlope <= nodeSlope) {
|
|
res.x = X + W / 2;
|
|
res.y = Y + (W * dY / 2 / dX);
|
|
// s += "\nRightborder";
|
|
break;
|
|
}
|
|
|
|
// Case: Intersects the left border
|
|
if (0 > dX &&
|
|
-1 * nodeSlope <= dirSlope &&
|
|
dirSlope <= nodeSlope) {
|
|
res.x = X - W / 2;
|
|
res.y = Y - (W * dY / 2 / dX);
|
|
// s += "\nLeftborder";
|
|
break;
|
|
}
|
|
|
|
// Case: Intersects the top border
|
|
if (0 < dY &&
|
|
( dirSlope <= -1 * nodeSlope ||
|
|
dirSlope >= nodeSlope )) {
|
|
res.x = X + (H * dX / 2 / dY);
|
|
res.y = Y + H / 2;
|
|
// s += "\nTop border";
|
|
break;
|
|
}
|
|
|
|
// Case: Intersects the bottom border
|
|
if (0 > dY &&
|
|
( dirSlope <= -1 * nodeSlope ||
|
|
dirSlope >= nodeSlope )) {
|
|
res.x = X - (H * dX / 2 / dY);
|
|
res.y = Y - H / 2;
|
|
// s += "\nBottom border";
|
|
break;
|
|
}
|
|
|
|
} while (false);
|
|
|
|
// s += "\nClipping point found at " + res.x + ", " + res.y;
|
|
// logDebug(s);
|
|
return res;
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Determines whether two nodes overlap or not
|
|
* @return : Amount of overlapping (0 => no overlap)
|
|
*/
|
|
var nodesOverlap = function(node1, node2, dX, dY) {
|
|
|
|
if (dX > 0) {
|
|
var overlapX = node1.maxX - node2.minX;
|
|
} else {
|
|
var overlapX = node2.maxX - node1.minX;
|
|
}
|
|
|
|
if (dY > 0) {
|
|
var overlapY = node1.maxY - node2.minY;
|
|
} else {
|
|
var overlapY = node2.maxY - node1.minY;
|
|
}
|
|
|
|
if (overlapX >= 0 && overlapY >= 0) {
|
|
return Math.sqrt(overlapX * overlapX + overlapY * overlapY);
|
|
} else {
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Calculates all edge forces
|
|
*/
|
|
var calculateEdgeForces = function(layoutInfo, cy, options) {
|
|
// Iterate over all edges
|
|
for (var i = 0; i < layoutInfo.edgeSize; i++) {
|
|
// Get edge, source & target nodes
|
|
var edge = layoutInfo.layoutEdges[i];
|
|
var sourceIx = layoutInfo.idToIndex[edge.sourceId];
|
|
var source = layoutInfo.layoutNodes[sourceIx];
|
|
var targetIx = layoutInfo.idToIndex[edge.targetId];
|
|
var target = layoutInfo.layoutNodes[targetIx];
|
|
|
|
// Get direction of line connecting both node centers
|
|
var directionX = target.positionX - source.positionX;
|
|
var directionY = target.positionY - source.positionY;
|
|
|
|
// If both centers are the same, do nothing.
|
|
// A random force has already been applied as node repulsion
|
|
if (0 === directionX && 0 === directionY) {
|
|
return;
|
|
}
|
|
|
|
// Get clipping points for both nodes
|
|
var point1 = findClippingPoint(source, directionX, directionY);
|
|
var point2 = findClippingPoint(target, -1 * directionX, -1 * directionY);
|
|
|
|
|
|
var lx = point2.x - point1.x;
|
|
var ly = point2.y - point1.y;
|
|
var l = Math.sqrt(lx * lx + ly * ly);
|
|
|
|
var force = Math.pow(edge.idealLength - l, 2) / options.edgeElasticity;
|
|
|
|
if (0 !== l) {
|
|
var forceX = force * lx / l;
|
|
var forceY = force * ly / l;
|
|
} else {
|
|
var forceX = 0;
|
|
var forceY = 0;
|
|
}
|
|
|
|
// Add this force to target and source nodes
|
|
source.offsetX += forceX;
|
|
source.offsetY += forceY;
|
|
target.offsetX -= forceX;
|
|
target.offsetY -= forceY;
|
|
|
|
// var s = 'Edge force between nodes ' + source.id + ' and ' + target.id;
|
|
// s += "\nDistance: " + l + " Force: (" + forceX + ", " + forceY + ")";
|
|
// logDebug(s);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Computes gravity forces for all nodes
|
|
*/
|
|
var calculateGravityForces = function(layoutInfo, cy, options) {
|
|
// var s = 'calculateGravityForces';
|
|
// logDebug(s);
|
|
for (var i = 0; i < layoutInfo.graphSet.length; i ++) {
|
|
var graph = layoutInfo.graphSet[i];
|
|
var numNodes = graph.length;
|
|
|
|
// s = "Set: " + graph.toString();
|
|
// logDebug(s);
|
|
|
|
// Compute graph center
|
|
if (0 === i) {
|
|
var centerX = layoutInfo.clientHeight / 2;
|
|
var centerY = layoutInfo.clientWidth / 2;
|
|
} else {
|
|
// Get Parent node for this graph, and use its position as center
|
|
var temp = layoutInfo.layoutNodes[layoutInfo.idToIndex[graph[0]]];
|
|
var parent = layoutInfo.layoutNodes[layoutInfo.idToIndex[temp.parentId]];
|
|
var centerX = parent.positionX;
|
|
var centerY = parent.positionY;
|
|
}
|
|
// s = "Center found at: " + centerX + ", " + centerY;
|
|
// logDebug(s);
|
|
|
|
// Apply force to all nodes in graph
|
|
for (var j = 0; j < numNodes; j++) {
|
|
var node = layoutInfo.layoutNodes[layoutInfo.idToIndex[graph[j]]];
|
|
// s = "Node: " + node.id;
|
|
var dx = centerX - node.positionX;
|
|
var dy = centerY - node.positionY;
|
|
var d = Math.sqrt(dx * dx + dy * dy);
|
|
if (d > 1.0) { // TODO: Use global variable for distance threshold
|
|
var fx = options.gravity * dx / d;
|
|
var fy = options.gravity * dy / d;
|
|
node.offsetX += fx;
|
|
node.offsetY += fy;
|
|
// s += ": Applied force: " + fx + ", " + fy;
|
|
} else {
|
|
// s += ": skypped since it's too close to center";
|
|
}
|
|
// logDebug(s);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : This function propagates the existing offsets from
|
|
* parent nodes to its descendents.
|
|
* @arg layoutInfo : layoutInfo Object
|
|
* @arg cy : cytoscape Object
|
|
* @arg options : Layout options
|
|
*/
|
|
var propagateForces = function(layoutInfo, cy, options) {
|
|
// Inline implementation of a queue, used for traversing the graph in BFS order
|
|
var queue = [];
|
|
var start = 0; // Points to the start the queue
|
|
var end = -1; // Points to the end of the queue
|
|
|
|
// logDebug('propagateForces');
|
|
|
|
// Start by visiting the nodes in the root graph
|
|
queue.push.apply(queue, layoutInfo.graphSet[0]);
|
|
end += layoutInfo.graphSet[0].length;
|
|
|
|
// Traverse the graph, level by level,
|
|
while (start <= end) {
|
|
// Get the node to visit and remove it from queue
|
|
var nodeId = queue[start++];
|
|
var nodeIndex = layoutInfo.idToIndex[nodeId];
|
|
var node = layoutInfo.layoutNodes[nodeIndex];
|
|
var children = node.children;
|
|
|
|
// We only need to process the node if it's compound
|
|
if (0 < children.length) {
|
|
var offX = node.offsetX;
|
|
var offY = node.offsetY;
|
|
|
|
// var s = "Propagating offset from parent node : " + node.id +
|
|
// ". OffsetX: " + offX + ". OffsetY: " + offY;
|
|
// s += "\n Children: " + children.toString();
|
|
// logDebug(s);
|
|
|
|
for (var i = 0; i < children.length; i++) {
|
|
var childNode = layoutInfo.layoutNodes[layoutInfo.idToIndex[children[i]]];
|
|
// Propagate offset
|
|
childNode.offsetX += offX;
|
|
childNode.offsetY += offY;
|
|
// Add children to queue to be visited
|
|
queue[++end] = children[i];
|
|
}
|
|
|
|
// Reset parent offsets
|
|
node.offsetX = 0;
|
|
node.offsetY = 0;
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Updates the layout model positions, based on
|
|
* the accumulated forces
|
|
*/
|
|
var updatePositions = function(layoutInfo, cy, options) {
|
|
// var s = 'Updating positions';
|
|
// logDebug(s);
|
|
|
|
// Reset boundaries for compound nodes
|
|
for (var i = 0; i < layoutInfo.nodeSize; i++) {
|
|
var n = layoutInfo.layoutNodes[i];
|
|
if (0 < n.children.length) {
|
|
// logDebug("Resetting boundaries of compound node: " + n.id);
|
|
n.maxX = undefined;
|
|
n.minX = undefined;
|
|
n.maxY = undefined;
|
|
n.minY = undefined;
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < layoutInfo.nodeSize; i++) {
|
|
var n = layoutInfo.layoutNodes[i];
|
|
if (0 < n.children.length) {
|
|
// No need to set compound node position
|
|
// logDebug("Skipping position update of node: " + n.id);
|
|
continue;
|
|
}
|
|
// s = "Node: " + n.id + " Previous position: (" +
|
|
// n.positionX + ", " + n.positionY + ").";
|
|
|
|
// Limit displacement in order to improve stability
|
|
var tempForce = limitForce(n.offsetX, n.offsetY, layoutInfo.temperature);
|
|
n.positionX += tempForce.x;
|
|
n.positionY += tempForce.y;
|
|
n.offsetX = 0;
|
|
n.offsetY = 0;
|
|
n.minX = n.positionX - n.width;
|
|
n.maxX = n.positionX + n.width;
|
|
n.minY = n.positionY - n.height;
|
|
n.maxY = n.positionY + n.height;
|
|
// s += " New Position: (" + n.positionX + ", " + n.positionY + ").";
|
|
// logDebug(s);
|
|
|
|
// Update ancestry boudaries
|
|
updateAncestryBoundaries(n, layoutInfo);
|
|
}
|
|
|
|
// Update size, position of compund nodes
|
|
for (var i = 0; i < layoutInfo.nodeSize; i++) {
|
|
var n = layoutInfo.layoutNodes[i];
|
|
if (0 < n.children.length) {
|
|
n.positionX = (n.maxX + n.minX) / 2;
|
|
n.positionY = (n.maxY + n.minY) / 2;
|
|
n.width = n.maxX - n.minX;
|
|
n.height = n.maxY - n.minY;
|
|
// s = "Updating position, size of compound node " + n.id;
|
|
// s += "\nPositionX: " + n.positionX + ", PositionY: " + n.positionY;
|
|
// s += "\nWidth: " + n.width + ", Height: " + n.height;
|
|
// logDebug(s);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Limits a force (forceX, forceY) to be not
|
|
* greater (in modulo) than max.
|
|
8 Preserves force direction.
|
|
*/
|
|
var limitForce = function(forceX, forceY, max) {
|
|
// var s = "Limiting force: (" + forceX + ", " + forceY + "). Max: " + max;
|
|
var force = Math.sqrt(forceX * forceX + forceY * forceY);
|
|
|
|
if (force > max) {
|
|
var res = {
|
|
x : max * forceX / force,
|
|
y : max * forceY / force
|
|
};
|
|
|
|
} else {
|
|
var res = {
|
|
x : forceX,
|
|
y : forceY
|
|
};
|
|
}
|
|
|
|
// s += ".\nResult: (" + res.x + ", " + res.y + ")";
|
|
// logDebug(s);
|
|
|
|
return res;
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Function used for keeping track of compound node
|
|
* sizes, since they should bound all their subnodes.
|
|
*/
|
|
var updateAncestryBoundaries = function(node, layoutInfo) {
|
|
// var s = "Propagating new position/size of node " + node.id;
|
|
var parentId = node.parentId;
|
|
if (null == parentId) {
|
|
// If there's no parent, we are done
|
|
// s += ". No parent node.";
|
|
// logDebug(s);
|
|
return;
|
|
}
|
|
|
|
// Get Parent Node
|
|
var p = layoutInfo.layoutNodes[layoutInfo.idToIndex[parentId]];
|
|
var flag = false;
|
|
|
|
// MaxX
|
|
if (null == p.maxX || node.maxX + p.padRight > p.maxX) {
|
|
p.maxX = node.maxX + p.padRight;
|
|
flag = true;
|
|
// s += "\nNew maxX for parent node " + p.id + ": " + p.maxX;
|
|
}
|
|
|
|
// MinX
|
|
if (null == p.minX || node.minX - p.padLeft < p.minX) {
|
|
p.minX = node.minX - p.padLeft;
|
|
flag = true;
|
|
// s += "\nNew minX for parent node " + p.id + ": " + p.minX;
|
|
}
|
|
|
|
// MaxY
|
|
if (null == p.maxY || node.maxY + p.padBottom > p.maxY) {
|
|
p.maxY = node.maxY + p.padBottom;
|
|
flag = true;
|
|
// s += "\nNew maxY for parent node " + p.id + ": " + p.maxY;
|
|
}
|
|
|
|
// MinY
|
|
if (null == p.minY || node.minY - p.padTop < p.minY) {
|
|
p.minY = node.minY - p.padTop;
|
|
flag = true;
|
|
// s += "\nNew minY for parent node " + p.id + ": " + p.minY;
|
|
}
|
|
|
|
// If updated boundaries, propagate changes upward
|
|
if (flag) {
|
|
// logDebug(s);
|
|
return updateAncestryBoundaries(p, layoutInfo);
|
|
}
|
|
|
|
// s += ". No changes in boundaries/position of parent node " + p.id;
|
|
// logDebug(s);
|
|
return;
|
|
};
|
|
|
|
|
|
/**
|
|
* @brief : Logs a debug message in JS console, if DEBUG is ON
|
|
*/
|
|
// var logDebug = function(text) {
|
|
// if (DEBUG) {
|
|
// console.debug(text);
|
|
// }
|
|
// };
|
|
|
|
|
|
// register the layout
|
|
$$('layout', 'cose', CoseLayout);
|
|
|
|
})(cytoscape);
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// default layout options
|
|
var defaults = {
|
|
// dagre algo options, uses default value on undefined
|
|
nodeSep: undefined, // the separation between adjacent nodes in the same rank
|
|
edgeSep: undefined, // the separation between adjacent edges in the same rank
|
|
rankSep: undefined, // the separation between adjacent nodes in the same rank
|
|
rankDir: undefined, // 'TB' for top to bottom flow, 'LR' for left to right
|
|
minLen: function( edge ){ return 1; }, // number of ranks to keep between the source and target of the edge
|
|
edgeWeight: function( edge ){ return 1; }, // higher weight edges are generally made shorter and straighter than lower weight edges
|
|
|
|
// general layout options
|
|
fit: true, // whether to fit to viewport
|
|
padding: 30, // fit padding
|
|
animate: false, // whether to transition the node positions
|
|
animationDuration: 500, // duration of animation in ms if enabled
|
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
ready: function(){}, // on layoutready
|
|
stop: function(){} // on layoutstop
|
|
};
|
|
|
|
// constructor
|
|
// options : object containing layout options
|
|
function DagreLayout( options ){
|
|
this.options = $$.util.extend(true, {}, defaults, options);
|
|
}
|
|
|
|
// runs the layout
|
|
DagreLayout.prototype.run = function(){
|
|
var options = this.options;
|
|
var layout = this;
|
|
|
|
$$.util.require('dagre', function(dagre){
|
|
|
|
var cy = options.cy; // cy is automatically populated for us in the constructor
|
|
var eles = options.eles;
|
|
|
|
var getVal = function( ele, val ){
|
|
return $$.is.fn(val) ? val.apply( ele, [ ele ] ) : val;
|
|
};
|
|
|
|
var bb = $$.util.makeBoundingBox( options.boundingBox ? options.boundingBox : {
|
|
x1: 0, y1: 0, w: cy.width(), h: cy.height()
|
|
} );
|
|
|
|
var g = new dagre.graphlib.Graph({
|
|
multigraph: true,
|
|
compound: true
|
|
});
|
|
|
|
var gObj = {};
|
|
var setGObj = function( name, val ){
|
|
if( val != null ){
|
|
gObj[ name ] = val;
|
|
}
|
|
};
|
|
|
|
setGObj( 'nodesep', options.nodeSep );
|
|
setGObj( 'edgesep', options.edgeSep );
|
|
setGObj( 'ranksep', options.rankSep );
|
|
setGObj( 'rankdir', options.rankDir );
|
|
|
|
g.setGraph( gObj );
|
|
|
|
g.setDefaultEdgeLabel(function() { return {}; });
|
|
g.setDefaultNodeLabel(function() { return {}; });
|
|
|
|
// add nodes to dagre
|
|
var nodes = eles.nodes();
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var node = nodes[i];
|
|
|
|
g.setNode( node.id(), {
|
|
width: node.width(),
|
|
height: node.height(),
|
|
name: node.id()
|
|
} );
|
|
|
|
// console.log( g.node(node.id()) );
|
|
}
|
|
|
|
// set compound parents
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var node = nodes[i];
|
|
|
|
if( node.isChild() ){
|
|
g.setParent( node.id(), node.parent().id() );
|
|
}
|
|
}
|
|
|
|
// add edges to dagre
|
|
var edges = eles.edges().stdFilter(function( edge ){
|
|
return !edge.source().isParent() && !edge.target().isParent(); // dagre can't handle edges on compound nodes
|
|
});
|
|
for( var i = 0; i < edges.length; i++ ){
|
|
var edge = edges[i];
|
|
|
|
g.setEdge( edge.source().id(), edge.target().id(), {
|
|
minlen: getVal( edge, options.minLen ),
|
|
weight: getVal( edge, options.edgeWeight ),
|
|
name: edge.id()
|
|
}, edge.id() );
|
|
|
|
// console.log( g.edge(edge.source().id(), edge.target().id(), edge.id()) );
|
|
}
|
|
|
|
dagre.layout( g );
|
|
|
|
var gNodeIds = g.nodes();
|
|
for( var i = 0; i < gNodeIds.length; i++ ){
|
|
var id = gNodeIds[i];
|
|
var n = g.node( id );
|
|
|
|
cy.getElementById(id).scratch().dagre = n;
|
|
}
|
|
|
|
var dagreBB;
|
|
|
|
if( options.boundingBox ){
|
|
dagreBB = { x1: Infinity, x2: -Infinity, y1: Infinity, y2: -Infinity };
|
|
nodes.forEach(function( node ){
|
|
var dModel = node.scratch().dagre;
|
|
|
|
dagreBB.x1 = Math.min( dagreBB.x1, dModel.x );
|
|
dagreBB.x2 = Math.max( dagreBB.x2, dModel.x );
|
|
|
|
dagreBB.y1 = Math.min( dagreBB.y1, dModel.y );
|
|
dagreBB.y2 = Math.max( dagreBB.y2, dModel.y );
|
|
});
|
|
|
|
dagreBB.w = dagreBB.x2 - dagreBB.x1;
|
|
dagreBB.h = dagreBB.y2 - dagreBB.y1;
|
|
} else {
|
|
dagreBB = bb;
|
|
}
|
|
|
|
var constrainPos = function( p ){
|
|
if( options.boundingBox ){
|
|
var xPct = (p.x - dagreBB.x1) / dagreBB.w;
|
|
var yPct = (p.y - dagreBB.y1) / dagreBB.h;
|
|
|
|
return {
|
|
x: bb.x1 + xPct * bb.w,
|
|
y: bb.y1 + yPct * bb.h
|
|
};
|
|
} else {
|
|
return p;
|
|
}
|
|
};
|
|
|
|
nodes.layoutPositions(layout, options, function(){
|
|
var dModel = this.scratch().dagre;
|
|
|
|
return constrainPos({
|
|
x: dModel.x,
|
|
y: dModel.y
|
|
});
|
|
});
|
|
|
|
}); // require
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
// register the layout
|
|
$$('layout', 'dagre', DagreLayout);
|
|
|
|
})(cytoscape);
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var defaults = {
|
|
fit: true, // whether to fit the viewport to the graph
|
|
padding: 30, // padding used on fit
|
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
|
|
rows: undefined, // force num of rows in the grid
|
|
columns: undefined, // force num of cols in the grid
|
|
position: function( node ){}, // returns { row, col } for element
|
|
sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
|
|
animate: false, // whether to transition the node positions
|
|
animationDuration: 500, // duration of animation in ms if enabled
|
|
ready: undefined, // callback on layoutready
|
|
stop: undefined // callback on layoutstop
|
|
};
|
|
|
|
function GridLayout( options ){
|
|
this.options = $$.util.extend({}, defaults, options);
|
|
}
|
|
|
|
GridLayout.prototype.run = function(){
|
|
var params = this.options;
|
|
var options = params;
|
|
|
|
var cy = params.cy;
|
|
var eles = options.eles;
|
|
var nodes = eles.nodes().not(':parent');
|
|
|
|
if( options.sort ){
|
|
nodes = nodes.sort( options.sort );
|
|
}
|
|
|
|
var bb = $$.util.makeBoundingBox( options.boundingBox ? options.boundingBox : {
|
|
x1: 0, y1: 0, w: cy.width(), h: cy.height()
|
|
} );
|
|
|
|
if( bb.h === 0 || bb.w === 0){
|
|
nodes.layoutPositions(this, options, function(){
|
|
return { x: bb.x1, y: bb.y1 };
|
|
});
|
|
|
|
} else {
|
|
|
|
// width/height * splits^2 = cells where splits is number of times to split width
|
|
var cells = nodes.size();
|
|
var splits = Math.sqrt( cells * bb.h/bb.w );
|
|
var rows = Math.round( splits );
|
|
var cols = Math.round( bb.w/bb.h * splits );
|
|
|
|
var small = function(val){
|
|
if( val == null ){
|
|
return Math.min(rows, cols);
|
|
} else {
|
|
var min = Math.min(rows, cols);
|
|
if( min == rows ){
|
|
rows = val;
|
|
} else {
|
|
cols = val;
|
|
}
|
|
}
|
|
};
|
|
|
|
var large = function(val){
|
|
if( val == null ){
|
|
return Math.max(rows, cols);
|
|
} else {
|
|
var max = Math.max(rows, cols);
|
|
if( max == rows ){
|
|
rows = val;
|
|
} else {
|
|
cols = val;
|
|
}
|
|
}
|
|
};
|
|
|
|
// if rows or columns were set in options, use those values
|
|
if( options.rows != null && options.columns != null ){
|
|
rows = options.rows;
|
|
cols = options.columns;
|
|
} else if( options.rows != null && options.columns == null ){
|
|
rows = options.rows;
|
|
cols = Math.ceil( cells / rows );
|
|
} else if( options.rows == null && options.columns != null ){
|
|
cols = options.columns;
|
|
rows = Math.ceil( cells / cols );
|
|
}
|
|
|
|
// otherwise use the automatic values and adjust accordingly
|
|
|
|
// if rounding was up, see if we can reduce rows or columns
|
|
else if( cols * rows > cells ){
|
|
var sm = small();
|
|
var lg = large();
|
|
|
|
// reducing the small side takes away the most cells, so try it first
|
|
if( (sm - 1) * lg >= cells ){
|
|
small(sm - 1);
|
|
} else if( (lg - 1) * sm >= cells ){
|
|
large(lg - 1);
|
|
}
|
|
} else {
|
|
|
|
// if rounding was too low, add rows or columns
|
|
while( cols * rows < cells ){
|
|
var sm = small();
|
|
var lg = large();
|
|
|
|
// try to add to larger side first (adds less in multiplication)
|
|
if( (lg + 1) * sm >= cells ){
|
|
large(lg + 1);
|
|
} else {
|
|
small(sm + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
var cellWidth = bb.w / cols;
|
|
var cellHeight = bb.h / rows;
|
|
|
|
if( options.avoidOverlap ){
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var node = nodes[i];
|
|
var w = node.outerWidth();
|
|
var h = node.outerHeight();
|
|
|
|
cellWidth = Math.max( cellWidth, w );
|
|
cellHeight = Math.max( cellHeight, h );
|
|
}
|
|
}
|
|
|
|
var cellUsed = {}; // e.g. 'c-0-2' => true
|
|
|
|
var used = function(row, col){
|
|
return cellUsed['c-' + row + '-' + col] ? true : false;
|
|
};
|
|
|
|
var use = function(row, col){
|
|
cellUsed['c-' + row + '-' + col] = true;
|
|
};
|
|
|
|
// to keep track of current cell position
|
|
var row = 0;
|
|
var col = 0;
|
|
var moveToNextCell = function(){
|
|
col++;
|
|
if( col >= cols ){
|
|
col = 0;
|
|
row++;
|
|
}
|
|
};
|
|
|
|
// get a cache of all the manual positions
|
|
var id2manPos = {};
|
|
for( var i = 0; i < nodes.length; i++ ){
|
|
var node = nodes[i];
|
|
var rcPos = options.position( node );
|
|
|
|
if( rcPos && (rcPos.row !== undefined || rcPos.col !== undefined) ){ // must have at least row or col def'd
|
|
var pos = {
|
|
row: rcPos.row,
|
|
col: rcPos.col
|
|
};
|
|
|
|
if( pos.col === undefined ){ // find unused col
|
|
pos.col = 0;
|
|
|
|
while( used(pos.row, pos.col) ){
|
|
pos.col++;
|
|
}
|
|
} else if( pos.row === undefined ){ // find unused row
|
|
pos.row = 0;
|
|
|
|
while( used(pos.row, pos.col) ){
|
|
pos.row++;
|
|
}
|
|
}
|
|
|
|
id2manPos[ node.id() ] = pos;
|
|
use( pos.row, pos.col );
|
|
}
|
|
}
|
|
|
|
var getPos = function(i, element){
|
|
var x, y;
|
|
|
|
if( element.locked() || element.isFullAutoParent() ){
|
|
return false;
|
|
}
|
|
|
|
// see if we have a manual position set
|
|
var rcPos = id2manPos[ element.id() ];
|
|
if( rcPos ){
|
|
x = rcPos.col * cellWidth + cellWidth/2 + bb.x1;
|
|
y = rcPos.row * cellHeight + cellHeight/2 + bb.y1;
|
|
|
|
} else { // otherwise set automatically
|
|
|
|
while( used(row, col) ){
|
|
moveToNextCell();
|
|
}
|
|
|
|
x = col * cellWidth + cellWidth/2 + bb.x1;
|
|
y = row * cellHeight + cellHeight/2 + bb.y1;
|
|
use( row, col );
|
|
|
|
moveToNextCell();
|
|
}
|
|
|
|
return { x: x, y: y };
|
|
|
|
};
|
|
|
|
nodes.layoutPositions( this, options, getPos );
|
|
}
|
|
|
|
return this; // chaining
|
|
|
|
};
|
|
|
|
$$('layout', 'grid', GridLayout);
|
|
|
|
})( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
// default layout options
|
|
var defaults = {
|
|
ready: function(){}, // on layoutready
|
|
stop: function(){} // on layoutstop
|
|
};
|
|
|
|
// constructor
|
|
// options : object containing layout options
|
|
function NullLayout( options ){
|
|
this.options = $$.util.extend(true, {}, defaults, options);
|
|
}
|
|
|
|
// runs the layout
|
|
NullLayout.prototype.run = function(){
|
|
var options = this.options;
|
|
var eles = options.eles; // elements to consider in the layout
|
|
var layout = this;
|
|
|
|
// cy is automatically populated for us in the constructor
|
|
var cy = options.cy; // jshint ignore:line
|
|
|
|
layout.trigger('layoutstart');
|
|
|
|
// puts all nodes at (0, 0)
|
|
eles.nodes().positions(function(){
|
|
return {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
});
|
|
|
|
// trigger layoutready when each node has had its position set at least once
|
|
layout.one('layoutready', options.ready);
|
|
layout.trigger('layoutready');
|
|
|
|
// trigger layoutstop when the layout stops (e.g. finishes)
|
|
layout.one('layoutstop', options.stop);
|
|
layout.trigger('layoutstop');
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
// called on continuous layouts to stop them before they finish
|
|
NullLayout.prototype.stop = function(){
|
|
return this; // chaining
|
|
};
|
|
|
|
// register the layout
|
|
$$('layout', 'null', NullLayout);
|
|
|
|
})(cytoscape);
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var defaults = {
|
|
positions: undefined, // map of (node id) => (position obj); or function(node){ return somPos; }
|
|
zoom: undefined, // the zoom level to set (prob want fit = false if set)
|
|
pan: undefined, // the pan level to set (prob want fit = false if set)
|
|
fit: true, // whether to fit to viewport
|
|
padding: 30, // padding on fit
|
|
animate: false, // whether to transition the node positions
|
|
animationDuration: 500, // duration of animation in ms if enabled
|
|
ready: undefined, // callback on layoutready
|
|
stop: undefined // callback on layoutstop
|
|
};
|
|
|
|
function PresetLayout( options ){
|
|
this.options = $$.util.extend(true, {}, defaults, options);
|
|
}
|
|
|
|
PresetLayout.prototype.run = function(){
|
|
var options = this.options;
|
|
var eles = options.eles;
|
|
|
|
var nodes = eles.nodes();
|
|
var posIsFn = $$.is.fn( options.positions );
|
|
|
|
function getPosition(node){
|
|
if( options.positions == null ){
|
|
return null;
|
|
}
|
|
|
|
if( posIsFn ){
|
|
return options.positions.apply( node, [ node ] );
|
|
}
|
|
|
|
var pos = options.positions[node._private.data.id];
|
|
|
|
if( pos == null ){
|
|
return null;
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
nodes.layoutPositions(this, options, function(i, node){
|
|
var position = getPosition(node);
|
|
|
|
if( node.locked() || position == null ){
|
|
return false;
|
|
}
|
|
|
|
return position;
|
|
});
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
$$('layout', 'preset', PresetLayout);
|
|
|
|
})(cytoscape);
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var defaults = {
|
|
fit: true, // whether to fit to viewport
|
|
padding: 30, // fit padding
|
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
animate: false, // whether to transition the node positions
|
|
animationDuration: 500, // duration of animation in ms if enabled
|
|
ready: undefined, // callback on layoutready
|
|
stop: undefined // callback on layoutstop
|
|
};
|
|
|
|
function RandomLayout( options ){
|
|
this.options = $$.util.extend(true, {}, defaults, options);
|
|
}
|
|
|
|
RandomLayout.prototype.run = function(){
|
|
var options = this.options;
|
|
var cy = options.cy;
|
|
var eles = options.eles;
|
|
var nodes = eles.nodes().not(':parent');
|
|
|
|
var bb = $$.util.makeBoundingBox( options.boundingBox ? options.boundingBox : {
|
|
x1: 0, y1: 0, w: cy.width(), h: cy.height()
|
|
} );
|
|
|
|
var getPos = function( i, node ){
|
|
return {
|
|
x: bb.x1 + Math.round( Math.random() * bb.w ),
|
|
y: bb.y1 + Math.round( Math.random() * bb.h )
|
|
};
|
|
};
|
|
|
|
nodes.layoutPositions( this, options, getPos );
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
// register the layout
|
|
$$(
|
|
'layout', // we're registering a layout
|
|
'random', // the layout name
|
|
RandomLayout // the layout prototype
|
|
);
|
|
|
|
})(cytoscape);
|
|
|
|
;( function( $$ ){ 'use strict';
|
|
|
|
/*
|
|
* This layout combines several algorithms:
|
|
*
|
|
* - It generates an initial position of the nodes by using the
|
|
* Fruchterman-Reingold algorithm (doi:10.1002/spe.4380211102)
|
|
*
|
|
* - Finally it eliminates overlaps by using the method described by
|
|
* Gansner and North (doi:10.1007/3-540-37623-2_28)
|
|
*/
|
|
|
|
var defaults = {
|
|
animate: true, // whether to show the layout as it's running
|
|
ready: undefined, // Callback on layoutready
|
|
stop: undefined, // Callback on layoutstop
|
|
fit: true, // Reset viewport to fit default simulationBounds
|
|
minDist: 20, // Minimum distance between nodes
|
|
padding: 20, // Padding
|
|
expandingFactor: -1.0, // If the network does not satisfy the minDist
|
|
// criterium then it expands the network of this amount
|
|
// If it is set to -1.0 the amount of expansion is automatically
|
|
// calculated based on the minDist, the aspect ratio and the
|
|
// number of nodes
|
|
maxFruchtermanReingoldIterations: 50, // Maximum number of initial force-directed iterations
|
|
maxExpandIterations: 4, // Maximum number of expanding iterations
|
|
boundingBox: undefined // Constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
};
|
|
|
|
function SpreadLayout( options ) {
|
|
this.options = $$.util.extend( {}, defaults, options );
|
|
}
|
|
|
|
function cellCentroid( cell ) {
|
|
var hes = cell.halfedges;
|
|
var area = 0,
|
|
x = 0,
|
|
y = 0;
|
|
var p1, p2, f;
|
|
|
|
for( var i = 0; i < hes.length; ++i ) {
|
|
p1 = hes[ i ].getEndpoint();
|
|
p2 = hes[ i ].getStartpoint();
|
|
|
|
area += p1.x * p2.y;
|
|
area -= p1.y * p2.x;
|
|
|
|
f = p1.x * p2.y - p2.x * p1.y;
|
|
x += ( p1.x + p2.x ) * f;
|
|
y += ( p1.y + p2.y ) * f;
|
|
}
|
|
|
|
area /= 2;
|
|
f = area * 6;
|
|
return {
|
|
x: x / f,
|
|
y: y / f
|
|
};
|
|
}
|
|
|
|
function sitesDistance( ls, rs ) {
|
|
var dx = ls.x - rs.x;
|
|
var dy = ls.y - rs.y;
|
|
return Math.sqrt( dx * dx + dy * dy );
|
|
}
|
|
|
|
SpreadLayout.prototype.run = function() {
|
|
|
|
var layout = this;
|
|
// var self = this;
|
|
var options = this.options;
|
|
|
|
$$.util.requires(['foograph', 'Voronoi'], function(foograph, Voronoi){
|
|
|
|
var cy = options.cy;
|
|
// var allNodes = cy.nodes();
|
|
var nodes = cy.nodes();
|
|
//var allEdges = cy.edges();
|
|
var edges = cy.edges();
|
|
var cWidth = cy.width();
|
|
var cHeight = cy.height();
|
|
var simulationBounds = options.boundingBox ? $$.util.makeBoundingBox( options.boundingBox ) : null;
|
|
var padding = options.padding;
|
|
var simBBFactor = Math.max( 1, Math.log(nodes.length) * 0.8 );
|
|
|
|
if( nodes.length < 100 ){
|
|
simBBFactor /= 2;
|
|
}
|
|
|
|
layout.trigger( {
|
|
type: 'layoutstart',
|
|
layout: layout
|
|
} );
|
|
|
|
var simBB = {
|
|
x1: 0,
|
|
y1: 0,
|
|
x2: cWidth * simBBFactor,
|
|
y2: cHeight * simBBFactor
|
|
};
|
|
|
|
if( simulationBounds ) {
|
|
simBB.x1 = simulationBounds.x1;
|
|
simBB.y1 = simulationBounds.y1;
|
|
simBB.x2 = simulationBounds.x2;
|
|
simBB.y2 = simulationBounds.y2;
|
|
}
|
|
|
|
simBB.x1 += padding;
|
|
simBB.y1 += padding;
|
|
simBB.x2 -= padding;
|
|
simBB.y2 -= padding;
|
|
|
|
var width = simBB.x2 - simBB.x1;
|
|
var height = simBB.y2 - simBB.y1;
|
|
|
|
// Get start time
|
|
var startTime = Date.now();
|
|
|
|
// layout doesn't work with just 1 node
|
|
if( nodes.size() <= 1 ) {
|
|
nodes.positions( {
|
|
x: Math.round( ( simBB.x1 + simBB.x2 ) / 2 ),
|
|
y: Math.round( ( simBB.y1 + simBB.y2 ) / 2 )
|
|
} );
|
|
|
|
if( options.fit ) {
|
|
cy.fit( options.padding );
|
|
}
|
|
|
|
// Get end time
|
|
var endTime = Date.now();
|
|
console.info( "Layout on " + nodes.size() + " nodes took " + ( endTime - startTime ) + " ms" );
|
|
|
|
layout.one( "layoutready", options.ready );
|
|
layout.trigger( "layoutready" );
|
|
|
|
layout.one( "layoutstop", options.stop );
|
|
layout.trigger( "layoutstop" );
|
|
|
|
return;
|
|
}
|
|
|
|
// First I need to create the data structure to pass to the worker
|
|
var pData = {
|
|
'width': width,
|
|
'height': height,
|
|
'minDist': options.minDist,
|
|
'expFact': options.expandingFactor,
|
|
'expIt': 0,
|
|
'maxExpIt': options.maxExpandIterations,
|
|
'vertices': [],
|
|
'edges': [],
|
|
'startTime': startTime,
|
|
'maxFruchtermanReingoldIterations': options.maxFruchtermanReingoldIterations
|
|
};
|
|
|
|
nodes.each(
|
|
function( i, node ) {
|
|
var nodeId = this._private.data.id;
|
|
pData[ 'vertices' ].push( {
|
|
id: nodeId,
|
|
x: 0,
|
|
y: 0
|
|
} );
|
|
} );
|
|
|
|
edges.each(
|
|
function() {
|
|
var srcNodeId = this.source().id();
|
|
var tgtNodeId = this.target().id();
|
|
pData[ 'edges' ].push( {
|
|
src: srcNodeId,
|
|
tgt: tgtNodeId
|
|
} );
|
|
} );
|
|
|
|
//Decleration
|
|
var t1 = $$.Thread();
|
|
// And to add the required scripts
|
|
//EXTERNAL 1
|
|
t1.require( foograph, 'foograph' );
|
|
//EXTERNAL 2
|
|
t1.require( Voronoi );
|
|
|
|
//Local function
|
|
t1.require( sitesDistance );
|
|
t1.require( cellCentroid );
|
|
|
|
function setPositions( pData ){ //console.log('set posns')
|
|
// First we retrieve the important data
|
|
// var expandIteration = pData[ 'expIt' ];
|
|
var dataVertices = pData[ 'vertices' ];
|
|
var vertices = [];
|
|
for( var i = 0; i < dataVertices.length; ++i ) {
|
|
var dv = dataVertices[ i ];
|
|
vertices[ dv.id ] = {
|
|
x: dv.x,
|
|
y: dv.y
|
|
};
|
|
}
|
|
/*
|
|
* FINALLY:
|
|
*
|
|
* We position the nodes based on the calculation
|
|
*/
|
|
nodes.positions(
|
|
function( i, node ) {
|
|
var id = node._private.data.id;
|
|
// var pos = node._private.position;
|
|
var vertex = vertices[ id ];
|
|
|
|
return {
|
|
x: Math.round( simBB.x1 + vertex.x ),
|
|
y: Math.round( simBB.y1 + vertex.y )
|
|
};
|
|
} );
|
|
|
|
if( options.fit ) {
|
|
cy.fit( options.padding );
|
|
}
|
|
|
|
cy.nodes().rtrigger( "position" );
|
|
}
|
|
|
|
var didLayoutReady = false;
|
|
t1.on('message', function(e){
|
|
var pData = e.message; //console.log('message', e)
|
|
|
|
if( !options.animate ){
|
|
return;
|
|
}
|
|
|
|
setPositions( pData );
|
|
|
|
if( !didLayoutReady ){
|
|
layout.trigger( "layoutready" );
|
|
|
|
didLayoutReady = true;
|
|
}
|
|
});
|
|
|
|
layout.one( "layoutready", options.ready );
|
|
|
|
t1.pass( pData ).run( function( pData ) {
|
|
|
|
foograph = eval('foograph');
|
|
Voronoi = eval('Voronoi');
|
|
|
|
// I need to retrieve the important data
|
|
var lWidth = pData[ 'width' ];
|
|
var lHeight = pData[ 'height' ];
|
|
var lMinDist = pData[ 'minDist' ];
|
|
var lExpFact = pData[ 'expFact' ];
|
|
var lMaxExpIt = pData[ 'maxExpIt' ];
|
|
var lMaxFruchtermanReingoldIterations = pData[ 'maxFruchtermanReingoldIterations' ];
|
|
|
|
// Prepare the data to output
|
|
var savePositions = function(){
|
|
pData[ 'width' ] = lWidth;
|
|
pData[ 'height' ] = lHeight;
|
|
pData[ 'expIt' ] = expandIteration;
|
|
pData[ 'expFact' ] = lExpFact;
|
|
|
|
pData[ 'vertices' ] = [];
|
|
for( var i = 0; i < fv.length; ++i ) {
|
|
pData[ 'vertices' ].push( {
|
|
id: fv[ i ].label,
|
|
x: fv[ i ].x,
|
|
y: fv[ i ].y
|
|
} );
|
|
}
|
|
};
|
|
|
|
var messagePositions = function(){
|
|
broadcast( pData );
|
|
};
|
|
|
|
/*
|
|
* FIRST STEP: Application of the Fruchterman-Reingold algorithm
|
|
*
|
|
* We use the version implemented by the foograph library
|
|
*
|
|
* Ref.: https://code.google.com/p/foograph/
|
|
*/
|
|
|
|
// We need to create an instance of a graph compatible with the library
|
|
var frg = new foograph.Graph( "FRgraph", false );
|
|
|
|
var frgNodes = {};
|
|
|
|
// Then we have to add the vertices
|
|
var dataVertices = pData[ 'vertices' ];
|
|
for( var ni = 0; ni < dataVertices.length; ++ni ) {
|
|
var id = dataVertices[ ni ][ 'id' ];
|
|
var v = new foograph.Vertex( id, Math.round( Math.random() * lHeight ), Math.round( Math.random() * lHeight ) );
|
|
frgNodes[ id ] = v;
|
|
frg.insertVertex( v );
|
|
}
|
|
|
|
var dataEdges = pData[ 'edges' ];
|
|
for( var ei = 0; ei < dataEdges.length; ++ei ) {
|
|
var srcNodeId = dataEdges[ ei ][ 'src' ];
|
|
var tgtNodeId = dataEdges[ ei ][ 'tgt' ];
|
|
frg.insertEdge( "", 1, frgNodes[ srcNodeId ], frgNodes[ tgtNodeId ] );
|
|
}
|
|
|
|
var fv = frg.vertices;
|
|
|
|
// Then we apply the layout
|
|
var iterations = lMaxFruchtermanReingoldIterations;
|
|
var frLayoutManager = new foograph.ForceDirectedVertexLayout( lWidth, lHeight, iterations, false, lMinDist );
|
|
|
|
frLayoutManager.callback = function(){
|
|
savePositions();
|
|
messagePositions();
|
|
};
|
|
|
|
frLayoutManager.layout( frg );
|
|
|
|
savePositions();
|
|
messagePositions();
|
|
|
|
/*
|
|
* SECOND STEP: Tiding up of the graph.
|
|
*
|
|
* We use the method described by Gansner and North, based on Voronoi
|
|
* diagrams.
|
|
*
|
|
* Ref: doi:10.1007/3-540-37623-2_28
|
|
*/
|
|
|
|
// We calculate the Voronoi diagram dor the position of the nodes
|
|
var voronoi = new Voronoi();
|
|
var bbox = {
|
|
xl: 0,
|
|
xr: lWidth,
|
|
yt: 0,
|
|
yb: lHeight
|
|
};
|
|
var vSites = [];
|
|
for( var i = 0; i < fv.length; ++i ) {
|
|
vSites[ fv[ i ].label ] = fv[ i ];
|
|
}
|
|
|
|
function checkMinDist( ee ) {
|
|
var infractions = 0;
|
|
// Then we check if the minimum distance is satisfied
|
|
for( var eei = 0; eei < ee.length; ++eei ) {
|
|
var e = ee[ eei ];
|
|
if( ( e.lSite != null ) && ( e.rSite != null ) && sitesDistance( e.lSite, e.rSite ) < lMinDist ) {
|
|
++infractions;
|
|
}
|
|
}
|
|
return infractions;
|
|
}
|
|
|
|
var diagram = voronoi.compute( fv, bbox );
|
|
|
|
// Then we reposition the nodes at the centroid of their Voronoi cells
|
|
var cells = diagram.cells;
|
|
for( var i = 0; i < cells.length; ++i ) {
|
|
var cell = cells[ i ];
|
|
var site = cell.site;
|
|
var centroid = cellCentroid( cell );
|
|
var currv = vSites[ site.label ];
|
|
currv.x = centroid.x;
|
|
currv.y = centroid.y;
|
|
}
|
|
|
|
if( lExpFact < 0.0 ) {
|
|
// Calculates the expanding factor
|
|
lExpFact = Math.max( 0.05, Math.min( 0.10, lMinDist / Math.sqrt( ( lWidth * lHeight ) / fv.length ) * 0.5 ) );
|
|
//console.info("Expanding factor is " + (options.expandingFactor * 100.0) + "%");
|
|
}
|
|
|
|
var prevInfractions = checkMinDist( diagram.edges );
|
|
//console.info("Initial infractions " + prevInfractions);
|
|
|
|
var bStop = ( prevInfractions <= 0 );
|
|
|
|
var voronoiIteration = 0;
|
|
var expandIteration = 0;
|
|
|
|
// var initWidth = lWidth;
|
|
|
|
while( !bStop ) {
|
|
++voronoiIteration;
|
|
for( var it = 0; it <= 4; ++it ) {
|
|
voronoi.recycle( diagram );
|
|
diagram = voronoi.compute( fv, bbox );
|
|
|
|
// Then we reposition the nodes at the centroid of their Voronoi cells
|
|
cells = diagram.cells;
|
|
for( var i = 0; i < cells.length; ++i ) {
|
|
var cell = cells[ i ];
|
|
var site = cell.site;
|
|
var centroid = cellCentroid( cell );
|
|
var currv = vSites[ site.label ];
|
|
currv.x = centroid.x;
|
|
currv.y = centroid.y;
|
|
}
|
|
}
|
|
|
|
var currInfractions = checkMinDist( diagram.edges );
|
|
//console.info("Current infractions " + currInfractions);
|
|
|
|
if( currInfractions <= 0 ) {
|
|
bStop = true;
|
|
} else {
|
|
if( currInfractions >= prevInfractions || voronoiIteration >= 4 ) {
|
|
if( expandIteration >= lMaxExpIt ) {
|
|
bStop = true;
|
|
} else {
|
|
lWidth += lWidth * lExpFact;
|
|
lHeight += lHeight * lExpFact;
|
|
bbox = {
|
|
xl: 0,
|
|
xr: lWidth,
|
|
yt: 0,
|
|
yb: lHeight
|
|
};
|
|
++expandIteration;
|
|
voronoiIteration = 0;
|
|
//console.info("Expanded to ("+width+","+height+")");
|
|
}
|
|
}
|
|
}
|
|
prevInfractions = currInfractions;
|
|
|
|
savePositions();
|
|
messagePositions();
|
|
}
|
|
|
|
savePositions();
|
|
return pData;
|
|
|
|
} ).then( function( pData ) {
|
|
// var expandIteration = pData[ 'expIt' ];
|
|
var dataVertices = pData[ 'vertices' ];
|
|
|
|
setPositions( pData );
|
|
|
|
// Get end time
|
|
var startTime = pData[ 'startTime' ];
|
|
var endTime = new Date();
|
|
console.info( "Layout on " + dataVertices.length + " nodes took " + ( endTime - startTime ) + " ms" );
|
|
|
|
layout.one( "layoutstop", options.stop );
|
|
|
|
if( !options.animate ){
|
|
layout.trigger( "layoutready" );
|
|
}
|
|
|
|
layout.trigger( "layoutstop" );
|
|
|
|
t1.stop();
|
|
} );
|
|
|
|
});
|
|
|
|
return this;
|
|
}; // run
|
|
|
|
SpreadLayout.prototype.stop = function() {};
|
|
|
|
$$( 'layout', 'spread', SpreadLayout );
|
|
|
|
|
|
} )( cytoscape );
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
var defaults = {
|
|
animate: true, // whether to show the layout as it's running
|
|
maxSimulationTime: 4000, // max length in ms to run the layout
|
|
ungrabifyWhileSimulating: false, // so you can't drag nodes during layout
|
|
fit: true, // whether to fit the viewport to the graph
|
|
padding: 30, // padding on fit
|
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
|
random: false, // whether to use random initial positions
|
|
infinite: false, // overrides all other options for a forces-all-the-time mode
|
|
ready: undefined, // callback on layoutready
|
|
stop: undefined, // callback on layoutstop
|
|
|
|
// springy forces
|
|
stiffness: 400,
|
|
repulsion: 400,
|
|
damping: 0.5
|
|
};
|
|
|
|
function SpringyLayout( options ){
|
|
this.options = $$.util.extend(true, {}, defaults, options);
|
|
}
|
|
|
|
SpringyLayout.prototype.run = function(){
|
|
var layout = this;
|
|
var self = this;
|
|
var options = this.options;
|
|
|
|
$$.util.require('Springy', function(Springy){
|
|
|
|
var simUpdatingPos = false;
|
|
|
|
var cy = options.cy;
|
|
layout.trigger({ type: 'layoutstart', layout: layout });
|
|
|
|
var eles = options.eles;
|
|
var nodes = eles.nodes().not(':parent');
|
|
var edges = eles.edges();
|
|
|
|
var bb = $$.util.makeBoundingBox( options.boundingBox ? options.boundingBox : {
|
|
x1: 0, y1: 0, w: cy.width(), h: cy.height()
|
|
} );
|
|
|
|
// make a new graph
|
|
var graph = new Springy.Graph();
|
|
|
|
// make some nodes
|
|
nodes.each(function(i, node){
|
|
node.scratch('springy', {
|
|
model: graph.newNode({
|
|
element: node
|
|
})
|
|
});
|
|
});
|
|
|
|
// connect them with edges
|
|
edges.each(function(i, edge){
|
|
var fdSrc = edge.source().scratch('springy').model;
|
|
var fdTgt = edge.target().scratch('springy').model;
|
|
|
|
edge.scratch('springy', {
|
|
model: graph.newEdge(fdSrc, fdTgt, {
|
|
element: edge
|
|
})
|
|
});
|
|
});
|
|
|
|
var sim = window.sim = new Springy.Layout.ForceDirected(graph, options.stiffness, options.repulsion, options.damping);
|
|
|
|
if( options.infinite ){
|
|
sim.minEnergyThreshold = -Infinity;
|
|
}
|
|
|
|
var currentBB = sim.getBoundingBox();
|
|
// var targetBB = {bottomleft: new Springy.Vector(-2, -2), topright: new Springy.Vector(2, 2)};
|
|
|
|
// convert to/from screen coordinates
|
|
var toScreen = function(p) {
|
|
currentBB = sim.getBoundingBox();
|
|
|
|
var size = currentBB.topright.subtract(currentBB.bottomleft);
|
|
var sx = p.subtract(currentBB.bottomleft).divide(size.x).x * bb.w + bb.x1;
|
|
var sy = p.subtract(currentBB.bottomleft).divide(size.y).y * bb.h + bb.x1;
|
|
|
|
return new Springy.Vector(sx, sy);
|
|
};
|
|
|
|
var fromScreen = function(s) {
|
|
currentBB = sim.getBoundingBox();
|
|
|
|
var size = currentBB.topright.subtract(currentBB.bottomleft);
|
|
var px = ((s.x - bb.x1) / bb.w) * size.x + currentBB.bottomleft.x;
|
|
var py = ((s.y - bb.y1) / bb.h) * size.y + currentBB.bottomleft.y;
|
|
|
|
return new Springy.Vector(px, py);
|
|
};
|
|
|
|
var movedNodes = cy.collection();
|
|
|
|
var numNodes = cy.nodes().size();
|
|
var drawnNodes = 1;
|
|
var fdRenderer = new Springy.Renderer(sim,
|
|
function clear() {
|
|
if( self.stopped ){ return; } // because springy is a buggy layout
|
|
|
|
if( movedNodes.length > 0 && options.animate ){
|
|
simUpdatingPos = true;
|
|
|
|
movedNodes.rtrigger('position');
|
|
|
|
if( options.fit ){
|
|
cy.fit( options.padding );
|
|
}
|
|
|
|
movedNodes = cy.collection();
|
|
|
|
simUpdatingPos = false;
|
|
}
|
|
},
|
|
|
|
function drawEdge(edge, p1, p2) {
|
|
// draw an edge
|
|
},
|
|
|
|
function drawNode(node, p) {
|
|
if( self.stopped ){ return; } // because springy is a buggy layout
|
|
|
|
var v = toScreen(p);
|
|
var element = node.data.element;
|
|
|
|
if( !element.locked() && !element.grabbed() ){
|
|
element._private.position = {
|
|
x: v.x,
|
|
y: v.y
|
|
};
|
|
movedNodes.merge(element);
|
|
} else {
|
|
//setLayoutPositionForElement(element);
|
|
}
|
|
|
|
if( drawnNodes == numNodes ){
|
|
layout.one('layoutready', options.ready);
|
|
layout.trigger({ type: 'layoutready', layout: layout });
|
|
}
|
|
|
|
drawnNodes++;
|
|
|
|
}
|
|
);
|
|
|
|
// set initial node points
|
|
nodes.each(function(i, ele){
|
|
if( !options.random ){
|
|
setLayoutPositionForElement(ele);
|
|
}
|
|
});
|
|
|
|
// update node positions when dragging
|
|
var dragHandler;
|
|
nodes.on('position', dragHandler = function(){
|
|
if( simUpdatingPos ){ return; }
|
|
|
|
setLayoutPositionForElement(this);
|
|
});
|
|
|
|
function setLayoutPositionForElement(element){
|
|
var fdId = element.scratch('springy').model.id;
|
|
var fdP = fdRenderer.layout.nodePoints[fdId].p;
|
|
var pos = element.position();
|
|
var positionInFd = (pos.x != null && pos.y != null) ? fromScreen(element.position()) : {
|
|
x: Math.random() * 4 - 2,
|
|
y: Math.random() * 4 - 2
|
|
};
|
|
|
|
fdP.x = positionInFd.x;
|
|
fdP.y = positionInFd.y;
|
|
}
|
|
|
|
var grabbableNodes = nodes.filter(":grabbable");
|
|
|
|
function start(){
|
|
self.stopped = false;
|
|
|
|
// disable grabbing if so set
|
|
if( options.ungrabifyWhileSimulating ){
|
|
grabbableNodes.ungrabify();
|
|
}
|
|
|
|
fdRenderer.start();
|
|
}
|
|
|
|
self.stopSystem = function(){
|
|
self.stopped = true;
|
|
|
|
graph.filterNodes(function(){
|
|
return false; // remove all nodes
|
|
});
|
|
|
|
if( options.ungrabifyWhileSimulating ){
|
|
grabbableNodes.grabify();
|
|
}
|
|
|
|
if( options.fit ){
|
|
cy.fit( options.padding );
|
|
}
|
|
|
|
nodes.off('drag position', dragHandler);
|
|
|
|
layout.one('layoutstop', options.stop);
|
|
layout.trigger({ type: 'layoutstop', layout: layout });
|
|
|
|
self.stopSystem = null;
|
|
};
|
|
|
|
start();
|
|
if( !options.infinite ){
|
|
setTimeout(function(){
|
|
self.stop();
|
|
}, options.maxSimulationTime);
|
|
}
|
|
|
|
}); // require
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
SpringyLayout.prototype.stop = function(){
|
|
if( this.stopSystem != null ){
|
|
this.stopSystem();
|
|
}
|
|
|
|
return this; // chaining
|
|
};
|
|
|
|
$$('layout', 'springy', SpringyLayout);
|
|
|
|
|
|
})(cytoscape);
|
|
|
|
;(function($$){ 'use strict';
|
|
|
|
function NullRenderer(options){
|
|
this.options = options;
|
|
}
|
|
|
|
NullRenderer.prototype.recalculateRenderedStyle = function(){
|
|
};
|
|
|
|
NullRenderer.prototype.notify = function(){
|
|
// the null renderer does nothing
|
|
};
|
|
|
|
$$('renderer', 'null', NullRenderer);
|
|
|
|
})( cytoscape );
|