kekule
Version:
Open source JavaScript toolkit for chemoinformatics
981 lines (898 loc) • 118 kB
JavaScript
/**
* @fileoverview
* This file contains util classes and functions about chem document.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /core/kekule.common.js
* requires /core/kekule.structures.js
* requires /core/kekule.chemReactions.js
* requires /core/kekule.glyph.chemGlyphs.js
*/
// consts for reaction arrangements
Kekule.ReactionLayoutXMode = {
LtoR: 1,
RtoL: -1
};
Kekule.ReactionLayoutYMode = {
BtoT: 1,
TtoB: -1
};
Kekule.ReactionObjectAlign = {
LEFT: 1,
RIGHT: 2,
TOP: 3,
BOTTOM: 4,
CENTER: 5
};
// reaction extraction global options
Kekule.globalOptions.add('reaction', {
// options for extracting reaction (chains) from chem document
extraction: {
ignoredReactionArrowTypes: null, // default is [Kekule.Glyph.ReactionArrowType.RETROSYNTHESIS], but since Kekule.Glyph.ReactionArrowType is defined after this, we can not use it directly here
substanceGapLengthThresholdRatioToDocRefLength: 3,
reactionArrowPerpendicularExpansionRatioToDocRefLength: 1/2,
reactionArrowHorizontalExpansionRationToDocRefLength: 3,
reactionSortRefLengthRatioToDocRefLength: 1,
reactionSortXMode: Kekule.ReactionLayoutXMode.LtoR,
reactionSortYMode: Kekule.ReactionLayoutYMode.TtoB,
reactionSortAxisWeightRatioXY: 1/5,
enableSiblingMerging: true,
enableMergeSharedProductAndReactant: true,
enableMergeOnProductOmission: true,
insertImplicitIntermediate: false
},
// options for auto-layout of reaction to chem object
layout: {
reactionGapLengthRatioToDocRefLength: 0.5, // gap between two reactions
substancePrimaryGapLengthRatioToDocRefLength: 0.5, // gap of substances in primary direction (e.g. between reactants and roducts)
substanceSecondaryGapLengthRatioToDocRefLength: 0.5, // gap of substances in secondary direction (e.g. between reagents)
plusSymbolSizeRatioToDocRefLength: 1,
reactionArrowMinSizeRatioToDocRefLength: 1.5,
reactionArrowPaddingRatioToDocRefLength: 0.5, // the reaction arrow should be longer than any of the reagents
layoutXMode: Kekule.ReactionLayoutXMode.LtoR,
layoutYMode: Kekule.ReactionLayoutYMode.TtoB,
primaryAxis: 'x',
mainSubstancePrimaryAxisAlignMode: Kekule.ReactionObjectAlign.LEFT, // reactants and products
mainSubstanceSecondaryAxisAlignMode: Kekule.ReactionObjectAlign.CENTER,
// assocSubstancePrimaryAxisAlignMode: Kekule.ReactionObjectAlign.CENTER,
// assocSubstanceSecondaryAxisAlignMode: Kekule.ReactionObjectAlign.CENTER,
assocSubstanceAlignMode: Kekule.ReactionObjectAlign.CENTER, // reagents above or below reaction arrow, only consider align in the primary axis direction
assocSubstanceStackOnPrimaryAxis: true, // if false, multiple assoc substances will to layout in secondary axis (e.g., primary is x, assoc substances will be layouted from/to top/bottom)
singleLine: false // if true, multiple reactions will be layout to one line rather than multiple lines
// reactionInlineBlockAlignMode: Kekule.ReactionObjectAlign.BASELINE // in single line mode, the multiple reaction box align mode. Default base line means the arrow position in secondary axis is same.
}
});
/**
* Utility functions for extraction chemical reactions from chem document.
* @namespace
*/
Kekule.ReactionExtractionUtils = {
_getReactionArrowsInDoc: function(chemDoc)
{
return chemDoc.filterChildren(function(child) {
// var ignoredTypes = [Kekule.Glyph.ReactionArrowType.RESONANCE, Kekule.Glyph.ReactionArrowType.RETROSYNTHESIS];
var ignoredTypes = Kekule.globalOptions.reaction.extraction.ignoredReactionArrowTypes || [Kekule.Glyph.ReactionArrowType.RETROSYNTHESIS];
return (child instanceof Kekule.Glyph.ReactionArrow) && (ignoredTypes.indexOf(child.getReactionType()) < 0);
}, true);
},
_getPlusSymbolsInDoc: function(chemDoc)
{
return chemDoc.filterChildren(function(child) {
return (child instanceof Kekule.Glyph.PlusSymbol);
}, true);
},
_getMoleculesInDoc: function(chemDoc)
{
return chemDoc.filterChildren(function(child) {
return (child instanceof Kekule.Molecule);
}, true);
},
_fillMoleculeGeometryMap: function(map, chemDoc, molecules, containerBoxLengthThreshold, expandedContainerBoxExpansion, normalContainerBoxExpansionRatio)
{
if (normalContainerBoxExpansionRatio === undefined)
normalContainerBoxExpansionRatio = 1/100; // expand the container box a little, let it easier to intersect with arrow line
var result = map; // new Kekule.MapEx();
for (var i = 0, l = molecules.length; i < l; ++i)
{
var containerBox = molecules[i].getExposedContainerBox ? molecules[i].getExposedContainerBox(Kekule.CoordMode.COORD2D) : molecules[i].getContainerBox(Kekule.CoordMode.COORD2D);
var centerCoord = {
x: (containerBox.x1 + containerBox.x2) / 2,
y: (containerBox.y1 + containerBox.y2) / 2
};
// if container box is quite small in width or height, we expand it a little to check whether it is intersect with reaction arrow line
var expanded = false;
var expandedContainerBox = containerBox;
if (containerBox.y2 - containerBox.y1 < containerBoxLengthThreshold) // expand in y direction
{
expandedContainerBox = Kekule.BoxUtils.inflateBox(expandedContainerBox, 0, expandedContainerBoxExpansion / 2);
expanded = true;
}
if (containerBox.x2 - containerBox.x1 < containerBoxLengthThreshold) // expand in x direction
{
expandedContainerBox = Kekule.BoxUtils.inflateBox(expandedContainerBox, expandedContainerBoxExpansion / 2, 0);
expanded = true;
}
if (normalContainerBoxExpansionRatio) // expand the container box a little, let it easier to intersect with arrow line
containerBox = Kekule.BoxUtils.inflateBox(containerBox,
(containerBox.x2 - containerBox.x1) * normalContainerBoxExpansionRatio / 2,
(containerBox.y2 - containerBox.y1) * normalContainerBoxExpansionRatio / 2);
result.set(molecules[i], {
containerBox: containerBox,
expandedContainerBox: expanded? expandedContainerBox: null,
centerCoord: centerCoord,
refLength: Math.max(Math.abs(containerBox.x2 - containerBox.x1), Math.abs(containerBox.y2 - containerBox.y1))
});
}
return result;
},
_fillSymbolGeometryMap: function(map, chemDoc, glyphSymbols)
{
var result = map; // new Kekule.MapEx();
for (var i = 0, l = glyphSymbols.length; i < l; ++i)
{
var containerBox = glyphSymbols[i].getExposedContainerBox ? glyphSymbols[i].getExposedContainerBox(Kekule.CoordMode.COORD2D) : glyphSymbols[i].getContainerBox(Kekule.CoordMode.COORD2D);
var centerCoord = {
x: (containerBox.x1 + containerBox.x2) / 2,
y: (containerBox.y1 + containerBox.y2) / 2
};
result.set(glyphSymbols[i], {
containerBox: containerBox,
centerCoord: centerCoord
});
}
return result;
},
_isContainerBoxEmpty: function(containerBox)
{
return !containerBox
|| (Kekule.NumUtils.isFloatEqual(containerBox.x1 - containerBox.x2, 0)
&& Kekule.NumUtils.isFloatEqual(containerBox.y1 - containerBox.y2, 0));
},
_getObjDetailNeighborEx: function(reactionInfo, objDetailOrIndex, substanceType, direction, candidateDetails, initialStep)
{
var molDetails = reactionInfo[substanceType + 'Details'];
var currMolDetail, molIndex;
var dir = direction || 1;
if (typeof(objDetailOrIndex) === 'object')
{
currMolDetail = objDetailOrIndex;
molIndex = molDetails.indexOf(currMolDetail);
}
else
{
molIndex = objDetailOrIndex;
currMolDetail = molDetails[molIndex];
}
if (!currMolDetail || molIndex < 0)
return {};
var step = (substanceType === 'reactant')? 1:
(substanceType === 'product')? -1:
(substanceType === 'reagent')? (currMolDetail.onTop? 1: -1): null;
if (!step)
return {};
step *= dir;
var neighborIndex = molIndex + step;
var neighborDetail = molDetails[neighborIndex];
if (substanceType === 'reagent' && neighborDetail && neighborDetail.onTop !== currMolDetail.onTop)
neighborDetail = null;
// if neighbor not in candidate list, check neighbor's neighbor
if (candidateDetails && neighborDetail && candidateDetails.indexOf(neighborDetail) < 0)
return RU._getObjDetailNeighborEx(reactionInfo, neighborDetail, substanceType, dir, candidateDetails, (initialStep || 0) + step);
return {neighborDetail: neighborDetail, step: step};
},
_getObjDetailNeighbor: function(reactionInfo, objDetailOrIndex, substanceType, direction, candidateDetails)
{
var neighborEx = RU._getObjDetailNeighborEx(reactionInfo, objDetailOrIndex, substanceType, direction, candidateDetails, 0);
return neighborEx && neighborEx.neighborDetail;
},
_getObjDetailDistanceToNeighborOrReactionArrowEx: function(reactionInfo, objDetailOrIndex, substanceType, candidatedDetails)
{
var molDetails = reactionInfo[substanceType + 'Details'];
var currMolDetail, molIndex;
if (typeof(objDetailOrIndex) === 'object')
{
currMolDetail = objDetailOrIndex;
molIndex = molDetails.indexOf(currMolDetail);
}
else
{
molIndex = objDetailOrIndex;
currMolDetail = molDetails[molIndex];
}
if (!currMolDetail || molIndex < 0)
return null;
/*
var step = (substanceType === 'reactant')? 1:
(substanceType === 'product')? -1:
(substanceType === 'reagent')? (currMolDetail.onTop? 1: -1): null;
if (!step)
return null;
var neighborIndex = molIndex + step;
var neighborMolDetail = molDetails[neighborIndex];
if (substanceType === 'reagent' && neighborMolDetail && neighborMolDetail.onTop !== currMolDetail.onTop)
neighborMolDetail = null;
*/
var neighborDetailEx = RU._getObjDetailNeighborEx(reactionInfo, objDetailOrIndex, substanceType, null, candidatedDetails);
var neighborDetail = neighborDetailEx.neighborDetail;
var distance;
if (substanceType === 'reagent')
{
if (neighborDetail)
distance = Math.min(currMolDetail.distanceToArrow, Kekule.CoordUtils.getDistance(currMolDetail.centerCoord, neighborDetail.centerCoord));
else
distance = currMolDetail.distanceToArrow; // currMolDetail.distanceToArrowCenter;
/*
distance = neighborDetail ?
(Math.abs(currMolDetail.distance - neighborDetail.distance)) :
currMolDetail.distance;
*/
}
else // product and reactant
{
distance = neighborDetail?
(Math.abs((currMolDetail.distance - currMolDetail.crossLengthOnReactionArrowLine / 2) - (neighborDetail.distance + neighborDetail.crossLengthOnReactionArrowLine / 2))):
currMolDetail.distance - currMolDetail.crossLengthOnReactionArrowLine / 2;
}
return {
distance: distance,
neighborDetail: neighborDetail,
neighborStep: neighborDetailEx.step // step distance to objDetail
};
},
_getObjDetailDistanceToNeighborOrReactionArrow: function(reactionInfo, objDetailOrIndex, substanceType, candidatedDetails)
{
return RU._getObjDetailDistanceToNeighborOrReactionArrowEx(reactionInfo, objDetailOrIndex, substanceType, candidatedDetails).distance;
},
_getObjDetailOverlapsEx: function(objDetails1, objDetails2, options)
{
var ops = options || {};
var calcDistancesToCenter = ops.calcDistancesToCenter;
var overlaps = [];
var overlapsWithExtra = [];
for (var i = 0, l = objDetails1.length; i < l; ++i)
{
var objDetail1 = objDetails1[i];
for (var j = 0, k = objDetails2.length; j < k; ++j)
{
var objDetail2 = objDetails2[j];
if (objDetail2.object === objDetail1.object)
{
overlaps.push(objDetail1.object);
if (calcDistancesToCenter)
{
overlapsWithExtra.push({
object: objDetail1.object,
distances: [
objDetail1.distance,
objDetail2.distance
]
});
}
}
}
}
var result = { overlaps: overlaps };
if (calcDistancesToCenter)
result.overlapsWithExtra = overlapsWithExtra;
if (ops.checkUnoverlaps)
{
var Unoverlaps = [[], []];
for (var i = 0, l = objDetails1.length; i < l; ++i)
{
if (overlaps.indexOf(objDetails1[i].object) < 0)
Unoverlaps[0].push(objDetails1[i]);
}
for (var i = 0, l = objDetails2.length; i < l; ++i)
{
if (overlaps.indexOf(objDetails2[i].object) < 0)
Unoverlaps[0].push(objDetails2[i]);
}
result.unoverlapDetails = Unoverlaps;
}
return result;
},
_getReactionDetailOverlapsEx: function(reactionInfo1, reactionInfo2, checkedSubstanceTypes, options)
{
if (!checkedSubstanceTypes)
checkedSubstanceTypes = ['reactant', 'product', 'reagent'];
var reactionComponentDetails1 = [];
var reactionComponentDetails2 = [];
for (var i = 0, l = checkedSubstanceTypes.length; i < l; ++i)
{
var substanceType = checkedSubstanceTypes[i];
var substanceDetails1 = reactionInfo1[substanceType + 'Details'];
var substanceDetails2 = reactionInfo2[substanceType + 'Details'];
reactionComponentDetails1 = reactionComponentDetails1.concat(substanceDetails1);
reactionComponentDetails2 = reactionComponentDetails2.concat(substanceDetails2);
}
return RU._getObjDetailOverlapsEx(reactionComponentDetails1, reactionComponentDetails2, options);
},
_getReactionDetailOverlaps: function(reactionInfo1, reactionInfo2, checkedSubstanceTypes)
{
return RU._getReactionDetailOverlapsEx(reactionInfo1, reactionInfo2, checkedSubstanceTypes).overlaps;
},
_getSubstanceTypeInReactionInfo: function(reactionInfo, object, checkedSubstanceTypes)
{
var molDetail = this._getObjDetailsInReactionInfo(object, reactionInfo, checkedSubstanceTypes);
return molDetail && molDetail.substanceType;
},
_getObjDetailsInReactionInfo: function(object, reactionInfo, checkedSubstanceTypes)
{
if (!checkedSubstanceTypes)
checkedSubstanceTypes = ['reactant', 'product', 'reagent'];
for (var i = 0, l = checkedSubstanceTypes.length; i < l; ++i)
{
var substanceType = checkedSubstanceTypes[i];
var fieldName = substanceType + 'Details';
var field = reactionInfo[fieldName];
for (var j = 0, k = field.length; j < k; ++j)
{
if (field[j].object === object)
{
return field[j];
}
}
}
return null;
},
_removeObjFromReactionInfo: function(object, reactionInfo, checkedSubstanceTypes)
{
if (!checkedSubstanceTypes)
checkedSubstanceTypes = ['reactant', 'product', 'reagent'];
for (var i = 0, l = checkedSubstanceTypes.length; i < l; ++i)
{
var substanceType = checkedSubstanceTypes[i];
var fieldName = substanceType + 'Details';
var field = reactionInfo[fieldName];
for (var j = field.length - 1; j >= 0; --j)
{
if (field[j].object === object)
{
field.splice(j, 1);
return;
}
}
}
},
_getPossibleReactionRelationWithOverlappingObject: function(reactionInfo1, reactionInfo2, overlappingObject, objDetails1, objDetails2)
{
if (!objDetails1)
objDetails1 = RU._getObjDetailsInReactionInfo(overlappingObject, reactionInfo1);
if (!objDetails2)
objDetails2 = RU._getObjDetailsInReactionInfo(overlappingObject, reactionInfo2);
var objSharable = false;
var possibleReactionRelation = null; // 0: parallel reaction sharing reactant, -1: reaction1 -> reaction2, 1: reaction2 -> reaction1, null: no relation
if (objDetails1 && objDetails2)
{
if (objDetails1.substanceType === 'reactant' && objDetails2.substanceType !== 'reagent')
{
objSharable = true;
possibleReactionRelation = (objDetails2.substanceType === 'product')? 1: 0;
}
else if (objDetails2.substanceType === 'reactant' && objDetails1.substanceType !== 'reagent')
{
objSharable = true;
possibleReactionRelation = (objDetails1.substanceType === 'product')? -1: 0;
}
}
return {
objSharable: objSharable,
possibleReactionRelation: possibleReactionRelation // 0: parallel reaction sharing reactant, -1: reaction1 -> reaction2, 1: reaction2 -> reaction1
};
},
_getPossibleReactionRelationWithOverlappingMoleculeSet: function(reactionInfo1, reactionInfo2, overlappingObjects)
{
var result;
for (var i = 0, l = overlappingObjects.length; i < l; ++i)
{
var possibleRelationOnCurr = RU._getPossibleReactionRelationWithOverlappingObject(reactionInfo1, reactionInfo2, overlappingObjects[i]);
if (result && (result.objSharable !== possibleRelationOnCurr.objSharable || result.possibleReactionRelation !== possibleRelationOnCurr.possibleReactionRelation))
throw 'Reaction relation can not be calculated'; // TODO: relation not match on different molecule, temporarily throw a exception
else
result = possibleRelationOnCurr;
}
return result.possibleReactionRelation;
},
// decide the overlapping molecule belongs to which reaction
_getOverlappingObjectArrangementInfo: function(reactionInfo1, reactionInfo2, object, unoverlappedDetails, objGeometryMap)
{
var getCoordRelPosToLines = function(coord, lineParamsList)
{
var result = [];
for (var i = 0, l = lineParamsList.length; i < l; ++i)
{
var relPos = Kekule.GeometryUtils.getPointRelPositionToLine(coord, lineParamsList[i]);
result.push(relPos);
}
return result;
};
var isReactantOrProductMolNeighboringToPlusSymbol = function(reactionInfo, objDetail) {
if (objDetail.substanceType !== 'reactant' && objDetail.substanceType !== 'product')
return false;
if (!(objDetail.object instanceof Kekule.Molecule))
return false;
// get the next neighbor
var neighborDetail = RU._getObjDetailNeighbor(reactionInfo, objDetail, objDetail.substanceType, null, null);
var result = neighborDetail && (neighborDetail.object instanceof Kekule.Glyph.PlusSymbol);
return result;
}
// var moleculeInShareZone = false;
// var molBelongReaction = null;
// var molGeoInfo = molGeometryMap.get(molecule);
var objDetails1 = RU._getObjDetailsInReactionInfo(object, reactionInfo1);
var objDetails2 = RU._getObjDetailsInReactionInfo(object, reactionInfo2);
var normalLineParamsList = [reactionInfo1.arrowDetails.normalLineEquationParams, reactionInfo2.arrowDetails.normalLineEquationParams];
// 计算两个反应箭头的共享区域与独享区域,作两个反应箭头中点出发的法线。
// 此外分子类型也与能否共享相关。一反应的产物可共享为另一反应的反应物,或两反应共享反应物,但两反应不能共享产物。至于Reagent,均不视作可共享。
/*
[molType1/molType2] reactant product reagent
reactant Y Y N
product Y N N
reagent N N N
*/
// 若两反应共享reactant,则两反应箭头起点必须处于同一法线分隔区域中才行。
var possibleRelationResult = RU._getPossibleReactionRelationWithOverlappingObject(reactionInfo1, reactionInfo2, object, objDetails1, objDetails2);
var objSharable = possibleRelationResult.objSharable;
var possibleReactionRelation = possibleRelationResult.possibleReactionRelation;
// further check
if (possibleReactionRelation === 0) // may share reactant
{
// 若共享反应物,则要求两反应箭头的起点位于法线分隔的同一区域中
// var molRelPositions = getCoordRelPosToLines(molGeoInfo.centerCoord, normalLineParamsList);
var reaction1StartingPointRelPositions = getCoordRelPosToLines(reactionInfo1.arrowDetails.coords[0], normalLineParamsList);
var reaction2StartingPointRelPositions = getCoordRelPosToLines(reactionInfo2.arrowDetails.coords[0], normalLineParamsList);
if (reaction1StartingPointRelPositions[0] !== reaction2StartingPointRelPositions[0] || reaction1StartingPointRelPositions[1] !== reaction2StartingPointRelPositions[1])
objSharable = false;
}
var result = {sharable: objSharable};
if (!objSharable)
{
/*
// reagent has the lowest priority
if (objDetails1.substanceType === 'reagent' && objDetails2.substanceType !== 'reagent')
{
result.belongedReactionIndex = 1;
result.belongedReactionInfo = reactionInfo2;
}
else if (objDetails1.substanceType !== 'reagent' && objDetails2.substanceType === 'reagent')
{
result.belongedReactionIndex = 0;
result.belongedReactionInfo = reactionInfo1;
}
else
*/
// 检查反应物或产物分子是否与 + 相邻,若是,则 molecule 属于 + 所在反应
var reactantOrProductNeighboringPlusSymbol1 = isReactantOrProductMolNeighboringToPlusSymbol(reactionInfo1, objDetails1);
var reactantOrProductNeighboringPlusSymbol2 = isReactantOrProductMolNeighboringToPlusSymbol(reactionInfo2, objDetails2);
if (reactantOrProductNeighboringPlusSymbol1 && !reactantOrProductNeighboringPlusSymbol2)
{
result.belongedReactionIndex = 0;
result.belongedReactionInfo = reactionInfo1;
}
else if (reactantOrProductNeighboringPlusSymbol2 && !reactantOrProductNeighboringPlusSymbol1)
{
result.belongedReactionIndex = 1;
result.belongedReactionInfo = reactionInfo2;
}
else
{
// /* 计算分子与两个反应箭头中点的距离,较近者拥有此分子 */
// if (molDetails2.distance < molDetails1.distance)
// 计算分子与不共享的临近分子的距离,较近者拥有此分子
// /* 注意此时分子在两个反应中,要么都是反应物,要么都是产物 */
var distanceInfoToNeighbor1 = RU._getObjDetailDistanceToNeighborOrReactionArrowEx(reactionInfo1, objDetails1, objDetails1.substanceType, unoverlappedDetails[0]);
var distanceInfoToNeighbor2 = RU._getObjDetailDistanceToNeighborOrReactionArrowEx(reactionInfo2, objDetails2, objDetails2.substanceType, unoverlappedDetails[1]);
var distanceToNeighbor1 = distanceInfoToNeighbor1.distance;
var distanceToNeighbor2 = distanceInfoToNeighbor2.distance;
if (distanceToNeighbor2 < distanceToNeighbor1)
{
result.belongedReactionIndex = 1;
result.belongedReactionInfo = reactionInfo2;
}
else
{
result.belongedReactionIndex = 0;
result.belongedReactionInfo = reactionInfo1;
}
}
}
else
{
result.reactionRelation = possibleReactionRelation;
}
return result;
/*
var reactionArrowCrossPoint = Kekule.GeometryUtils.getCrossPointOfLines(
reactionInfo1.arrowDetails.coords[0], reactionInfo1.arrowDetails.coords[1],
reactionInfo2.arrowDetails.coords[0], reactionInfo2.arrowDetails.coords[0],
true, true
);
if (!reactionArrowCrossPoint)
{
// 两箭头平行无交点,则两法线分隔为三区域,中间区域共享,两端区域独享
}
else
{
// 两箭头(延长线)相交,此时二法线将平面分隔为四个区域。该交点所处的区域即为两个反应的共享区域,其余区域均为独享。
}
*/
/*
{
// calc the delta of two reaction arrow centers
// if delta.x < 0, means reactionArrow2 is on the left side of reactionArrow1; otherwise on the right side
// if delta.y < 0, means reactionArrow1 is on the top side of reactionArrow1; otherwise on the bottom side
var reactionArrowCenterCoordDelta = Kekule.CoordUtils.substract(reactionInfo2.arrowDetails.centerCoord, reactionInfo1.arrowDetails.centerCoord);
var molGeometryInfo = molGeometryMap.get(molecule);
var molCenterCoord = molGeometryInfo.centerCoord;
// 直线l:Ax+By+C=0分坐标平面为两个半平面:一个半平面内的点的坐标满足不等式 Ax+By+C>0;另一个半平面内的点的坐标满足不等式Ax+By+C<0
var molCenterRelPositions = getCoordRelPosToLines(molCenterCoord, normalLineParamsList);
if (molCenterRelPositions[0] === 0)
{
// on normal line 1, belongs to reaction 1
}
else if (molCenterRelPositions[1] === 0)
{
// on normal line 2, belongs to reaction 2
}
else if (Math.sign(molCenterRelPositions[0]) !== Math.sign(molCenterRelPositions)[1])
{
// on sharing zone?
var reactionArrowCommonCenterCoord = Kekule.CoordUtils.getCenter([reactionInfo1.centerCoord, reactionInfo2.centerCoord]);
var reactionArrowCommonCenterCoordRelPositions = getCoordRelPosToLines(reactionArrowCommonCenterCoord, normalLineParamsList);
if ((Math.sign(reactionArrowCommonCenterCoordRelPositions[0]) === Math.sign(molCenterRelPositions[0]))
&& (Math.sign(reactionArrowCommonCenterCoordRelPositions[1]) === Math.sign(molCenterRelPositions[1])))
{
// belongs to sharing zone
moleculeInShareZone = true;
}
else
{
// belongs to opposite region of sharing zone
}
}
else
{
// 在两条法线的同一侧,属于某一反应
}
}
if (!moleculeInShareZone)
{
// decide the molecule belongs to which reaction
}
*/
},
_calcReactionChainGeometry: function(reactionInfos, objGeometryMap, options)
{
var result = {};
// consider reactant and product only, bypass reagents
var objCenterCoordSum = {};
var objCenterCount = 0;
var geoObjs = [];
for (var i = 0, l = reactionInfos.length; i < l; ++i)
{
var reactionInfo = reactionInfos[i];
geoObjs = geoObjs.concat(reactionInfo.reactantDetails).concat(reactionInfo.productDetails);
objCenterCoordSum = Kekule.CoordUtils.add(objCenterCoordSum, reactionInfo.arrowDetails.centerCoord);
++objCenterCount;
}
for (var i = 0, l = geoObjs.length; i < l; ++i)
{
var obj = geoObjs[i].object;
var objGeometryInfo = objGeometryMap.get(obj);
if (objGeometryInfo)
{
result.containerBox = Kekule.BoxUtils.getContainerBox(objGeometryInfo.containerBox, result);
objCenterCoordSum = Kekule.CoordUtils.add(objCenterCoordSum, objGeometryInfo.centerCoord);
++objCenterCount;
}
}
result.centerCoord = Kekule.CoordUtils.divide(objCenterCoordSum, objCenterCount);
return result;
},
_extractReactionInfoFromChemDocument: function(chemDoc, reactionArrow, plusSymbols, targetMolecules, objGeometryMap, options)
{
var ops = options || {};
// var cloneMolecules = (ops.cloneMolecules === undefined)? true: !!ops.cloneMolecules;
var calcContainerBoxes = ops.calcContainerBoxes || false;
if (!reactionArrow)
{
reactionArrow = RU._getReactionArrowsInDoc(chemDoc)[0];
}
if (!reactionArrow)
return null;
if (!plusSymbols)
{
plusSymbols = RU._getPlusSymbolsInDoc(chemDoc);
}
var arrowCoords = [];
for (var i = 0, l = reactionArrow.getNodeCount(); i < l; ++i)
{
var endNode = reactionArrow.getNodeAt(i);
arrowCoords.push(endNode.getAbsCoord2D());
}
var arrowCenterCoord = Kekule.CoordUtils.getCenter(arrowCoords);
var reactionContainerBox = Kekule.BoxUtils.createBox(arrowCoords[0], arrowCoords[1]); // the large container box to be contain all reaction components
var reactantContainerBox, productContainerBox, reagentContainerBox;
// normal line cross to arrow
var arrowNormalLineCoords = [
{x: -(arrowCoords[0].y - arrowCenterCoord.y) + arrowCenterCoord.x, y: (arrowCoords[0].x - arrowCenterCoord.x) + arrowCenterCoord.y},
{x: -(arrowCoords[1].y - arrowCenterCoord.y) + arrowCenterCoord.x, y: (arrowCoords[1].x - arrowCenterCoord.x) + arrowCenterCoord.y}
];
var arrowNormalLineEquationParams = Kekule.GeometryUtils.getLineGeneralEquationParams(arrowNormalLineCoords[0], arrowNormalLineCoords[1]);
var arrowStartingPointRelPositionToNormalLine = Kekule.GeometryUtils.getPointRelPositionToLine(arrowCoords[0], arrowNormalLineEquationParams);
/*
var arrowBox = reactionArrow.getExposedContainerBox? reactionArrow.getExposedContainerBox(): reactionArrow.getContainerBox();
var arrowCoords = [
{x: arrowBox.x1, y: arrowBox.y1},
{x: arrowBox.x2, y: arrowBox.y2}
];
*/
var arrowLength = Kekule.CoordUtils.getDistance(arrowCoords[0], arrowCoords[1]);
var arrowDirection = Math.sign(arrowCoords[1].x - arrowCoords[0].x || arrowCoords[1].y - arrowCoords[0].y);
var arrowNormalLineDirection = Math.sign(arrowNormalLineCoords[1].x - arrowNormalLineCoords[0].x || arrowNormalLineCoords[1].y - arrowNormalLineCoords[0].y);
/*
var arrowCoordDelta = Kekule.CoordUtils.substract(arrowCoords[1], arrowCoords[0]);
var arrowPrimaryAxis = (Math.abs(arrowCoordDelta.y) > Math.abs(arrowCoordDelta.x))? 'y': 'x';
*/
var reactionAutoRefLength = arrowLength;
// some geometry params
// TODO: current fixed ratio
var reactionArrowPerpendicularExpansionRatio = ops.reactionArrowPerpendicularExpansionRatio || 0.6; // TODO: currently the default ratio is fixed
var reactionArrowHorizontalExpansionRatio = 1 + (ops.reactionArrowHorizontalExpansionRatio || 0.6); // TODO: currently the default ratio is fixed
var substanceGapThreshold = ops.substanceGapLengthThreshold || (reactionAutoRefLength * 2); // substance faraway larger than this distance will not be regarded as reaction component
var reactionArrowPerpendicularExpansion = ops.reactionArrowPerpendicularExpansion || reactionAutoRefLength * reactionArrowPerpendicularExpansionRatio;
var reactionArrowHorizontalExpansion = ops.reactionArrowHorizontalExpansion || reactionAutoRefLength * reactionArrowHorizontalExpansionRatio;
var calcPointRelationInfoToReactionArrowLine = function(pointCoord, perpendicularCrossPointCoord)
{
if (!perpendicularCrossPointCoord)
perpendicularCrossPointCoord = Kekule.GeometryUtils.getPerpendicularCrossPointFromCoordToLine(objCenterCoord, arrowCoords[0], arrowCoords[1], true);
var perpendicularDistance = Kekule.CoordUtils.getDistance(pointCoord, perpendicularCrossPointCoord);
if (perpendicularDistance * 2 < reactionArrowPerpendicularExpansion)
{
// inside the expansion area of reaction arrow in horizontal direction, should be a substance of reaction
crossPointDirection = Math.sign((perpendicularCrossPointCoord.x - arrowCenterCoord.x) || (perpendicularCrossPointCoord.y - arrowCenterCoord.y));
objInReactionArrowHorizontalDirection = true;
distance = Kekule.CoordUtils.getDistance(perpendicularCrossPointCoord, arrowCenterCoord);
return {
inReactionArrowHorizontalDirection: objInReactionArrowHorizontalDirection,
crossPointDirection: crossPointDirection,
distance: distance,
perpendicularDistance: perpendicularDistance
}
}
else
return { inReactionArrowHorizontalDirection: false }
}
var molecules = targetMolecules || RU._getMoleculesInDoc(chemDoc);
var molAndPlusSymbols = [].concat(molecules).concat(plusSymbols); // we mix the plus symbol and molecule, since they acts similar in reactants and products (but not in reagents)
// var molContainerBoxes = [];
var reactantDetails = [], productDetails = [], reagentDetails = [];
for (var i = 0, l = molAndPlusSymbols.length; i < l; ++i) {
var currObj = molAndPlusSymbols[i];
var objInReactionArrowHorizontalDirection = false;
var objInReactionArrowVerticalDirection = false;
var crossPointDirection, crossPointToNormalLineDirection;
var containerBox, expandedContainerBox, objCenterCoord;
if (!(currObj instanceof Kekule.Glyph.PlusSymbol)) // currObj is molecule
{
var molGeometryInfo = objGeometryMap.get(currObj);
containerBox = molGeometryInfo.containerBox;
expandedContainerBox = molGeometryInfo.expandedContainerBox;
objCenterCoord = molGeometryInfo.centerCoord;
reactionAutoRefLength = Math.max(reactionAutoRefLength, molGeometryInfo.refLength);
}
else // currObj is plus symbol
{
var symbolGeometryInfo = objGeometryMap.get(currObj);
containerBox = symbolGeometryInfo.containerBox; // we regard the plus symbol as a single point
objCenterCoord = symbolGeometryInfo.centerCoord;
}
var distance = null, distanceOnNormalLine = null, perpendicularDistance = null, crossLengthOnReactionArrowLine = null;
// check if molecule is on the horizontal direction of arrow line, if so, it may be a reactant or product
var perpendicularCrossPointCoord = Kekule.GeometryUtils.getPerpendicularCrossPointFromCoordToLine(objCenterCoord, arrowCoords[0], arrowCoords[1], true);
if (RU._isContainerBoxEmpty(containerBox) || !(currObj instanceof Kekule.Molecule))
{
// plus symbol or molecule with no ctab (but with formula), we need to handle it in another way.
var relationInfo = calcPointRelationInfoToReactionArrowLine(objCenterCoord, perpendicularCrossPointCoord);
if (relationInfo.inReactionArrowHorizontalDirection)
{
objInReactionArrowHorizontalDirection = true;
crossPointDirection = relationInfo.crossPointDirection;
distance = relationInfo.distance;
perpendicularDistance = relationInfo.perpendicularDistance;
}
}
else
{
// molecule with ctab and container box, check if the reaction arrow line direction crosses the box four edges
var crossPoints = Kekule.GeometryUtils.getCrossPointsOfLineAndBox(arrowCoords, containerBox, true);
if (!crossPoints || !crossPoints.length)
{
// here we expand the box to the normal line direction by reactionArrowPerpendicularExpansion,
// to allow some very small molecule to be regarded as reactant or product
if (expandedContainerBox)
crossPoints = Kekule.GeometryUtils.getCrossPointsOfLineAndBox(arrowCoords, expandedContainerBox, true);
/*
var expandedContainerBox = containerBox;
if (containerBox.y2 - containerBox.y1 < reactionArrowPerpendicularExpansion) // expand in y direction
expandedContainerBox = Kekule.BoxUtils.inflateBox(expandedContainerBox, 0, reactionArrowPerpendicularExpansion / 2);
if (containerBox.x2 - containerBox.x1 < reactionArrowPerpendicularExpansion) // expand in x direction
expandedContainerBox = Kekule.BoxUtils.inflateBox(expandedContainerBox, reactionArrowPerpendicularExpansion / 2, 0);
crossPoints = Kekule.GeometryUtils.getCrossPointsOfLineAndBox(arrowCoords, expandedContainerBox, true);
*/
}
if (crossPoints && crossPoints.length)
{
// molecule containing box intersecting with the line direction of arrow should be the reactant and product
var crossPointCenter = Kekule.CoordUtils.getCenter(crossPoints);
crossLengthOnReactionArrowLine = (crossPoints.length > 1)? Kekule.CoordUtils.getDistance(crossPoints[0], crossPoints[1]): 0;
distance = Kekule.CoordUtils.getDistance(crossPointCenter, arrowCenterCoord);
/*
var crossPointDirectionX = crossPointCenter.x - arrowCenterCoord.x;
var crossPointDirectionY = crossPointCenter.y - arrowCenterCoord.y;
if (arrowPrimaryAxis === 'y')
crossPointDirection = Math.sign(crossPointDirectionY || crossPointDirectionX);
else
crossPointDirection = Math.sign(crossPointDirectionX || crossPointDirectionY);
*/
var crossPointRelPositionToArrowNormalLine = Kekule.GeometryUtils.getPointRelPositionToLine(crossPointCenter, arrowNormalLineEquationParams);
if (crossPointRelPositionToArrowNormalLine === arrowStartingPointRelPositionToNormalLine)
crossPointDirection = -arrowDirection; // on contrast to the direction of arrow line
else
crossPointDirection = arrowDirection; // on same side of arrow
// crossPointDirection = Math.sign((crossPointCenter.x - arrowCenterCoord.x) || (crossPointCenter.y - arrowCenterCoord.y));
objInReactionArrowHorizontalDirection = true;
perpendicularDistance = Kekule.CoordUtils.getDistance(objCenterCoord, crossPointCenter);
}
}
// check if molecule/symbol is on the vertical direction of arrow line, if so, it may be a reagent
var perpendicularCrossPointCoordToNormalLine = Kekule.GeometryUtils.getPerpendicularCrossPointFromCoordToLine(objCenterCoord, arrowNormalLineCoords[0], arrowNormalLineCoords[1], true);
var perpendicularDistanceToNormalLine = Kekule.CoordUtils.getDistance(objCenterCoord, perpendicularCrossPointCoordToNormalLine);
if (perpendicularDistanceToNormalLine * 2 < arrowLength + reactionArrowHorizontalExpansion)
{
// inside the expansion area of reaction arrow in vertical direction, should be a reagent of reaction
crossPointToNormalLineDirection = Math.sign((perpendicularCrossPointCoordToNormalLine.x - arrowCenterCoord.x) || (perpendicularCrossPointCoordToNormalLine.y - arrowCenterCoord.y));
objInReactionArrowVerticalDirection = true;
distanceOnNormalLine = Kekule.CoordUtils.getDistance(perpendicularCrossPointCoordToNormalLine, arrowCenterCoord);
}
if (objInReactionArrowHorizontalDirection && objInReactionArrowVerticalDirection)
{
// a molecule can be both regarded as reactant/product and reagent, decide its role with distance
if (perpendicularDistanceToNormalLine * 2 < arrowLength)
{
// on the vertical direction of arrow without expansion, regard it as reagent
objInReactionArrowHorizontalDirection = false;
}
else if (perpendicularDistance <= perpendicularDistanceToNormalLine)
objInReactionArrowVerticalDirection = false;
else
objInReactionArrowHorizontalDirection = false;
}
if (objInReactionArrowHorizontalDirection) // reactant or product
{
var halfReactionArrowLength = arrowLength / 2;
if (crossPointDirection === arrowDirection)
{
// on the end side of arrow, product
// product and reactant distance should be measure to a end of arrow (not center of arrow), so substract half length here
productDetails.push({
'substanceType': 'product', 'distance': distance - halfReactionArrowLength,
'containerBox': containerBox, 'centerCoord': objCenterCoord,
'crossLengthOnReactionArrowLine': crossLengthOnReactionArrowLine || 0,
'object': molAndPlusSymbols[i]
});
if (calcContainerBoxes)
productContainerBox = Kekule.BoxUtils.getContainerBox(productContainerBox, containerBox);
}
else
{
// on the start side of arrow, reactant
// product and reactant distance should be measure to a end of arrow (not center of arrow), so substract half length here
reactantDetails.push({
'substanceType': 'reactant', 'distance': distance - halfReactionArrowLength,
'containerBox': containerBox, 'centerCoord': objCenterCoord,
'crossLengthOnReactionArrowLine': crossLengthOnReactionArrowLine || 0,
'object': molAndPlusSymbols[i],
});
if (calcContainerBoxes)
reactantContainerBox = Kekule.BoxUtils.getContainerBox(reactantContainerBox, containerBox);
}
}
else if (objInReactionArrowVerticalDirection) // reagent
{
if (currObj instanceof Kekule.Molecule)
{
// only molecule can be reagent, so here we ignores the plus symbol
var distanceToArrowCenter = Kekule.CoordUtils.getDistance(objCenterCoord, arrowCenterCoord);
var distanceToArrowLineSegment = Kekule.GeometryUtils.getDistanceFromPointToLine(objCenterCoord, arrowCoords[0], arrowCoords[1], false);
reagentDetails.push({
'substanceType': 'reagent', 'onTop': crossPointToNormalLineDirection == arrowNormalLineDirection,
'containerBox': containerBox, 'centerCoord': objCenterCoord, 'distance': distanceOnNormalLine,
'distanceToArrowCenter': distanceToArrowCenter,
'distanceToArrow': distanceToArrowLineSegment,
'object': molAndPlusSymbols[i]
});
if (calcContainerBoxes)
reagentContainerBox = Kekule.BoxUtils.getContainerBox(reagentContainerBox, containerBox);
}
}
}
var result = null;
if (reactantDetails.length || productDetails.length)
{
// sort reactants and products with distance to arrow
reactantDetails.sort(function(a, b) { return b.distance - a.distance;});
productDetails.sort(function(a, b) { return a.distance - b.distance;});
// sort reagent
reagentDetails.sort(function(a, b) {
if (a.onTop === b.onTop) {
return a.onTop? (b.distance - a.distance): (a.distance - b.distance);
} else {
return a.onTop? -1: 1;
}
});
var roughReactionInfo = {
reactantDetails: reactantDetails,
productDetails: productDetails,
reagentDetails: reagentDetails
};
// erase substances too faraway, and record the delta distance to neighbor
var distanceThreshold = substanceGapThreshold;
for (var l = reactantDetails.length - 1, i = l; i >= 0; --i)
{
var deltaDistance = RU._getObjDetailDistanceToNeighborOrReactionArrow(roughReactionInfo, i, 'reactant');
if (deltaDistance > distanceThreshold) // too faraway from nearby substance, remove all following ones
{
reactantDetails.splice(0, i + 1);
break;
}
// reactantDetails[i].distanceToNeighbor = deltaDistance;
}
for (var l = productDetails.length, i = 0; i < l; ++i)
{
var deltaDistance = RU._getObjDetailDistanceToNeighborOrReactionArrow(roughReactionInfo, i, 'product');
if (deltaDistance > distanceThreshold) // too faraway from nearby substance, remove all following ones
{
productDetails.splice(i, l);
break;
}
// productDetails[i].distanceToNeighbor = deltaDistance;
}
for (var l = reagentDetails.length - 1, i = l; i >= 0; --i)
{
if (!reagentDetails[i].onTop)
continue;
var deltaDistance = RU._getObjDetailDistanceToNeighborOrReactionArrow(roughReactionInfo, i, 'reagent');
if (deltaDistance > distanceThreshold) // too faraway from nearby substance, remove all following ones
{
reagentDetails.splice(0, i + 1);
break;
}
// reagentDetails[i].distanceToNeighbor = deltaDistance;
}
for (var l = reagentDetails.length, i = 0; i < l; ++i)
{
if (reagentDetails[i].onTop)
continue;
var deltaDistance = RU._getObjDetailDistanceToNeighborOrReactionArrow(roughReactionInfo, i, 'reagent');
if (deltaDistance > distanceThreshold) // too faraway from nearby substance, remove all following ones
{
reagentDetails.splice(i, l);
break;
}
// reagentDetails[i].distanceToNeighbor = deltaDistance;
}
result = {
reactantDetails: reactantDetails,
productDetails: productDetails,
reagentDetails: reagentDetails,
arrowDetails: {
coords: arrowCoords,
centerCoord: arrowCenterCoord,
length: arrowLength,
direction: arrowDirection,
normalLineCoords: arrowNormalLineCoords,
normalLineEquationParams: arrowNormalLineEquationParams,
reactionArrowType: reactionArrow.getReactionTy