import * as util from '../util';
import * as is from '../is';
import Promise from '../promise';
const styfn = {};
const TRUE = 't';
const FALSE = 'f';
styfn.apply = function( eles ){
let self = this;
let _p = self._private;
let cy = _p.cy;
let updatedEles = cy.collection();
for( let ie = 0; ie < eles.length; ie++ ){
let ele = eles[ ie ];
let cxtMeta = self.getContextMeta( ele );
if( cxtMeta.empty ){
continue;
}
let cxtStyle = self.getContextStyle( cxtMeta );
let app = self.applyContextStyle( cxtMeta, cxtStyle, ele );
if( ele._private.appliedInitStyle ){
self.updateTransitions( ele, app.diffProps );
} else {
ele._private.appliedInitStyle = true;
}
let hintsDiff = self.updateStyleHints( ele );
if( hintsDiff ){
updatedEles.push( ele );
}
}
return updatedEles;
};
styfn.getPropertiesDiff = function( oldCxtKey, newCxtKey ){
let self = this;
let cache = self._private.propDiffs = self._private.propDiffs || {};
let dualCxtKey = oldCxtKey + '-' + newCxtKey;
let cachedVal = cache[ dualCxtKey ];
if( cachedVal ){
return cachedVal;
}
let diffProps = [];
let addedProp = {};
for( let i = 0; i < self.length; i++ ){
let cxt = self[ i ];
let oldHasCxt = oldCxtKey[ i ] === TRUE;
let newHasCxt = newCxtKey[ i ] === TRUE;
let cxtHasDiffed = oldHasCxt !== newHasCxt;
let cxtHasMappedProps = cxt.mappedProperties.length > 0;
if( cxtHasDiffed || ( newHasCxt && cxtHasMappedProps )){
let props;
if( cxtHasDiffed && cxtHasMappedProps ){
props = cxt.properties; } else if( cxtHasDiffed ){
props = cxt.properties; } else if( cxtHasMappedProps ){
props = cxt.mappedProperties; }
for( let j = 0; j < props.length; j++ ){
let prop = props[ j ];
let name = prop.name;
let laterCxtOverrides = false;
for( let k = i + 1; k < self.length; k++ ){
let laterCxt = self[ k ];
let hasLaterCxt = newCxtKey[ k ] === TRUE;
if( !hasLaterCxt ){ continue; }
laterCxtOverrides = laterCxt.properties[ prop.name ] != null;
if( laterCxtOverrides ){ break; } }
if( !addedProp[ name ] && !laterCxtOverrides ){
addedProp[ name ] = true;
diffProps.push( name );
}
} }
}
cache[ dualCxtKey ] = diffProps;
return diffProps;
};
styfn.getContextMeta = function( ele ){
let self = this;
let cxtKey = '';
let diffProps;
let prevKey = ele._private.styleCxtKey || '';
for( let i = 0; i < self.length; i++ ){
let context = self[ i ];
let contextSelectorMatches = context.selector && context.selector.matches( ele );
if( contextSelectorMatches ){
cxtKey += TRUE;
} else {
cxtKey += FALSE;
}
}
diffProps = self.getPropertiesDiff( prevKey, cxtKey );
ele._private.styleCxtKey = cxtKey;
return {
key: cxtKey,
diffPropNames: diffProps,
empty: diffProps.length === 0
};
};
styfn.getContextStyle = function( cxtMeta ){
let cxtKey = cxtMeta.key;
let self = this;
let cxtStyles = this._private.contextStyles = this._private.contextStyles || {};
if( cxtStyles[ cxtKey ] ){ return cxtStyles[ cxtKey ]; }
let style = {
_private: {
key: cxtKey
}
};
for( let i = 0; i < self.length; i++ ){
let cxt = self[ i ];
let hasCxt = cxtKey[ i ] === TRUE;
if( !hasCxt ){ continue; }
for( let j = 0; j < cxt.properties.length; j++ ){
let prop = cxt.properties[ j ];
style[ prop.name ] = prop;
}
}
cxtStyles[ cxtKey ] = style;
return style;
};
styfn.applyContextStyle = function( cxtMeta, cxtStyle, ele ){
let self = this;
let diffProps = cxtMeta.diffPropNames;
let retDiffProps = {};
let types = self.types;
for( let i = 0; i < diffProps.length; i++ ){
let diffPropName = diffProps[ i ];
let cxtProp = cxtStyle[ diffPropName ];
let eleProp = ele.pstyle( diffPropName );
if( !cxtProp ){ if( !eleProp ){
continue; } else if( eleProp.bypass ){
cxtProp = { name: diffPropName, deleteBypassed: true };
} else {
cxtProp = { name: diffPropName, delete: true };
}
}
if( eleProp === cxtProp ){ continue; }
if(
cxtProp.mapped === types.fn && eleProp != null && eleProp.mapping != null && eleProp.mapping.value === cxtProp.value ){ let mapping = eleProp.mapping; let fnValue = mapping.fnValue = cxtProp.value( ele );
if( fnValue === mapping.prevFnValue ){ continue; }
}
let retDiffProp = retDiffProps[ diffPropName ] = {
prev: eleProp
};
self.applyParsedProperty( ele, cxtProp );
retDiffProp.next = ele.pstyle( diffPropName );
if( retDiffProp.next && retDiffProp.next.bypass ){
retDiffProp.next = retDiffProp.next.bypassed;
}
}
return {
diffProps: retDiffProps
};
};
styfn.updateStyleHints = function(ele){
let _p = ele._private;
let self = this;
let propNames = self.propertyGroupNames;
let propGrKeys = self.propertyGroupKeys;
let propHash = ( ele, propNames, seedKey ) => self.getPropertiesHash( ele, propNames, seedKey );
let oldStyleKey = _p.styleKey;
if( ele.removed() ){ return false; }
let isNode = _p.group === 'nodes';
let overriddenStyles = ele._private.style;
propNames = Object.keys( overriddenStyles );
for( let i = 0; i < propGrKeys.length; i++ ){
let grKey = propGrKeys[i];
_p.styleKeys[ grKey ] = [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ];
}
let updateGrKey1 = (val, grKey) => _p.styleKeys[ grKey ][0] = util.hashInt( val, _p.styleKeys[ grKey ][0] );
let updateGrKey2 = (val, grKey) => _p.styleKeys[ grKey ][1] = util.hashIntAlt( val, _p.styleKeys[ grKey ][1] );
let updateGrKey = (val, grKey) => {
updateGrKey1(val, grKey);
updateGrKey2(val, grKey);
};
let updateGrKeyWStr = (strVal, grKey) => {
for( let j = 0; j < strVal.length; j++ ){
let ch = strVal.charCodeAt(j);
updateGrKey1(ch, grKey);
updateGrKey2(ch, grKey);
}
};
let N = 2000000000;
let cleanNum = val => (-128 < val && val < 128) && Math.floor(val) !== val ? N - ((val * 1024) | 0) : val;
for( let i = 0; i < propNames.length; i++ ){
let name = propNames[i];
let parsedProp = overriddenStyles[ name ];
if( parsedProp == null ){ continue; }
let propInfo = this.properties[name];
let type = propInfo.type;
let grKey = propInfo.groupKey;
let normalizedNumberVal;
if( propInfo.hashOverride != null ){
normalizedNumberVal = propInfo.hashOverride(ele, parsedProp);
} else if( parsedProp.pfValue != null ){
normalizedNumberVal = parsedProp.pfValue;
}
let numberVal = propInfo.enums == null ? parsedProp.value : null;
let haveNormNum = normalizedNumberVal != null;
let haveUnitedNum = numberVal != null;
let haveNum = haveNormNum || haveUnitedNum;
let units = parsedProp.units;
if( type.number && haveNum && !type.multiple ){
let v = haveNormNum ? normalizedNumberVal : numberVal;
updateGrKey(cleanNum(v), grKey);
if( !haveNormNum && units != null ){
updateGrKeyWStr(units, grKey);
}
} else {
updateGrKeyWStr(parsedProp.strValue, grKey);
}
}
let hash = [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ];
for( let i = 0; i < propGrKeys.length; i++ ){
let grKey = propGrKeys[i];
let grHash = _p.styleKeys[ grKey ];
hash[0] = util.hashInt( grHash[0], hash[0] );
hash[1] = util.hashIntAlt( grHash[1], hash[1] );
}
_p.styleKey = util.combineHashes(hash[0], hash[1]);
let sk = _p.styleKeys;
_p.labelDimsKey = util.combineHashesArray(sk.labelDimensions);
let labelKeys = propHash( ele, ['label'], sk.labelDimensions );
_p.labelKey = util.combineHashesArray(labelKeys);
_p.labelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, labelKeys));
if( !isNode ){
let sourceLabelKeys = propHash( ele, ['source-label'], sk.labelDimensions );
_p.sourceLabelKey = util.combineHashesArray(sourceLabelKeys);
_p.sourceLabelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, sourceLabelKeys));
let targetLabelKeys = propHash( ele, ['target-label'], sk.labelDimensions );
_p.targetLabelKey = util.combineHashesArray(targetLabelKeys);
_p.targetLabelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, targetLabelKeys));
}
if( isNode ){
let { nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie } = _p.styleKeys;
let nodeKeys = [ nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie ].filter(k => k != null).reduce(util.hashArrays, [
util.DEFAULT_HASH_SEED,
util.DEFAULT_HASH_SEED_ALT
]);
_p.nodeKey = util.combineHashesArray(nodeKeys);
_p.hasPie = pie != null && pie[0] !== util.DEFAULT_HASH_SEED && pie[1] !== util.DEFAULT_HASH_SEED_ALT;
}
return oldStyleKey !== _p.styleKey;
};
styfn.clearStyleHints = function(ele){
let _p = ele._private;
_p.styleCxtKey = '';
_p.styleKeys = {};
_p.styleKey = null;
_p.labelKey = null;
_p.labelStyleKey = null;
_p.sourceLabelKey = null;
_p.sourceLabelStyleKey = null;
_p.targetLabelKey = null;
_p.targetLabelStyleKey = null;
_p.nodeKey = null;
_p.hasPie = null;
};
styfn.applyParsedProperty = function( ele, parsedProp ){
let self = this;
let prop = parsedProp;
let style = ele._private.style;
let flatProp;
let types = self.types;
let type = self.properties[ prop.name ].type;
let propIsBypass = prop.bypass;
let origProp = style[ prop.name ];
let origPropIsBypass = origProp && origProp.bypass;
let _p = ele._private;
let flatPropMapping = 'mapping';
let getVal = p => {
if( p == null ){
return null;
} else if( p.pfValue != null ){
return p.pfValue;
} else {
return p.value;
}
};
let checkTriggers = () => {
let fromVal = getVal(origProp);
let toVal = getVal(prop);
self.checkTriggers( ele, prop.name, fromVal, toVal );
};
if(
parsedProp.name === 'curve-style'
&& ele.isEdge()
&& (
( parsedProp.value !== 'bezier'
&& ele.isLoop()
) || ( parsedProp.value === 'haystack'
&& ( ele.source().isParent() || ele.target().isParent() )
)
)
){
prop = parsedProp = this.parse( parsedProp.name, 'bezier', propIsBypass );
}
if( prop.delete ){ style[ prop.name ] = undefined;
checkTriggers();
return true;
}
if( prop.deleteBypassed ){ if( !origProp ){
checkTriggers();
return true;
} else if( origProp.bypass ){ origProp.bypassed = undefined;
checkTriggers();
return true;
} else {
return false; }
}
if( prop.deleteBypass ){ if( !origProp ){
checkTriggers();
return true;
} else if( origProp.bypass ){ style[ prop.name ] = origProp.bypassed;
checkTriggers();
return true;
} else {
return false; }
}
let printMappingErr = function(){
util.warn( 'Do not assign mappings to elements without corresponding data (i.e. ele `' + ele.id() + '` has no mapping for property `' + prop.name + '` with data field `' + prop.field + '`); try a `[' + prop.field + ']` selector to limit scope to elements with `' + prop.field + '` defined' );
};
switch( prop.mapped ){ case types.mapData: {
let fields = prop.field.split( '.' );
let fieldVal = _p.data;
for( let i = 0; i < fields.length && fieldVal; i++ ){
let field = fields[ i ];
fieldVal = fieldVal[ field ];
}
if( fieldVal == null ){
printMappingErr();
return false;
}
let percent;
if( !is.number( fieldVal ) ){ util.warn('Do not use continuous mappers without specifying numeric data (i.e. `' + prop.field + ': ' + fieldVal + '` for `' + ele.id() + '` is non-numeric)');
return false;
} else {
let fieldWidth = prop.fieldMax - prop.fieldMin;
if( fieldWidth === 0 ){ percent = 0;
} else {
percent = (fieldVal - prop.fieldMin) / fieldWidth;
}
}
if( percent < 0 ){
percent = 0;
} else if( percent > 1 ){
percent = 1;
}
if( type.color ){
let r1 = prop.valueMin[0];
let r2 = prop.valueMax[0];
let g1 = prop.valueMin[1];
let g2 = prop.valueMax[1];
let b1 = prop.valueMin[2];
let b2 = prop.valueMax[2];
let a1 = prop.valueMin[3] == null ? 1 : prop.valueMin[3];
let a2 = prop.valueMax[3] == null ? 1 : prop.valueMax[3];
let 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 = { bypass: prop.bypass, name: prop.name,
value: clr,
strValue: 'rgb(' + clr[0] + ', ' + clr[1] + ', ' + clr[2] + ')'
};
} else if( type.number ){
let calcValue = prop.valueMin + (prop.valueMax - prop.valueMin) * percent;
flatProp = this.parse( prop.name, calcValue, prop.bypass, flatPropMapping );
} else {
return false; }
if( !flatProp ){ printMappingErr();
return false;
}
flatProp.mapping = prop; prop = flatProp;
break;
}
case types.data: {
let fields = prop.field.split( '.' );
let fieldVal = _p.data;
for( let i = 0; i < fields.length && fieldVal; i++ ){
let field = fields[ i ];
fieldVal = fieldVal[ field ];
}
if( fieldVal != null ){
flatProp = this.parse( prop.name, fieldVal, prop.bypass, flatPropMapping );
}
if( !flatProp ){ printMappingErr();
return false;
}
flatProp.mapping = prop; prop = flatProp;
break;
}
case types.fn: {
let fn = prop.value;
let fnRetVal = prop.fnValue != null ? prop.fnValue : fn( ele );
prop.prevFnValue = fnRetVal;
if( fnRetVal == null ){
util.warn('Custom function mappers may not return null (i.e. `' + prop.name + '` for ele `' + ele.id() + '` is null)');
return false;
}
flatProp = this.parse( prop.name, fnRetVal, prop.bypass, flatPropMapping );
if( !flatProp ){
util.warn('Custom function mappers may not return invalid values for the property type (i.e. `' + prop.name + '` for ele `' + ele.id() + '` is invalid)');
return false;
}
flatProp.mapping = util.copy( prop ); prop = flatProp;
break;
}
case undefined:
break;
default:
return false; }
if( propIsBypass ){
if( origPropIsBypass ){ prop.bypassed = origProp.bypassed; } else { prop.bypassed = origProp;
}
style[ prop.name ] = prop;
} else { if( origPropIsBypass ){ origProp.bypassed = prop;
} else { style[ prop.name ] = prop;
}
}
checkTriggers();
return true;
};
styfn.cleanElements = function( eles, keepBypasses ){
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
this.clearStyleHints(ele);
ele.dirtyCompoundBoundsCache();
ele.dirtyBoundingBoxCache();
if( !keepBypasses ){
ele._private.style = {};
} else {
let style = ele._private.style;
let propNames = Object.keys(style);
for( let j = 0; j < propNames.length; j++ ){
let propName = propNames[j];
let eleProp = style[ propName ];
if( eleProp != null ){
if( eleProp.bypass ){
eleProp.bypassed = null;
} else {
style[ propName ] = null;
}
}
}
}
}
};
styfn.update = function(){
let cy = this._private.cy;
let eles = cy.mutableElements();
eles.updateStyle();
};
styfn.updateTransitions = function( ele, diffProps ){
let self = this;
let _p = ele._private;
let props = ele.pstyle( 'transition-property' ).value;
let duration = ele.pstyle( 'transition-duration' ).pfValue;
let delay = ele.pstyle( 'transition-delay' ).pfValue;
if( props.length > 0 && duration > 0 ){
let style = {};
let anyPrev = false;
for( let i = 0; i < props.length; i++ ){
let prop = props[ i ];
let styProp = ele.pstyle( prop );
let diffProp = diffProps[ prop ];
if( !diffProp ){ continue; }
let prevProp = diffProp.prev;
let fromProp = prevProp;
let toProp = diffProp.next != null ? diffProp.next : styProp;
let diff = false;
let initVal;
let initDt = 0.000001;
if( !fromProp ){ continue; }
if( is.number( fromProp.pfValue ) && is.number( toProp.pfValue ) ){
diff = toProp.pfValue - fromProp.pfValue; initVal = fromProp.pfValue + initDt * diff;
} else if( is.number( fromProp.value ) && is.number( toProp.value ) ){
diff = toProp.value - fromProp.value; initVal = fromProp.value + initDt * diff;
} 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;
}
if( diff ){
style[ prop ] = toProp.strValue; this.applyBypass( ele, prop, initVal ); anyPrev = true;
}
}
if( !anyPrev ){ return; }
_p.transitioning = true;
( new Promise(function( resolve ){
if( delay > 0 ){
ele.delayAnimation( delay ).play().promise().then( resolve );
} else {
resolve();
}
}) ).then(function(){
return ele.animation( {
style: style,
duration: duration,
easing: ele.pstyle( 'transition-timing-function' ).value,
queue: false
} ).play().promise();
}).then(function(){
self.removeBypasses( ele, props );
ele.emitAndNotify('style');
_p.transitioning = false;
});
} else if( _p.transitioning ){
this.removeBypasses( ele, props );
ele.emitAndNotify('style');
_p.transitioning = false;
}
};
styfn.checkTrigger = function( ele, name, fromValue, toValue, getTrigger, onTrigger ){
let prop = this.properties[ name ];
let triggerCheck = getTrigger( prop );
if( triggerCheck != null && triggerCheck( fromValue, toValue ) ){
onTrigger(prop);
}
};
styfn.checkZOrderTrigger = function( ele, name, fromValue, toValue ){
this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersZOrder, () => {
this._private.cy.notify('zorder', ele);
});
};
styfn.checkBoundsTrigger = function( ele, name, fromValue, toValue ){
this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersBounds, prop => {
ele.dirtyCompoundBoundsCache();
ele.dirtyBoundingBoxCache();
if( prop.triggersBoundsOfParallelBeziers
&& ( name === 'curve-style' && (fromValue === 'bezier' || toValue === 'bezier') )
){
ele.parallelEdges().forEach(pllEdge => {
if( pllEdge.isBundledBezier() ){
pllEdge.dirtyBoundingBoxCache();
}
});
}
if(
prop.triggersBoundsOfConnectedEdges
&& ( name === 'display' && (fromValue === 'none' || toValue === 'none') )
){
ele.connectedEdges().forEach(edge => {
edge.dirtyBoundingBoxCache();
});
}
});
};
styfn.checkTriggers = function( ele, name, fromValue, toValue ){
ele.dirtyStyleCache();
this.checkZOrderTrigger( ele, name, fromValue, toValue );
this.checkBoundsTrigger( ele, name, fromValue, toValue );
};
export default styfn;