UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,908 lines (1,854 loc) 165 kB
/** * @fileoverview * Some global symbols used in Kekule library. * @author Partridge Jiang */ /* * requires /lan/classes.js * requires /utils/kekule.utils.js * requires /localization */ "use strict"; (function(){ //var OT = Kekule.OBJDEF_TEXTS; // modify defineProp method of ObjectEx, add auto title/description supports if (Kekule.PROP_AUTO_TITLE) { var originMethod = ClassEx.getPrototype(ObjectEx).defineProp; var OBJ_PROP_TITLE_TEXT_PREFIX = 'TITLE_'; var OBJ_PROP_DESCRIPTION_TEXT_PREFIX = 'DES_'; var newMethod = function(propName, options) { if (!options.title && !options.description) // all not set, auto decide { var className = this.getClassName(); if (className.startsWith('Kekule.')) // remove Kekule prefix { className = className.substr(7); } //var textObj = Object.getCascadeFieldValue(className, Kekule.OBJDEF_TEXTS); //if (textObj) { //var titleField = Kekule.OBJDEF_TEXTS.TITLE_PREFIX + propName; //var desField = Kekule.OBJDEF_TEXTS.DESCRIPTION_PREFIX + propName; var titlePrefix = OBJ_PROP_TITLE_TEXT_PREFIX; var destPrefix = OBJ_PROP_DESCRIPTION_TEXT_PREFIX; var titleField = titlePrefix + propName; var desField = destPrefix + propName; var title = Kekule.Localization.findValue('OBJDEF_TEXTS.' + className + '.' + titleField); var description = Kekule.Localization.findValue('OBJDEF_TEXTS.' + className + '.' + desField); options.title = title; //textObj[titleField]; options.description = description; //textObj[desField]; } } return originMethod.apply(this, [propName, options]); }; ClassEx.getPrototype(ObjectEx).defineProp = newMethod; } /** * Enumeration of exception types. * @class */ Kekule.ExceptionLevel = { /** Fatal error, usually cause the stop of program execution. */ ERROR: -1, /** Error but not fatal, the program can still run but need to display the error message to user. */ NOT_FATAL_ERROR: -10, /** Serious exception, but the program can still run. */ WARNING: -2, /** Minor exception, user usually need not to know about it. */ NOTE: -3, /** Log message, just for debug use. */ LOG: -4, /** * Returns all excpetion level values. * @returns {Array} */ getAllLevels: function() { var EL = Kekule.ExceptionLevel; return [EL.ERROR, EL.NOT_FATAL_ERROR, EL.WARNING, EL.NOTE, EL.LOG]; }, /** * Returns a string identity for exception level. * @param {Int} level * @returns {String} */ levelToString: function(level) { var EL = Kekule.ExceptionLevel; return (level === EL.ERROR || level === EL.NOT_FATAL_ERROR)? 'error': (level === EL.WARNING)? 'warning': (level === EL.NOTE)? 'note': 'log'; } }; /** * Error level, alias of ExceptionLevel */ Kekule.ErrorLevel = Kekule.ExceptionLevel; /** * Throw an exception in ExceptionHandler, same as {@link Kekule.ExceptionHandler.throwException} */ Kekule.throwException = function(e, exceptionLevel) { if (Kekule.exceptionHandler) { return Kekule.exceptionHandler.throwException(e, exceptionLevel); } else { var EL = Kekule.ExceptionLevel; if ((!exceptionLevel) || (exceptionLevel === EL.ERROR)) { if (typeof(e) === 'string') e = new Kekule.Exception(e); throw e; } else { if (typeof(console) !== 'undefined') { if (typeof(e) !== 'string') e = e.message; var method = (exceptionLevel === EL.NOT_FATAL_ERROR)? console.error: (exceptionLevel === EL.WARNING)? console.warn: (exceptionLevel === EL.NOTE)? console.info: console.log; if (method) method(e); else console.log(e); } } } }; /** * Returns the message of exception object or string. * @param {Variant} e * @returns {String} */ Kekule.getExceptionMsg = function(e) { if (typeof(e) === 'string') return e; else if (typeof(e) === 'object') return e.message; else return ''; }; /** * Throw an exception in ExceptionHandler, same as {@link Kekule.ExceptionHandler.throwException} * @function */ Kekule.raise = Kekule.throwException; /** * Throw an {@link Kekule.ExceptionLevel.Error} or {@link Kekule.ExceptionLevel.NOT_FATAL_ERROR} in ExceptionHandler. */ Kekule.error = function(e, notFatal) { /* if (typeof(e) == 'string') e = new Kekule.Error(e); if (Kekule.exceptionHandler) return Kekule.exceptionHandler.throwException(e); else throw e; */ var level = notFatal? Kekule.ExceptionLevel.NOT_FATAL_ERROR: Kekule.ExceptionLevel.ERROR; return Kekule.raise(e, level); }; /** * Throw an {@link Kekule.ChemError} in ExceptionHandler. */ Kekule.chemError = function(e, notFatal) { if (typeof(e) == 'string') e = new Kekule.ChemError(e); /* if (Kekule.exceptionHandler) return Kekule.exceptionHandler.throwException(e); else throw e; */ return Kekule.error(e, notFatal); }; /** * Raise a warning message (but do not raise error). * @param e */ Kekule.warn = function(e) { return Kekule.raise(e, Kekule.ExceptionLevel.WARNING); }; /** * Raise a informative hint message. * @param e */ Kekule.notify = function(e) { return Kekule.raise(e, Kekule.ExceptionLevel.NOTE); }; /** * Raise a log message. * @param e */ Kekule.log = function(e) { return Kekule.raise(e, Kekule.ExceptionLevel.LOG); }; /** * Check if localization resource is imported and available. * If it returns true, l10n constants can be used. * @returns {Bool} */ Kekule.hasLocalRes = function() { return !!Kekule.LOCAL_RES; }; /** * An root object to store default options for many operations (e.g., ring search, stereo perception). * User can modify concrete options to change the default action of some functions. * @object */ Kekule.globalOptions = { add: function(optionName, valueOrHash) { var oldValue = Object.getCascadeFieldValue(optionName, Kekule.globalOptions); if (oldValue && DataType.isObjectValue(oldValue) && DataType.isObjectValue(valueOrHash)) // value already exists and can be extended { Object.extend(oldValue, valueOrHash); } else Object.setCascadeFieldValue(optionName, valueOrHash, Kekule.globalOptions, true); }, get: function(optionName, defaultValue) { var result = Object.getCascadeFieldValue(optionName, Kekule.globalOptions); if (typeof(result) === 'undefined') result = defaultValue; return result; } }; /** * A class to implement str => variant mapping. * @class */ Kekule.HashEx = Class.create( /** @lends Kekule.HashEx# */ { /** @private */ CLASS_NAME: 'Kekule.HashEx', /** @private */ PROP_NAME_PREFIX: '__$key$__', /** @constructs */ initialize: function() { this._map = {}; }, /** * Free resources. */ finalize: function() { this._map = null; }, /** @private */ keyToPropName: function(key) { return this.PROP_NAME_PREFIX + key; // add a prefix to avoid possible name conflicts with default object }, /** * Check whether a value has been associated to the key string. * @param {String} key * @returns {Bool} */ has: function(key) { return this._map.hasOwnProperty(this.keyToPropName(key)); }, /** * Set the value for the key string. * @param {String} key * @param {Variant} value */ set: function(key, value) { this._map[this.keyToPropName(key)] = value; return this; }, /** * Returns the value associated to the key string, * @param {String} key * @param {Variant} defaultValue Return this value if key does not exist. * @returns {Variant} */ get: function(key, defaultValue) { var propName = this.keyToPropName(key); if (this._map.hasOwnProperty(propName)) return this._map[propName]; else return defaultValue; }, /** * Removes any value associated to the key string. * @param {String} key */ remove: function(key) { var propName = this.keyToPropName(key); if (this._map.hasOwnProperty(propName)) { delete this._map[propName]; } return this; }, /** * Clear all key and values in map. */ clear: function() { this._map = {}; } }); /** * A class to implement obj => obj mappings. * @class * * @params {Bool} nonWeak Whether try to create this map as a weak one (do not disturb GC). */ Kekule.MapEx = Class.create( /** @lends Kekule.MapEx# */ { /** @private */ CLASS_NAME: 'Kekule.MapEx', /** @constructs */ initialize: function(nonWeak, enableCache) { this.isWeak = !nonWeak; if (!Kekule.MapEx._inited) Kekule.MapEx._init(); // try use built in var _implClass = nonWeak? Kekule.MapEx._implementationNonWeak: Kekule.MapEx._implementationWeak; if (!_implClass) // fall back to JavaScript implementation { this._keys = []; this._values = []; } else this._implementation = new _implClass(); if (enableCache) this._cache = {}; // cache enabled for performance /* if ((!Kekule.MapEx._implementation) || (!this.isWeak)) // use JavaScript implementation if (!this._implementation) { this.keys = []; this.values = []; } else // use built-in implementation this._implementation = new Kekule.MapEx._implementation(); */ }, /** * Free resources. */ finalize: function(/*$super*/) { if (!this._implementation) { this._keys = null; this._values = null; this._cache = null; } }, /** * Set the value for the key object. * @param {Variant} key * @param {Variant} value */ set: function(key, value) { if (this._implementation) this._implementation.set(key, value); else { var index = this._keys.indexOf(key); if (index >= 0) this._values[index] = value; else { this._keys.push(key); this._values.push(value); } } if (this._cache && this._cache.key === key) { this._cache.value = value; } //console.log(key, value, this.get(key)); return this; }, /** * Returns the value associated to the key object, * @param {Variant} key * @param {Variant} defaultValue Return this value if key does not exist. * @returns {Variant} */ get: function(key, defaultValue) { if (this._cache && this._cache.key === key) // has cache, return directly return this._cache.value; var result; var found = false; if (this._implementation) { if (this._implementation.has(key)) { result = this._implementation.get(key); found = true; } } else { var index = this._keys.indexOf(key); if (index >= 0) { result = this._values[index]; found = true; } } if (found && this._cache) // set cache { this._cache.key = key; this._cache.value = result; } return found? result: defaultValue; }, /** * Check whether a value has been associated to the key object. * @param {Variant} key * @returns {Bool} */ has: function(key) { if (this._implementation) return this._implementation.has(key); else { var index = this._keys.indexOf(key); return index >= 0; } }, /** * Removes any value associated to the key object. * @param {Object} key */ remove: function(key) { if (this._cache && this._cache.key === key) // clear cache this._cache = {}; if (this._implementation) { return this._implementation[!this._implementation || 'delete'](key); // avoid IE regard delete as a reserved word } else { var index = this._keys.indexOf(key); if (index >= 0) { this._keys.splice(index, 1); this._values.splice(index, 1); } } return this; }, /** * Clear all key and values in map. */ clear: function() { if (this._cache) // clear cache this._cache = {}; if (!this._implementation || this._debug) { this._keys = []; this._values = []; } //else if (this._implementation) { if (this.isWeak) Kekule.error(Kekule.$L('ErrorMsg.CANNOT_CLEAR_WEAKMAP')/*Kekule.ErrorMsg.CANNOT_CLEAR_WEAKMAP*/); else this._implementation.clear(); } return this; }, /** * Returns an array of all keys in map. * @returns {Array} */ getKeys: function() { if (!this._implementation) return [].concat(this._keys); // clone the array, avoid modification else { if (this.isWeak) { // weak map, can not return keys Kekule.error(Kekule.$L('ErrorMsg.CANNOT_GET_KEY_LIST_IN_WEAKMAP')/*Kekule.ErrorMsg.CANNOT_GET_KEY_LIST_IN_WEAKMAP*/); return []; } else { var iter = this._implementation.keys(); var result; if (Array.from) result = Array.from(iter); else if (iter.next) { result = []; var nextResult = iter.next(); while (!nextResult.done) { result.push(nextResult.value); nextResult = iter.next(); } } else { result = []; for (var item in iter) { result.push(item); } } return result; } } }, /** * Returns an array of all values in map. * @returns {Array} */ getValues: function() { if (!this._implementation) return this._values; else { if (this.isWeak) { // weak map, can not return keys Kekule.error(Kekule.$L('ErrorMsg.CANNOT_GET_VALUE_LIST_IN_WEAKMAP')/*Kekule.ErrorMsg.CANNOT_GET_VALUE_LIST_IN_WEAKMAP*/); return []; } else { var iter = this._implementation.values(); var result; if (Array.from) result = Array.from(iter); else if (iter.next) { result = []; var nextResult = iter.next(); while (!nextResult.done) { result.push(nextResult.value); nextResult = iter.next(); } } else { result = []; for (var item in iter) { result.push(item); } } return result; } } }, /** * Return count all map items. * @returns {Int} */ getLength: function() { return this.getKeys().length; } }); /** @private */ Kekule.MapEx._init = function() { Kekule.MapEx._implementationWeak = null; // use JavaScript implementation Kekule.MapEx._implementationNonWeak = null; // use JavaScript implementation if (Kekule.$jsRoot.WeakMap) // Fx6 and above, use built-in WeakMap object Kekule.MapEx._implementationWeak = Kekule.$jsRoot.WeakMap; if (Kekule.$jsRoot.Map && Kekule.$jsRoot.Map.prototype.keys) // the implementation of Map in IE does not support keys() and values() Kekule.MapEx._implementationNonWeak = Kekule.$jsRoot.Map; Kekule.MapEx._inited = true; }; Kekule.MapEx._inited = false; /** * A class to implement obj1, obj2 => obj mappings. * @class * * @params {Bool} nonWeak Whether try to create this map as a weak one (do not disturb GC). */ Kekule.TwoTupleMapEx = Class.create( /** @lends Kekule.TwoTupleMapEx# */ { /** @private */ CLASS_NAME: 'Kekule.TwoTupleMapEx', /** @constructs */ initialize: function(nonWeak, enableCache) { this.nonWeak = nonWeak; this.map = new Kekule.MapEx(nonWeak, enableCache); if (enableCache) this._level1Cache = {}; // a cache for quickly find level 1 map }, /** * Free resources. */ finalize: function() { this._level1Cache = null; this.map.finalize(); }, /** @private */ getSecondLevelMap: function(key1, allowCreate) { if (key1 && this._level1Cache && (key1 === this._level1Cache.key) && this._level1Cache.value) { return this._level1Cache.value; } else { var result = this.map.get(key1); if ((!result) && allowCreate) { result = new Kekule.MapEx(this.nonWeak); this.map.set(key1, result); } /* // set to cache this._level1Cache.key = key1; this._level1Cache.value = result; */ return result; } }, /** * Set the value for the key object. * @param {Variant} key1 * @param {Variant} key2 * @param {Variant} value */ set: function(key1, key2, value) { var map = this.getSecondLevelMap(key1, true); map.set(key2, value); return this; }, /** * Returns the value associated to the key object, * @param {Variant} key1 * @param {Variant} key2 * @param {Variant} defaultValue Return this value if key does not exist. * @returns {Variant} */ get: function(key1, key2, defaultValue) { var map = this.getSecondLevelMap(key1, false); if (map) return map.get(key2, defaultValue); else return defaultValue; }, /** * Check whether a value has been associated to the key object. * @param {Variant} key1 * @param {Variant} key2 * @returns {Bool} */ has: function(key1, key2) { var map = this.getSecondLevelMap(key1, false); if (map) return map.has(key2); else return false; }, /** * Removes any value associated to the key object. * @param {Variant} key1 * @param {Variant} key2 */ remove: function(key1, key2) { var map = this.getSecondLevelMap(key1, false); if (map) return map.remove(key2); }, /** * Clear all key and values in map. */ clear: function() { this.map.clear(); if (this._level1Cache) this._level1Cache = {}; } }); /** * Enumeration of coordinate mode. * @class */ Kekule.CoordMode = { /** Unknown dimension, not useful in most cases. */ UNKNOWN: 0, /** 2D coordinate */ COORD2D: 2, /** 3D coordinate */ COORD3D: 3 }; /** * Enumeration of the anchor point position of a chem object. * @enum */ Kekule.ObjAnchorPosition = { /** At the corner of object box (e.g., top left corner of 2D box). */ CORNER: 0, /** At the center of object box. */ CENTER: 1, DEFAULT: 0 }; /** * Class to help to define some classes with similar interfaces. * @class * @@private */ Kekule.ClassDefineUtils = { /// Methods to expand support for size2D and size3D property /** * @class * @private */ CommonSizeMethods: { /** * Get size of specified mode. * @param {Int} coordMode Value from {@link Kekule.CoordMode}, mode of coordinate. * @param {Bool} allowSizeBorrow If corresponding size of 2D/3D not found, whether * size of another mode can be used instead. * If true, 2D size can be expanded to 3D one as {x, y, 0} and 3D * reduced to 2D one as {x, y}. * @returns {Hash} */ getSizeOfMode: function(coordMode, allowSizeBorrow) { if (coordMode === Kekule.CoordMode.COORD3D) return this.getSize3D(allowSizeBorrow); else return this.getSize2D(allowSizeBorrow); }, /** * Set size of specified mode. * @param {Hash} value Value of size. {x, y} or {x, y, z}. * @param {Int} coordMode Value from {@link Kekule.CoordMode}, mode of coordinate. */ setSizeOfMode: function(value, coordMode) { if (coordMode === Kekule.CoordMode.COORD3D) return this.setSize3D(value); else return this.setSize2D(value); }, /** * Returns a min 2D box containing this object. * @param {Int} coordMode Value from {@link Kekule.CoordMode}, mode of coordinate. * @param {Bool} allowCoordBorrow * @returns {Hash} */ getBoxOfMode: function(coordMode, allowCoordBorrow) { if (coordMode === Kekule.CoordMode.COORD3D) return this.getBox3D(allowCoordBorrow); else return this.getBox2D(allowCoordBorrow); } }, /** * @class * @private */ Size2DMethods: { /* * Get size2D property, if this property is null, then create new hash for it. * Note this method will return a direct instance of size2D hash (instead of a * clone as in getSize2D). * @returns {Hash} {x, y} */ fetchSize2D: function() { var c = this.getPropStoreFieldValue('size2D'); if (!c) { c = {}; this.setPropStoreFieldValue('size2D', c); } return c; }, /** @private */ notifySize2DChanged: function(newValue) { this.notifyPropSet('size2D', newValue); }, /** * Check if this object has a 2D size. * @param {Bool} allowSizeBorrow * @returns {Bool} */ hasSize2D: function(allowSizeBorrow) { var c = this.getPropStoreFieldValue('size2D'); //this.getCoord2D(); var result = (c) && ((typeof(c.x) != 'undefined') || (typeof(c.y) != 'undefined')); if ((!result) && allowSizeBorrow) result = this.hasSize3D? this.hasSize3D(): false; return result; }, /** * Returns a min 2D box containing this object. * @param {Bool} allowCoordBorrow * @returns {Hash} */ getBox2D: function(allowCoordBorrow) { var coord1 = this.getCoord2D(allowCoordBorrow) || {'x': 0, 'y': 0}; var size = this.getSize2D(allowCoordBorrow); if (/*coord1 &&*/ size && (size.x || size.y)) { // usually coord point is on top-left corner, in object coord system, y is on the largest edge // of object, so -size.y is used to get the bottom edge of box. var delta = {'x': size.x, 'y': -size.y}; var coord2 = Kekule.CoordUtils.add(coord1, delta); return {'x1': coord1.x, 'y1': coord1.y, 'x2': coord2.x, 'y2': coord2.y}; } else return {'x1': coord1.x, 'y1': coord1.y, 'x2': coord1.x, 'y2': coord1.y}; }, /** * Get x (width) value of size2D property. * @param {Bool} allowSizeBorrow * @returns {Float} x */ get2DSizeX: function(allowSizeBorrow) { var c = this.getSize2D(allowSizeBorrow); return c? c.x || 0: 0; }, /** * Change x (width) value of size2D property. * @param {Float} x */ set2DSizeX: function(x) { var c = this.fetchSize2D(); if (c.x != x) { c.x = x; this.notifySize2DChanged(c); } }, /** * Get y (height) value of size2D property. * @param {Bool} allowSizeBorrow * @returns {Float} y */ get2DSizeY: function(allowSizeBorrow) { var c = this.getSize2D(allowSizeBorrow); return c? c.y || 0: 0; }, /** * Change y (height) value of size2D property. * @param {Float} y */ set2DSizeY: function(y) { var c = this.fetchSize2D(); if (c.y != y) { c.y = y; this.notifySize2DChanged(c); } } }, /** * @class * @private */ Size3DMethods: { /* * Get size3D property, if this property is null, then create new hash for it. * Note this method will return a direct instance of size3D hash (instead of a * clone as in getSize3D). * @returns {Hash} {x, y} */ fetchSize3D: function() { var c = this.getPropStoreFieldValue('size3D'); if (!c) { c = {}; this.setPropStoreFieldValue('size3D', c); } return c; }, /** @private */ notifySize3DChanged: function(newValue) { this.notifyPropSet('size3D', newValue); }, /** * Check if this object has a 3D size. * @param {Bool} allowSizeBorrow * @returns {Bool} */ hasSize3D: function(allowSizeBorrow) { var c = this.getPropStoreFieldValue('size3D'); var result = (c) && ((typeof(c.x) !== 'undefined') || (typeof(c.y) !== 'undefined') || (typeof(c.z) !== 'undefined')); if ((!result) && allowSizeBorrow) result = this.hasSize2D? this.hasSize2D(): false; return result; }, /** * Returns a min 3D box containing this object. * @param {Bool} allowCoordBorrow * @returns {Hash} */ getBox3D: function(allowCoordBorrow) { var coord1 = this.getCoord3D(allowCoordBorrow); var size = this.getSize3D(allowCoordBorrow); if (coord1 && size) { var coord2 = Kekule.CoordUtils.add(coord1, size); return {'x1': coord1.x, 'y1': coord1.y, 'z1': coord1.z, 'x2': coord2.x, 'y2': coord2.y, 'z2': coord2.z}; } }, /** * Get x (width) value of size3D property. * @param {Bool} allowSizeBorrow * @returns {Float} x */ get3DSizeX: function(allowSizeBorrow) { var c = this.getSize3D(allowSizeBorrow); return c? c.x || 0: 0; }, /** * Change x (width) value of size3D property. * @param {Float} x */ set3DSizeX: function(x) { var c = this.fetchSize3D(); if (c.x != x) { c.x = x; this.notifySize3DChanged(c); } }, /** * Get y (height) value of size3D property. * @param {Bool} allowSizeBorrow * @returns {Float} y */ get3DSizeY: function(allowSizeBorrow) { var c = this.getSize3D(allowSizeBorrow); return c? c.y || 0: 0; }, /** * Change y (height) value of size3D property. * @param {Float} y */ set3DSizeY: function(y) { var c = this.fetchSize3D(); if (c.y != y) { c.y = y; this.notifySize3DChanged(c); } }, /** * Get z (depth) value of size3D property. * @param {Bool} allowSizeBorrow * @returns {Float} z */ get3DSizeZ: function(allowSizeBorrow) { var c = this.getSize3D(allowSizeBorrow); return c? c.z || 0: 0; }, /** * Change y (height) value of size3D property. * @param {Float} z */ set3DSizeZ: function(z) { var c = this.fetchSize3D(); if (c.z != z) { c.z = z; this.notifySize3DChanged(c); } } }, /** * Define size2D/size3D related properties and methods to a class. * @param {Object} aClass Should be class object. * @param {Array} props Which properties should be defined. e.g. ['size2D', 'size3D']. * If not set, all 2D and 3D properties will be defined. */ addStandardSizeSupport: function(aClass, props) { ClassEx.extend(aClass, Kekule.ClassDefineUtils.CommonSizeMethods); if ((!props) || (props.indexOf('size2D') >= 0)) { ClassEx.defineProp(aClass, 'size2D', { 'dataType': DataType.HASH, // clone result object so that user can not modify x/y directly from getter 'getter': function(allowSizeBorrow, allowCreateNew) { var c = this.getPropStoreFieldValue('size2D'); if ((!c) && allowSizeBorrow) { if (this.hasSize3D()) { c = this.getSize3D(); } } if ((!c) && allowCreateNew) { c = {'x': 0, 'y': 0}; this.setPropStoreFieldValue('size2D', c); } var result = c? {'x': c.x, 'y': c.y}: undefined; if (result) result = Kekule.CoordUtils.absValue(result); // size should always be positive value return result; }, // clone value from input 'setter': function(value) { var c = this.fetchSize2D(); if (value.x !== undefined) c.x = value.x; if (value.y !== undefined) c.y = value.y; this.notifySize2DChanged(c); } }); ClassEx.extend(aClass, Kekule.ClassDefineUtils.Size2DMethods); } if ((!props) || (props.indexOf('size3D') >= 0)) { ClassEx.defineProp(aClass, 'size3D', { 'dataType': DataType.HASH, // clone result object so that user can not modify x/y directly from getter 'getter': function(allowSizeBorrow, allowCreateNew) { var c = this.getPropStoreFieldValue('size3D'); if ((!c) && allowSizeBorrow) { if (this.hasSize2D()) { c = this.getSize2D(); c.z = 0; } } if ((!c) && allowCreateNew) { c = {'x': 0, 'y': 0, 'z': 0}; this.setPropStoreFieldValue('size3D', c); } var result = c? {'x': c.x, 'y': c.y, 'z': c.z}: undefined; return result; }, // clone value from input 'setter': function(value){ var c = this.fetchSize3D(); if (value.x !== undefined) c.x = value.x; if (value.y !== undefined) c.y = value.y; if (value.z !== undefined) c.z = value.z; this.notifySize3DChanged(c); } }); ClassEx.extend(aClass, Kekule.ClassDefineUtils.Size3DMethods); } }, /// Methods to expand support for Coord2D and Coord3D property /** * @class * @private */ CommonCoordMethods: /** @lends Kekule.ClassDefineUtils.CommonCoordMethods# */ { /** * Returns the anchor position of this object. * @param {Int} coordMode * @returns {Int} Value from {@link Kekule.ObjAnchorPosition} */ getObjAnchorPosition: function(coordMode) { var result = this.doGetObjAnchorPosition && this.doGetObjAnchorPosition(coordMode); result = result || Kekule.ObjAnchorPosition.DEFAULT; return result; }, /** * Returns the parent object that influences the abs coord of this object. * @param {Int} coordMode * @returns {Kekule.ChemObject} */ getCoordParent: function(coordMode) { return (this.getParent && this.getParent()) || null; }, /** * Get coordinate of specified mode. * @param {Int} coordMode Value from {@link Kekule.CoordMode}, mode of coordinate. * @param {Bool} allowCoordBorrow If corresponding coord of 2D/3D not found, whether * coord of another mode can be used instead. * If true, 2D coord can be expanded to 3D one as {x, y, 0} and 3D * reducts to 2D one as {x, y}. * @returns {Hash} */ getCoordOfMode: function(coordMode, allowCoordBorrow) { if (coordMode === Kekule.CoordMode.COORD3D) return this.getCoord3D(allowCoordBorrow); else return this.getCoord2D(allowCoordBorrow); }, /** * Set coordinate of specified mode. * @param {Hash} value Value of coordinate. {x, y} or {x, y, z}. * @param {Int} coordMode Value from {@link Kekule.CoordMode}, mode of coordinate. */ setCoordOfMode: function(value, coordMode) { if (coordMode === Kekule.CoordMode.COORD3D) return this.setCoord3D(value); else return this.setCoord2D(value); }, /** * Get absolute coordinate of specified mode. * @param {Int} coordMode Value from {@link Kekule.CoordMode}, mode of coordinate. * @param {Bool} allowCoordBorrow * @returns {Hash} */ getAbsCoordOfMode: function(coordMode, allowCoordBorrow) { if (coordMode === Kekule.CoordMode.COORD3D) return this.getAbsCoord3D(allowCoordBorrow); else return this.getAbsCoord2D(allowCoordBorrow); }, /** * Set absolute coordinate of specified mode. * @param {Hash} value Value of coordinate. {x, y} or {x, y, z}. * @param {Int} coordMode Value from {@link Kekule.CoordMode}, mode of coordinate. */ setAbsCoordOfMode: function(value, coordMode) { if (coordMode === Kekule.CoordMode.COORD3D) return this.setAbsCoord3D(value); else return this.setAbsCoord2D(value); }, /** * Check if this node has a coordinate on coordMode. * @param {Int} coordMode * @param {Bool} allowCoordBorrow * @returns {Bool} */ hasCoordOfMode: function(coordMode, allowCoordBorrow) { if (coordMode === Kekule.CoordMode.COORD3D) return this.hasCoord3D(allowCoordBorrow); else return this.hasCoord2D(allowCoordBorrow); }, /** * Returns the indirect coord data storage object in coordMode. * @param {Int} coordMode * @returns {Hash} */ getIndirectCoordStorageOfMode: function(coordMode) { var s = this.getIndirectCoordStorage && this.getIndirectCoordStorage(); return s && s[coordMode]; }, /** * Set the indirect coord data storage object in coordMode. * @param {Int} coordMode * @param {Hash} data */ setIndirectCoordStorageOfMode: function(coordMode, data) { var s = this.getIndirectCoordStorage && this.getIndirectCoordStorage(); if (s) s[coordMode] = data; return this; }, /** * Default methods of getting relative coord values. * Desendants can override this method. * @returns {Hash} * @private */ calcIndirectCoordValue: function(coordMode, allowCoordBorrow) { var coordFields = ['x', 'y']; if (coordMode === Kekule.CoordMode.COORD3D) coordFields.push('z'); var ratios = this.getIndirectCoordStorage()[coordMode]; var refCoords = this.getIndirectCoordRefCoords(coordMode, allowCoordBorrow); if (Kekule.ArrayUtils.isArray(refCoords)) refCoords = refCoords[0]; var refLengthes = this.getIndirectCoordRefLengths(coordMode, allowCoordBorrow); if (ratios && refCoords && refLengthes) { var result = {}; var notUnset = Kekule.ObjUtils.notUnset; for (var i = 0, l = coordFields.length; i < l; ++i) { var field = coordFields[i]; var ratio = ratios[field]; var refCoord = refCoords[field]; var refLength = refLengthes[field]; if (notUnset(ratio) && notUnset(refCoord) && notUnset(refLength)) { var v; if (refLength === 0) // ratio maybe infinite, avoid a NaN result v = refCoord; else v = refCoord + refLength * ratio; result[field] = v; } /* else { return null; } */ } //console.log('calc', coordMode, refCoords, refLengthes, ratios, coordFields, result); return result; } else return null; }, /** * Default methods of getting relative coord ratios from coord value. * Desendants can override this method. * @returns {Hash} * @private */ calcIndirectCoordStorage: function(coordMode, coordValue, oldCoordValue, allowCoordBorrow) { var coordFields = ['x', 'y']; if (coordMode === Kekule.CoordMode.COORD3D) coordFields.push('z'); var ratios = this.getIndirectCoordStorageOfMode(coordMode); var refCoords = this.getIndirectCoordRefCoords(coordMode, allowCoordBorrow); if (Kekule.ArrayUtils.isArray(refCoords)) refCoords = refCoords[0]; var refLengthes = this.getIndirectCoordRefLengths(coordMode, allowCoordBorrow); if (ratios && refCoords && refLengthes) { var newRatios = {}; var notUnset = Kekule.ObjUtils.notUnset; for (var i = 0, l = coordFields.length; i < l; ++i) { var field = coordFields[i]; var coord = coordValue[field]; var refCoord = refCoords[field]; var refLength = refLengthes[field]; if (notUnset(coord) && notUnset(refCoord) && notUnset(refLength)) { var ratio; if (refLength !== 0) // avoid divided by zero ratio = (coord - refCoord) / refLength; else ratio = 1; newRatios[field] = ratio; } else { return null; } } return newRatios; } else return null; }, /** * Default methods of updating the relative coord ratios. * Desendants can override this method. * @private */ saveIndirectCoordValue: function(coordMode, coordValue, oldCoordValue, allowCoordBorrow) { if (coordValue) { var newRatios = this.calcIndirectCoordStorage(coordMode, coordValue, oldCoordValue, allowCoordBorrow); if (newRatios) { this.setIndirectCoordStorageOfMode(coordMode, newRatios); return newRatios; } } return null; }, /** * Method to return the reference lengths used by using relative mode coord. * @returns {Variant} A numberic length or hash. * @private */ getIndirectCoordRefLengths: function(coordMode, allowCoordBorrow) { return null; }, /** * Method to return the reference coords used by using relative mode coord. * @returns {Array} * @private */ getIndirectCoordRefCoords: function(coordMode, allowCoordBorrow) { return null; } }, /** * @class * @private */ Coord2DMethods: /** @lends Kekule.ClassDefineUtils.Coord2DMethods# */ { /* * Get coord2D property, if this property is null, then create new hash for it. * Note this method will return a direct instance of coord2D hash (instead of a * clone as in getCoord2D). * //@param {Bool} allowCoordBorrow * @returns {Hash} {x, y} */ fetchCoord2D: function(/*allowCoordBorrow*/) { var c = this.getPropStoreFieldValue('coord2D'); if (!c) { c = {}; this.setPropStoreFieldValue('coord2D', c); } return c; /* console.log('fetch2D'); var result = this.getCoord2D(allowCoordBorrow, true); // allow create console.log(result); return result; */ }, /** @private */ notifyCoord2DChanged: function(newValue) { this.notifyPropSet('coord2D', newValue); }, /** * Check if this node has a 2D coordinate. * @param {Bool} allowCoordBorrow * @returns {Bool} */ hasCoord2D: function(allowCoordBorrow) { var c = this.getPropStoreFieldValue('coord2D'); //this.getCoord2D(); var result = (c) && ((typeof(c.x) != 'undefined') || (typeof(c.y) != 'undefined')); if ((!result) && allowCoordBorrow) result = this.hasCoord3D? this.hasCoord3D(): false; return result; }, /** * Get x coordinate of coord2D property. * @param {Bool} allowCoordBorrow * @returns {Float} x */ get2DX: function(allowCoordBorrow) { var c = this.getCoord2D(allowCoordBorrow); return c? c.x || 0: 0; //return this.hasCoord2D()? this.getCoord2D().x || 0: 0; }, /** * Change x coordinate of coord2D property. * @param {Float} x */ set2DX: function(x) { var c = this.fetchCoord2D(); if (c.x != x) { c.x = x; this.notifyCoord2DChanged(c); } }, /** * Get y coordinate of coord2D property. * @param {Bool} allowCoordBorrow * @returns {Float} y */ get2DY: function(allowCoordBorrow) { //return this.hasCoord2D()? this.getCoord2D().y || 0: 0; var c = this.getCoord2D(allowCoordBorrow); return c? c.y || 0: 0; }, /** * Change y coordinate of coord2D property. * @param {Float} y */ set2DY: function(y) { var c = this.fetchCoord2D(); if (c.y != y) { c.y = y; this.notifyCoord2DChanged(c); } } }, /** * @class * @private */ Coord3DMethods: /** @lends Kekule.ClassDefineUtils.Coord3DMethods# */ { /* * Get coord3D property, if this property is null, then create new hash for it. * Note this method will return a direct instance of coord2D hash (instead of a * clone as in getCoord3D). * //@param {Bool} allowCoordBorrow * @returns {Hash} {x, y, z} */ fetchCoord3D: function(/*allowCoordBorrow*/) { var c = this.getPropStoreFieldValue('coord3D'); if (!c) { c = {}; this.setPropStoreFieldValue('coord3D', c); } return c; //return this.getCoord3D(allowCoordBorrow, true); // allow create }, /** @private */ notifyCoord3DChanged: function(newValue) { this.notifyPropSet('coord3D', newValue); }, /** * Check if this node has a 3D coordinate. * @param {Bool} allowCoordBorrow * @returns {Bool} */ hasCoord3D: function(allowCoordBorrow) { var c = this.getPropStoreFieldValue('coord3D'); //this.getCoord3D(); var result = (c) && ((typeof(c.x) != 'undefined') || (typeof(c.y) != 'undefined') || (typeof(c.z) != 'undefined')); if ((!result) && allowCoordBorrow) result = this.hasCoord2D? this.hasCoord2D(): false; return result; }, /** * Get x coordinate of coord3D property. * @param {Bool} allowCoordBorrow * @returns {Float} x */ get3DX: function(allowCoordBorrow) { //return this.hasCoord3D()? this.getCoord3D().x || 0: 0; var c = this.getCoord3D(allowCoordBorrow); return c? c.x || 0: 0; }, /** * Change x coordinate of coord3D property. * @param {Float} x */ set3DX: function(x) { var c = this.fetchCoord3D(); if (c.x != x) { c.x = x; this.notifyCoord3DChanged(c); } }, /** * Get y coordinate of coord3D property. * @param {Bool} allowCoordBorrow * @returns {Float} y */ get3DY: function(allowCoordBorrow) { //return this.hasCoord3D()? this.getCoord3D().y || 0: 0; var c = this.getCoord3D(allowCoordBorrow); return c? c.y || 0: 0; }, /** * Change y coordinate of coord3D property. * @param {Float} y */ set3DY: function(y) { var c = this.fetchCoord3D(); if (c.y != y) { c.y = y; this.notifyCoord3DChanged(c); } }, /** * Get z coordinate of coord3D property. * @param {Bool} allowCoordBorrow * @returns {Float} z */ get3DZ: function(allowCoordBorrow) { //return this.hasCoord3D()? this.getCoord3D().z || 0: 0; var c = this.getCoord3D(allowCoordBorrow); return c? c.z || 0: 0; }, /** * Change z coordinate of coord3D property. * @param {Float} z */ set3DZ: function(z) { var c = this.fetchCoord3D(); if (c.z != z) { c.z = z; this.notifyCoord3DChanged(c); } } }, /** * Define coord2D/coord3D related properties and methods to a class. * @param {Object} aClass Should be class object. * @param {Array} props Which properties should be defined. e.g. ['coord3D', 'absCoord3D']. * If not set, all 2D and 3D properties will be defined. */ addStandardCoordSupport: function(aClass, props) { ClassEx.extend(aClass, Kekule.ClassDefineUtils.CommonCoordMethods); if ((!props) || (props.indexOf('coord2D') >= 0)) { ClassEx.defineProp(aClass, 'coord2D', { 'dataType': DataType.HASH, // clone result object so that user can not modify x/y directly from getter 'getter': function(allowCoordBorrow, allowCreateNew) { if (this.getEnableIndirectCoord()) { var rc = this.calcIndirectCoordValue(Kekule.CoordMode.COORD2D, allowCoordBorrow); if (rc) return rc; } var c = this.getPropStoreFieldValue('coord2D'); if ((!c) && allowCoordBorrow) { if (this.hasCoord3D()) { c = this.getCoord3D(); } } if ((!c) && allowCreateNew) { c = {'x': 0, 'y': 0}; this.setPropStoreFieldValue('coord2D', c); } var result = c? {'x': c.x, 'y': c.y}: undefined; return result; /* if (!this.hasCoord2D()) { return undefined; } var c = this.getPropStoreFieldValue('coord2D'); if ((!c) && allowCreateNew) this.setPropStoreFieldValue('coord2D', {}); var r = {}; r = Object.extend(r, c); return r; */ }, // clone value from input 'setter': function(value) { var c = this.fetchCoord2D(); if (this.getEnableIndirectCoord()) { //console.log('save relative', value); this.saveIndirectCoordValue(Kekule.CoordMode.COORD2D, value, c); } if (value.x !== undefined) c.x = value.x; if (value.y !== undefined) c.y = value.y; this.notifyCoord2DChanged(c); } }); ClassEx.extend(aClass, Kekule.ClassDefineUtils.Coord2DMethods); } if ((!props) || (props.indexOf('coord3D') >= 0)) { ClassEx.defineProp(aClass, 'coord3D', { 'dataType': DataType.HASH, // clone result object so that user can not modify x/y directly from getter 'getter': function(allowCoordBorrow, allowCreateNew) { if (this.getEnableIndirectCoord()) { var rc = this.calcIndirectCoordValue(Kekule.CoordMode.COORD2D, allowCoordBorrow); if (rc) return rc; } var c = this.getPropStoreFieldValue('coord3D'); if ((!c) && allowCoordBorrow) { if (this.hasCoord2D()) { c = this.getCoord2D(); c.z = 0; } } if ((!c) && allowCreateNew) { c = {'x': 0, 'y': 0, 'z': 0}; this.setPropStoreFieldValue('coord3D', c); } var result = c? {'x': c.x, 'y': c.y, 'z': c.z}: undefined; return result; /* if (!this.hasCoord3D()) return undefined; var c = this.getPropStoreFieldValue('coord3D'); if ((!c) && allowCreateNew) this.setPropStoreFieldValue('coord2D', {}); var r = {}; r = Object.extend(r, c); return r; */ }, // clone value from input 'setter': function(value){ var c = this.fetchCoord3D(); if (this.getEnableIndirectCoord()) this.saveIndirectCoordValue(Kekule.CoordMode.COORD3D, value, c); if (value.x !== undefined) c.x = value.x; if (value.y !== undefined) c.y = value.y; if (value.z !== undefined) c.z = value.z; this.notifyCoord3DChanged(c); } }); ClassEx.extend(aClass, Kekule.ClassDefineUtils.Coord3DMethods); } if ((!props) || (props.indexOf('absCoord2D') >= 0)) { ClassEx.defineProp(aClass, 'absCoord2D', { 'dataType': DataType.HASH, 'serializable': false, 'getter': function(allowCoordBorrow){ var c = this.getCoord2D(allowCoordBorrow) || {}; //var p = this.getParent? this.getParent(): null; var p = this.getCoordParent(Kekule.CoordMode.COORD2D); var result; //if (p && p.hasCoord2D && p.hasCoord2D()) // has parent, consider parent coordinate to get absolute position if (p && p.getAbsCoord2D) { return Kekule.CoordUtils.add(c, p.getAbsCoord2D(allowCoordBorrow) || {}); } else //return Object.extend({}, c); return {'x': c.x, 'y': c.y}; }, 'setter': function(value, allowCoordBorrow){ var c = value; var p = this.getCoordParent(Kekule.CoordMode.COORD2D); if (p && p.getAbsCoord2D) // has parent, consider parent coordinate to get absolute position c = Kekule.CoordUtils.substract(value, p.getAbsCoord2D(allowCoordBorrow)); this.setCoord2D(c); } }); } if ((!props) || (props.indexOf('absCoord3D') >= 0)) { ClassEx.defineProp(aClass, 'absCoord3D', { 'dataType': DataType.HASH, 'serializable': false, 'getter': function(allowCoordBorrow){ var c = this.getCoord3D(allowCoordBorrow) || {}; //var p = this.getParent? this.getParent(): null; var p = this.getCoordParent(Kekule.CoordMode.COORD3D); //if (p && p.hasCoord3D && p.hasCoord3D()) // has parent, consider parent coordinate to get absolute position if (p && p.getAbsCoord3D) return Kekule.CoordUtils.add(c, p.getAbsCoord3D(allowCoordBorrow) || {}); else //return Object.extend({}, c); return {'x': c.x, 'y': c.y, 'z': c.z}; }, 'setter': function(value, allowCoordBorrow){ var c = value; var p = this.getCoordParent(Kekule.CoordMode.COORD2D); if (p && p.getAbsCoord3D) // has parent, consider parent coordinate to get absolute position c = Kekule.CoordUtils.substract(value, p.getAbsCoord3D(allowCoordBorrow)); this.setCoord3D(c); } }); } if (!props || (props.indexOf('enableIndirectCoord') >= 0)) { ClassEx.defineProp(aClass, 'enableIndirectCoord', { 'scope': Class.PropertyScope.PUBLIC, 'dataType': DataType.BOOL, 'serializable': true, 'setter': function(value) { if (this.getPropStoreFieldValue('enableIndirectCoord') != value) { if (value) // when enable relative coord, save the coord ratio first { var coord2D = this.getCoord2D(); var coord3D = this.getCoord3D(); if (coord2D) this.saveIndirectCoordValue(Kekule.CoordMode.COORD2D, coord2D, coord2D, true); if (coord3D) this.saveIndirectCoordValue(Kekule.CoordMode.COORD3D, coord3D, coord3D, true); } this.setPropStoreFieldValue('enableIndirectCoord', value); } } }); } if (!props || (props.indexOf('indirectCoordStorage') >= 0)) { ClassEx.defineProp(aClass, 'indirectCoordStorage', { //'scope': Class.PropertyScope.PUBLIC, 'dataType': DataType.HASH, 'serializable': function(){ return !!this.getEnableIndirectCoord(); }, 'setter': null, 'getter': function() { var result = this.getPropStoreFieldValue('indirectCoordStorage'); if (!result) { result = {}; result[Kekule.CoordMode.COORD2D] = {}; result[Kekule.CoordMode.COORD3D] = {}; this.setPropStoreFieldValue('indirectCoordStorage', result); } return result; } }); } }, /// Methods to expand support for ExtraObjMap /** * @class * @private */ ExtraObjMapMethods: { /** * Get extra mapped property of obj. * @param {Object} obj * @param {String} propName * @returns {Variant} If propName is set, returns the prop value. Otherwise return a hash contains all extra properties. * @private */ getExtraProp: function(obj, propName) { var info = this.getExtraObjMap().get(obj); if (info) return propName? info[propName]: info; else return undefined; }, /** * Set extra mapped property of obj. * @param {Object} obj * @param {String} propName * @param {Variant} propValue * @private */ setExtraProp: function(obj, propName, propValue) { var info = this.getExtraProp(obj); if (info) info[propName] = propValue; else { var info = {}; info[propName] = propValue; this.getExtraObjMap().set(obj, info); } }, /** * Remove extra mapped property of obj. * @param {Object} obj * @param {String} propName If not set, all properties on obj will be removed. * @private */ removeExtraProp: function(obj, propName) { if (propName) { var info = this.getExtraProp(obj); info[propName] = undefined; delete info[propName]; } else this.getExtraObjMap().remove(obj); } }, /** * Define extraObjMap related properties and methods to a class. * @param {Object} aClass Should be class object. */ addExtraObjMapSupport: function(aClass) { var mapName = 'extraObjMap'; ClassEx.defineProp(aClass, mapName, { 'dataType': 'Kekule.MapEx', 'serializable': false,