|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import * as util from '../../util'; |
|
import * as math from '../../math'; |
|
import * as is from '../../is'; |
|
|
|
var DEBUG; |
|
|
|
|
|
|
|
|
|
var defaults = { |
|
|
|
ready: function(){}, |
|
|
|
|
|
stop: function(){}, |
|
|
|
|
|
|
|
|
|
|
|
animate: true, |
|
|
|
|
|
animationEasing: undefined, |
|
|
|
|
|
animationDuration: undefined, |
|
|
|
|
|
|
|
|
|
animateFilter: function ( node, i ){ return true; }, |
|
|
|
|
|
|
|
|
|
animationThreshold: 250, |
|
|
|
|
|
refresh: 20, |
|
|
|
|
|
fit: true, |
|
|
|
|
|
padding: 30, |
|
|
|
|
|
boundingBox: undefined, |
|
|
|
|
|
nodeDimensionsIncludeLabels: false, |
|
|
|
|
|
randomize: false, |
|
|
|
|
|
componentSpacing: 40, |
|
|
|
|
|
nodeRepulsion: function( node ){ return 2048; }, |
|
|
|
|
|
nodeOverlap: 4, |
|
|
|
|
|
idealEdgeLength: function( edge ){ return 32; }, |
|
|
|
|
|
edgeElasticity: function( edge ){ return 32; }, |
|
|
|
|
|
nestingFactor: 1.2, |
|
|
|
|
|
gravity: 1, |
|
|
|
|
|
numIter: 1000, |
|
|
|
|
|
initialTemp: 1000, |
|
|
|
|
|
coolingFactor: 0.99, |
|
|
|
|
|
minTemp: 1.0 |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
function CoseLayout( options ){ |
|
this.options = util.extend( {}, defaults, options ); |
|
this.options.layout = this; |
|
|
|
|
|
const nodes = this.options.eles.nodes(); |
|
const edges = this.options.eles.edges(); |
|
const notEdges = edges.filter((e) => { |
|
const sourceId = e.source().data('id'); |
|
const targetId = e.target().data('id'); |
|
const hasSource = nodes.some((n) => n.data('id') === sourceId); |
|
const hasTarget = nodes.some((n) => n.data('id') === targetId); |
|
return !hasSource || !hasTarget; |
|
}); |
|
this.options.eles = this.options.eles.not(notEdges); |
|
} |
|
|
|
|
|
|
|
|
|
CoseLayout.prototype.run = function(){ |
|
var options = this.options; |
|
var cy = options.cy; |
|
var layout = this; |
|
|
|
layout.stopped = false; |
|
|
|
if( options.animate === true || options.animate === false ){ |
|
layout.emit( { type: 'layoutstart', layout: layout } ); |
|
} |
|
|
|
|
|
if( true === options.debug ){ |
|
DEBUG = true; |
|
} else { |
|
DEBUG = false; |
|
} |
|
|
|
|
|
var layoutInfo = createLayoutInfo( cy, layout, options ); |
|
|
|
|
|
if( DEBUG ){ |
|
printLayoutInfo( layoutInfo ); |
|
} |
|
|
|
|
|
if (options.randomize) { |
|
randomizePositions( layoutInfo, cy ); |
|
} |
|
|
|
var startTime = util.performanceNow(); |
|
|
|
var refresh = function(){ |
|
refreshPositions( layoutInfo, cy, options ); |
|
|
|
|
|
if( true === options.fit ){ |
|
cy.fit( options.padding ); |
|
} |
|
}; |
|
|
|
var mainLoop = function( i ){ |
|
if( layout.stopped || i >= options.numIter ){ |
|
|
|
return false; |
|
} |
|
|
|
|
|
step( layoutInfo, options, i ); |
|
|
|
|
|
layoutInfo.temperature = layoutInfo.temperature * options.coolingFactor; |
|
|
|
|
|
if( layoutInfo.temperature < options.minTemp ){ |
|
|
|
return false; |
|
} |
|
|
|
return true; |
|
}; |
|
|
|
var done = function(){ |
|
if( options.animate === true || options.animate === false ){ |
|
refresh(); |
|
|
|
|
|
layout.one('layoutstop', options.stop); |
|
layout.emit({ type: 'layoutstop', layout: layout }); |
|
} else { |
|
var nodes = options.eles.nodes(); |
|
var getScaledPos = getScaleInBoundsFn(layoutInfo, options, nodes); |
|
|
|
nodes.layoutPositions(layout, options, getScaledPos); |
|
} |
|
}; |
|
|
|
var i = 0; |
|
var loopRet = true; |
|
|
|
if( options.animate === true ){ |
|
var frame = function(){ |
|
var f = 0; |
|
|
|
while( loopRet && f < options.refresh ){ |
|
loopRet = mainLoop(i); |
|
|
|
i++; |
|
f++; |
|
} |
|
|
|
if( !loopRet ){ |
|
separateComponents( layoutInfo, options ); |
|
done(); |
|
} else { |
|
var now = util.performanceNow(); |
|
|
|
if( now - startTime >= options.animationThreshold ){ |
|
refresh(); |
|
} |
|
|
|
util.requestAnimationFrame(frame); |
|
} |
|
}; |
|
|
|
frame(); |
|
} else { |
|
while( loopRet ){ |
|
loopRet = mainLoop(i); |
|
|
|
i++; |
|
} |
|
|
|
separateComponents( layoutInfo, options ); |
|
done(); |
|
} |
|
|
|
return this; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
CoseLayout.prototype.stop = function(){ |
|
this.stopped = true; |
|
|
|
if( this.thread ){ |
|
this.thread.stop(); |
|
} |
|
|
|
this.emit( 'layoutstop' ); |
|
|
|
return this; |
|
}; |
|
|
|
CoseLayout.prototype.destroy = function(){ |
|
if( this.thread ){ |
|
this.thread.stop(); |
|
} |
|
|
|
return this; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var createLayoutInfo = function( cy, layout, options ){ |
|
|
|
var edges = options.eles.edges(); |
|
var nodes = options.eles.nodes(); |
|
var bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : { |
|
x1: 0, y1: 0, w: cy.width(), h: cy.height() |
|
} ); |
|
|
|
var layoutInfo = { |
|
isCompound: cy.hasCompoundNodes(), |
|
layoutNodes: [], |
|
idToIndex: {}, |
|
nodeSize: nodes.size(), |
|
graphSet: [], |
|
indexToGraph: [], |
|
layoutEdges: [], |
|
edgeSize: edges.size(), |
|
temperature: options.initialTemp, |
|
clientWidth: bb.w, |
|
clientHeight: bb.h, |
|
boundingBox: bb |
|
}; |
|
|
|
var components = options.eles.components(); |
|
var id2cmptId = {}; |
|
|
|
for( var i = 0; i < components.length; i++ ){ |
|
var component = components[ i ]; |
|
|
|
for( var j = 0; j < component.length; j++ ){ |
|
var node = component[ j ]; |
|
|
|
id2cmptId[ node.id() ] = i; |
|
} |
|
} |
|
|
|
|
|
for( var i = 0; i < layoutInfo.nodeSize; i++ ){ |
|
var n = nodes[ i ]; |
|
var nbb = n.layoutDimensions( options ); |
|
|
|
var tempNode = {}; |
|
tempNode.isLocked = n.locked(); |
|
tempNode.id = n.data( 'id' ); |
|
tempNode.parentId = n.data( 'parent' ); |
|
tempNode.cmptId = id2cmptId[ n.id() ]; |
|
tempNode.children = []; |
|
tempNode.positionX = n.position( 'x' ); |
|
tempNode.positionY = n.position( 'y' ); |
|
tempNode.offsetX = 0; |
|
tempNode.offsetY = 0; |
|
tempNode.height = nbb.w; |
|
tempNode.width = nbb.h; |
|
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 = parseFloat( n.style( 'padding' ) ); |
|
tempNode.padRight = parseFloat( n.style( 'padding' ) ); |
|
tempNode.padTop = parseFloat( n.style( 'padding' ) ); |
|
tempNode.padBottom = parseFloat( n.style( 'padding' ) ); |
|
|
|
|
|
tempNode.nodeRepulsion = is.fn( options.nodeRepulsion ) ? options.nodeRepulsion(n) : options.nodeRepulsion; |
|
|
|
|
|
layoutInfo.layoutNodes.push( tempNode ); |
|
|
|
layoutInfo.idToIndex[ tempNode.id ] = i; |
|
} |
|
|
|
|
|
var queue = []; |
|
var start = 0; |
|
var end = -1; |
|
|
|
var tempGraph = []; |
|
|
|
|
|
|
|
for( var i = 0; i < layoutInfo.nodeSize; i++ ){ |
|
var n = layoutInfo.layoutNodes[ i ]; |
|
var p_id = n.parentId; |
|
|
|
if( null != p_id ){ |
|
|
|
layoutInfo.layoutNodes[ layoutInfo.idToIndex[ p_id ] ].children.push( n.id ); |
|
} else { |
|
|
|
queue[ ++end ] = n.id; |
|
tempGraph.push( n.id ); |
|
} |
|
} |
|
|
|
|
|
layoutInfo.graphSet.push( tempGraph ); |
|
|
|
|
|
while( start <= end ){ |
|
|
|
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 ){ |
|
|
|
layoutInfo.graphSet.push( children ); |
|
|
|
for( var i = 0; i < children.length; i++ ){ |
|
queue[ ++end ] = children[ i ]; |
|
} |
|
} |
|
} |
|
|
|
|
|
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; |
|
} |
|
} |
|
|
|
|
|
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' ); |
|
|
|
|
|
var idealLength = is.fn( options.idealEdgeLength ) ? options.idealEdgeLength(e) : options.idealEdgeLength; |
|
var elasticity = is.fn( options.edgeElasticity ) ? options.edgeElasticity(e) : options.edgeElasticity; |
|
|
|
|
|
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 ){ |
|
|
|
var lca = findLCA( tempEdge.sourceId, tempEdge.targetId, layoutInfo ); |
|
|
|
|
|
var lcaGraph = layoutInfo.graphSet[ lca ]; |
|
var depth = 0; |
|
|
|
|
|
var tempNode = layoutInfo.layoutNodes[ sourceIx ]; |
|
while( -1 === lcaGraph.indexOf( tempNode.id ) ){ |
|
tempNode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ tempNode.parentId ] ]; |
|
depth++; |
|
} |
|
|
|
|
|
tempNode = layoutInfo.layoutNodes[ targetIx ]; |
|
while( -1 === lcaGraph.indexOf( tempNode.id ) ){ |
|
tempNode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ tempNode.parentId ] ]; |
|
depth++; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
idealLength *= depth * options.nestingFactor; |
|
} |
|
|
|
tempEdge.idealLength = idealLength; |
|
tempEdge.elasticity = elasticity; |
|
|
|
layoutInfo.layoutEdges.push( tempEdge ); |
|
} |
|
|
|
|
|
return layoutInfo; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var findLCA = function( node1, node2, layoutInfo ){ |
|
|
|
var res = findLCA_aux( node1, node2, 0, layoutInfo ); |
|
if( 2 > res.count ){ |
|
|
|
|
|
return 0; |
|
} else { |
|
return res.graph; |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var findLCA_aux = function( node1, node2, graphIx, layoutInfo ){ |
|
var graph = layoutInfo.graphSet[ graphIx ]; |
|
|
|
if( -1 < graph.indexOf( node1 ) && -1 < graph.indexOf( node2 ) ){ |
|
return {count: 2, graph: graphIx}; |
|
} |
|
|
|
|
|
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( 0 === children.length ){ |
|
continue; |
|
} |
|
|
|
var childGraphIx = layoutInfo.indexToGraph[ layoutInfo.idToIndex[ children[0] ] ]; |
|
var result = findLCA_aux( node1, node2, childGraphIx, layoutInfo ); |
|
if( 0 === result.count ){ |
|
|
|
continue; |
|
} else if( 1 === result.count ){ |
|
|
|
c++; |
|
if( 2 === c ){ |
|
|
|
break; |
|
} |
|
} else { |
|
|
|
return result; |
|
} |
|
} |
|
|
|
return {count: c, graph: graphIx}; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
if( process.env.NODE_ENV !== 'production' ){ |
|
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; |
|
|
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
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 ]; |
|
|
|
|
|
if( 0 === n.children.length && !n.isLocked ){ |
|
n.positionX = Math.random() * width; |
|
n.positionY = Math.random() * height; |
|
} |
|
} |
|
}; |
|
|
|
var getScaleInBoundsFn = function( layoutInfo, options, 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; |
|
} |
|
|
|
return function( ele, i ){ |
|
var lnode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ ele.data( 'id' ) ] ]; |
|
|
|
if( options.boundingBox ){ |
|
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 |
|
}; |
|
} |
|
}; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var refreshPositions = function( layoutInfo, cy, options ){ |
|
|
|
|
|
|
|
var layout = options.layout; |
|
var nodes = options.eles.nodes(); |
|
var getScaledPos = getScaleInBoundsFn(layoutInfo, options, nodes); |
|
|
|
nodes.positions(getScaledPos); |
|
|
|
|
|
if( true !== layoutInfo.ready ){ |
|
|
|
|
|
layoutInfo.ready = true; |
|
layout.one( 'layoutready', options.ready ); |
|
layout.emit( { type: 'layoutready', layout: this } ); |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var step = function( layoutInfo, options, step ){ |
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateNodeForces( layoutInfo, options ); |
|
|
|
calculateEdgeForces( layoutInfo, options ); |
|
|
|
calculateGravityForces( layoutInfo, options ); |
|
|
|
propagateForces( layoutInfo, options ); |
|
|
|
updatePositions( layoutInfo, options ); |
|
}; |
|
|
|
|
|
|
|
|
|
var calculateNodeForces = function( layoutInfo, options ){ |
|
|
|
|
|
|
|
|
|
for( var i = 0; i < layoutInfo.graphSet.length; i ++ ){ |
|
var graph = layoutInfo.graphSet[ i ]; |
|
var numNodes = graph.length; |
|
|
|
|
|
|
|
|
|
|
|
|
|
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, options ); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
var randomDistance = function( max ){ |
|
return -max + 2 * max * Math.random(); |
|
}; |
|
|
|
|
|
|
|
|
|
var nodeRepulsion = function( node1, node2, layoutInfo, options ){ |
|
|
|
|
|
var cmptId1 = node1.cmptId; |
|
var cmptId2 = node2.cmptId; |
|
|
|
if( cmptId1 !== cmptId2 && !layoutInfo.isCompound ){ return; } |
|
|
|
|
|
var directionX = node2.positionX - node1.positionX; |
|
var directionY = node2.positionY - node1.positionY; |
|
var maxRandDist = 1; |
|
|
|
|
|
|
|
if( 0 === directionX && 0 === directionY ){ |
|
directionX = randomDistance( maxRandDist ); |
|
directionY = randomDistance( maxRandDist ); |
|
} |
|
|
|
var overlap = nodesOverlap( node1, node2, directionX, directionY ); |
|
|
|
if( overlap > 0 ){ |
|
|
|
|
|
|
|
|
|
var force = options.nodeOverlap * overlap; |
|
|
|
|
|
var distance = Math.sqrt( directionX * directionX + directionY * directionY ); |
|
|
|
var forceX = force * directionX / distance; |
|
var forceY = force * directionY / distance; |
|
|
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
var point1 = findClippingPoint( node1, directionX, directionY ); |
|
var point2 = findClippingPoint( node2, -1 * directionX, -1 * directionY ); |
|
|
|
|
|
var distanceX = point2.x - point1.x; |
|
var distanceY = point2.y - point1.y; |
|
var distanceSqr = distanceX * distanceX + distanceY * distanceY; |
|
var distance = Math.sqrt( distanceSqr ); |
|
|
|
|
|
|
|
var force = ( node1.nodeRepulsion + node2.nodeRepulsion ) / distanceSqr; |
|
var forceX = force * distanceX / distance; |
|
var forceY = force * distanceY / distance; |
|
} |
|
|
|
|
|
if( !node1.isLocked ){ |
|
node1.offsetX -= forceX; |
|
node1.offsetY -= forceY; |
|
} |
|
|
|
if( !node2.isLocked ){ |
|
node2.offsetX += forceX; |
|
node2.offsetY += forceY; |
|
} |
|
|
|
|
|
|
|
|
|
return; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
var findClippingPoint = function( node, dX, dY ){ |
|
|
|
|
|
var X = node.positionX; |
|
var Y = node.positionY; |
|
var H = node.height || 1; |
|
var W = node.width || 1; |
|
var dirSlope = dY / dX; |
|
var nodeSlope = H / W; |
|
|
|
|
|
|
|
|
|
|
|
|
|
var res = {}; |
|
|
|
|
|
if( 0 === dX && 0 < dY ){ |
|
res.x = X; |
|
|
|
res.y = Y + H / 2; |
|
|
|
return res; |
|
} |
|
|
|
|
|
if( 0 === dX && 0 > dY ){ |
|
res.x = X; |
|
res.y = Y + H / 2; |
|
|
|
|
|
return res; |
|
} |
|
|
|
|
|
if( 0 < dX && |
|
-1 * nodeSlope <= dirSlope && |
|
dirSlope <= nodeSlope ){ |
|
res.x = X + W / 2; |
|
res.y = Y + (W * dY / 2 / dX); |
|
|
|
|
|
return res; |
|
} |
|
|
|
|
|
if( 0 > dX && |
|
-1 * nodeSlope <= dirSlope && |
|
dirSlope <= nodeSlope ){ |
|
res.x = X - W / 2; |
|
res.y = Y - (W * dY / 2 / dX); |
|
|
|
|
|
return res; |
|
} |
|
|
|
|
|
if( 0 < dY && |
|
( dirSlope <= -1 * nodeSlope || |
|
dirSlope >= nodeSlope ) ){ |
|
res.x = X + (H * dX / 2 / dY); |
|
res.y = Y + H / 2; |
|
|
|
|
|
return res; |
|
} |
|
|
|
|
|
if( 0 > dY && |
|
( dirSlope <= -1 * nodeSlope || |
|
dirSlope >= nodeSlope ) ){ |
|
res.x = X - (H * dX / 2 / dY); |
|
res.y = Y - H / 2; |
|
|
|
|
|
return res; |
|
} |
|
|
|
|
|
|
|
return res; |
|
}; |
|
|
|
|
|
|
|
|
|
var calculateEdgeForces = function( layoutInfo, options ){ |
|
|
|
for( var i = 0; i < layoutInfo.edgeSize; i++ ){ |
|
|
|
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 ]; |
|
|
|
|
|
var directionX = target.positionX - source.positionX; |
|
var directionY = target.positionY - source.positionY; |
|
|
|
|
|
|
|
if( 0 === directionX && 0 === directionY ){ |
|
continue; |
|
} |
|
|
|
|
|
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 ) / edge.elasticity; |
|
|
|
if( 0 !== l ){ |
|
var forceX = force * lx / l; |
|
var forceY = force * ly / l; |
|
} else { |
|
var forceX = 0; |
|
var forceY = 0; |
|
} |
|
|
|
|
|
if( !source.isLocked ){ |
|
source.offsetX += forceX; |
|
source.offsetY += forceY; |
|
} |
|
|
|
if( !target.isLocked ){ |
|
target.offsetX -= forceX; |
|
target.offsetY -= forceY; |
|
} |
|
|
|
|
|
|
|
|
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
var calculateGravityForces = function( layoutInfo, options ){ |
|
if (options.gravity === 0) { |
|
return; |
|
} |
|
|
|
var distThreshold = 1; |
|
|
|
|
|
|
|
for( var i = 0; i < layoutInfo.graphSet.length; i ++ ){ |
|
var graph = layoutInfo.graphSet[ i ]; |
|
var numNodes = graph.length; |
|
|
|
|
|
|
|
|
|
|
|
if( 0 === i ){ |
|
var centerX = layoutInfo.clientHeight / 2; |
|
var centerY = layoutInfo.clientWidth / 2; |
|
} else { |
|
|
|
var temp = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[0] ] ]; |
|
var parent = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ temp.parentId ] ]; |
|
var centerX = parent.positionX; |
|
var centerY = parent.positionY; |
|
} |
|
|
|
|
|
|
|
|
|
for( var j = 0; j < numNodes; j++ ){ |
|
var node = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[ j ] ] ]; |
|
|
|
|
|
if( node.isLocked ){ continue; } |
|
|
|
var dx = centerX - node.positionX; |
|
var dy = centerY - node.positionY; |
|
var d = Math.sqrt( dx * dx + dy * dy ); |
|
if( d > distThreshold ){ |
|
var fx = options.gravity * dx / d; |
|
var fy = options.gravity * dy / d; |
|
node.offsetX += fx; |
|
node.offsetY += fy; |
|
|
|
} else { |
|
|
|
} |
|
|
|
} |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var propagateForces = function( layoutInfo, options ){ |
|
|
|
var queue = []; |
|
var start = 0; |
|
var end = -1; |
|
|
|
|
|
|
|
|
|
queue.push.apply( queue, layoutInfo.graphSet[0] ); |
|
end += layoutInfo.graphSet[0].length; |
|
|
|
|
|
while( start <= end ){ |
|
|
|
var nodeId = queue[ start++ ]; |
|
var nodeIndex = layoutInfo.idToIndex[ nodeId ]; |
|
var node = layoutInfo.layoutNodes[ nodeIndex ]; |
|
var children = node.children; |
|
|
|
|
|
if( 0 < children.length && !node.isLocked ){ |
|
var offX = node.offsetX; |
|
var offY = node.offsetY; |
|
|
|
|
|
|
|
|
|
|
|
|
|
for( var i = 0; i < children.length; i++ ){ |
|
var childNode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ children[ i ] ] ]; |
|
|
|
childNode.offsetX += offX; |
|
childNode.offsetY += offY; |
|
|
|
queue[ ++end ] = children[ i ]; |
|
} |
|
|
|
|
|
node.offsetX = 0; |
|
node.offsetY = 0; |
|
} |
|
|
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
var updatePositions = function( layoutInfo, options ){ |
|
|
|
|
|
|
|
|
|
for( var i = 0; i < layoutInfo.nodeSize; i++ ){ |
|
var n = layoutInfo.layoutNodes[ i ]; |
|
if( 0 < n.children.length ){ |
|
|
|
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 || n.isLocked ){ |
|
|
|
|
|
continue; |
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
updateAncestryBoundaries( n, layoutInfo ); |
|
} |
|
|
|
|
|
for( var i = 0; i < layoutInfo.nodeSize; i++ ){ |
|
var n = layoutInfo.layoutNodes[ i ]; |
|
if( 0 < n.children.length && !n.isLocked ){ |
|
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; |
|
|
|
|
|
|
|
|
|
} |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
var limitForce = function( forceX, forceY, 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 |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
return res; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
var updateAncestryBoundaries = function( node, layoutInfo ){ |
|
|
|
var parentId = node.parentId; |
|
if( null == parentId ){ |
|
|
|
|
|
|
|
return; |
|
} |
|
|
|
|
|
var p = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ parentId ] ]; |
|
var flag = false; |
|
|
|
|
|
if( null == p.maxX || node.maxX + p.padRight > p.maxX ){ |
|
p.maxX = node.maxX + p.padRight; |
|
flag = true; |
|
|
|
} |
|
|
|
|
|
if( null == p.minX || node.minX - p.padLeft < p.minX ){ |
|
p.minX = node.minX - p.padLeft; |
|
flag = true; |
|
|
|
} |
|
|
|
|
|
if( null == p.maxY || node.maxY + p.padBottom > p.maxY ){ |
|
p.maxY = node.maxY + p.padBottom; |
|
flag = true; |
|
|
|
} |
|
|
|
|
|
if( null == p.minY || node.minY - p.padTop < p.minY ){ |
|
p.minY = node.minY - p.padTop; |
|
flag = true; |
|
|
|
} |
|
|
|
|
|
if( flag ){ |
|
|
|
return updateAncestryBoundaries( p, layoutInfo ); |
|
} |
|
|
|
|
|
|
|
return; |
|
}; |
|
|
|
var separateComponents = function( layoutInfo, options ){ |
|
var nodes = layoutInfo.layoutNodes; |
|
var components = []; |
|
|
|
for( var i = 0; i < nodes.length; i++ ){ |
|
var node = nodes[ i ]; |
|
var cid = node.cmptId; |
|
var component = components[ cid ] = components[ cid ] || []; |
|
|
|
component.push( node ); |
|
} |
|
|
|
var totalA = 0; |
|
|
|
for( var i = 0; i < components.length; i++ ){ |
|
var c = components[ i ]; |
|
|
|
if( !c ){ continue; } |
|
|
|
c.x1 = Infinity; |
|
c.x2 = -Infinity; |
|
c.y1 = Infinity; |
|
c.y2 = -Infinity; |
|
|
|
for( var j = 0; j < c.length; j++ ){ |
|
var n = c[ j ]; |
|
|
|
c.x1 = Math.min( c.x1, n.positionX - n.width / 2 ); |
|
c.x2 = Math.max( c.x2, n.positionX + n.width / 2 ); |
|
c.y1 = Math.min( c.y1, n.positionY - n.height / 2 ); |
|
c.y2 = Math.max( c.y2, n.positionY + n.height / 2 ); |
|
} |
|
|
|
c.w = c.x2 - c.x1; |
|
c.h = c.y2 - c.y1; |
|
|
|
totalA += c.w * c.h; |
|
} |
|
|
|
components.sort( function( c1, c2 ){ |
|
return c2.w * c2.h - c1.w * c1.h; |
|
} ); |
|
|
|
var x = 0; |
|
var y = 0; |
|
var usedW = 0; |
|
var rowH = 0; |
|
var maxRowW = Math.sqrt( totalA ) * layoutInfo.clientWidth / layoutInfo.clientHeight; |
|
|
|
for( var i = 0; i < components.length; i++ ){ |
|
var c = components[ i ]; |
|
|
|
if( !c ){ continue; } |
|
|
|
for( var j = 0; j < c.length; j++ ){ |
|
var n = c[ j ]; |
|
|
|
if( !n.isLocked ){ |
|
n.positionX += (x - c.x1); |
|
n.positionY += (y - c.y1); |
|
} |
|
} |
|
|
|
x += c.w + options.componentSpacing; |
|
usedW += c.w + options.componentSpacing; |
|
rowH = Math.max( rowH, c.h ); |
|
|
|
if( usedW > maxRowW ){ |
|
y += rowH + options.componentSpacing; |
|
x = 0; |
|
usedW = 0; |
|
rowH = 0; |
|
} |
|
} |
|
}; |
|
|
|
export default CoseLayout; |
|
|