kekule
Version:
Open source JavaScript toolkit for chemoinformatics
693 lines (676 loc) • 21 kB
JavaScript
/**
* @fileoverview
* Error checker are special classes that reports the error or warning of chem object structures.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /core/kekule.common.js
* requires /core/kekule.structures.js
*
* requires /localization/kekule.localize.general.js
*/
(function(){
"use strict";
var EL = Kekule.ErrorLevel;
/**
* Namespace for error check system.
* @namespace
*/
Kekule.IssueCheck = {};
/**
* Predefined error code of error checking.
* @enum
*/
Kekule.IssueCheck.IssueCode = {
ERROR_UNKNOWN: 0,
ERROR_ATOM_VALENCE_ABNORMAL: 1101,
ERROR_BOND_ORDER_EXCEED: 1201
};
var EC = Kekule.IssueCheck.IssueCode;
Kekule.IssueCheck.CheckerIds = {
ATOM_VALENCE: 'atomValence',
BOND_ORDER: 'bondOrder'
};
var CIDs = Kekule.IssueCheck.CheckerIds;
/**
* The util class to register and store checker class info and instance.
* @class
*/
Kekule.IssueCheck.CheckerManager = {
_checkerMap: {},
/**
* Register to manager.
* @param {String} id
* @param {Class} checkerClass
*/
register: function(id, checkerClass)
{
if (!id)
id = ClassEx.getClassName(checkerClass);
var old = ICM._checkerMap[id];
if (!old || old.checkerClass !== checkerClass)
ICM._checkerMap[id] = {'checkerClass': checkerClass};
},
/**
* Unregister from class.
* @param {String} id
*/
unregister: function(id)
{
ICM._checkerMap[id] = null;
},
/**
* Returns the registered checker class.
* @param {String} id
* @returns {Class} Checker class or null if not found.
*/
getCheckerClass: function(id)
{
var item = ICM._checkerMap[id];
return item && item.checkerClass;
},
/**
* Returns the instance of registered checker class.
* Note the instance will be cached in manager. So multiple calls of this function only returns one single instance.
* @param {String} id
* @returns {Kekule.IssueCheck.BaseChecker} Checker instance or null if not found.
*/
getCheckerInstance: function(id)
{
var result = null;
var item = ICM._checkerMap[id];
if (item)
{
if (!item.instance)
{
var checkerClass = item.checkerClass;
item.instance = new checkerClass();
}
result = item.instance;
}
return result;
}
};
var ICM = Kekule.IssueCheck.CheckerManager;
/**
* A root object to perform issue check on one root chem object.
* It will extract all child objects that need to be check and pass them to the concrete checkers.
* @class
* @augments ObjectEx
*
* @property {Bool} ignoreUnexposedObjs Whether unexposed objects should be also checked.
* @property {Bool} enabled If false, call the execute() method of executor will do nothing.
* // @property {Array} checkers Concrete checkers.
* @property {Array} checkerIds IDs of checker classes used in this executor.
* @property {Number} durationLimit In millisecond. If this value is set, the executor will try to end the check within this limit.
* @property {Bool} allFinished Whether all the check job has been finished within durationLimit.
*/
/**
* Invoked after do a checking process.
* event param of it has field: {checkResults}
* @name Kekule.IssueCheck.Executor#execute
* @event
*/
Kekule.IssueCheck.Executor = Class.create(ObjectEx,
/** @lends Kekule.IssueCheck.Executor# */
{
/** @private */
CLASS_NAME: 'Kekule.IssueCheck.Executor',
/** @constructs */
initialize: function()
{
// debug
//this.setPropStoreFieldValue('checkers', [new Kekule.IssueCheck.AtomValenceChecker(), new Kekule.IssueCheck.BondOrderChecker()]);
this.tryApplySuper('initialize');
},
/** @private */
initProperties: function()
{
this.defineProp('ignoreUnexposedObjs', {'dataType': DataType.ARRAY});
this.defineProp('enabled', {'dataType': DataType.BOOL});
this.defineProp('durationLimit', {'dataType': DataType.NUMBER});
this.defineProp('allFinished', {'dataType': DataType.BOOL, 'setter': null, 'serializable': false});
this.defineProp('checkerIds', {'dataType': DataType.ARRAY,
'setter': function(value)
{
var checkers = [];
var ids = Kekule.ArrayUtils.toArray(value);
for (var i = 0, l = ids.length; i < l; ++i)
{
var checker = ICM.getCheckerInstance(ids[i]);
if (checker)
checkers.push(checker);
}
this.setPropStoreFieldValue('checkers', checkers);
}
});
// private
this.defineProp('checkers', {'dataType': DataType.ARRAY, 'serializable': false, 'setter': null});
},
/** @ignore */
initPropValues: function()
{
this.tryApplySuper('initPropValues');
this.setIgnoreUnexposedObjs(true);
this.setEnabled(true);
},
/** @ignore */
doFinalize: function()
{
this.setPropStoreFieldValue('checkers', null);
this.tryApplySuper('doFinalize');
},
/**
* Perform the error check on root object (target).
* @param {Kekule.ChemObject} target
* @param {Hash} options
* @returns {Array} Report items of all checks.
*/
execute: function(target, options)
{
if (!this.getEnabled())
return null;
var durationLimit = this.getDurationLimit();
var deadline = durationLimit && Date.now() + durationLimit;
//var startTime = Date.now();
// first phrase, determinate which objects should be checked
var op = this.doPrepareOptions(options);
var regMap = new Kekule.MapEx();
var allCheckers = this.getCheckers();
// filter out enabled checkers
var checkers = [];
for (var i = 0, l = allCheckers.length; i < l; ++i)
{
if (allCheckers[i].getEnabled())
checkers.push(allCheckers[i]);
}
this._getAllObjsNeedCheck(target, target, checkers, regMap, op);
// second phrase, do the concrete check of all checkers
var allFinished = true;
var result = [];
for (var i = 0, l = checkers.length; i < l; ++i)
{
var checker = checkers[i];
checker.setDeadline(deadline);
//try
{
var objs = regMap.get(checker);
if (objs && objs.length)
{
var reportItems = checker.check(objs, target, op);
if (reportItems && reportItems.length)
result = result.concat(reportItems);
allFinished = allFinished && checker.getAllFinished();
}
}
//catch(e)
{
}
}
this.setPropStoreFieldValue('allFinished', allFinished);
this.invokeEvent('execute', {'checkResults': result, 'allFinished': allFinished});
//var endTime = Date.now();
//console.log('consume', endTime - startTime, 'ms');
return result;
},
/**
* Perform a recheck on objects.
* @param {Kekule.IssueCheck.BaseChecker} checker
* @param {Array} objects
* @param {Kekule.ChemObject} root
* @param {Hash} options
* @returns {Array} Report items. If no error is found any more, null will be returned.
*/
recheck: function(checker, objects, root, options)
{
var actualObjs;
if (root)
{
actualObjs = [];
// filter out the children of root
for (var i = 0, l = objects.length; i < l; ++i)
{
if (objects[i].isChildOf(root))
{
actualObjs.push(objects[i]);
}
}
}
else
actualObjs = objects;
return actualObjs.length? checker.check(objects, options || {}): null;
},
/**
* Prepare the check options from input.
* Desendants may override this method.
* @private
*/
doPrepareOptions: function(inputOptions)
{
return options || {};
},
/** @private */
_getAllObjsNeedCheck: function(root, parent, checkers, regMap, options)
{
var checkUnexposed = !this.getIgnoreUnexposedObjs();
if (this._isObjExposed(root) || checkUnexposed)
{
for (var i = 0, l = parent.getChildCount(); i < l; ++i)
{
var child = parent.getChildAt(i);
if (this._isObjExposed(child) || checkUnexposed)
{
for (var j = 0, k = checkers.length; j < k; ++j)
{
var checker = checkers[j];
if (checker.applicable(child, root, options))
{
var regObjs = regMap.get(checker);
if (!regObjs)
{
regObjs = [child];
regMap.set(checker, regObjs);
}
else
regObjs.push(child);
}
}
// iterate child's children
this._getAllObjsNeedCheck(root, child, checkers, regMap, options);
}
}
}
},
/** @private */
_isObjExposed: function(obj)
{
return !obj.isExposed || obj.isExposed();
}
});
/**
* Represent the checking result of a issue checker object.
* This is an abstract class, and should not be used directly.
* Each concrete checker class should has a corresponding check result class.
* @class
* @augments ObjectEx
* @param {Int} errorLevel Value from {@link Kekule.ErrorLevel}
* @param {Int} errorCode A custom value to represent the error type.
* @param {Hash} data Extra error data.
* @param {Array} targets Related chem objects.
* @param {Object} reporter The checker who has published this result.
*
* @property {Int} level Value from {@link Kekule.ErrorLevel}
* @property {Int} code A custom value to represent the error type.
* @property {Hash} data Extra error data.
* @property {Array} targets Related chem objects.
* @property {Object} reporter The checker who has published this result.
*/
Kekule.IssueCheck.CheckResult = Class.create(ObjectEx,
/** @lends Kekule.IssueCheck.CheckResult# */
{
/** @private */
CLASS_NAME: 'Kekule.IssueCheck.CheckResult',
/** @private */
DEF_ERROR_CODE: EC.UNKNOWN,
/**
* @constructs
*/
initialize: function(errorLevel, errorCode, data, targets, reporter)
{
this.setPropStoreFieldValue('level', errorLevel || EL.ERROR);
this.setPropStoreFieldValue('code', errorCode || this.DEF_ERROR_CODE);
this.setPropStoreFieldValue('data', data);
this.setPropStoreFieldValue('targets', targets);
this.setPropStoreFieldValue('reporter', reporter);
this.tryApplySuper('initialize');
},
doFinalize: function()
{
this.setData(null);
this.setTargets(null);
this.setReporter(null);
this.tryApplySuper('doFinalize');
},
/** @private */
initProperties: function()
{
this.defineProp('level', {'dataType': DataType.INT});
this.defineProp('code', {'dataType': DataType.INT});
this.defineProp('data', {'dataType': DataType.HASH});
this.defineProp('targets', {'dataType': DataType.Array, 'serializable': false});
this.defineProp('reporter', {'dataType': DataType.OBJECTEX, 'serializable': false});
this.defineProp('msg', {'dataType': DataType.STRING, 'serializable': false, 'setter': null,
'getter': function() { return this.getMessage(); }
})
//this.defineProp('hasSolution', {'dataType': DataType.BOOL});
},
/**
* Returns the value stored in data property.
* @param {String} key
* @returns {Variant}
*/
getDataValue: function(key)
{
return (this.getData() || {})[key];
},
/**
* Returns the human readable error message from error level, code and data.
* Desendants should override this method.
* @returns {String}
*/
getMessage: function()
{
return Kekule.$L('ErrorCheckMsg.GENERAL_ERROR_WITH_CODE').format(this.getErrorCode());
}
});
/**
* The base checker class.
* @class
* @augments ObjectEx
*
* @property {Bool} enabled
* @property {Number} deadline Milliseconds elapsed since January 1, 1970 00:00:00 UTC.
* If this value is set, the checker should try to end the job before this.
* @property {Bool} allFinished Whether all the check job has been finished before deadline.
*/
Kekule.IssueCheck.BaseChecker = Class.create(ObjectEx,
/** @lends Kekule.IssueCheck.BaseChecker# */
{
/** @private */
CLASS_NAME: 'Kekule.IssueCheck.BaseChecker',
/** @constructs */
initialize: function()
{
this.tryApplySuper('initialize');
},
/** @private */
initProperties: function()
{
//this.defineProp('targets', {'dataType': DataType.ARRAY});
this.defineProp('enabled', {'dataType': DataType.BOOL});
this.defineProp('deadline', {'dataType': DataType.NUMBER, 'serializable': false});
this.defineProp('allFinished', {'dataType': DataType.BOOL, 'setter': null, 'serializable': false});
},
/** @ignore */
initPropValues: function()
{
this.tryApplySuper('initPropValues');
this.setEnabled(true);
},
/** @private */
_createReport: function(reportClass, level, code, data, targets)
{
return new reportClass(level, code, data, targets, this);
},
/**
* Check whether this checker can be applied to target object.
* @param {Kekule.ChemObject} target
* @param {Kekule.ChemObject} rootObj
* @param {Hash} options
* @returns {Bool}
*/
applicable: function(target, rootObj, options)
{
return this.doApplicable(target, rootObj, options || {});
},
/**
* Do actual work of {@link Kekule.IssueCheck.BaseChecker.applicable}
* Desendants should override this method.
* @param {Kekule.ChemObject} target
* @param {Kekule.ChemObject} rootObj
* @param {Hash} options
* @returns {Bool}
*/
doApplicable: function(target, rootObj, options)
{
return false;
},
/**
* Check on a series of target objects.
* Note the targets are all passed the detection and all be applicable.
* @param {Array} targets
* @param {Kekule.ChemObject} rootObj
* @param {Hash} options
* @returns {Array} Report items.
*/
check: function(targets, rootObj, options)
{
return this.doCheck(targets, rootObj, options || {});
},
/**
* Do actual work of {@link Kekule.IssueCheck.BaseChecker.check}.
* Descendants may override this method.
* @param {Array} targets
* @param {Kekule.ChemObject} rootObj
* @param {Hash} options
* @returns {Array} Report items.
*/
doCheck: function(targets, rootObj, options)
{
this.setPropStoreFieldValue('allFinished', false);
var result = [];
var ddl = this.getDeadline() || null;
var terminated = false;
for (var i = 0, l = targets.length; i < l; ++i)
{
if (ddl)
{
var currTime = Date.now();
if (currTime >= ddl)
{
terminated = true;
break;
}
}
var childResult = this.doCheckOnTarget(targets[i], i, targets, rootObj, options) || [];
result = result.concat(childResult);
}
this.setPropStoreFieldValue('allFinished', !!terminated);
return result;
},
/**
* Do concrete check on single target.
* Descendants may override this method.
* @param {Object} target
* @param {Int} targetIndex
* @param {Array} targets
* @param {Kekule.ChemObject} rootObj
* @param {Hash} options
* @returns {Array} Report items.
*/
doCheckOnTarget: function(target, targetIndex, targets, rootObj, options)
{
}
});
/**
* The checker to check whether the valence of atom in molecule is right.
* @class
* @augments Kekule.IssueCheck.BaseChecker
*/
Kekule.IssueCheck.AtomValenceChecker = Class.create(Kekule.IssueCheck.BaseChecker,
/** @lends Kekule.IssueCheck.AtomValenceChecker# */
{
/** @private */
CLASS_NAME: 'Kekule.IssueCheck.AtomValenceChecker',
/** @constructs */
initialize: function()
{
this.tryApplySuper('initialize');
},
/** @ignore */
doApplicable: function(target, rootObj, options)
{
return (target instanceof Kekule.Atom) && (target.isNormalAtom());
},
/** @ignore */
doCheckOnTarget: function(target, targetIndex, targets, rootObj, options)
{
var reportItem = this.checkValence(target);
return reportItem? [reportItem]: null;
},
/** @private */
checkValence: function(atom)
{
var currValence = atom.getValence();
var charge = atom.getCharge() || 0;
var possibleValences = this._getPossibleValences(atom.getAtomicNumber(), charge);
if (possibleValences.length && possibleValences.indexOf(currValence) < 0) // current is abnormal
{
return this._createReport(Kekule.IssueCheck.AtomValenceChecker.Result,
EL.ERROR, EC.ERROR_ATOM_VALENCE_ABNORMAL,
{'currValence': currValence, 'possibleValences': possibleValences},
[atom]);
}
else // no error
return null;
},
/** @private */
_getPossibleValences: function(atomicNum, charge)
{
var result = [];
var info = Kekule.ValenceUtils.getPossibleMdlValenceInfo(atomicNum, charge);
if (info && info.valences && !info.unexpectedCharge) // if abnormal charge is meet, we can not determinate the valence precisely, just ignore here
{
result = [].concat(info.valences);
}
return result;
}
});
// register
ICM.register(CIDs.ATOM_VALENCE, Kekule.IssueCheck.AtomValenceChecker);
/**
* Represent the checking result of {@link Kekule.IssueCheck.AtomValenceChecker}.
* @class
* @augments Kekule.IssueCheck.CheckResult
*/
Kekule.IssueCheck.AtomValenceChecker.Result = Class.create(Kekule.IssueCheck.CheckResult,
/** @lends Kekule.IssueCheck.AtomValenceChecker.Result# */
{
/** @private */
CLASS_NAME: 'Kekule.IssueCheck.AtomValenceChecker.Result',
/** @private */
DEF_ERROR_CODE: EC.ERROR_ATOM_VALENCE_ABNORMAL,
/** @ignore */
getMessage: function()
{
var currValence = this.getDataValue('currValence');
var possibleValences = this.getDataValue('possibleValences');
var msg = (possibleValences.length <= 1)?
Kekule.$L('ErrorCheckMsg.ATOM_VALENCE_ERROR_WITH_SUGGEST'):
Kekule.$L('ErrorCheckMsg.ATOM_VALENCE_ERROR_WITH_SUGGESTS');
var atom = this.getTargets()[0];
var atomId = atom.getId();
//var atomSymbol = atom.getSymbol();
//var atomLabel = atomId? atomId + '(' + atomSymbol + ')': atomSymbol;
var atomLabel = atom.getLabel? atom.getLabel(): atom.getSymbol();
var suggests = possibleValences.join('/');
return msg.format(atomLabel, currValence, suggests);
}
});
/**
* The checker to check whether the order of bond is suitable.
* @class
* @augments Kekule.IssueCheck.BaseChecker
*/
Kekule.IssueCheck.BondOrderChecker = Class.create(Kekule.IssueCheck.BaseChecker,
/** @lends Kekule.IssueCheck.BondOrderChecker# */
{
/** @private */
CLASS_NAME: 'Kekule.IssueCheck.BondOrderChecker',
/** @ignore */
doApplicable: function(target, rootObj, options)
{
return (target instanceof Kekule.Bond) && target.isCovalentBond();
},
/** @ignore */
doCheckOnTarget: function(target, targetIndex, targets, rootObj, options)
{
var reportItem = this.checkBondOrder(target);
return reportItem? [reportItem]: null;
},
/** @private */
checkBondOrder: function(bond)
{
var bondValence = bond.getBondValence && bond.getBondValence();
if (bondValence)
{
var connectedNodes = bond.getConnectedChemNodes();
var maxOrder = 0;
for (var i = 0, l = connectedNodes.length; i < l; ++i)
{
var m = this._getAllowedMaxBondOrder(connectedNodes[i]);
if (m > 0 && (!maxOrder || m < maxOrder))
maxOrder = m;
}
if (maxOrder)
{
if (bondValence > maxOrder) // error
{
return this._createReport(Kekule.IssueCheck.BondOrderChecker.Result, EL.ERROR, EC.ERROR_BOND_ORDER_EXCEED,
{'currOrder': bondValence, 'maxOrder': maxOrder}, [bond]);
}
}
}
return null;
},
/** @private */
_getAllowedMaxBondOrder: function(atom)
{
var result = 0;
if (atom instanceof Kekule.Atom && atom.isNormalAtom())
{
var currValence = atom.getValence();
var atomTypes = Kekule.AtomTypeDataUtil.getAllAtomTypes(atom.getAtomicNumber());
if (atomTypes)
{
for (var i = 0, l = atomTypes.length; i < l; ++i)
{
if (currValence <= atomTypes[i].bondOrderSum)
{
result = atomTypes[i].maxBondOrder;
break;
}
}
}
}
return result;
}
});
// register
ICM.register(CIDs.BOND_ORDER, Kekule.IssueCheck.BondOrderChecker);
/**
* Represent the checking result of {@link Kekule.IssueCheck.BondOrderChecker}.
* @class
* @augments Kekule.IssueCheck.CheckResult
*/
Kekule.IssueCheck.BondOrderChecker.Result = Class.create(Kekule.IssueCheck.CheckResult,
/** @lends Kekule.IssueCheck.BondOrderChecker.Result# */
{
/** @private */
CLASS_NAME: 'Kekule.IssueCheck.BondOrderChecker.Result',
/** @private */
DEF_ERROR_CODE: EC.ERROR_BOND_ORDER_EXCEED,
/** @ignore */
getMessage: function()
{
var currOrder = this.getDataValue('currOrder');
var maxOrder = this.getDataValue('maxOrder');
var bond = this.getTargets()[0];
//var bondLabel = bond.getId() || '';
var connectedNodes = bond.getConnectedChemNodes();
var nodeLabels = [];
for (var i = 0, l = connectedNodes.length; i < l; ++i)
{
var node = connectedNodes[i];
var nodeLabel = node.getLabel && node.getLabel();
if (nodeLabel)
nodeLabels.push(nodeLabel);
}
var bondLabel = (nodeLabels.length > 1)? nodeLabels.join('-'): null;
var msg = bondLabel? Kekule.$L('ErrorCheckMsg.BOND_WITH_ID_ORDER_EXCEED_ALLOWED_WITH_SUGGEST').format(bondLabel, currOrder, maxOrder)
: Kekule.$L('ErrorCheckMsg.BOND_ORDER_EXCEED_ALLOWED_WITH_SUGGEST').format(currOrder, maxOrder);
return msg;
}
});
})();