import * as util from '../../util';
import * as math from '../../math';
import * as is from '../../is';
const defaults = {
fit: true, directed: false, padding: 30, circle: false, grid: false, spacingFactor: 1.75, boundingBox: undefined, avoidOverlap: true, nodeDimensionsIncludeLabels: false, roots: undefined, depthSort: undefined, animate: false, animationDuration: 500, animationEasing: undefined, animateFilter: function ( node, i ){ return true; }, ready: undefined, stop: undefined, transform: function (node, position ){ return position; } };
const deprecatedOptionDefaults = {
maximal: false, acyclic: false, };
const getInfo = ele => ele.scratch('breadthfirst');
const setInfo = (ele, obj) => ele.scratch('breadthfirst', obj);
function BreadthFirstLayout( options ){
this.options = util.extend( {}, defaults, deprecatedOptionDefaults, options );
}
BreadthFirstLayout.prototype.run = function(){
let params = this.options;
let options = params;
let cy = params.cy;
let eles = options.eles;
let nodes = eles.nodes().filter( n => !n.isParent() );
let graph = eles;
let directed = options.directed;
let maximal = options.acyclic || options.maximal || options.maximalAdjustments > 0;
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
let roots;
if( is.elementOrCollection( options.roots ) ){
roots = options.roots;
} else if( is.array( options.roots ) ){
let rootsArray = [];
for( let i = 0; i < options.roots.length; i++ ){
let id = options.roots[ i ];
let ele = cy.getElementById( id );
rootsArray.push( ele );
}
roots = cy.collection( rootsArray );
} else if( is.string( options.roots ) ){
roots = cy.$( options.roots );
} else {
if( directed ){
roots = nodes.roots();
} else {
let components = eles.components();
roots = cy.collection();
for( let i = 0; i < components.length; i++ ){
let comp = components[i];
let maxDegree = comp.maxDegree( false );
let compRoots = comp.filter( function( ele ){
return ele.degree( false ) === maxDegree;
} );
roots = roots.add( compRoots );
}
}
}
let depths = [];
let foundByBfs = {};
let addToDepth = ( ele, d ) => {
if( depths[d] == null ){
depths[d] = [];
}
let i = depths[d].length;
depths[d].push( ele );
setInfo( ele, {
index: i,
depth: d
} );
};
let changeDepth = ( ele, newDepth ) => {
let { depth, index } = getInfo( ele );
depths[ depth ][ index ] = null;
addToDepth( ele, newDepth );
};
graph.bfs( {
roots: roots,
directed: options.directed,
visit: function( node, edge, pNode, i, depth ){
let ele = node[0];
let id = ele.id();
addToDepth( ele, depth );
foundByBfs[ id ] = true;
}
} );
let orphanNodes = [];
for( let i = 0; i < nodes.length; i++ ){
let ele = nodes[ i ];
if( foundByBfs[ ele.id() ] ){
continue;
} else {
orphanNodes.push( ele );
}
}
let assignDepthsAt = function( i ){
let eles = depths[ i ];
for( let j = 0; j < eles.length; j++ ){
let ele = eles[ j ];
if( ele == null ){
eles.splice( j, 1 );
j--;
continue;
}
setInfo(ele, {
depth: i,
index: j
});
}
};
let assignDepths = function(){
for( let i = 0; i < depths.length; i++ ){
assignDepthsAt( i );
}
};
let adjustMaximally = function( ele, shifted ){
let eInfo = getInfo( ele );
let incomers = ele.incomers().filter( el => el.isNode() && eles.has(el) );
let maxDepth = -1;
let id = ele.id();
for( let k = 0; k < incomers.length; k++ ){
let incmr = incomers[k];
let iInfo = getInfo( incmr );
maxDepth = Math.max( maxDepth, iInfo.depth );
}
if( eInfo.depth <= maxDepth ){
if( !options.acyclic && shifted[id] ){
return null;
}
let newDepth = maxDepth + 1;
changeDepth( ele, newDepth );
shifted[id] = newDepth;
return true;
}
return false;
};
if( directed && maximal ){
let Q = [];
let shifted = {};
let enqueue = n => Q.push(n);
let dequeue = () => Q.shift();
nodes.forEach( n => Q.push(n) );
while( Q.length > 0 ){
let ele = dequeue();
let didShift = adjustMaximally( ele, shifted );
if( didShift ){
ele.outgoers().filter( el => el.isNode() && eles.has(el) ).forEach( enqueue );
} else if( didShift === null ){
util.warn('Detected double maximal shift for node `' + ele.id() + '`. Bailing maximal adjustment due to cycle. Use `options.maximal: true` only on DAGs.');
break; }
}
}
assignDepths();
let minDistance = 0;
if( options.avoidOverlap ){
for( let i = 0; i < nodes.length; i++ ){
let n = nodes[ i ];
let nbb = n.layoutDimensions( options );
let w = nbb.w;
let h = nbb.h;
minDistance = Math.max( minDistance, w, h );
}
}
let cachedWeightedPercent = {};
let getWeightedPercent = function( ele ){
if( cachedWeightedPercent[ ele.id() ] ){
return cachedWeightedPercent[ ele.id() ];
}
let eleDepth = getInfo( ele ).depth;
let neighbors = ele.neighborhood();
let percent = 0;
let samples = 0;
for( let i = 0; i < neighbors.length; i++ ){
let neighbor = neighbors[ i ];
if( neighbor.isEdge() || neighbor.isParent() || !nodes.has( neighbor ) ){
continue;
}
let bf = getInfo( neighbor );
if (bf == null){ continue; }
let index = bf.index;
let depth = bf.depth;
if( index == null || depth == null ){
continue;
}
let nDepth = depths[ depth ].length;
if( depth < eleDepth ){ percent += index / nDepth;
samples++;
}
}
samples = Math.max( 1, samples );
percent = percent / samples;
if( samples === 0 ){ percent = 0;
}
cachedWeightedPercent[ ele.id() ] = percent;
return percent;
};
let sortFn = function( a, b ){
let apct = getWeightedPercent( a );
let bpct = getWeightedPercent( b );
let diff = apct - bpct;
if( diff === 0 ){
return util.sort.ascending( a.id(), b.id() ); } else {
return diff;
}
};
if (options.depthSort !== undefined) {
sortFn = options.depthSort;
}
for( let i = 0; i < depths.length; i++ ){
depths[ i ].sort( sortFn );
assignDepthsAt( i );
}
let orphanDepth = [];
for( let i = 0; i < orphanNodes.length; i++ ){
orphanDepth.push( orphanNodes[i] );
}
depths.unshift( orphanDepth );
assignDepths();
let biggestDepthSize = 0;
for( let i = 0; i < depths.length; i++ ){
biggestDepthSize = Math.max( depths[ i ].length, biggestDepthSize );
}
let center = {
x: bb.x1 + bb.w / 2,
y: bb.x1 + bb.h / 2
};
let maxDepthSize = depths.reduce( (max, eles) => Math.max(max, eles.length), 0 );
let getPosition = function( ele ){
let { depth, index } = getInfo( ele );
let depthSize = depths[ depth ].length;
let distanceX = Math.max( bb.w / ( (options.grid ? maxDepthSize : depthSize) + 1 ), minDistance );
let distanceY = Math.max( bb.h / (depths.length + 1), minDistance );
let radiusStepSize = Math.min( bb.w / 2 / depths.length, bb.h / 2 / depths.length );
radiusStepSize = Math.max( radiusStepSize, minDistance );
if( !options.circle ){
let epos = {
x: center.x + (index + 1 - (depthSize + 1) / 2) * distanceX,
y: (depth + 1) * distanceY
};
return epos;
} else {
let radius = radiusStepSize * depth + radiusStepSize - (depths.length > 0 && depths[0].length <= 3 ? radiusStepSize / 2 : 0);
let 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 )
};
}
};
eles.nodes().layoutPositions( this, options, getPosition );
return this; };
export default BreadthFirstLayout;