kekule
Version:
Open source JavaScript toolkit for chemoinformatics
743 lines (725 loc) • 20 kB
JavaScript
/**
* @fileoverview
* Provides basic interface of calculation task.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /core/kekule.root.js
* requires /xbrowsers/kekule.x.js
*/
(function(){
"use strict";
/**
* Namespace for all structure calculators.
* @namespace
*/
Kekule.Calculator = {
};
/** @ignore */
Kekule.Calculator.Utils = {
_instanceMaxIndex: 0,
generateUid: function()
{
++Kekule.Calculator.Utils._instanceMaxIndex;
return 'Kekule.Calculator.' + Kekule.Calculator.Utils._instanceMaxIndex;
}
};
/**
* Returns default path to store calculator web worker scripts.
* @returns {String}
*/
Kekule.Calculator.getWorkerBasePath = function()
{
var isMin = Kekule.isUsingMinJs(); //Kekule.scriptSrcInfo.useMinFile;
var path = isMin? 'workers/': 'calculation/workers/';
path = Kekule.getScriptPath() + path; // Kekule.scriptSrcInfo.path + path;
return path;
};
/**
* An base class to perform molecule calculation task.
* @class
* @augments ObjectEx
*
* @property {Bool} async Whether the calculation is performed asynchronously.
* Note: the asynchronous calculation is performed by web worked. If web worker
* is not supported by browser, the calculation will always be run directly on main thread.
*/
Kekule.Calculator.Base = Class.create(ObjectEx,
/** @lends Kekule.Calculator.Base# */
{
/** @private */
CLASS_NAME: 'Kekule.Calculator.Base',
/** @private */
WORKER_SHARED_COUNT_FIELD: '__$workerSharedCount$__',
/** @constructs */
initialize: function()
{
this.setPropStoreFieldValue('uid', this._generateUid());
this.tryApplySuper('initialize');
},
/** @private */
initProperties: function()
{
this.defineProp('async', {'dataType': DataType.BOOL});
// private
this.defineProp('worker', {'dataType': DataType.OBJECT, 'setter': null});
this.defineProp('uid', {'dataType': DataType.STRING, 'setter': null}); // every calculator should has a unique id
},
/** @ignore */
initPropValues: function(/*$super*/)
{
this.tryApplySuper('initPropValues') /* $super() */;
this.setAsync(true);
this.reactWorkerMessageBind = this.reactWorkerMessage.bind(this);
this.reactWorkerErrorBind = this.reactWorkerError.bind(this);
},
/** @ignore */
doFinalize: function()
{
this.finalizeWorker();
this.tryApplySuper('doFinalize');
},
/** @private */
_generateUid: function()
{
return Kekule.Calculator.Utils.generateUid();
},
/**
* Returns whether the worker this calculator created is shared by multiple instances.
* Desendants may override this method.
* @returns {Bool}
* @private
*/
isWorkerShared: function()
{
return false;
},
/** @private */
_incWorkerSharedCount: function(worker)
{
if (this.isWorkerShared())
{
var v = worker[this.WORKER_SHARED_COUNT_FIELD] || 0;
++v;
worker[this.WORKER_SHARED_COUNT_FIELD] = v;
}
},
/** @private */
_decWorkerSharedCount: function(worker)
{
if (this.isWorkerShared())
{
var v = worker[this.WORKER_SHARED_COUNT_FIELD] || 0;
if (v)
--v;
worker[this.WORKER_SHARED_COUNT_FIELD] = v;
}
},
/** @private */
_isWorkerInSharingState: function(worker)
{
return this.isWorkerShared() && ((worker[this.WORKER_SHARED_COUNT_FIELD] || 0) > 0);
},
/**
* Returns default path to store calculator web worker scripts.
* @returns {String}
*/
getWorkerBasePath: function()
{
return Kekule.Calculator.getWorkerBasePath();
},
/**
* Run the calculation task. Result will be returned by callback function.
* @param {Func} callback Callback function called when calculation is done.
* @param {Func} errCallback Callback function called when error occurs.
* errCallback function should has param (err) where err is the possible error object (or string).
* @param {Func} msgCallback Callback function that receives log messages from calculator. Callback(msgData).
*/
execute: function(callback, errCallback, msgCallback)
{
var self = this;
/*
var done = function()
{
if (callback)
callback.apply(this, arguments);
//self.finalizeWorker();
};
*/
this._doneCallback = callback; //done;
this._errCallback = errCallback;
this._msgCallback = msgCallback;
if (this.getAsync() && this.isWorkerSupported() && this.fetchWorker()) // try using worker
{
var w = this.getWorker();
this._incWorkerSharedCount(w);
this._installWorkerEventReceiver(w);
this.workerStartCalc(w);
}
else // sync
{
var err, executed;
try
{
executed = this.executeSync(callback);
}
catch(e)
{
err = e;
}
if (err)
{
Kekule.error(err);
this.error(err);
}
else if (executed)
{
if (err)
this.error(err);
else
this.done();
}
}
},
/**
* Run the calculation task in sync mode.
* Descendants should override this method.
* @returns {Bool} If calculation is all done, function should returns true.
* Otherwise false should be returned and done() should be called manually.
* @private
*/
executeSync: function(callback)
{
this._doneCallback = callback; //done;
var result = this.doExecuteSync(callback);
if (result)
this.done();
return result;
},
/**
* Do actual work of method executeSync.
* Descendants should override this method.
* @returns {Bool}
*/
doExecuteSync: function(callback)
{
// do nothing here
},
/**
* Called when error occurs in calculation.
* @param {Object} err
* @private
*/
error: function(err)
{
if (this._errCallback)
this._errCallback(err);
else
{
//Kekule.error(err);
throw err;
}
},
/**
* Called when the calculation job is done.
* @private
*/
done: function()
{
if (this._doneCallback)
this._doneCallback.apply(this, arguments);
if (this.getWorker())
this.workerJobDone();
},
/**
* Terminate the calculation process in worker.
* Note: this function is available only in async mode.
*/
halt: function()
{
var w = this.getWorker();
if (w)
{
w.terminate();
this.error(Kekule.$L('ErrorMsg.CALC_TERMINATED_BY_USER'));
}
//this.done(Kekule.$L('ErrorMsg.CALC_TERMINATED_BY_USER'));
},
/**
* Returns whether script worker can be used in current environment.
* @returns {Bool}
*/
isWorkerSupported: function()
{
return Kekule.BrowserFeature.workers;
},
/**
* Returns the worked instance stored in {@link Kekule.Calculator.Base.worker} property.
* If the property is empty, create a worker with {@link Kekule.Calculator.Base.createWorker}.
* @returns {Object}
* @private
*/
fetchWorker: function()
{
var result = this.getWorker();
if (!result)
result = this.createWorker();
return result;
},
/**
* Create a new web worker to run calculation task.
* @private
*/
createWorker: function()
{
if (this.isWorkerSupported())
{
var url = this.getWorkerScriptFile();
if (url)
{
var w = new Worker(url);
this.setPropStoreFieldValue('worker', w);
return w;
}
}
},
/** @private */
_installWorkerEventReceiver: function(worker)
{
worker.addEventListener('message', this.reactWorkerMessageBind);
worker.addEventListener('error', this.reactWorkerErrorBind);
},
/** @private */
_uninstallWorkerEventReceiver: function(worker)
{
worker.removeEventListener('message', this.reactWorkerMessageBind);
worker.removeEventListener('error', this.reactWorkerErrorBind);
},
/**
* Returns the work script file URL.
* Descendants should override this method.
* @returns {String}
*/
getWorkerScriptFile: function()
{
return null;
},
/**
* Notify the worker to import other script file.
* @private
*/
importWorkerScriptFile: function(url)
{
this.postWorkerMessage({'type': 'importScript', 'url': url});
},
/**
* Post message to worker.
* @param {Hash} msg
*/
postWorkerMessage: function(msg)
{
// ensure msg has uid field of self
var m = Object.extend({'uid': this.getUid()}, msg);
//console.log('[msg sent to worker]', m);
var w = this.getWorker();
if (w)
w.postMessage(m);
},
/**
* React message evoked by worker.
* @param {Object} e
*/
reactWorkerMessage: function(e)
{
//console.log('react msg', e.data.uid, this.getUid());
// check if message is sent to self
if (e.data.uid === this.getUid())
{
if (this._msgCallback)
this._msgCallback(e.data);
return this.doReactWorkerMessage(e.data, e);
}
else
return null;
},
/**
* Do actual job of reactWorkerMessage.
* Descendants should override this method.
* @param {Variant} data
* @param {Object} e
* @private
*/
doReactWorkerMessage: function(data, e)
{
// do nothing here
},
/**
* React error evoked by worker.
* @param {Object} e
*/
reactWorkerError: function(e)
{
//Kekule.error(e.message);
//this.done(e.message);
this.error(e.message);
},
/**
* Notify the worker that the calculation should be started, essential params
* should also be passed into worker.
* Descendants should override this method.
* @param {Object} worker
* @private
*/
workerStartCalc: function(worker)
{
// do nothing here
},
workerJobDone: function()
{
var w = this.getWorker();
if (w)
{
//console.log('worker done', this.getUid());
this._uninstallWorkerEventReceiver(w);
this._decWorkerSharedCount(w);
/*
if (!this._isWorkerInSharingState(w)) // avoid terminate shared worker too early
{
//console.log('TERMINATE');
w.terminate();
}
this.setPropStoreFieldValue('worker', null);
*/
}
},
/**
* Called when calculation job in worker is finished.
* @private
*/
finalizeWorker: function()
{
var w = this.getWorker();
if (w)
{
//console.log('worker done', this.getUid());
if (!this._isWorkerInSharingState(w)) // avoid terminate shared worker too early
{
//console.log('TERMINATE');
w.terminate();
}
this.setPropStoreFieldValue('worker', null);
}
}
});
/**
* Abstract class to generate 2D/3D structure from 0D or 3D/2D one.
* The concrete generator should inherit from this class.
* @class
* @augments Kekule.Calculator.Base
*
* @property {Kekule.StructureFragment} sourceMol Source molecule.
* @property {Kekule.StructureFragment} generatedMol 2D/3D molecule structure generated from sourceMol.
* @property {Kekule.MapEx} childObjMap A map of child objects in sourceMol to generatedMol.
* @property {Hash} options Options to generate 2D/3D structure.
* A special field { modifySource: bool } can be set to true to change the coordinates of sourceMol as well.
*/
Kekule.Calculator.AbstractStructureGenerator = Class.create(Kekule.Calculator.Base,
/** @lends Kekule.Calculator.AbstractStructureGenerator# */
{
/** @private */
CLASS_NAME: 'Kekule.Calculator.AbstractStructureGenerator',
/** @private */
initProperties: function()
{
this.defineProp('sourceMol', {'dataType': 'Kekule.StructureFragment', 'serializable': false});
this.defineProp('generatedMol', {'dataType': 'Kekule.StructureFragment', 'serializable': false});
this.defineProp('childObjMap', {'dataType': 'Kekule.MapEx', 'serializable': false});
this.defineProp('options', {'dataType': DataType.HASH,
'getter': function()
{
var result = this.getPropStoreFieldValue('options');
if (!result)
{
result = {};
this.setPropStoreFieldValue('options', result);
}
return result;
}
});
},
/**
* Returns the coord mode (2D or 3D) this generator generates.
* Descendants should override this method.
* @returns {Int}
*/
getGeneratorCoordMode: function()
{
return Kekule.CoordMode.COORD3D;
},
/** @ignore */
done: function()
{
this._modifySourceAccordingToGeneratedMol(this.getSourceMol(), this.getGeneratedMol(), this.getChildObjMap());
this.tryApplySuper('done');
},
/**
* Set generatedMol and childObjMap.
* If this.getOptions().modifySource is true, in this method, the sourceMol will be modified.
* @param {Kekule.StructureFragment} generatedMol
* @param {Kekule.MapEx} childObjMap
* @private
*/
_modifySourceAccordingToGeneratedMol: function(srcMol, generatedMol, childObjMap)
{
// update coordinates of source mol
var map = childObjMap;
if (map)
{
var coordMode = this.getGeneratorCoordMode();
var keys = map.getKeys();
srcMol.beginUpdate();
try
{
for (var i = 0, l = keys.length; i < l; ++i)
{
if (keys[i] instanceof Kekule.ChemStructureNode)
{
var srcNode = keys[i];
var destNode = map.get(srcNode);
var coord = destNode.getCoordOfMode(coordMode);
srcNode.setCoordOfMode(coord, coordMode);
}
}
}
finally
{
srcMol.endUpdate();
}
}
}
});
/**
* Manager of some common calculation services.
* @object
*/
Kekule.Calculator.ServiceManager = {
/** @private */
_serviceInfos: {},
/** @private */
_getServiceClassInfos: function(serviceName, canCreate)
{
var result = CS._serviceInfos[serviceName];
if (!result && canCreate)
{
result = [];
CS._serviceInfos[serviceName] = result;
}
return result;
},
/** @private */
_findServiceClassInfoItemIndex: function(serviceName, serviceClass, serviceId)
{
var classInfos = CS._getServiceClassInfos(serviceName, false);
if (classInfos)
{
for (var i = classInfos.length - 1; i >= 0; --i)
{
var info = classInfos[i];
if (info)
{
if (!serviceClass || info.serviceClass === serviceClass)
{
if (!serviceId || info.id === serviceId)
return i;
}
}
}
}
return -1;
},
/**
* Register a class to perform calculation service.
* @param {String} serviceName
* @param {Class} serviceClass
* @param {String} serviceId An ID of this service class.
* The user can use this id to access to service class instance precisely.
* @param {Number} priorityLevel
*/
register: function(serviceName, serviceClass, serviceId, priorityLevel)
{
var classInfos = CS._getServiceClassInfos(serviceName, true);
var index = CS._findServiceClassInfoItemIndex(serviceName, serviceClass);
if (index >= 0) // already exists, send it to tail
{
classInfos.splice(index, 1);
}
classInfos.push({'serviceClass': serviceClass, 'id': serviceId, 'priorityLevel': priorityLevel || 0});
},
/**
* Unregister a class to perform calculation service.
* @param {String} serviceName
* @param {Class} serviceClass
*/
unregister: function(serviceName, serviceClass)
{
var index = CS._findServiceClassInfoItemIndex(serviceName, serviceClass);
if (index >= 0) // already exists, unregister it
{
var classInfos = CS._getServiceClassInfos(serviceName, false);
classInfos.splice(index, 1);
}
},
/** @private */
_getRegisteredServiceInfo: function(serviceName, serviceId)
{
var currPriority = -1;
var result = null;
var classInfos = CS._getServiceClassInfos(serviceName);
if (classInfos)
{
for (var i = 0, l = classInfos.length; i < l; ++i)
{
var info = classInfos[i];
if (info)
{
if (serviceId && info.id === serviceId)
{
result = info;
break;
}
else
{
if (!result || info.priorityLevel >= currPriority)
{
result = info;
currPriority = info.priorityLevel;
}
}
}
}
}
return result;
},
/**
* Get the most recent registered class for service.
* @param {String} serviceName
* @param {String} serviceId If this id is not set, the class with the highest priority level will be returned.
* @return {Class}
*/
getServiceClass: function(serviceName, serviceId)
{
var info = CS._getRegisteredServiceInfo(serviceName, serviceId);
return info && info.serviceClass;
}
};
var CS = Kekule.Calculator.ServiceManager;
/**
* Predefined service names of calculation.
* @enum
*/
Kekule.Calculator.Services = {
GEN2D: '2D structure generator',
GEN3D: '3D structure generator'
};
/**
* Check if a certain service can be executed with calculator.
* @param {String} serviceName
* @returns {Bool}
*/
Kekule.Calculator.hasService = function(serviceName)
{
return !!CS.getServiceClass(serviceName);
};
/**
* Generate 2D or 3D structure based on sourceMol.
* This method seek for registered calculation service with genSeviceName.
* @param {Kekule.StructureFragment} sourceMol
* @param {String} genServiceName
* @param {Hash} options
* @param {Func} callback Callback function when the calculation job is done. Callback(generatedMol, childObjMap).
* @param {Func} errCallback Callback function when error occurs in calculation. Callback(err).
* @param {Func} msgCallback Callback function that receives log messages from calculator. Callback(msgData).
* @returns {Object} Created calculation object.
*/
Kekule.Calculator.generateStructure = function(sourceMol, genSeviceName, options, callback, errCallback, msgCallback)
{
var serviceName = genSeviceName || Kekule.Calculator.Services.GEN3D;
var c = CS.getServiceClass(serviceName);
if (c)
{
var o = new c();
var childObjMap;
if (o.setChildObjMap)
{
childObjMap = new Kekule.MapEx(true);
o.setChildObjMap(childObjMap);
}
var done = function()
{
if (callback)
callback(o.getGeneratedMol(), childObjMap);
};
var error = function(err)
{
if (errCallback)
errCallback(err);
};
var onMsg = function(msgData)
{
if (msgCallback)
msgCallback(msgData);
};
try
{
o.setSourceMol(sourceMol);
o.setOptions(options);
if (options && options.sync)
o.setAsync(false);
o.execute(done, error, onMsg);
}
catch(e)
{
error(e);
}
//o.finalize();
return o;
}
else
{
var errMsg = Kekule.$L('ErrorMsg.CALC_SERVICE_UNAVAILABLE').format(serviceName);
if (errCallback)
errCallback(errMsg);
//Kekule.error(errMsg);
return null;
}
};
/**
* Generate 3D structure based on 2D or 0D sourceMol.
* This method seek for registered GEN3D calculation service.
* @param {Kekule.StructureFragment} sourceMol
* @param {Hash} options
* @param {Func} callback Callback function when the calculation job is done. Callback(generatedMol).
* @param {Func} errCallback Callback function when error occurs in calculation. Callback(err).
* @param {Func} msgCallback Callback function that receives log messages from calculator. Callback(msgData).
* @returns {Object} Created calculation object.
*/
Kekule.Calculator.generate3D = function(sourceMol, options, callback, errCallback, msgCallback)
{
return Kekule.Calculator.generateStructure(sourceMol, Kekule.Calculator.Services.GEN3D, options, callback, errCallback, msgCallback);
}
/**
* Generate 2D structure based on 3D or 0D sourceMol.
* This method seek for registered GEN2D calculation service.
* @param {Kekule.StructureFragment} sourceMol
* @param {Hash} options
* @param {Func} callback Callback function when the calculation job is done. Callback(generatedMol).
* @param {Func} errCallback Callback function when error occurs in calculation. Callback(err).
* @param {Func} msgCallback Callback function that receives log messages from calculator. Callback(msgData).
* @returns {Object} Created calculation object.
*/
Kekule.Calculator.generate2D = function(sourceMol, options, callback, errCallback, msgCallback)
{
return Kekule.Calculator.generateStructure(sourceMol, Kekule.Calculator.Services.GEN2D, options, callback, errCallback, msgCallback);
}
})();