cytoscape
Version:
Graph theory (a.k.a. network) library for analysis and visualisation
1,037 lines (825 loc) • 29.3 kB
JavaScript
import * as is from '../../is.mjs';
import { assignBoundingBox, expandBoundingBoxSides, clearBoundingBox, expandBoundingBox, makeBoundingBox, copyBoundingBox, shiftBoundingBox, updateBoundingBox } from '../../math.mjs';
import { defaults, getPrefixedProperty, hashIntsArray, memoize } from '../../util/index.mjs';
let fn, elesfn;
fn = elesfn = {};
elesfn.renderedBoundingBox = function( options ){
let bb = this.boundingBox( options );
let cy = this.cy();
let zoom = cy.zoom();
let pan = cy.pan();
let x1 = bb.x1 * zoom + pan.x;
let x2 = bb.x2 * zoom + pan.x;
let y1 = bb.y1 * zoom + pan.y;
let y2 = bb.y2 * zoom + pan.y;
return {
x1: x1,
x2: x2,
y1: y1,
y2: y2,
w: x2 - x1,
h: y2 - y1
};
};
elesfn.dirtyCompoundBoundsCache = function(silent = false){
let cy = this.cy();
if( !cy.styleEnabled() || !cy.hasCompoundNodes() ){ return this; }
this.forEachUp( ele => {
if( ele.isParent() ){
let _p = ele._private;
_p.compoundBoundsClean = false;
_p.bbCache = null;
if(!silent){
ele.emitAndNotify('bounds');
}
}
} );
return this;
};
elesfn.updateCompoundBounds = function(force = false){
let cy = this.cy();
// not possible to do on non-compound graphs or with the style disabled
if( !cy.styleEnabled() || !cy.hasCompoundNodes() ){ return this; }
// save cycles when batching -- but bounds will be stale (or not exist yet)
if( !force && cy.batching() ){ return this; }
function update( parent ){
if( !parent.isParent() ){ return; }
let _p = parent._private;
let children = parent.children();
let includeLabels = parent.pstyle( 'compound-sizing-wrt-labels' ).value === 'include';
let min = {
width: {
val: parent.pstyle( 'min-width' ).pfValue,
left: parent.pstyle( 'min-width-bias-left' ),
right: parent.pstyle( 'min-width-bias-right' )
},
height: {
val: parent.pstyle( 'min-height' ).pfValue,
top: parent.pstyle( 'min-height-bias-top' ),
bottom: parent.pstyle( 'min-height-bias-bottom' )
}
};
let bb = children.boundingBox( {
includeLabels: includeLabels,
includeOverlays: false,
// updating the compound bounds happens outside of the regular
// cache cycle (i.e. before fired events)
useCache: false
} );
let pos = _p.position;
// if children take up zero area then keep position and fall back on stylesheet w/h
if( bb.w === 0 || bb.h === 0 ){
bb = {
w: parent.pstyle('width').pfValue,
h: parent.pstyle('height').pfValue
};
bb.x1 = pos.x - bb.w/2;
bb.x2 = pos.x + bb.w/2;
bb.y1 = pos.y - bb.h/2;
bb.y2 = pos.y + bb.h/2;
}
function computeBiasValues( propDiff, propBias, propBiasComplement ){
let biasDiff = 0;
let biasComplementDiff = 0;
let biasTotal = propBias + propBiasComplement;
if( propDiff > 0 && biasTotal > 0 ){
biasDiff = ( propBias / biasTotal ) * propDiff;
biasComplementDiff = ( propBiasComplement / biasTotal ) * propDiff;
}
return {
biasDiff: biasDiff,
biasComplementDiff: biasComplementDiff
};
}
function computePaddingValues( width, height, paddingObject, relativeTo ) {
// Assuming percentage is number from 0 to 1
if(paddingObject.units === '%') {
switch(relativeTo) {
case 'width':
return width > 0 ? paddingObject.pfValue * width : 0;
case 'height':
return height > 0 ? paddingObject.pfValue * height : 0;
case 'average':
return ( width > 0 ) && ( height > 0 ) ? paddingObject.pfValue * ( width + height ) / 2 : 0;
case 'min':
return ( width > 0 ) && ( height > 0 ) ? ( ( width > height ) ? paddingObject.pfValue * height : paddingObject.pfValue * width ) : 0;
case 'max':
return ( width > 0 ) && ( height > 0 ) ? ( ( width > height ) ? paddingObject.pfValue * width : paddingObject.pfValue * height ) : 0;
default:
return 0;
}
} else if(paddingObject.units === 'px') {
return paddingObject.pfValue;
} else {
return 0;
}
}
let leftVal = min.width.left.value;
if( min.width.left.units === 'px' && min.width.val > 0 ){
leftVal = ( leftVal * 100 ) / min.width.val;
}
let rightVal = min.width.right.value;
if( min.width.right.units === 'px' && min.width.val > 0 ){
rightVal = ( rightVal * 100 ) / min.width.val;
}
let topVal = min.height.top.value;
if( min.height.top.units === 'px' && min.height.val > 0 ){
topVal = ( topVal * 100 ) / min.height.val;
}
let bottomVal = min.height.bottom.value;
if( min.height.bottom.units === 'px' && min.height.val > 0 ){
bottomVal = ( bottomVal * 100 ) / min.height.val;
}
let widthBiasDiffs = computeBiasValues( min.width.val - bb.w, leftVal, rightVal );
let diffLeft = widthBiasDiffs.biasDiff;
let diffRight = widthBiasDiffs.biasComplementDiff;
let heightBiasDiffs = computeBiasValues( min.height.val - bb.h, topVal, bottomVal );
let diffTop = heightBiasDiffs.biasDiff;
let diffBottom = heightBiasDiffs.biasComplementDiff;
_p.autoPadding = computePaddingValues( bb.w, bb.h, parent.pstyle( 'padding' ), parent.pstyle( 'padding-relative-to' ).value );
_p.autoWidth = Math.max(bb.w, min.width.val);
pos.x = (- diffLeft + bb.x1 + bb.x2 + diffRight) / 2;
_p.autoHeight = Math.max(bb.h, min.height.val);
pos.y = (- diffTop + bb.y1 + bb.y2 + diffBottom) / 2;
}
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
let _p = ele._private;
if( !_p.compoundBoundsClean || force ){
update( ele );
if( !cy.batching() ){
_p.compoundBoundsClean = true;
}
}
}
return this;
};
let noninf = function( x ){
if( x === Infinity || x === -Infinity ){
return 0;
}
return x;
};
let updateBounds = function( b, x1, y1, x2, y2 ){
// don't update with zero area boxes
if( x2 - x1 === 0 || y2 - y1 === 0 ){ return; }
// don't update with null dim
if( x1 == null || y1 == null || x2 == null || y2 == null ){ return; }
b.x1 = x1 < b.x1 ? x1 : b.x1;
b.x2 = x2 > b.x2 ? x2 : b.x2;
b.y1 = y1 < b.y1 ? y1 : b.y1;
b.y2 = y2 > b.y2 ? y2 : b.y2;
b.w = b.x2 - b.x1;
b.h = b.y2 - b.y1;
};
let updateBoundsFromBox = function( b, b2 ){
if( b2 == null ){ return b; }
return updateBounds( b, b2.x1, b2.y1, b2.x2, b2.y2 );
};
let prefixedProperty = function( obj, field, prefix ){
return getPrefixedProperty( obj, field, prefix );
};
let updateBoundsFromArrow = function( bounds, ele, prefix ){
if( ele.cy().headless() ){ return; }
let _p = ele._private;
let rstyle = _p.rstyle;
let halfArW = rstyle.arrowWidth / 2;
let arrowType = ele.pstyle( prefix + '-arrow-shape' ).value;
let x;
let y;
if( arrowType !== 'none' ){
if( prefix === 'source' ){
x = rstyle.srcX;
y = rstyle.srcY;
} else if( prefix === 'target' ){
x = rstyle.tgtX;
y = rstyle.tgtY;
} else {
x = rstyle.midX;
y = rstyle.midY;
}
// always store the individual arrow bounds
let bbs = _p.arrowBounds = _p.arrowBounds || {};
let bb = bbs[prefix] = bbs[prefix] || {};
bb.x1 = x - halfArW;
bb.y1 = y - halfArW;
bb.x2 = x + halfArW;
bb.y2 = y + halfArW;
bb.w = bb.x2 - bb.x1;
bb.h = bb.y2 - bb.y1;
expandBoundingBox(bb, 1);
updateBounds( bounds, bb.x1, bb.y1, bb.x2, bb.y2 );
}
};
let updateBoundsFromLabel = function( bounds, ele, prefix ){
if( ele.cy().headless() ){ return; }
let prefixDash;
if( prefix ){
prefixDash = prefix + '-';
} else {
prefixDash = '';
}
let _p = ele._private;
let rstyle = _p.rstyle;
let label = ele.pstyle( prefixDash + 'label' ).strValue;
if( label ){
let halign = ele.pstyle( 'text-halign' );
let valign = ele.pstyle( 'text-valign' );
let labelWidth = prefixedProperty( rstyle, 'labelWidth', prefix );
let labelHeight = prefixedProperty( rstyle, 'labelHeight', prefix );
let labelX = prefixedProperty( rstyle, 'labelX', prefix );
let labelY = prefixedProperty( rstyle, 'labelY', prefix );
let marginX = ele.pstyle( prefixDash + 'text-margin-x' ).pfValue;
let marginY = ele.pstyle( prefixDash + 'text-margin-y' ).pfValue;
let isEdge = ele.isEdge();
let rotation = ele.pstyle( prefixDash + 'text-rotation' );
let outlineWidth = ele.pstyle( 'text-outline-width' ).pfValue;
let borderWidth = ele.pstyle( 'text-border-width' ).pfValue;
let halfBorderWidth = borderWidth / 2;
let padding = ele.pstyle( 'text-background-padding' ).pfValue;
let marginOfError = 2; // expand to work around browser dimension inaccuracies
let lh = labelHeight;
let lw = labelWidth;
let lw_2 = lw / 2;
let lh_2 = lh / 2;
let lx1, lx2, ly1, ly2;
if( 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;
}
}
// shift by margin and expand by outline and border
let leftPad = marginX - Math.max( outlineWidth, halfBorderWidth ) - padding - marginOfError;
let rightPad = marginX + Math.max( outlineWidth, halfBorderWidth ) + padding + marginOfError;
let topPad = marginY - Math.max( outlineWidth, halfBorderWidth ) - padding - marginOfError;
let botPad = marginY + Math.max( outlineWidth, halfBorderWidth ) + padding + marginOfError;
lx1 += leftPad;
lx2 += rightPad;
ly1 += topPad;
ly2 += botPad;
// always store the unrotated label bounds separately
let bbPrefix = prefix || 'main';
let bbs = _p.labelBounds;
let bb = bbs[bbPrefix] = bbs[bbPrefix] || {};
bb.x1 = lx1;
bb.y1 = ly1;
bb.x2 = lx2;
bb.y2 = ly2;
bb.w = lx2 - lx1;
bb.h = ly2 - ly1;
bb.leftPad = leftPad;
bb.rightPad = rightPad;
bb.topPad = topPad;
bb.botPad = botPad;
let isAutorotate = ( isEdge && rotation.strValue === 'autorotate' );
let isPfValue = ( rotation.pfValue != null && rotation.pfValue !== 0 );
if( isAutorotate || isPfValue ){
let theta = isAutorotate ? prefixedProperty( _p.rstyle, 'labelAngle', prefix ) : rotation.pfValue;
let cos = Math.cos( theta );
let sin = Math.sin( theta );
// rotation point (default value for center-center)
let xo = (lx1 + lx2)/2;
let yo = (ly1 + ly2)/2;
if( !isEdge ){
switch( halign.value ){
case 'left':
xo = lx2;
break;
case 'right':
xo = lx1;
break;
}
switch( valign.value ){
case 'top':
yo = ly2;
break;
case 'bottom':
yo = ly1;
break;
}
}
let rotate = function( x, y ){
x = x - xo;
y = y - yo;
return {
x: x * cos - y * sin + xo,
y: x * sin + y * cos + yo
};
};
let px1y1 = rotate( lx1, ly1 );
let px1y2 = rotate( lx1, ly2 );
let px2y1 = rotate( lx2, ly1 );
let px2y2 = rotate( lx2, ly2 );
lx1 = Math.min( px1y1.x, px1y2.x, px2y1.x, px2y2.x );
lx2 = Math.max( px1y1.x, px1y2.x, px2y1.x, px2y2.x );
ly1 = Math.min( px1y1.y, px1y2.y, px2y1.y, px2y2.y );
ly2 = Math.max( px1y1.y, px1y2.y, px2y1.y, px2y2.y );
}
let bbPrefixRot = bbPrefix + 'Rot';
let bbRot = bbs[bbPrefixRot] = bbs[bbPrefixRot] || {};
bbRot.x1 = lx1;
bbRot.y1 = ly1;
bbRot.x2 = lx2;
bbRot.y2 = ly2;
bbRot.w = lx2 - lx1;
bbRot.h = ly2 - ly1;
updateBounds( bounds, lx1, ly1, lx2, ly2 );
updateBounds( _p.labelBounds.all, lx1, ly1, lx2, ly2 );
}
return bounds;
};
let updateBoundsFromOutline = function( bounds, ele ){
if( ele.cy().headless() ){ return; }
let outlineOpacity = ele.pstyle('outline-opacity').value;
let outlineWidth = ele.pstyle('outline-width').value;
if (outlineOpacity > 0 && outlineWidth > 0) {
let outlineOffset = ele.pstyle('outline-offset').value;
let nodeShape = ele.pstyle( 'shape' ).value;
let outlineSize = outlineWidth + outlineOffset;
let scaleX = (bounds.w + outlineSize * 2) / bounds.w;
let scaleY = (bounds.h + outlineSize * 2) / bounds.h;
let xOffset = 0;
let yOffset = 0;
if (["diamond", "pentagon", "round-triangle"].includes(nodeShape)) {
scaleX = (bounds.w + outlineSize * 2.4) / bounds.w;
yOffset = -outlineSize/3.6;
} else if (["concave-hexagon", "rhomboid", "right-rhomboid"].includes(nodeShape)) {
scaleX = (bounds.w + outlineSize * 2.4) / bounds.w;
} else if (nodeShape === "star") {
scaleX = (bounds.w + outlineSize * 2.8) / bounds.w;
scaleY = (bounds.h + outlineSize * 2.6) / bounds.h;
yOffset = -outlineSize / 3.8;
} else if (nodeShape === "triangle") {
scaleX = (bounds.w + outlineSize * 2.8) / bounds.w;
scaleY = (bounds.h + outlineSize * 2.4) / bounds.h;
yOffset = -outlineSize/1.4;
} else if (nodeShape === "vee") {
scaleX = (bounds.w + outlineSize * 4.4) / bounds.w;
scaleY = (bounds.h + outlineSize * 3.8) / bounds.h;
yOffset = -outlineSize * .5;
}
let hDelta = (bounds.h * scaleY) - bounds.h;
let wDelta = (bounds.w * scaleX) - bounds.w;
expandBoundingBoxSides(bounds, [Math.ceil(hDelta/2), Math.ceil(wDelta/2)]);
if (xOffset != 0 || yOffset !== 0) {
let oBounds = shiftBoundingBox(bounds, xOffset, yOffset);
updateBoundingBox(bounds, oBounds);
}
}
};
// get the bounding box of the elements (in raw model position)
let boundingBoxImpl = function( ele, options ){
let cy = ele._private.cy;
let styleEnabled = cy.styleEnabled();
let headless = cy.headless();
let bounds = makeBoundingBox();
let _p = ele._private;
let isNode = ele.isNode();
let isEdge = ele.isEdge();
let ex1, ex2, ey1, ey2; // extrema of body / lines
let x, y; // node pos
let rstyle = _p.rstyle;
let manualExpansion = isNode && styleEnabled ? ele.pstyle('bounds-expansion').pfValue : [0];
// must use `display` prop only, as reading `compound.width()` causes recursion
// (other factors like width values will be considered later in this function anyway)
let isDisplayed = ele => ele.pstyle('display').value !== 'none';
let displayed = (
!styleEnabled
|| (
isDisplayed(ele)
// must take into account connected nodes b/c of implicit edge hiding on display:none node
&& ( !isEdge || ( isDisplayed(ele.source()) && isDisplayed(ele.target()) ) )
)
);
if( displayed ){ // displayed suffices, since we will find zero area eles anyway
let overlayOpacity = 0;
let overlayPadding = 0;
if( styleEnabled && options.includeOverlays ){
overlayOpacity = ele.pstyle( 'overlay-opacity' ).value;
if( overlayOpacity !== 0 ){
overlayPadding = ele.pstyle( 'overlay-padding' ).value;
}
}
let underlayOpacity = 0;
let underlayPadding = 0;
if( styleEnabled && options.includeUnderlays ){
underlayOpacity = ele.pstyle( 'underlay-opacity' ).value;
if( underlayOpacity !== 0 ){
underlayPadding = ele.pstyle( 'underlay-padding' ).value;
}
}
let padding = Math.max(overlayPadding, underlayPadding);
let w = 0;
let wHalf = 0;
if( styleEnabled ){
w = ele.pstyle( 'width' ).pfValue;
wHalf = w / 2;
}
if( isNode && options.includeNodes ){
let pos = ele.position();
x = pos.x;
y = pos.y;
let w = ele.outerWidth();
let halfW = w / 2;
let h = ele.outerHeight();
let halfH = h / 2;
// handle node dimensions
/////////////////////////
ex1 = x - halfW;
ex2 = x + halfW;
ey1 = y - halfH;
ey2 = y + halfH;
updateBounds( bounds, ex1, ey1, ex2, ey2 );
if( styleEnabled && options.includeOutlines ){
updateBoundsFromOutline( bounds, ele );
}
} else if( isEdge && options.includeEdges ){
if( styleEnabled && !headless ){
let curveStyle = ele.pstyle( 'curve-style').strValue;
// handle edge dimensions (rough box estimate)
//////////////////////////////////////////////
ex1 = Math.min( rstyle.srcX, rstyle.midX, rstyle.tgtX );
ex2 = Math.max( rstyle.srcX, rstyle.midX, rstyle.tgtX );
ey1 = Math.min( rstyle.srcY, rstyle.midY, rstyle.tgtY );
ey2 = Math.max( rstyle.srcY, rstyle.midY, rstyle.tgtY );
// take into account edge width
ex1 -= wHalf;
ex2 += wHalf;
ey1 -= wHalf;
ey2 += wHalf;
updateBounds( bounds, ex1, ey1, ex2, ey2 );
// precise edges
////////////////
if( curveStyle === 'haystack' ){
let hpts = rstyle.haystackPts;
if( hpts && hpts.length === 2 ){
ex1 = hpts[0].x;
ey1 = hpts[0].y;
ex2 = hpts[1].x;
ey2 = hpts[1].y;
if( ex1 > ex2 ){
let temp = ex1;
ex1 = ex2;
ex2 = temp;
}
if( ey1 > ey2 ){
let temp = ey1;
ey1 = ey2;
ey2 = temp;
}
updateBounds( bounds, ex1 - wHalf, ey1 - wHalf, ex2 + wHalf, ey2 + wHalf );
}
} else if(
curveStyle === 'bezier' || curveStyle === 'unbundled-bezier'
|| curveStyle.endsWith('segments') || curveStyle.endsWith('taxi')
){
let pts;
switch( curveStyle ){
case 'bezier':
case 'unbundled-bezier':
pts = rstyle.bezierPts;
break;
case 'segments':
case 'taxi':
case 'round-segments':
case 'round-taxi':
pts = rstyle.linePts;
break;
}
if( pts != null ){
for( let j = 0; j < pts.length; j++ ){
let pt = pts[ j ];
ex1 = pt.x - wHalf;
ex2 = pt.x + wHalf;
ey1 = pt.y - wHalf;
ey2 = pt.y + wHalf;
updateBounds( bounds, ex1, ey1, ex2, ey2 );
}
}
} // bezier-like or segment-like edge
} else { // headless or style disabled
// fallback on source and target positions
//////////////////////////////////////////
let n1 = ele.source();
let n1pos = n1.position();
let n2 = ele.target();
let n2pos = n2.position();
ex1 = n1pos.x;
ex2 = n2pos.x;
ey1 = n1pos.y;
ey2 = n2pos.y;
if( ex1 > ex2 ){
let temp = ex1;
ex1 = ex2;
ex2 = temp;
}
if( ey1 > ey2 ){
let temp = ey1;
ey1 = ey2;
ey2 = temp;
}
// take into account edge width
ex1 -= wHalf;
ex2 += wHalf;
ey1 -= wHalf;
ey2 += wHalf;
updateBounds( bounds, ex1, ey1, ex2, ey2 );
} // headless or style disabled
} // edges
// handle edge arrow size
/////////////////////////
if( styleEnabled && options.includeEdges && isEdge ){
updateBoundsFromArrow( bounds, ele, 'mid-source', options );
updateBoundsFromArrow( bounds, ele, 'mid-target', options );
updateBoundsFromArrow( bounds, ele, 'source', options );
updateBoundsFromArrow( bounds, ele, 'target', options );
}
// ghost
////////
if( styleEnabled ){
let ghost = ele.pstyle('ghost').value === 'yes';
if( ghost ){
let gx = ele.pstyle('ghost-offset-x').pfValue;
let gy = ele.pstyle('ghost-offset-y').pfValue;
updateBounds( bounds, bounds.x1 + gx, bounds.y1 + gy, bounds.x2 + gx, bounds.y2 + gy );
}
}
// always store the body bounds separately from the labels
let bbBody = _p.bodyBounds = _p.bodyBounds || {};
assignBoundingBox(bbBody, bounds);
expandBoundingBoxSides(bbBody, manualExpansion);
expandBoundingBox(bbBody, 1); // expand to work around browser dimension inaccuracies
// overlay
//////////
if( styleEnabled ){
ex1 = bounds.x1;
ex2 = bounds.x2;
ey1 = bounds.y1;
ey2 = bounds.y2;
updateBounds( bounds, ex1 - padding, ey1 - padding, ex2 + padding, ey2 + padding );
}
// always store the body bounds separately from the labels
let bbOverlay = _p.overlayBounds = _p.overlayBounds || {};
assignBoundingBox(bbOverlay, bounds);
expandBoundingBoxSides(bbOverlay, manualExpansion);
expandBoundingBox(bbOverlay, 1); // expand to work around browser dimension inaccuracies
// handle label dimensions
//////////////////////////
let bbLabels = _p.labelBounds = _p.labelBounds || {};
if( bbLabels.all != null ){
clearBoundingBox(bbLabels.all);
} else {
bbLabels.all = makeBoundingBox();
}
if( styleEnabled && options.includeLabels ){
if( options.includeMainLabels ){
updateBoundsFromLabel( bounds, ele, null, options );
}
if( isEdge ){
if( options.includeSourceLabels ){
updateBoundsFromLabel( bounds, ele, 'source', options );
}
if( options.includeTargetLabels ){
updateBoundsFromLabel( bounds, ele, 'target', options );
}
}
} // style enabled for labels
} // if displayed
bounds.x1 = noninf( bounds.x1 );
bounds.y1 = noninf( bounds.y1 );
bounds.x2 = noninf( bounds.x2 );
bounds.y2 = noninf( bounds.y2 );
bounds.w = noninf( bounds.x2 - bounds.x1 );
bounds.h = noninf( bounds.y2 - bounds.y1 );
if( bounds.w > 0 && bounds.h > 0 && displayed ){
expandBoundingBoxSides( bounds, manualExpansion );
// expand bounds by 1 because antialiasing can increase the visual/effective size by 1 on all sides
expandBoundingBox( bounds, 1 );
}
return bounds;
};
let getKey = function( opts ){
let i = 0;
let tf = val => (val ? 1 : 0) << i++;
let key = 0;
key += tf( opts.incudeNodes );
key += tf( opts.includeEdges );
key += tf( opts.includeLabels );
key += tf( opts.includeMainLabels );
key += tf( opts.includeSourceLabels );
key += tf( opts.includeTargetLabels );
key += tf( opts.includeOverlays );
key += tf( opts.includeOutlines );
return key;
};
let getBoundingBoxPosKey = ele => {
let r = x => Math.round(x);
if( ele.isEdge() ){
let p1 = ele.source().position();
let p2 = ele.target().position();
return hashIntsArray([ r(p1.x), r(p1.y), r(p2.x), r(p2.y) ]);
} else {
let p = ele.position();
return hashIntsArray([ r(p.x), r(p.y) ]);
}
};
let cachedBoundingBoxImpl = function( ele, opts ){
let _p = ele._private;
let bb;
let isEdge = ele.isEdge();
let key = opts == null ? defBbOptsKey : getKey( opts );
let usingDefOpts = key === defBbOptsKey;
if( _p.bbCache == null ){
bb = boundingBoxImpl( ele, defBbOpts );
_p.bbCache = bb;
_p.bbCachePosKey = getBoundingBoxPosKey( ele );
} else {
bb = _p.bbCache;
}
// not using def opts => need to build up bb from combination of sub bbs
if( !usingDefOpts ){
let isNode = ele.isNode();
bb = makeBoundingBox();
if( (opts.includeNodes && isNode) || (opts.includeEdges && !isNode) ){
if( opts.includeOverlays ){
updateBoundsFromBox(bb, _p.overlayBounds);
} else {
updateBoundsFromBox(bb, _p.bodyBounds);
}
}
if( opts.includeLabels ){
if( opts.includeMainLabels && (!isEdge || (opts.includeSourceLabels && opts.includeTargetLabels)) ){
updateBoundsFromBox(bb, _p.labelBounds.all);
} else {
if( opts.includeMainLabels ){
updateBoundsFromBox(bb, _p.labelBounds.mainRot);
}
if( opts.includeSourceLabels ){
updateBoundsFromBox(bb, _p.labelBounds.sourceRot);
}
if( opts.includeTargetLabels ){
updateBoundsFromBox(bb, _p.labelBounds.targetRot);
}
}
}
bb.w = bb.x2 - bb.x1;
bb.h = bb.y2 - bb.y1;
}
return bb;
};
let defBbOpts = {
includeNodes: true,
includeEdges: true,
includeLabels: true,
includeMainLabels: true,
includeSourceLabels: true,
includeTargetLabels: true,
includeOverlays: true,
includeUnderlays: true,
includeOutlines: true,
useCache: true
};
const defBbOptsKey = getKey( defBbOpts );
const filledBbOpts = defaults( defBbOpts );
elesfn.boundingBox = function( options ){
let bounds;
let useCache = (options === undefined || options.useCache === undefined || options.useCache === true);
let isDirty = memoize(ele => {
let _p = ele._private;
return _p.bbCache == null || _p.styleDirty || _p.bbCachePosKey !== getBoundingBoxPosKey(ele);
}, ele => ele.id());
// the main usecase is ele.boundingBox() for a single element with no/def options
// specified s.t. the cache is used, so check for this case to make it faster by
// avoiding the overhead of the rest of the function
if (useCache && this.length === 1 && !isDirty(this[0])) {
if (options === undefined) {
options = defBbOpts;
} else {
options = filledBbOpts( options );
}
bounds = cachedBoundingBoxImpl(this[0], options);
} else {
bounds = makeBoundingBox();
options = options || defBbOpts;
let opts = filledBbOpts(options);
let eles = this;
let cy = eles.cy();
let styleEnabled = cy.styleEnabled();
// cache the isDirty state for all eles, edges first since they depend on node state
this.edges().forEach(isDirty);
this.nodes().forEach(isDirty);
if(styleEnabled) {
this.recalculateRenderedStyle(useCache);
}
this.updateCompoundBounds(!useCache);
for (let i = 0; i < eles.length; i++) {
let ele = eles[i];
if (isDirty(ele)) {
ele.dirtyBoundingBoxCache();
}
updateBoundsFromBox(bounds, cachedBoundingBoxImpl(ele, opts));
}
}
bounds.x1 = noninf( bounds.x1 );
bounds.y1 = noninf( bounds.y1 );
bounds.x2 = noninf( bounds.x2 );
bounds.y2 = noninf( bounds.y2 );
bounds.w = noninf( bounds.x2 - bounds.x1 );
bounds.h = noninf( bounds.y2 - bounds.y1 );
return bounds;
};
elesfn.dirtyBoundingBoxCache = function(){
for( let i = 0; i < this.length; i++ ){
let _p = this[i]._private;
_p.bbCache = null;
_p.bbCachePosKey = null;
_p.bodyBounds = null;
_p.overlayBounds = null;
_p.labelBounds.all = null;
_p.labelBounds.source = null;
_p.labelBounds.target = null;
_p.labelBounds.main = null;
_p.labelBounds.sourceRot = null;
_p.labelBounds.targetRot = null;
_p.labelBounds.mainRot = null;
_p.arrowBounds.source = null;
_p.arrowBounds.target = null;
_p.arrowBounds['mid-source'] = null;
_p.arrowBounds['mid-target'] = null;
}
this.emitAndNotify('bounds');
return this;
};
// private helper to get bounding box for custom node positions
// - good for perf in certain cases but currently requires dirtying the rendered style
// - would be better to not modify the nodes but the nodes are read directly everywhere in the renderer...
// - try to use for only things like discrete layouts where the node position would change anyway
elesfn.boundingBoxAt = function( fn ){
let nodes = this.nodes();
let cy = this.cy();
let hasCompoundNodes = cy.hasCompoundNodes();
let parents = cy.collection();
if( hasCompoundNodes ){
parents = nodes.filter(node => node.isParent());
nodes = nodes.not(parents);
}
if( is.plainObject( fn ) ){
let obj = fn;
fn = function(){ return obj; };
}
let storeOldPos = (node, i) => node._private.bbAtOldPos = fn(node, i);
let getOldPos = (node) => node._private.bbAtOldPos;
cy.startBatch();
(
nodes
.forEach(storeOldPos)
.silentPositions(fn)
);
if( hasCompoundNodes ){
parents.dirtyCompoundBoundsCache();
parents.dirtyBoundingBoxCache();
parents.updateCompoundBounds(true); // force update b/c we're inside a batch cycle
}
let bb = copyBoundingBox( this.boundingBox({ useCache: false }) );
nodes.silentPositions(getOldPos);
if( hasCompoundNodes ){
parents.dirtyCompoundBoundsCache();
parents.dirtyBoundingBoxCache();
parents.updateCompoundBounds(true); // force update b/c we're inside a batch cycle
}
cy.endBatch();
return bb;
};
fn.boundingbox = fn.bb = fn.boundingBox;
fn.renderedBoundingbox = fn.renderedBoundingBox;
export default elesfn;