/**
* @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,
/** 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
};
/**
* 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.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.Error} in ExceptionHandler.
*/
Kekule.error = function(e)
{
/*
if (typeof(e) == 'string')
e = new Kekule.Error(e);
if (Kekule.exceptionHandler)
return Kekule.exceptionHandler.throwException(e);
else
throw e;
*/
return Kekule.raise(e, Kekule.ExceptionLevel.ERROR);
};
/**
* Throw an {@link Kekule.ChemError} in ExceptionHandler.
*/
Kekule.chemError = function(e)
{
if (typeof(e) == 'string')
e = new Kekule.ChemError(e);
/*
if (Kekule.exceptionHandler)
return Kekule.exceptionHandler.throwException(e);
else
throw e;
*/
return Kekule.error(e);
};
/**
* 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) // value already exists
{
if (DataType.isObjectValue(oldValue) && (DataType.isObjectValue(valueOrHash)))
{
Object.extend(oldValue, valueOrHash);
}
}
else
Object.setCascadeFieldValue(optionName, valueOrHash, Kekule.globalOptions, true);
}
};
/**
* 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['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
{
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
{
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
};
/**
* 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 = fetchSize2D();
if (c.x != x)
{
c.x = x;
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 = fetchSize2D();
if (c.y != y)
{
c.y = y;
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 = fetchSize3D();
if (c.x != x)
{
c.x = x;
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 = fetchSize3D();
if (c.y != y)
{
c.y = y;
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 = fetchSize3D();
if (c.z != z)
{
c.z = z;
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# */
{
/**
* 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);
}
},
/**
* @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)
{
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 (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)
{
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 (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 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){
var c = value;
if (this.getParent && this.getParent() && this.getParent().getAbsCoord2D) // has parent, consider parent coordinate to get absolute position
c = Kekule.CoordUtils.substract(value, this.getParent().getAbsCoord2D());
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;
//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){
var c = value;
if (this.getParent && this.getParent()) // has parent, consider parent coordinate to get absolute position
c = Kekule.CoordUtils.substract(value, this.getParent().getAbsCoord3D());
this.setCoord3D(c);
}
});
}
},
/// 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,
'getter': function()
{
var result = this.getPropStoreFieldValue(mapName);
if (!result)
{
result = new Kekule.MapEx(true, true); // enable cache
this.setPropStoreFieldValue(mapName, result);
}
return result;
}
});
ClassEx.extend(aClass, Kekule.ClassDefineUtils.ExtraObjMapMethods);
},
/**
* @class
* @private
*/
ExtraTwoTupleObjMapMethods: {
/**
* Get extra mapped property of obj.
* @param {Object} obj1
* @param {Object} obj2
* @param {String} propName
* @returns {Variant} If propName is set, returns the prop value. Otherwise return a hash contains all extra properties.
* @private
*/
getExtraProp2: function(obj1, obj2, propName)
{
var info = (this.__$__k__extraTwoTupleObjMap__$__ || this.getExtraTwoTupleObjMap()).get(obj1, obj2);
if (info)
return propName? info[propName]: info;
else
return undefined;
},
/**
* Set extra mapped property of obj.
* @param {Object} obj1
* @param {Object} obj2
* @param {String} propName
* @param {Variant} propValue
* @private
*/
setExtraProp2: function(obj1, obj2, propName, propValue)
{
var info = this.getExtraProp2(obj1, obj2);
if (info)
info[propName] = propValue;
else
{
var info = {};
info[propName] = propValue;
(this.__$__k__extraTwoTupleObjMap__$__ || this.getExtraTwoTupleObjMap()).set(obj1, obj2, info);
}
},
/**
* Remove extra mapped property of obj.
* @param {Object} obj1
* @param {Object} obj2
* @param {String} propName If not set, all properties on obj will be removed.
* @private
*/
removeExtraProp2: function(obj1, obj2, propName)
{
if (propName)
{
var info = this.getExtraProp2(obj1, obj2);
info[propName] = undefined;
delete info[propName];
}
else
this.getExtraTwoTupleObjMap().remove(obj1, obj2);
}
},
addExtraTwoTupleObjMapSupport: function(aClass)
{
var mapName = 'extraTwoTupleObjMap';
ClassEx.defineProp(aClass, mapName, {
'dataType': 'Kekule.TwoTupleMapEx', 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue(mapName);
if (!result)
{
result = new Kekule.TwoTupleMapEx(true, true); // enable cache
this.setPropStoreFieldValue(mapName, result);
this.__$__k__extraTwoTupleObjMap__$__ = result; // a field to store the map, for performance
}
return result;
}
});
ClassEx.extend(aClass, Kekule.ClassDefineUtils.ExtraTwoTupleObjMapMethods);
}
};
/**
* A manager class to store object property settings.
* Settings is a hash with property name and values, e.g.:
* {'id': 'object1', 'enabled': false}.
* Those settings can be applied to instance of {@link Kekule.ChemObject}
* by {@link Kekule.ChemObject.predefinedSetting} property.
* Settings can be get out by settingName which is recommended to have name like 'Kekule.ChemObject.Setting1'.
* Name of object class should be used as a prefix. Then user can use code chemObject.setPredefinedSetting('Setting1')
* to set settings.
* @class
*/
Kekule.ObjPropSettingManager = {
/** @private */
_items: new Kekule.MapEx(true),
register: function(name, settings)
{
Kekule.ObjPropSettingManager._items.set(name, settings);
},
unregister: function(name)
{
Kekule.ObjPropSettingManager._items.remove(name);
},
getSetting: function(name)
{
return Kekule.ObjPropSettingManager._items.get(name);
}
};
// predefinedSetting string can be split by ',', each setting will be applied to object one by one
ClassEx.defineProp(ObjectEx, 'predefinedSetting', {'dataType': DataType.STRING,
'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
var vs = Kekule.StrUtils.splitTokens(value || '', ',');
//console.log('predefine', this.getClassName(), vs);
if (vs.length)
{
for (var i = 0, l = vs.length; i < l; ++i)
{
var currClass = this.getClass();
var v = vs[i];
var setting = null;
var sname = '';
while (currClass && !setting)
{
var cname = ClassEx.getClassName(currClass);
var sname = cname + '.' + v;
var setting = Kekule.ObjPropSettingManager.getSetting(sname);
currClass = ClassEx.getSuperClass(currClass);
}
if (!setting)
{
setting = Kekule.ObjPropSettingManager.getSetting(v);
//sname = v;
}
if (setting)
{
this.setPropValues(setting);
}
}
this.setPropStoreFieldValue('predefinedSetting', sname);
}
}
});
/**
* Enumeration of interaction types of chem objects in editor.
* @enum
*/
Kekule.ChemObjInteractMode = {
/* A default object, can be selected and moved alone. */
DEFAULT: 0,
/* Can be selected but can not be moved alone. */
UNMOVABLE: 1,
/* Can be moved but can not be selected alone. */
UNSELECTABLE: 2,
/** A uninteractive object, can not be selected or moved alone */
HIDDEN: 3
};
/**
* Enumeration of comparison method for {@link Kekule.ChemObject.compare} method.
* @enum
*/
Kekule.ComparisonMethod = {
/** Use default behavior. Different object may have different behaviors. */
DEFAULT: 0,
/* Explicitly set comparing on object properties directly. */
PROPERTIES: 1,
/** Compare only chemistry structures. */
CHEM_STRUCTURE: 10
};
/**
* A util class to compare Kekule objects through {@link Kekule.ChemObject.compare} method.
* @class
*/
Kekule.ObjComparer = {
/**
* Check if two objects are equivalent.
* @param {Kekule.ChemObj} obj1
* @param {Kekule.ChemObj} obj2
* @param {Hash} options Comparison objects, different class may require different options. <br />
* For example, you can use {'method': {@link Kekule.ComparisonMethod.CHEM_STRUCTURE}} to indicating that only the chem structure
* data should be compared. <br />
* You can also use {'properties': ['propName1', 'propName2']} to manually assign properties that
* need to be compared. <br />
* Custom comparison method can also be appointed as {'customMethod': myComparisonFunc}, then the
* comparison will be actually called as myComparisonFunc(thisObj, targetObj, options).
* @returns {Bool}
*/
equal: function(obj1, obj2, options)
{
return Kekule.ObjComparer.compare(obj1, obj2, options) === 0;
},
/**
* Compare two objects.
* @param {Kekule.ChemObj} obj1
* @param {Kekule.ChemObj} obj2
* @param {Hash} options Comparison objects, different class may require different options. <br />
* For example, you can use {'method': {@link Kekule.ComparisonMethod.CHEM_STRUCTURE}} to indicating that only the chem structure
* data should be compared. <br />
* You can also use {'properties': ['propName1', 'propName2']} to manually assign properties that
* need to be compared. <br />
* Custom comparison method can also be appointed as {'customMethod': myComparisonFunc}, then the
* comparison will be actually called as myComparisonFunc(thisObj, targetObj, options).
* @returns {Int} Returns 0 when two objects are equivalent,
* or -1 for "inferior" to obj1 and +1 for "superior" to obj1.
*/
compare: function(obj1, obj2, options)
{
return Kekule.ObjComparer._compareValue(obj1, obj2, options);
},
/**
* Compare on two values, used for object comparison.
* @param {Variant} v1
* @param {Variant} v2
* @param {Hash} options
* @returns {Int}
* @private
*/
_compareValue: function(v1, v2, options)
{
var D = DataType;
if (v1 === v2)
return 0;
// the two following comparison handles same type simple values, such as number, bool, string and date
if (v1 < v2 && !(v1 > v2))
return -1;
else if (v1 > v2 && !(v1 < v2))
return 1;
else // need more complex check
{
var result = null; // not determinated
var type1 = D.getType(v1);
var type2 = D.getType(v2);
if (type1 !== type2) // not same type
{
var typeIndexes = [
D.UNKNOWN, D.VARIANT, D.UNDEFINED, D.BOOL, D.NUMBER, D.INT, D.FLOAT,
D.STRING, D.ARRAY, D.FUNCTION, D.DATE, D.OBJECT, D.OBJECTEX, D.CLASS
];
var index1 = typeIndexes.indexOf(type1);
var index2 = typeIndexes.indexOf(type2);
if (index1 < 0 && index2 < 0) // all unknown type
result = null; // can not determinate, suspend
else
result = index1 - index2;
}
else // with the same type, usually function and objects
{
if (!v1 && v2) // v1 is null, 0, false, undefined etc. but v2 not
result = -1;
else if (v1 && !v2) // on the contrary, here there is no possiblity that !v1 && !v2
result = 1;
else // v1 && v2
{
if (v1.doCompare)
result = v1.doCompare(v2, options);
else if (v2.doCompare)
result = -v2.doCompare(v1, options);
else // no compare method
{
if (type1 === DataType.ARRAY) // compare array
{
result = v1.length - v2.length;
if (!result)
{
for (var i = 0, l = v1.length; i < l; ++i)
{
result = Kekule.ObjComparer._compareValue(v1[i], v2[i], options);
if (!!result)
break;
}
}
}
else if (type1 === DataType.OBJECT) // two values are objects
{
// TODO: not very efficient, need to refine in future
var s1 = JSON.toString(v1);
var s2 = JSON.toString(v2);
result = (s1 < s2)? -1:
(s1 > s2)? 1: 0;
}
else
result = null;
}
}
}
if (result === null) // undeterminated in above
{
var s1 = (v1.toString && v1.toString()) || '';
var s2 = (v2.toString && v2.toString()) || '';
result = (s1 < s2)? -1:
(s1 > s2)? 1: null;
// if s1 === s2, we still can not determinate, here we simply returns -1
if (result === null)
result = -1;
}
return result;
}
}
};
/**
* Root class for all object related to chemistry in Kekule library.
* @class
* @augments ObjectEx
* @param {String} id Id of this object.
* @property {String} id Id of this object. Can be set by users.
* @property {Int} interactMode Value from {@link Kekule.ChemObjInteractMode}.
* //@property {String} iid Internal ID of object, when an instance is created, an unique iid will be
* // set automatically. This value is helpful to find a special object in molecule or chem space.
* // User should not set or modify this value.
* @property {Kekule.ChemSpace} owner Owner of this object. Can be null.
* @property {Array} scalarAttribs Scalar attributes about this object. Each item should be {@link Kekule.Scalar}.
* @property {Hash} info Additional information of object, for example:
* {
* title: 'Object Title',
* author: 'Author Name'
* }
* @property {Kekule.ChemObject} parent Direct parent object.
* @property {Kekule.ChemObject} owner
* @property {Object} srcInfo Stores the source data of ChemObject when read object from external file, a object that
* can has the following fields: {data, dataType, format, mimeType, fileExt, possibleFileExts, url}.
*/
Kekule.ChemObject = Class.create(ObjectEx,
/** @lends Kekule.ChemObject# */
{
/** @private */
CLASS_NAME: 'Kekule.ChemObject',
/** @constructs */
initialize: function($super, id)
{
$super();
if (id)
this.setId(id);
this.setBubbleEvent(true); // allow event bubble
// react on change (both on self and children)
// when object is modified,clear srcInfo information
this.addEventListener('change', function(e)
{
var srcInfo = this.getPropStoreFieldValue('srcInfo');
if (srcInfo && srcInfo.data)
{
var target = e.target;
var propNames = e.changedPropNames;
var clearSrcInfo = false;
for (var i = 0, l = propNames.length; i < l; ++i)
{
var name = propNames[i];
if (name && name !== 'srcInfo' && target.isPropertySerializable(name))
{
clearSrcInfo = true;
break;
}
else if (name === '[children]') // special prop name indicating children has been changed
{
clearSrcInfo = true;
break;
}
}
if (clearSrcInfo)
{
//this.setPropStoreFieldValue('srcInfo', undefined);
//console.log('clear src info', propNames);
srcInfo.data = null;
}
}
}, this);
},
/** @private */
doFinalize: function($super)
{
/*
this.setParent(null);
this.setOwner(null);
*/
this.removeSelf();
$super();
},
/** @private */
initProperties: function()
{
this.defineProp('id', {'dataType': DataType.STRING});
this.defineProp('interactMode', {'dataType': DataType.INT});
/*
this.defineProp('iid', {
'dataType': DataType.STRING, 'scope': Class.PropertyScope.PRIVATE
});
*/
this.defineProp('info',
{
'dataType': DataType.HASH,
'getter': function(canCreate)
{
var r = this.getPropStoreFieldValue('info');
if ((!r) && canCreate)
{
r = {};
this.setPropStoreFieldValue('info', r);
}
return r;
}
});
this.defineProp('scalarAttribs', {
'dataType': DataType.ARRAY,
'setter': null
});
this.defineProp('parent', {'dataType': 'Kekule.ChemObject', 'serializable': false, 'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
var oldParent = this.getParent();
if (oldParent === value)
return;
if (oldParent && oldParent._removeChildObj) // remove from old parent
{
oldParent._removeChildObj(this);
}
this.setPropStoreFieldValue('parent', value);
if (value && value._appendChildObj) // add to new parent
{
value._appendChildObj(this);
}
this.parentChanged(value);
}
});
this.defineProp('owner', {
'dataType': 'Kekule.ChemSpace',
'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
var oldOwner = this.getOwner();
if (oldOwner === value)
return;
if (oldOwner)
oldOwner._removeOwnedObj(this);
this.setPropStoreFieldValue('owner', value);
if (value)
value._appendOwnedObj(this);
this.ownerChanged(value);
}
});
this.defineProp('srcInfo', {'dataType': DataType.OBJECT,
'serializable': false, // not allowed to assign from objects and save to stream
'scope': Class.PropertyScope.PUBLISHED,
'getter': function()
{
var result = this.getPropStoreFieldValue('srcInfo');
if (!result)
{
result = {};
this.setPropStoreFieldValue('srcInfo', result);
}
return result;
}
});
},
/** @ignore */
initPropValues: function($super)
{
$super();
this.setEnableObjectChangeEvent(true);
},
/** @private */
getHigherLevelObj: function()
{
return this.getParent() || this.getOwner();
},
/**
* Return a prefix to automatically generate id of chem object.
* Descendants should override this method.
* @returns {String}
* @private
*/
getAutoIdPrefix: function()
{
return 'o';
},
/**
* Check whether this object can be selected alone in editor.
*/
isSelectable: function()
{
var IM = Kekule.ChemObjInteractMode;
var interactMode = this.getInteractMode() || IM.DEFAULT;
return !(interactMode & IM.UNSELECTABLE);
},
/**
* Check whether this object can be moved alone in editor.
*/
isMovable: function()
{
var IM = Kekule.ChemObjInteractMode;
var interactMode = this.getInteractMode() || IM.DEFAULT;
return !(interactMode & IM.UNMOVABLE);
},
/**
* Returns nearest selectable chem object.
* Usually this method will return this object, but if this object is not selectable,
* parent object will be returned instead.
* @returns {Kekule.ChemObject}
*/
getNearestSelectableObject: function()
{
return this.isSelectable()? this:
this.getParent()? this.getParent().getNearestSelectableObject():
null;
},
/**
* Returns nearest movable chem object.
* Usually this method will return this object, but if this object is not selectable,
* parent object will be returned instead.
* @returns {Kekule.ChemObject}
*/
getNearestMovableObject: function()
{
return this.isMovable()? this:
this.getParent()? this.getParent().getNearestMovableObject():
null;
},
/**
* Remove current object away from parent and owner.
*/
removeSelf: function()
{
this.setParent(null);
this.setOwner(null);
},
/** @private */
notifyScalarAttribsChange: function()
{
this.notifyPropSet('scalarAttribs', this.getPropStoreFieldValue('scalarAttribs'));
},
/** @private */
notifyInfoChange: function()
{
this.notifyPropSet('info', this.getPropStoreFieldValue('info'));
},
/**
* Called after a new owner property is set. Descendants can override this method.
* @private
*/
ownerChanged: function(newOwner)
{
// change scalar owners
for (var i = 0, l = this.getScalarAttribCount(); i < l; ++i)
this.getScalarAttribAt(i).setOwner(newOwner);
// change owner of children
for (var i = 0, l = this.getChildCount(); i < l; ++i)
{
var c = this.getChildAt(i);
if (c && c.setOwner)
c.setOwner(newOwner);
}
},
/**
* Called after a new parent property is set. Descendants can override this method.
* @private
*/
parentChanged: function(newParent)
{
// do nothing here
},
/**
* Check if this object is a child (direct or indirect) of another object.
* @param obj
*/
isChildOf: function(obj)
{
var p = this.getParent();
while (p && (p !== obj))
p = p.getParent? p.getParent(): null;
return (!!p);
},
/**
* Returns next sibling object in parent.
* If parent not set, null will be returned.
* @returns {Object}
*/
getNextSibling: function()
{
var parent = this.getParent();
if (parent && parent.getNextSiblingOfChild)
{
return parent.getNextSiblingOfChild(this);
}
else
return null;
},
/**
* Get count of child objects.
* To a simple chem object without any child, this method will always return 0.
* Descendants need to override this method if they has children.
* @returns {Int}
*/
getChildCount: function()
{
return 0;
},
/**
* Get child object at index.
* To a simple chem object without any child, this method will always return null.
* Descendants need to override this method if they has children.
* @param {Int} index
* @returns {Variant}
*/
getChildAt: function(index)
{
return null;
},
/**
* Get the index of obj in children list.
* To a simple chem object without any child, this method will always return -1.
* Descendants need to override this method if they has children.
* @param {Variant} obj
* @returns {Int} Index of obj or -1 when not found.
*/
indexOfChild: function(obj)
{
return -1;
},
/**
* Remove obj from children.
* @param {Variant} obj
* @returns {Variant} Child object removed.
*/
removeChild: function(obj)
{
// do nothing here
return null;
},
/**
* Insert obj before refChild in children list.
* If refChild is null or does not exists, obj will be append to tail of list.
* Descendants may override this method.
* @param {Variant} obj
* @param {Variant} refChildr
* @return {Int} Index of obj after inserting.
*/
insertBefore: function(obj, refChild)
{
// do nothing here
return -1;
},
/**
* Add obj to the tail of children list.
* @param {Variant} obj
* @return {Int} Index of obj after appending.
*/
appendChild: function(obj)
{
return this.insertBefore(obj, null);
},
/**
* Run a cascade function on all children (and their sub children).
* @param {Function} func The function has one param: obj. It should not modify the children structure of this object.
*/
cascadeOnChildren: function(func)
{
if (!func)
return this;
for (var i = 0, l = this.getChildCount(); i < l; ++i)
{
var obj = this.getChildAt(i);
if (obj.cascadeOnChildren)
obj.cascadeOnChildren(func);
func(obj);
}
return this;
},
/**
* Run a cascade function on self and all children (and their sub children).
* @param {Function} func The function has one param: obj. It should not modify the children structure of this object.
*/
cascade: function(func)
{
if (!func)
return this;
this.cascadeOnChildren(func);
func(this);
return this;
},
/**
* Check if this object contains no data.
* @returns {Bool}
*/
isEmpty: function()
{
//return (this.getScalarAttribCount() <= 0) && (this.getInfoKeys().length <= 0);
return false; // usually chem object always contains information.
},
/**
* Set id property to null on this object and all its children.
*/
clearIds: function()
{
this.setId(undefined);
for (var i = 0, l = this.getChildCount(); i < l; ++i)
{
var child = this.getChildAt(i);
if (child.clearIds)
child.clearIds();
}
},
/**
* Assign data in this object to targetObj.
* @param {ObjectEx} targetObj
* @param {Bool} withId If set to true, id of current object will be copied to targetObj,
* otherwise targetObj's id will be cleared.
*/
assignTo: function($super, targetObj, withId)
{
var result = $super(targetObj);
if (!withId && targetObj.clearIds)
targetObj.clearIds();
return result;
},
/**
* Assign data in srcObj to this object.
* @param {ObjectEx} srcObj
* @param {Bool} withId If set to true, id of srcObj will be copied to this object,
* otherwise this object's id will be cleared.
*/
assign: function(srcObj, withId)
{
return srcObj.assignTo(this, withId);
},
/**
* Returns a cloned object.
* @param {Bool} withId If set to true, id of this object will cloned to new object,
* otherwise id of new object will be cleared.
* @returns {Kekule.ChemObject}
*/
clone: function(withId)
{
var classObj = this.getClass();
var result = new classObj();
this.assignTo(result, withId);
return result;
},
/**
* Get count of scalar attributes.
* @returns {Int}
*/
getScalarAttribCount: function()
{
var a = this.getScalarAttribs();
return a? a.length: 0;
},
/**
* Get scalar attrib object at index.
* @param {Int} index
* @returns {Kekule.Scalar}
*/
getScalarAttribAt: function(index)
{
var a = this.getScalarAttribs();
return a? a[index]: null;
},
/**
* Append a scalar object to scalarAttribs array.
* @param {Kekule.Scalar} scalar
*/
appendScalarAttrib: function(scalar)
{
if (!this.getScalarAttribs())
this.setPropStoreFieldValue('scalarAttribs', []);
var r = Kekule.ArrayUtils.pushUniqueEx(this.getScalarAttribs(), scalar);
if (r.isPushed)
{
scalar.setParent(this);
this.notifyScalarAttribsChange();
}
return r.index;
},
/**
* Remove scalar at index in scalarAttribs array.
* @param {Int} index
* @returns {Kekule.Scalar} Object removed or null.
*/
removeScalarAttribAt: function(index)
{
if (!this.getScalarAttribs())
return null;
else
{
var r = Kekule.ArrayUtils.removeAt(this.getScalarAttribs(), index);
if (r)
{
r.setParent(null);
this.notifyScalarAttribsChange();
}
return r;
}
},
/**
* Remove scalar in scalarAttribs array.
* @param {Kekule.Scalar} scalar Object to be removed.
* @returns {Kekule.Scalar} Object removed or null.
*/
removeScalarAttrib: function(scalar)
{
if (!this.getScalarAttribs())
return null;
else
{
var r = Kekule.ArrayUtils.remove(this.getScalarAttribs(), scalar);
if (r)
{
r.setParent(null);
this.notifyScalarAttribsChange();
}
return r;
}
},
/**
* Returns all keys in {@link Kekule.ChemObject#info} property.
* @returns {Array}
*/
getInfoKeys: function()
{
return this.getInfo()? Kekule.ObjUtils.getOwnedFieldNames(this.getInfo()): [];
},
/**
* Get item value from info hash.
* @param key Key of information item.
*/
getInfoValue: function(key)
{
return this.getInfo()? this.getInfo()[key]: null;
},
/**
* Set an item value in info hash. If key already exists, its value will be overwritten.
* @param key Key of information item.
* @param value Value of information item.
*/
setInfoValue: function(key, value)
{
this.doGetInfo(true)[key] = value;
this.notifyInfoChange();
},
/**
* Calculate the box to contain the object.
* Descendants may override this method.
* @param {Int} coordMode Determine to calculate 2D or 3D box. Value from {@link Kekule.CoordMode}.
* @param {Bool} allowCoordBorrow
* @returns {Hash} Box information. {x1, y1, z1, x2, y2, z2} (in 2D mode z1 and z2 will not be set).
*/
getContainerBox: function(coordMode, allowCoordBorrow)
{
// defaultly returns coord and size
if (this.getAbsCoordOfMode)
{
var coord1 = this.getAbsCoordOfMode(coordMode, allowCoordBorrow) || {};
var coord2 = coord1;
if (this.getSizeOfMode)
{
var size = this.getSizeOfMode(coordMode, allowCoordBorrow) || {};
if (coordMode === Kekule.CoordMode.COORD3D)
var coord2 = Kekule.CoordUtils.add(coord1, this.getSizeOfMode(coordMode, allowCoordBorrow) || {});
else // 2D
{
coord2 = {
x: coord1.x + size.x,
y: coord1.y - size.y
};
}
}
var result = Kekule.BoxUtils.createBox(coord1, coord2);
//console.log('get box', this.getClassName(), result, coord1, coord2);
return result;
}
else
return null;
},
/**
* Check if this object is equivalent to targetObj.
* @param {Kekule.ChemObj} targetObj
* @param {Hash} options Comparison objects, different class may require different options. <br />
* For example, you can use {'method': {@link Kekule.ComparisonMethod.CHEM_STRUCTURE}} to indicating that only the chem structure
* data should be compared. <br />
* You can also use {'properties': ['propName1', 'propName2']} to manually assign properties that
* need to be compared. <br />
* Custom comparison method can also be appointed as {'customMethod': myComparisonFunc}, then the
* comparison will be actually called as myComparisonFunc(thisObj, targetObj, options).
* @returns {Bool}
*/
equal: function(targetObj, options)
{
return this.compare(targetObj, options) === 0;
},
/**
* Compare this object to a targetObj.
* @param {Kekule.ChemObj} targetObj
* @param {Hash} options Comparison objects, different class may require different options. <br />
* For example, you can use {'method': {@link Kekule.ComparisonMethod.CHEM_STRUCTURE}} to indicating that only the chem structure
* data should be compared. <br />
* You can also use {'properties': ['propName1', 'propName2']} to manually assign properties that
* need to be compared. <br />
* Custom comparison method can also be appointed as {'customMethod': myComparisonFunc}, then the
* comparison will be actually called as myComparisonFunc(thisObj, targetObj, options).
* @returns {Int} Returns 0 when two objects are equivalent,
* or -1 for "inferior" to targetObj and +1 for "superior" to targetObj.
*/
compare: function(targetObj, options)
{
if (targetObj === this) // same object, simply returns 0
return 0;
if (!targetObj)
return 1;
var actualOps = this.doGetActualCompareOptions(options);
//console.log(this.getClassName(), options, actualOps);
var result;
if (actualOps.customMethod)
result = actualOps.customMethod(this, targetObj, actualOps);
else
{
result = this.doCompare(targetObj, actualOps);
}
return (result === 0)? 0: (result < 0)? -1: 1; // standardize result
},
/**
* Handles input options for {@link Kekule.ChemObj.compare}.
* Descendants may override this method.
* @param {Hash} options
* @returns {Hash}
* @private
*/
doGetActualCompareOptions: function(options)
{
return options || {};
},
/**
* Do actual work of {@link Kekule.ChemObj.compare}. Descendants should override this method.
* @param {Kekule.ChemObj} targetObj
* @param {Hash} options Comparison objects, different class may require different options.
* @returns {Int} Returns 0 when two objects are equivalent and non-zero value when they are not equivalent.
* (Usually -1 for "inferior" to targetObj and +1 for "superior" to targetObj).
* @private
*/
doCompare: function(targetObj, options)
{
// here we use a simple approach
if (targetObj === this)
return 0;
else // if not same, check if they are based on same class
{
var selfClass = this.getClass();
var targetClass = targetObj.getClass && targetObj.getClass();
if (selfClass !== targetClass) // not same type of class
{
var selfClassName = this.getClassName();
var targetClassName = (targetObj.getClassName && targetObj.getClassName()) || '';
return (selfClassName < targetClassName)? -1: 1;
}
else // same class, must compare details, here we use default approach, comparing properties
{
var result = this.doCompareOnProperties(targetObj, options);
if (!result && options.extraComparisonProperties)
{
for (var i = 0, l = options.extraComparisonProperties.length; i < l; ++i)
{
var propName = options.extraComparisonProperties[i];
if (this.hasProperty(propName) && targetObj.hasProperty && targetObj.hasProperty(propName))
{
var v1 = this.getPropValue(propName);
var v2 = targetObj.getPropValue(propName);
//console.log('compare extra', this.getClassName(), propName, v1, v2);
result = this.doCompareOnValue(v1, v2, options);
if (!result)
break;
}
}
}
return result;
}
}
},
/**
* The default approach to compare two object based on properties.
* This method will check essential properties of targetObj and this object,
* comparing them one by one to get the result.
* @param {Kekule.ChemObj} targetObj
* @param {Hash} options
* @returns {Int}
* @private
*/
doCompareOnProperties: function(targetObj, options)
{
var propNames;
if (options.properties)
propNames = options.properties;
else
{
// first extract all properties that should be compared
propNames = this.doGetComparisonPropNames(options);
if (!propNames) // returns null, need to compare all public and published properties
{
propNames = this.doGetDefaultComparisonPropNames();
}
}
// then compare each property values
var result = 0;
for (var i = 0, l = propNames.length; i < l; ++i)
{
result = this.doCompareProperty(targetObj, propNames[i], options);
if (result !== 0)
return result;
}
// all properties returns 0 (equal), returns 0
return 0;
},
/**
* Returns all essential property names that should be handled during object comparison.
* Descendants may override this method.
* @param {Hash} options
* @returns {Array} Returns property names or null to indicating that all properties should be compared.
* @private
*/
doGetComparisonPropNames: function(options)
{
return null;
},
/**
* Returns all default property names that should be handled during object comparison.
* Descendants may override this method.
* @param {Hash} options
* @returns {Array}
* @private
*/
doGetDefaultComparisonPropNames: function()
{
var propNames = [];
var props = this.getPropListOfScopes([Class.PropertyScope.PUBLIC, Class.PropertyScope.PUBLISHED]);
for (var i = 0, l = props.getLength(); i < l; ++i)
{
var propInfo = props.getPropInfoAt(i);
propNames.push(propInfo.name);
}
return propNames;
},
/**
* Compare property value of targetObj and this object, used for object comparison.
* Descendants may override this method.
* @param {Kekule.ChemObj} targetObj
* @param {String} propName
* @param {Hash} options
* @returns {Int}
* @private
*/
doCompareProperty: function(targetObj, propName, options)
{
var v2 = targetObj.getPropValue && targetObj.getPropValue(propName);
var v1 = this.getPropValue(propName);
if (v1 === v2) // simple data type equal, or same object (including null)
return 0;
else
{
var result = this.doCompareOnValue(v1, v2, options);
//console.log('compare', propName, v1, v2, result);
return result;
}
},
/**
* Compare on two values, used for object comparison.
* Descendants may override this method.
* @param {Variant} v1
* @param {Variant} v2
* @param {Hash} options
* @returns {Int}
* @private
*/
doCompareOnValue: function(v1, v2, options)
{
return Kekule.ObjComparer._compareValue(v1, v2, options);
}
});
/**
* Class to hold a general data with unit in chemistry, such as temperature, weight, pressure and so on.
* @class
* @augments Kekule.ChemObject
* @param {id} id Id of this object.
* @param {String} name Name of scalar value.
* @param {Variant} value Value of scalar. Should be primary type (string, number).
* @param {String} unit Unit of scalar.
*
* @property {String} name Name of scalar value.
* @property {Variant} value Value of scalar. Should be primary type (string, number).
* @property {Variant} errorValue Error value of scalar. Should be the same type with value.
* @property {String} unit Unit of scalar.
* @property {String} title Informative title of scalar value.
*/
Kekule.Scalar = Class.create(Kekule.ChemObject,
/** @lends Kekule.Scalar# */
{
/** @private */
CLASS_NAME: 'Kekule.Scalar',
/** @private */
initialize: function($super, id, name, value, unit)
{
$super(id);
if (name)
this.setName(name);
if (typeof(value) !== 'undefined')
this.setValue(value);
if (unit)
this.setUnit(unit);
},
initProperties: function()
{
this.defineProp('name', {'dataType': DataType.STRING});
this.defineProp('value', {'dataType': DataType.PRIMARY});
this.defineProp('errorValue', {'dataType': DataType.PRIMARY});
this.defineProp('unit', {'dataType': DataType.STRING});
this.defineProp('title', {'dataType': DataType.STRING});
}
});
/**
* A list to hold a set of other {@link Kekule.ChemObject}.
* @class
* @augments Kekule.ChemObject
* @param {String} id Id of this object.
* @param {Class} itemBaseClass Base class of each item in list.
* If this value is null, any type of object can be inserted into list.
* @param {Bool} transparent If true, the parent object of children in list will not be the list itself,
* but relay to parent of list.
*
* @property {Class} itemBaseClass Base class of each item in list.
* If this value is null, any type of object can be inserted into list.
* @property {Array} items Items in list. All items owner is the same as the owner of list.
*/
Kekule.ChemObjList = Class.create(Kekule.ChemObject,
/** @lends Kekule.ChemObjList# */
{
/** @private */
CLASS_NAME: 'Kekule.ChemObjList',
/** @constructs */
initialize: function($super, id, itemBaseClass, transparent)
{
$super(id);
this.setPropStoreFieldValue('itemBaseClass', itemBaseClass);
this._transparent = !!transparent;
},
/** @private */
initProperties: function()
{
this.defineProp('itemBaseClass', {'dataType': DataType.CLASS, 'serializable': false, 'scope': Class.PropertyScope.PUBLIC});
this.defineProp('items', {
'dataType': DataType.ARRAY,
'scope': Class.PropertyScope.PUBLIC,
'setter': null,
'getter': function(canCreate)
{
var r = this.getPropStoreFieldValue('items');
if ((!r) && canCreate)
{
r = [];
this.setPropStoreFieldValue('items', r);
}
return r;
}
});
},
/** @private */
loaded: function($super)
{
// update parent and owner of children
this.ownerChanged(this.getOwner());
this.parentChanged(this.getParent());
},
/* @private */
/*
ownerChanged: function($super, newOwner)
{
this.changeAllItemsOwner();
$super(newOwner);
},
*/
/** @private */
parentChanged: function($super, newParent)
{
this.changeAllItemsParent();
$super(newParent);
},
/** @ignore */
isEmpty: function()
{
//var result = $super();
return result && (this.getItemCount() <= 0);
},
/** @private */
checkItemType: function(item)
{
var c = this.getItemBaseClass();
var r = c? item instanceof c: true;
if (!r)
Kekule.raise(Kekule.$L('ErrorMsg.LIST_ITEM_CLASS_MISMATCH')/*Kekule.ErrorMsg.LIST_ITEM_CLASS_MISMATCH*/);
return r;
},
/** @private */
notifyItemsChange: function()
{
this.notifyPropSet('items', this.getPropStoreFieldValue('items'));
},
/** @private */
getItemOwner: function()
{
return this.getOwner(); // || this;
},
/** @private */
changeAllItemsOwner: function()
{
var items = this.getItems();
if (items)
{
var owner = this.getItemOwner();
for (var i = 0, l = items.length; i < l; ++i)
{
var obj = items[i];
if (obj && obj.setOwner)
obj.setOwner(owner);
}
}
},
/** @private */
getItemParent: function()
{
return this._transparent? this.getParent() || this: this;
},
/** @private */
changeAllItemsParent: function()
{
var items = this.getItems();
if (items)
{
var parent = this.getItemParent();
for (var i = 0, l = items.length; i < l; ++i)
{
var obj = items[i];
if (obj && obj.setParent)
obj.setParent(parent);
}
}
},
/**
* Get count of child objects.
* @returns {Int}
*/
getItemCount: function()
{
var a = this.getPropStoreFieldValue('items');
return a? a.length: 0;
},
/**
* Get child object at index.
* @param {Int} index
* @returns {Variant}
*/
getItemAt: function(index)
{
var a = this.getPropStoreFieldValue('items');
return a? a[index]: null;
},
/**
* Get the index of obj in children array.
* @param {Variant} obj
* @returns {Int} Index of obj or -1 when not found.
*/
indexOf: function(obj)
{
var a = this.getPropStoreFieldValue('items');
return a? a.indexOf(obj): -1;
},
/**
* Returns next sibling object to childObj.
* @param {Object} childObj
* @returns {Object}
*/
getNextSiblingOfChild: function(childObj)
{
var index = this.indexOf(childObj);
return (index >= 0)? this.getItemAt(index + 1): null;
},
/** @ignore */
getChildCount: function()
{
return this.getItemCount();
},
/** @ignore */
getChildAt: function(index)
{
return this.getItemAt(index);
},
/** @ignore */
indexOfChild: function(obj)
{
return this.indexOf(obj);
},
/**
* Append obj to children array. If obj already inside, nothing will be done.
* @param {Object} obj
* @returns {Int} Index of obj after appending.
*/
append: function(obj)
{
this.checkItemType(obj);
if (obj)
{
var r = Kekule.ArrayUtils.pushUniqueEx(this.doGetItems(true), obj);
if (r.isPushed)
{
this.notifyItemsChange();
if (obj.setOwner)
obj.setOwner(this.getItemOwner());
if (obj.setParent)
obj.setParent(this.getItemParent());
}
return r.index;
}
else
return -1;
},
/**
* Insert obj to index of children array. If obj already inside, its position will be changed.
* @param {Object} obj
* @param {Object} index
* @return {Int} Index of obj after inserting.
*/
insert: function(obj, index)
{
this.checkItemType(obj);
if (obj)
{
var r = Kekule.ArrayUtils.insertUniqueEx(this.doGetItems(true), obj, index);
if (r.isInserted)
{
this.notifyItemsChange();
if (obj.setOwner)
obj.setOwner(this.getItemOwner());
if (obj.setParent)
obj.setParent(this.getItemParent());
}
return r.index;
}
else
return -1;
},
/**
* Insert obj before refChild in list. If refChild is null or does not exists, obj will be append to tail of list.
* @param {Object} obj
* @param {Object} refChild
* @return {Int} Index of obj after inserting.
*/
insertBefore: function(obj, refChild)
{
if (!refChild)
return this.append(obj);
else
{
var refIndex = this.indexOf(refChild);
return (refIndex >= 0)? this.insert(obj, refIndex): this.append(obj);
}
},
/**
* Remove a child at index.
* @param {Int} index
* @returns {Variant} Child object removed.
*/
removeAt: function(index)
{
if (this.getItems())
{
var r = Kekule.ArrayUtils.removeAt(this.getItems(), index);
if (r)
{
if (r.setOwner)
r.setOwner(null);
if (r.setParent)
r.setParent(null);
this.notifyItemsChange();
}
return r;
}
},
/**
* Remove a child at index.
* @param {Int} index
* @returns {Variant} Child object removed.
*/
removeChildAt: function(index)
{
return this.removeAt(index);
},
/**
* Remove obj from children array.
* @param {Variant} obj
* @returns {Variant} Child object removed.
*/
remove: function(obj)
{
if (this.getItems())
{
var r = Kekule.ArrayUtils.remove(this.getItems(), obj);
if (r)
{
if (r.setOwner)
r.setOwner(null);
if (r.setParent)
r.setParent(null);
this.notifyItemsChange();
}
return r;
}
},
/**
* Remove obj from children array. Same as method remove.
* @param {Variant} obj
* @returns {Variant} Child object removed.
*/
removeChild: function($super, obj)
{
return this.remove(obj) || $super(obj);
},
/**
* Return array contains all objects in list.
*/
toArray: function()
{
return this.getItems();
},
/**
* Calculate the box to contain the objects in list.
* Descendants may override this method.
* @param {Int} coordMode Determine to calculate 2D or 3D box. Value from {@link Kekule.CoordMode}.
* @param {Bool} allowCoordBorrow
* @returns {Hash} Box information. {x1, y1, z1, x2, y2, z2} (in 2D mode z1 and z2 will not be set).
*/
getContainerBox: function(coordMode, allowCoordBorrow)
{
var result;
var childObjs = this.toArray();
for (var i = 0, l = childObjs.length; i < l; ++i)
{
var obj = childObjs[i];
var box = obj.getContainerBox? obj.getContainerBox(coordMode, allowCoordBorrow): null;
if (box)
{
if (!result)
result = Kekule.BoxUtils.clone(box); //Object.extend({}, box);
else
result = Kekule.BoxUtils.getContainerBox(result, box);
}
}
return result;
}
});
/**
* An element to hold a list of other chem objects or elements in chem space.
* @class
* @augments Kekule.ChemObject
* @param {String} id Id of this element.
*
* @property {Kekule.ChemObjList} children Direct child objects in this space. The owner of children must all be this space.
*/
Kekule.ChemSpaceElement = Class.create(Kekule.ChemObject,
/** @lends Kekule.ChemSpaceElement# */
{
/** @private */
CLASS_NAME: 'Kekule.ChemSpaceElement',
/** @constructs */
initialize: function($super, id)
{
$super(id);
var list = new Kekule.ChemObjList(null, Kekule.ChemObject, true); // create transparent list
list.setParent(this);
list.addEventListener('change', function()
{
this.objectChange(['children']);
}, this);
this.setPropStoreFieldValue('children', list);
},
/** @private */
initProperties: function()
{
this.defineProp('children', {'dataType': 'Kekule.ChemObjList', 'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
var old = this.getChildren();
if (old)
{
old.finalize();
}
if (value)
{
value.setParent(this);
value.setOwner(this.getOwner());
}
this.setPropStoreFieldValue('children', value);
}
});
},
/** @ignore */
ownerChanged: function($super, newOwner)
{
// change owners of children
this.getChildren().setOwner(newOwner);
},
/** @private */
_removeChildObj: function(obj)
{
var index = this.indexOfChild(obj);
if (index >= 0)
{
this.removeChildAt(index);
}
},
/* @ignore */
/*
isEmpty: function($super)
{
var result = $super();
return result && (this.getChildCount() <= 0);
},
*/
/**
* Get count of child objects.
* @returns {Int}
*/
getChildCount: function()
{
return this.getChildren().getItemCount();
},
/**
* Get child object at index.
* @param {Int} index
* @returns {Variant}
*/
getChildAt: function(index)
{
return this.getChildren().getItemAt(index);
},
/**
* Get the index of obj in children list.
* @param {Variant} obj
* @returns {Int} Index of obj or -1 when not found.
*/
indexOfChild: function(obj)
{
return this.getChildren().indexOf(obj);
},
/**
* Returns next sibling object to childObj.
* @param {Object} childObj
* @returns {Object}
*/
getNextSiblingOfChild: function(childObj)
{
return this.getChildren().getNextSiblingOfChild(childObj);
},
/**
* Append obj to children list. If obj already inside, nothing will be done.
* @param {Object} obj
* @returns {Int} Index of obj after appending.
*/
appendChild: function(obj)
{
return this.getChildren().append(obj);
},
/**
* Insert obj to index of children list. If obj already inside, its position will be changed.
* @param {Object} obj
* @param {Object} index
* @return {Int} Index of obj after inserting.
*/
insertChild: function(obj, index)
{
return this.getChildren().insert(obj, index);
},
/**
* Insert obj before refChild in list. If refChild is null or does not exists, obj will be append to tail of list.
* @param {Object} obj
* @param {Object} refChild
* @return {Int} Index of obj after inserting.
*/
insertBefore: function(obj, refChild)
{
return this.getChildren().insertBefore(obj, refChild);
},
/**
* Remove a child at index.
* @param {Int} index
* @returns {Variant} Child object removed.
*/
removeChildAt: function(index)
{
return this.getChildren().removeChildAt(index);
},
/**
* Remove obj from children list.
* @param {Variant} obj
* @returns {Variant} Child object removed.
*/
removeChild: function($super, obj)
{
return this.getChildren().removeChild(obj) || $super(obj);
}
});
/**
* An object to hold a set of other {@link Kekule.ChemObject}.
* {@link Kekule.ChemObject#owner} property of each object the space should be set to this one.
* In one space, id of each object should be unique.
* @class
* @augments Kekule.ChemObject
* @param {String} id Id of this object.
*
* @property {Array} ownedObjs Objects owned by this space.
* @property {Kekule.ChemSpaceElement} root The root element of space.
* @property {Bool} enableAutoId When set to true, object inserted into space without id
* will be automatically assigned a id (usually a prefix plus a number, e.g. m12).
* @property {Hash} screenSize In px, size of space on screen, {x, y}.
* @property {Hash} size2D The 2D dimension (width, height) of glyph, {x, y}.
* @property {Hash} size3D The 3D dimension (width, height, depth) of glyph, {x, y, z}.
*/
Kekule.ChemSpace = Class.create(Kekule.ChemObject,
/** @lends Kekule.ChemSpace# */
{
/** @private */
CLASS_NAME: 'Kekule.ChemSpace',
/** @constructs */
initialize: function($super, id)
{
$super(id);
this._autoIdMap = {}; // private
this.setPropStoreFieldValue('enableAutoId', true);
this.setPropStoreFieldValue('ownedObjs', []);
var root = new Kekule.ChemSpaceElement();
this.setPropStoreFieldValue('root', root);
root.setParent(this);
root.setOwner(this);
},
/** @private */
initProperties: function()
{
this.defineProp('ownedObjs', {'dataType': DataType.ARRAY, 'serializable': false, 'setter': null, 'scope': Class.PropertyScope.PUBLIC});
this.defineProp('root', {'dataType': 'Kekule.ChemSpaceElement', 'setter': null});
this.defineProp('enableAutoId', {'dataType': DataType.BOOL});
this.defineProp('objScreenLengthRatio', {'dataType': DataType.FLOAT, 'serializable': false,
'scope': Class.PropertyScope.PRIVATE
}); // private
this.defineProp('screenSize', {'dataType': DataType.HASH,
'getter': function()
{
var o = this.getPropStoreFieldValue('screenSize') || {};
return {'x': o.x, 'y': o.y};
},
'setter': function(value)
{
var o = this.getPropStoreFieldValue('screenSize');
if (!o)
{
o = {};
this.setPropStoreFieldValue('screenSize', o);
}
o.x = value.x;
o.y = value.y;
}
});
},
/** @private */
loaded: function($super)
{
$super();
var root = this.getRoot();
if (root)
{
root.setParent(this);
root.setOwner(this);
}
},
/** @private */
notifyOwnedObjsChange: function()
{
this.notifyPropSet('ownedObjs', this.getPropStoreFieldValue('ownedObjs'));
},
/* @ignore */
/*
isEmpty: function($super)
{
var result = $super();
return result && (this.getChildCount() <= 0) && (this.getOwnedObjCount() <= 0);
},
*/
/**
* Returns all children of root.
* @returns {Array}
*/
getChildren: function()
{
return this.getRoot().getChildren().toArray();
},
/**
* Get count of child objects in root.
* @returns {Int}
*/
getChildCount: function()
{
return this.getRoot().getChildCount();
},
/**
* Get child object at index.
* @param {Int} index
* @returns {Variant}
*/
getChildAt: function(index)
{
return this.getRoot().getChildAt(index);
},
/**
* Get the index of obj in children list of root.
* @param {Variant} obj
* @returns {Int} Index of obj or -1 when not found.
*/
indexOfChild: function(obj)
{
return this.getRoot().indexOfChild(obj);
},
/**
* Returns next sibling object to childObj.
* @param {Object} childObj
* @returns {Object}
*/
getNextSiblingOfChild: function(childObj)
{
return this.getRoot().getNextSiblingOfChild(childObj);
},
/**
* Append obj to children list of root. If obj already inside, nothing will be done.
* @param {Object} obj
* @returns {Int} Index of obj after appending.
*/
appendChild: function(obj)
{
return this.getRoot().appendChild(obj);
},
/**
* Insert obj to index of children list of root. If obj already inside, its position will be changed.
* @param {Object} obj
* @param {Object} index
* @return {Int} Index of obj after inserting.
*/
insertChild: function(obj, index)
{
return this.getRoot().insertChild(obj, index);
},
/**
* Insert obj before refChild in list of root. If refChild is null or does not exists, obj will be append to tail of list.
* @param {Object} obj
* @param {Object} refChild
* @return {Int} Index of obj after inserting.
*/
insertBefore: function(obj, refChild)
{
return this.getRoot().insertBefore(obj, refChild);
},
/**
* Remove a child at index.
* @param {Int} index
* @returns {Variant} Child object removed.
*/
removeChildAt: function(index)
{
return this.getRoot().removeChildAt(index);
},
/**
* Remove obj from children list of root.
* @param {Variant} obj
* @returns {Variant} Child object removed.
*/
removeChild: function($super, obj)
{
return this.getRoot().removeChild(obj) || $super(obj);
},
/**
* Search ownedObjs and find the one matching id.
* @param {String} id
* @returns {Kekule.ChemObject} Object found or null.
*/
getObjById: function(id)
{
for (var i = 0, l = this.getOwnedObjCount(); i < l; ++i)
{
var obj = this.getOwnedObjAt(i);
if (obj.getId)
if (obj.getId() === id)
return obj;
}
return null;
},
/**
* Generate a unique id for obj.
* @param {Object} obj Can be null to generate a generic id.
* @param {Int} fromIndex Try from startIndex to get a prefix+index style id. Can be null.
* @returns {String} Unique id generated.
*/
getAutoId: function(obj, fromIndex)
{
var prefix;
if (obj)
prefix = obj.getAutoIdPrefix? obj.getAutoIdPrefix(): 'o';
else
prefix = 'o';
var prefix = obj.getAutoIdPrefix? obj.getAutoIdPrefix(): 'o';
var index;
if (Kekule.ObjUtils.notUnset(fromIndex))
index = fromIndex;
else
{
/*
index = this._autoIdMap[prefix] || 0;
++index;
*/
index = 1;
}
var index = this._getAutoIdIndex(prefix, index);
this._autoIdMap[prefix] = index;
return prefix + Number(index).toString();
},
/** @private */
_getAutoIdIndex: function(prefix, fromIndex)
{
var start = fromIndex || 0;
var i = start;
var id = prefix + Number(i).toString();
while (this.getObjById(id)) // repeat until no duplicated id
{
++i;
id = prefix + Number(i).toString();
}
return i;
},
/**
* Assign id automatically to a owned object.
* @param {Kekule.ChemObject} obj
* @private
*/
assignAutoIdToObj: function(obj)
{
if (obj.setId)
{
var id = this.getAutoId(obj);
obj.setId(id);
}
},
/**
* Return count of ownedObjs.
* @returns {Int}
*/
getOwnedObjCount: function()
{
return this.getOwnedObjs().length;
},
/**
* Get owned object at index.
* @param {Int} index
* @returns {Kekule.ChemObject}
*/
getOwnedObjAt: function(index)
{
return this.getOwnedObjs()[index];
},
/**
* Get index of obj in ownedObjs list.
* @param {Object} obj
* @returns {Int}
*/
indexOfOwnedObj: function(obj)
{
return this.getOwnedObjs().indexOf(obj);
},
// Append or remove methods should not be called directly on ChemSpace.
// You should change owner property of ChemObject instead.
/**
* Add a new obj to this space. If obj already inside, do nothing.
* @param {Kekule.ChemObject} obj
* @private
*/
_appendOwnedObj: function(obj)
{
if (this.indexOfOwnedObj(obj) >= 0) // already inside, do nothing
return;
else
{
if (this.getEnableAutoId())
{
if (obj.getId && !obj.getId())
this.assignAutoIdToObj(obj);
}
this.getOwnedObjs().push(obj);
this.notifyOwnedObjsChange();
}
},
/**
* Remove an ownedObj at index.
* @param {Int} index
* @private
*/
_removeOwnedObjAt: function(index)
{
var obj = this.getOwnedObjAt(index);
if (obj)
{
this.getOwnedObjs().splice(index, 1);
this.notifyOwnedObjsChange();
}
},
/**
* Remove obj from ownerObjs list/
* @param {Kekule.ChemObject} obj
* @private
*/
_removeOwnedObj: function(obj)
{
var index = this.getOwnedObjs().indexOf(obj);
if (index >= 0)
this._removeOwnedObjAt(index);
}
});
Kekule.ClassDefineUtils.addStandardSizeSupport(Kekule.ChemSpace);
/**
* An document to hold a set of other {@link Kekule.ChemObject}, like document in HTML or XML.
* {@link Kekule.ChemObject#owner} property of each object the space should be set to this one.
* In one document, id of each object should be unique.
* @class
* @augments Kekule.ChemSpace
* @param {String} id Id of this object.
*
* //@property {Kekule.ChemObject} docObj Root object of document, like documentElement in HTML or XML.
*/
Kekule.ChemDocument = Class.create(Kekule.ChemSpace,
/** @lends Kekule.ChemDocument# */
{
/** @private */
CLASS_NAME: 'Kekule.ChemDocument',
/** @private */
initProperties: function()
{
/*
this.defineProp('docObj', {
'dataType': 'Kekule.ChemObject',
'setter': function(value)
{
var old = this.getDocObj();
if (old)
old.setOwner(null);
if (value)
value.setOwner(this);
this.setPropStoreFieldValue('docObj', value);
}
});
*/
},
/** @ignore */
getAutoIdPrefix: function()
{
return 'd';
}
});
})();