/**
* @fileoverview
* Util methods for chem editor.
* @author Partridge Jiang
*/
/*
* requires /core/kekule.common.js
* requires /core/kekule.structures.js
* requires /core/kekule.chemUtils.js
* requires /utils/kekule.utils.js
*/
(function()
{
/**
* Util methods about chem structure.
* @class
*/
Kekule.Editor.StructureUtils = {
/**
* Returns nodes or connectors that should be removed cascadely with chemStructObj.
* @param {Object} chemStructObj
* @returns {Array}
* @deprecated
*/
getCascadeDeleteObjs: function(chemStructObj)
{
return Kekule.ChemStructureUtils.getCascadeDeleteObjs(chemStructObj);
},
/**
* Returns objects directly link arround chemObj.
* If chemObj is a node, returns node.getLinkedObjs(); if chemObj is connector, returns connector.getLinkedObjs() + connector.getConnectedObjs()
*/
getSurroundingObjs: function(chemObj)
{
var objs = chemObj.getLinkedObjs();
if (chemObj instanceof Kekule.ChemStructureConnector) // a connector, should also consider other connectors connecting it
{
var extraObjs = chemObj.getConnectedObjs();
Kekule.ArrayUtils.pushUnique(objs, extraObjs);
}
return objs;
},
/**
* Get direction angle most empty space to a chem object.
* This function is in 2D mode.
* @param {Kekule.ChemStructureObject} obj
* @param {Array} linkedObjs Objects around obj.
* @param {Bool} allowCoordBorrow
* @returns {Float}
*/
calcMostEmptyDirectionAngleOfChemObj: function(obj, linkedObjs, allowCoordBorrow)
{
var angles = [];
if (!linkedObjs)
linkedObjs = obj.getLinkedObjs();
var baseCoord = obj.getAbsBaseCoord2D(allowCoordBorrow);
for (var i = 0, l = linkedObjs.length; i < l; ++i)
{
var c = linkedObjs[i].getAbsBaseCoord2D(allowCoordBorrow);
c = Kekule.CoordUtils.substract(c, baseCoord);
var angle = Math.atan2(c.y, c.x);
if (angle < 0)
angle = Math.PI * 2 + angle;
angles.push(angle);
}
angles.sort();
var l = angles.length;
if (l === 0)
return 0;
else if (l === 1) // only one connector
return -angles[0];
else // more than two connectors
{
var max = 0;
var index = 0;
for (var i = 0; i < l; ++i)
{
var a1 = angles[i];
var a2 = angles[(i + 1) % l];
var delta = a2 - a1;
if (delta < 0)
delta += Math.PI * 2;
if (delta > max)
{
max = delta;
index = i;
}
}
var result = angles[index] + max / 2;
return result;
}
},
/**
* When adding a new bond to a node, this function will calculate the most suitable angle (related to X axis) of the bond direction.
* @param {Object} startingObj
* @param {Float} defBondAngle Default bond angle of this type of bond.
* @param {Bool} allowCoordBorrow
* @returns {Float}
*/
calcPreferred2DBondGrowingDirection: function(startingObj, defBondAngle, allowCoordBorrow)
{
var startingCoord = startingObj.getAbsBaseCoord2D(allowCoordBorrow);
var connectedObjs = Kekule.Editor.StructureUtils.getSurroundingObjs(startingObj);
var connectedObjCount = connectedObjs? connectedObjs.length: 0;
switch (connectedObjCount)
{
case 0: // no object connected, just add a bond in defAngle
{
return defBondAngle; //(Math.PI - defAngle) / 2;
}
case 1: // only one bond, add to defAngles
{
var refObj = connectedObjs[0];
var refCoord = refObj.getAbsBaseCoord2D(allowCoordBorrow);
var refVector = Kekule.CoordUtils.substract(refCoord, startingCoord);
var refAngle = Math.atan2(refVector.y, refVector.x);
var angle1 = refAngle - defBondAngle;
var angle2 = refAngle + defBondAngle;
if (angle1 < 0)
angle1 += Math.PI * 2;
if (angle2 < 0)
angle2 += Math.PI * 2;
var finalAngle;
// we have two appliable angles, if they are not the same, choose the one closest to horizontal line
if (angle1 !== angle2)
{
var ca1 = Math.min((angle1 > Math.PI)? Math.abs(angle1 - Math.PI * 2): angle1, Math.abs(angle1 - Math.PI));
var ca2 = Math.min((angle2 > Math.PI)? Math.abs(angle2 - Math.PI * 2): angle2, Math.abs(angle2 - Math.PI));
finalAngle = (ca1 <= ca2)? angle1: angle2;
}
else
finalAngle = angle1;
return finalAngle;
}
default: // more than one bond, add to most empty direction
{
var finalAngle = Kekule.Editor.StructureUtils.calcMostEmptyDirectionAngleOfChemObj(startingObj, connectedObjs, allowCoordBorrow);
return finalAngle;
}
}
},
/**
* When adding a new bond to a node, this function will calculate the most suitable location of the bond direction.
* @param {Object} startingObj
* @param {Float} bondLength
* @param {Float} defAngle Default bond angle of this type of bond.
* @param {Bool} allowCoordBorrow
* @returns {Hash} Coord of the bond's ending point.
*/
calcPreferred2DBondGrowingLocation: function(startingObj, bondLength, defAngle, allowCoordBorrow)
{
var startingCoord = startingObj.getAbsBaseCoord2D(allowCoordBorrow);
/*
var connectedObjs = startingObj.getLinkedObjs();
if (startingObj instanceof Kekule.ChemStructureConnector) // a connector, should also consider other connectors connecting it
{
var extraObjs = startingObj.getConnectedObjs();
Kekule.ArrayUtils.pushUnique(connectedObjs, extraObjs);
}
*/
/*
var connectedObjs = Kekule.Editor.StructureUtils.getSurroundingObjs(startingObj);
var connectedObjCount = connectedObjs? connectedObjs.length: 0;
switch (connectedObjCount)
{
case 0: // no object connected, just add a bond in defAngle
{
var direction = defAngle; //(Math.PI - defAngle) / 2;
return Kekule.CoordUtils.add(startingCoord, {'x': bondLength * Math.cos(direction), 'y': bondLength * Math.sin(direction)});
//return Kekule.CoordUtils.add(startingCoord, {'x': bondLength, 'y': 0});
}
case 1: // only one bond, add to defAngles
{
var refObj = connectedObjs[0];
var refCoord = refObj.getAbsBaseCoord2D();
var refVector = Kekule.CoordUtils.substract(refCoord, startingCoord);
var refAngle = Math.atan2(refVector.y, refVector.x);
var angle1 = refAngle - defAngle;
var angle2 = refAngle + defAngle;
if (angle1 < 0)
angle1 += Math.PI * 2;
if (angle2 < 0)
angle2 += Math.PI * 2;
var finalAngle;
// we have two appliable angles, if they are not the same, choose the one closest to horizontal line
if (angle1 !== angle2)
{
var ca1 = Math.min((angle1 > Math.PI)? Math.abs(angle1 - Math.PI * 2): angle1, Math.abs(angle1 - Math.PI));
var ca2 = Math.min((angle2 > Math.PI)? Math.abs(angle2 - Math.PI * 2): angle2, Math.abs(angle2 - Math.PI));
finalAngle = (ca1 <= ca2)? angle1: angle2;
}
else
finalAngle = angle1;
var result = {'x': bondLength * Math.cos(finalAngle), 'y': bondLength * Math.sin(finalAngle)};
result = Kekule.CoordUtils.add(result, startingCoord);
return result;
}
default: // more than one bond, add to most empty direction
{
var finalAngle = Kekule.Editor.StructureUtils.calcMostEmptyDirectionAngleOfChemObj(startingObj, connectedObjs);
var result = {'x': bondLength * Math.cos(finalAngle), 'y': bondLength * Math.sin(finalAngle)};
result = Kekule.CoordUtils.add(result, startingCoord);
return result;
}
}
*/
var direction = Kekule.Editor.StructureUtils.calcPreferred2DBondGrowingDirection(startingObj, defAngle, allowCoordBorrow);
return Kekule.CoordUtils.add(startingCoord, {'x': bondLength * Math.cos(direction), 'y': bondLength * Math.sin(direction)});
},
/**
* Check if two hash object that stores bond properties (type, order, stereo) is same in chemical means.
* @param {Hash} src
* @param {Hash} target
* @param {Array} propNames Property names to be compared
* @returns {boolean}
*/
isBondPropsMatch: function(src, target, propNames)
{
var specialFields = ['bondType', 'bondOrder', 'stereo']; // these three fields should be compared specially, since they may bave default null/undefined values
//var normalFields = AU.exclude(propNames, specialFields);
var result = true;
// compare special fields, regard null/undefined/0 as same
if (result && propNames.indexOf('bondType') >= 0)
result = (src.bondType == target.bondType || (!src.bondType && !target.bondType));
if (result && src.bondType === Kekule.BondType.COVALENT) // order/stereo only works in covalent bond
{
if (result && propNames.indexOf('bondOrder') >= 0 || (!src.bondOrder && !target.bondOrder))
result = (src.bondOrder == target.bondOrder);
if (result && propNames.indexOf('stereo') >= 0)
{
result = (src.stereo == target.stereo || (!src.stereo && !target.stereo));
}
}
if (result)
result = Kekule.ObjUtils.equal(src, target, specialFields);
return result;
},
/**
* Returns label represents the chem node situation.
* @param {Kekule.ChemStructureNode} node
* @param {Object} labelConfigs
* @returns {String}
*/
getChemStructureNodeLabel: function(node, labelConfigs)
{
//var labelConfigs = this.getLabelConfigs();
if (node.getIsotopeId) // atom
return node.getIsotopeId();
else if (node instanceof Kekule.SubGroup)
{
var groupLabel = node.getAbbr() || node.getFormulaText();
if (labelConfigs)
groupLabel = groupLabel || labelConfigs.getRgroup();
return groupLabel;
}
else
{
var ri = node.getCoreDisplayRichTextItem(null, null, labelConfigs);
return Kekule.Render.RichTextUtils.toText(ri);
}
},
/**
* Returns label represents all the chem nodes situation.
* @param {Array} nodes
* @param {Object} labelConfigs
* @returns {String}
*/
getAllChemStructureNodesLabel: function(nodes, labelConfigs)
{
var nodeLabel;
for (var i = 0, l = nodes.length; i < l; ++i)
{
var node = nodes[i];
var currLabel = Kekule.Editor.StructureUtils.getChemStructureNodeLabel(node, labelConfigs);
if (!nodeLabel)
nodeLabel = currLabel;
else
{
if (nodeLabel !== currLabel) // different label, currently has different nodes
{
return null;
}
}
}
return nodeLabel;
},
/**
* Returns HTML code represents all the chem nodes situation.
* @param {Array} nodes
* @param {Object} labelConfigs
* @returns {String}
*/
getAllChemStructureNodesHtmlCode: function(nodes, hydrogenDisplayLevel, showCharge, labelConfigs)
{
var result;
for (var i = 0, l = nodes.length; i < l; ++i)
{
var node = nodes[i];
var currRichText = node.getCoreDisplayRichTextItem(hydrogenDisplayLevel, showCharge, labelConfigs);
var currHtmlCode = Kekule.Render.RichTextUtils.toSimpleHtmlCode(currRichText);
if (!result)
result = currHtmlCode;
else
{
if (result !== currHtmlCode) // different label, currently has different nodes
{
return null;
}
}
}
return result;
},
/**
* Returns center abs base coord of a structure.
* @param {Kekule.StructureFragment} structureFragment
* @param {Int} coordMode
* @param {Bool} allowCoordBorrow
* @returns {Hash}
*/
getStructureCenterAbsBaseCoord: function(structureFragment, coordMode, allowCoordBorrow)
{
var result = null;
var add = Kekule.CoordUtils.add;
var nodeCount = structureFragment.getNodeCount();
for (var i = 0; i < nodeCount; ++i)
{
var n = structureFragment.getNodeAt(i);
var coord = n.getAbsBaseCoord(coordMode, allowCoordBorrow);
if (!result)
result = coord;
else
result = add(result, coord);
}
result = Kekule.CoordUtils.divide(result, nodeCount);
return result;
}
};
Kekule.Editor.RepositoryStructureUtils = {
/** @private */
_calcNodeMergeAdjustRotateAngle: function(editor, mergeNode, destNode)
{
var result = 0;
var coordMode = editor.getCoordMode();
// TODO: currently only handles 2D situation
if (coordMode !== Kekule.CoordMode.COORD3D)
{
var allowCoordBorrow = editor.getAllowCoordBorrow();
var structConfigs = editor.getEditorConfigs().getStructureConfigs();
var targetSurroundingObjs = Kekule.Editor.StructureUtils.getSurroundingObjs(mergeNode);
var destSurroundingObjs = Kekule.Editor.StructureUtils.getSurroundingObjs(destNode);
if (targetSurroundingObjs.length === 1) // only one bond connected to mergeNode in repository
{
var connector = mergeNode.getLinkedConnectorAt(0);
var bondOrder = connector.getBondOrder? connector.getBondOrder(): 0;
var bondAngle = structConfigs.getNewBondDefAngle(destNode, bondOrder);
refAngle = Kekule.Editor.StructureUtils.calcPreferred2DBondGrowingDirection(destNode, bondAngle, allowCoordBorrow);
var vector = Kekule.ChemStructureUtils.getAbsCoordVectorBetweenObjs(mergeNode, targetSurroundingObjs[0], coordMode, allowCoordBorrow);
var targetOriginAngle = Math.atan2(vector.y, vector.x);
result = refAngle - targetOriginAngle;
}
else if (destSurroundingObjs.length === 1) // only one bond connected to dest node
{
var connector = destNode.getLinkedConnectorAt(0);
var bondOrder = connector.getBondOrder? connector.getBondOrder(): 0;
var bondAngle = structConfigs.getNewBondDefAngle(mergeNode, bondOrder);
var refAngle = Kekule.Editor.StructureUtils.calcPreferred2DBondGrowingDirection(mergeNode, bondAngle, allowCoordBorrow);
var vector = Kekule.ChemStructureUtils.getAbsCoordVectorBetweenObjs(destNode, destSurroundingObjs[0], coordMode, allowCoordBorrow);
var destOriginAngle = Math.atan2(vector.y, vector.x);
result = destOriginAngle - refAngle;
}
else // more than one bonds in both mergeNode and destNode
{
var bondAngle = structConfigs.getNewBondDefAngle(destNode, null);
var refAngle = Kekule.Editor.StructureUtils.calcPreferred2DBondGrowingDirection(destNode, bondAngle, allowCoordBorrow);
var targetBondAngleRange = {};
for (var i = 0, l = targetSurroundingObjs.length; i < l; ++i)
{
var vector = Kekule.ChemStructureUtils.getAbsCoordVectorBetweenObjs(mergeNode, targetSurroundingObjs[i], coordMode, allowCoordBorrow);
var angle = Math.atan2(vector.y, vector.x);
if (Kekule.ObjUtils.isUnset(targetBondAngleRange.min) || (angle < targetBondAngleRange.min))
targetBondAngleRange.min = angle;
if (Kekule.ObjUtils.isUnset(targetBondAngleRange.max) || (angle > targetBondAngleRange.max))
targetBondAngleRange.max = angle;
}
var middleAngle = (targetBondAngleRange.max + targetBondAngleRange.min) / 2;
result = refAngle - middleAngle;
}
}
return result;
},
/** @private */
_calcConnectorMergeTransformParams: function(editor, mergeConnector, destConnector)
{
var targetObj0 = mergeConnector.getConnectedObjAt(0);
var targetObj1 = mergeConnector.getConnectedObjAt(1);
var destObj0 = destConnector.getConnectedObjAt(0);
var destObj1 = destConnector.getConnectedObjAt(1);
var targetCoord0 = editor.getObjCoord(targetObj0);
var targetCoord1 = editor.getObjCoord(targetObj1);
var destCoord0 = editor.getObjCoord(destObj0);
var destCoord1 = editor.getObjCoord(destObj1);
// TODO: currently only handles 2D situation
var result = Kekule.CoordUtils.calcCoordGroup2DTransformParams(targetCoord0, targetCoord1, destCoord1, destCoord0);
// targetCoord0 map to destCoord1, as in merging of ring, ring order are always reversed
return result;
},
/** @private */
calcRepObjInitialTransformParams: function(editor, repItem, repResult, destObj, targetCoord)
{
var repBaseCoord = repResult.baseObjCoord;
var editorRefLength = editor.getDefBondLength();
var coordScale = editorRefLength / repItem.getRefLength();
var rotateAngle;
var center = repBaseCoord;
//var targetCoord = screenCoord;
if (repResult.mergeObj)
{
var destObj = repResult.mergeDest || destObj;
if (destObj) // targetCoord decide by merge position
{
targetCoord = editor.getObjectScreenCoord(destObj);
if ((repResult.mergeObj instanceof Kekule.ChemStructureNode)
&& (destObj instanceof Kekule.ChemStructureNode)) // node merge, calc initial angle by bond
{
rotateAngle = Kekule.Editor.RepositoryStructureUtils._calcNodeMergeAdjustRotateAngle(editor, repResult.mergeObj, destObj);
//console.log(center);
}
else if ((repResult.mergeObj instanceof Kekule.ChemStructureConnector)
&& (destObj instanceof Kekule.ChemStructureConnector)) // connector merge
{
// return directly
return Kekule.Editor.RepositoryStructureUtils._calcConnectorMergeTransformParams(editor, repResult.mergeObj, destObj);
}
}
}
var objCoord = editor.translateCoord(targetCoord, Kekule.Editor.CoordSys.SCREEN, Kekule.Editor.CoordSys.CHEM);
if (repBaseCoord)
{
objCoord = Kekule.CoordUtils.substract(objCoord, repBaseCoord);
}
var transformParams = {
'scale': coordScale,
'translateX': objCoord.x,
'translateY': objCoord.y,
'translateZ': objCoord.z,
'rotateAngle': rotateAngle,
'center': center
};
return transformParams;
},
/** @private */
transformChemObjectsCoordAndSize: function(editor, objects, transformParams)
{
var coordMode = editor.getCoordMode();
var allowCoordBorrow = editor.getAllowCoordBorrow();
var matrix = (coordMode === Kekule.CoordMode.COORD3D)?
Kekule.CoordUtils.calcTransform3DMatrix(transformParams):
Kekule.CoordUtils.calcTransform2DMatrix(transformParams);
var childTransformParams = Object.extend({}, transformParams);
childTransformParams = Object.extend(childTransformParams, {
'translateX': 0,
'translateY': 0,
'translateZ': 0,
'center': {'x': 0, 'y': 0, 'z': 0}
});
var childMatrix = (coordMode === Kekule.CoordMode.COORD3D)?
Kekule.CoordUtils.calcTransform3DMatrix(childTransformParams):
Kekule.CoordUtils.calcTransform2DMatrix(childTransformParams);
for (var i = 0, l = objects.length; i < l; ++i)
{
var obj = objects[i];
obj.transformAbsCoordByMatrix(matrix, childMatrix, coordMode, true, allowCoordBorrow);
obj.scaleSize(transformParams.scale, coordMode, true, allowCoordBorrow);
}
}
};
})();