kekule
Version:
Open source JavaScript toolkit for chemoinformatics
1,980 lines (1,949 loc) • 118 kB
JavaScript
/**
* @fileoverview
* Some common utility functions.
* @author Partridge Jiang
*/
/*
* requires core/kekule.common.js
* requires /localizations/
*/
/*
if (!this.Kekule)
{
Kekule = {};
}
*/
/**
* Return value of the first setted params.
* @function
*/
Kekule.oneOf = function()
{
var args = arguments;
var notUnset = Kekule.ObjUtils.notUnset;
for (var i = 0, l = args.length; i < l; ++i)
{
if (notUnset(args[i]))
return args[i];
}
return null;
};
/**
* A class to help to manipulate Kekule classes.
* @class
*/
Kekule.ClassUtils = {
/**
* Get name without Kekule prefix
* @param {String} className
* @returns {String}
*/
getShortClassName: function(className)
{
var p = className.indexOf('.');
if (p >= 0)
return className.substring(p + 1);
else
return className;
},
/**
* Returns last name part of className (e.g., 'ClassA' from 'Kekule.Scope.ClassA').
* @param {String} className
* @returns {String}
*/
getLastClassName: function(className)
{
var p = className.lastIndexOf('.');
return (p >= 0)? className.substring(p + 1): className;
},
/**
* Make class behavior like a singleton, add getInstance static method to it.
* @param {Object} classObj
* @returns {Object}
*/
makeSingleton: function(classObj)
{
return Object.extend(classObj, {
_instance: null,
getInstance: function(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)
{
if (!classObj._instance)
classObj._instance = new classObj(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10);
return classObj._instance;
}
});
}
};
/**
* Util methods about number.
* @class
*/
Kekule.NumUtils = {
/**
* Check if a value is a normal number (not NaN).
* @param {Variant} value
* @returns {Bool}
*/
isNormalNumber: function(value)
{
return (typeof(value) === 'number') && !isNaN(value);
},
/**
* Check if a number is integer.
* @param {Number} num
* @returns {Bool}
*/
isInteger: function(num)
{
return (typeof(num) === 'number') && (parseInt(num, 10) === parseFloat(num));
},
/**
* Output a string with decimalsLength.
* Not the same to Number.toFixed, this method will not pad with zero.
* For example, call toDecimals(5.3456, 2) will return '5.35',
* but call toDecimals(5.1, 2) will simply return '5.1',
* @param {Number} num
* @param {Int} decimalsLength
* @returns {String}
*/
toDecimals: function(num, decimalsLength)
{
if (Kekule.NumUtils.isInteger(num))
return num.toString();
var times = Math.pow(10, decimalsLength);
var n = Math.round(num * times);
n = n / times;
var i = parseInt(n);
var f = n - i;
var sf = f.toString();
if (sf.length > decimalsLength + 2)
{
sf = sf.substr(2, decimalsLength);
return i.toString() + '.' + sf;
}
else
return n.toString();
},
/**
* Output a string with precision length.
* Not the same to Number.toPrecision, this method will not pad with zero to the right.
* For example, call toPrecision(5.3456, 2) will return '5.35',
* but call toPrecision(5.1, 2) will simply return '5.1'.
* @param {Number} num
* @param {Int} precision
* @param {Bool} doNotPadZeroOnRight
* @param {Bool} preserveIntPart If true, when the digits of number int part larger than precision length, all of them will be preserved.
* @returns {String}
*/
toPrecision: function(num, precision, doNotPadZeroOnRight, preserveIntPart)
{
var sign = Math.sign(num);
var n = Math.abs(num);
var decimalPointPos = Kekule.NumUtils.getDecimalPointPos(n);
var precisionPos = decimalPointPos + 1 - precision;
var result;
if (precisionPos >= 0)
result = preserveIntPart? Math.round(n): n.toPrecision(precision);
else
{
var v = Math.round(n * Math.pow(10, -precisionPos));
var s = v.toString();
var strLength = s.length;
var dpos = Math.abs(precisionPos);
if (strLength === dpos)
result = '0.' + s;
else if (strLength > dpos)
result = s.substr(0, strLength - dpos) + '.' + s.substr(strLength - dpos);
else
result = 0.0.toFixed(dpos - strLength) + s;
if (doNotPadZeroOnRight)
{
var removeCount = 0;
var c = result.charAt(result.length - 1 - removeCount);
while (c === '0' || c === '.')
{
++removeCount;
if (c === '.')
break;
else
c = result.charAt(result.length - 1 - removeCount);
}
result = result.substr(0, result.length - removeCount);
}
}
if (sign < 0)
result = '-' + result;
return result;
},
/**
* Returns the position of decimal point to the first digit of number.
* E,g. getDecimalPointPos(224.22) === 2, getDecimalPointPos(0.0022422) = -3, getDecimalPointPos(0) = 0,
* @param {Number} num
* @returns {Int}
*/
getDecimalPointPos: function(num)
{
if (Kekule.NumUtils.isFloatEqual(num, 0))
return 0;
var n = Math.abs(num);
var ratio = (n > 1)? 10: 0.1;
var delta = (n > 1)? 1: -1;
var compValue = 1;
var result = 0;
while ((n > 1 && compValue <= n) || (n < 1 && compValue > n))
{
compValue *= ratio;
result += delta;
}
return (n > 1)? (result - 1): result;
},
/**
* Returns the heading digit of a number.
* For example, it returns 3 for number 35.12, returns 1 for number 0.123.
* @param {String} num
*/
getHeadingDigit: function(num)
{
if (Kekule.NumUtils.isFloatEqual(num, 0))
return 0;
var n = Math.abs(num);
var decimalPointPos = Kekule.NumUtils.getDecimalPointPos(n);
var c = Math.pow(10, decimalPointPos);
return Math.floor(n / c);
},
/**
* Check if f1 and f2 are equal.
* Since float can not be stored exactly in computer, when abs(f1-f2) <= threshold,
* this function will returns true.
* @param {Float} f1
* @param {Float} f2
* @param {Float} threshold If not set, a default value will be used.
* @returns {Bool}
*/
isFloatEqual: function(f1, f2, threshold)
{
if (Kekule.ObjUtils.isUnset(threshold)) // auto threshold
threshold = Math.min(Math.abs(f1), Math.abs(f2)) * 1e-15; //threshold = 1e-100;
return Math.abs(f1 - f2) <= threshold;
},
/**
* Check if f1 is equal, less or greater to f2.
* Since float can not be stored exactly in computer, when abs(f1-f2) <= threshold,
* this function will returns 0.
* @param {Float} f1
* @param {Float} f2
* @param {Float} threshold If not set, a default value will be used.
* @returns {Int}
*/
compareFloat: function(f1, f2, threshold)
{
return Kekule.NumUtils.isFloatEqual(f1, f2, threshold)? 0:
(f1 < f2)? -1: 1;
},
/**
* Returns a primes array from 2 to max number.
* @param {Int} max
* @returns {Array}
*/
getPrimes: function(max)
{
var sieve = [], i, j, primes = [];
for (i = 2; i <= max; ++i) {
if (!sieve[i]) {
// i has not been marked -- it is prime
primes.push(i);
for (j = i << 1; j <= max; j += i) {
sieve[j] = true;
}
}
}
return primes;
}
};
/**
* Util methods about object and JSON.
* @class
*/
Kekule.ObjUtils = {
/**
* Returns prototype of obj.
* @param {Object} obj
*/
getPrototypeOf: function(obj)
{
return Object.getPrototypeOf? Object.getPrototypeOf(obj): (obj.prototype || obj.__proto__);
},
/**
* Returns the property descriptor of propName in obj (and its prototypes if param checkPrototype is true).
* @param {Object} obj
* @param {String} propName
* @param {Bool} checkPrototype
* @returns {Hash}
*/
getPropertyDescriptor: function(obj, propName, checkPrototype)
{
var getOwn = Object.getOwnPropertyDescriptor;
if (getOwn)
{
var result = getOwn(obj, propName);
if (!result && checkPrototype && (obj.prototype || obj.__proto__))
result = Kekule.ObjUtils.getPropertyDescriptor(obj.prototype || obj.__proto__, propName, checkPrototype);
return result;
}
else
return null;
},
/**
* Return all name of direct fields of obj. Note that functions will not be included.
* @param {Object} obj
* @param {Bool} includeFuncFields Set to true to include function fields in obj.
* @returns {Array} Array of field names
*/
getOwnedFieldNames: function(obj, includeFuncFields)
{
var result = [];
for (var fname in obj)
{
if (obj.hasOwnProperty(fname) && (includeFuncFields || typeof(obj[fname]) != 'function'))
result.push(fname);
}
return result;
},
/**
* Delete obj[oldName] and add a newName prop with the same value.
* Note that only own property can be replaced.
* @param {Object} obj
* @param {String} oldName
* @param {String} newName
* @returns {Object} obj itself.
*/
replacePropName: function(obj, oldName, newName)
{
if (obj[oldName] !== undefined)
{
if (obj.hasOwnProperty(oldName))
{
obj[newName] = obj[oldName];
delete obj[oldName];
}
else
Kekule.raise(/*Kekule.ErrorMsg.NON_OWN_PROPERTY_CANNOT_BE_REPLACED*/Kekule.$L('ErrorMsg.NON_OWN_PROPERTY_CANNOT_BE_REPLACED'));
}
return obj;
},
/**
* Check if a value is null or undefined.
* @param {Variant} value
* @returns {Bool}
*/
isUnset: function(value)
{
return (value === null) || (typeof(value) == 'undefined');
},
/**
* Check if a value is not null and undefined.
* @param {Variant} value
* @returns {Bool}
*/
notUnset: function(value)
{
return (value !== null) && (typeof(value) !== 'undefined');
},
/**
* Check if src and dest has the same fields and has the same values.
* @param {Object} src
* @param {Object} dest
* @param {Array} excludingFields These fields will not be compared.
* @returns {Bool}
*/
equal: function(src, dest, excludingFields)
{
var checkedFields = [];
for (var fname in src)
{
if (excludingFields && (excludingFields.indexOf(fname) >= 0))
continue;
if (src[fname] !== dest[fname])
return false;
checkedFields.push(fname);
}
// check if there are additional fields in dest, if true, two objects are not same
for (var fname in dest)
{
if (excludingFields && (excludingFields.indexOf(fname) >= 0))
continue;
if (checkedFields.indexOf(fname) < 0) // has unchecked field in dest object
return false;
}
return true;
},
/**
* Check if fields in condition has the same value with src.
* @param {Variant} src Object or ObjectEx instance.
* @param {Hash} condition
* @return {Bool}
*/
match: function(src, condition)
{
var isObjectEx = (src instanceof ObjectEx);
for (var fname in condition)
{
if (condition.hasOwnProperty(fname))
{
var oldValue = isObjectEx? src.getPropValue(fname): src[fname];
if (oldValue !== condition[fname])
return false;
}
}
return true;
},
/**
* Check if obj is instance of one class of classes array.
* @param {Object} obj
* @param {Array} classes
* @return {Bool}
*/
isInstanceOf: function(obj, classes)
{
if (!DataType.isArrayValue(classes)) // a simple class
return (obj instanceof classes);
else // array
{
for (var i = 0, l = classes.length; i < l; ++i)
{
if (obj instanceof classes[i])
return true;
}
return false;
}
},
/**
* Returns an array of cascade names that matches all leaf field of obj.
* For example, call on object {a: {b: 1, c: 2}, b: 2} will returns ['a.b', 'a.c', 'b'];
* @param {Object} obj
* @param {Bool} includeFuncFields
* @returns {Array}
*/
getLeafFieldCascadeNames: function(obj, includeFuncFields)
{
var fillCascadeNames = function(names, prefix, obj, includeFuncFields)
{
for (var fname in obj)
{
if (obj.hasOwnProperty(fname) && (includeFuncFields || typeof(obj[fname]) != 'function'))
{
var value = obj[fname];
if (typeof(value) === 'object')
{
fillCascadeNames(names, prefix + fname + '.', value, includeFuncFields);
}
else
names.push(prefix + fname);
}
}
};
var result = [];
fillCascadeNames(result, '', obj, includeFuncFields);
return result;
}
};
/**
* Util methods about Array.
* @class
*/
Kekule.ArrayUtils = {
/**
* Check if value is an array.
* @param {Variant} value
* @returns {Bool}
*/
isArray: function(value)
{
if (value)
return ((typeof(value) == 'object') && (value.length !== undefined));
else
return false;
},
/**
* If value is array, returns value directly, otherwise returns a array containing value.
*/
toArray: function(value)
{
if (Kekule.ArrayUtils.isArray(value))
return value;
else
return [value];
},
/**
* Returns a new array with the same items as src.
* @param {Array} src
* @returns {Array}
*/
clone: function(src)
{
return src.slice(0);
},
/**
* creates a new array with all elements that pass the test implemented by the provided function.
* @param {Array} src
* @param {Func} filterFunc
* @param {Object} thisArg
*/
filter: function(src, filterFunc, thisArg)
{
if (!filterFunc)
return Kekule.ArrayUtils.clone(src);
if (src.filter) // built-in support
return src.filter(filterFunc, thisArg);
else
{
var result = [];
for (var i = 0, l = src.length; i < l; ++i)
{
var item = src[i];
if (filterFunc.apply(thisArg, [item, i, src]))
result.push(item);
}
return result;
}
},
/**
* Divide array into several small ones, each containing memberCount numbers of elements.
* @param {Array} src
* @param {Int} memberCount
* @returns {Array} Array of array.
*/
divide: function(src, memberCount)
{
var result = [];
var curr = [];
var offset = 0;
for (var i = 0, l = src.length; i < l; ++i)
{
curr.push(src[i]);
++offset;
if (offset >= memberCount)
{
result.push(curr);
curr = [];
offset = 0;
}
}
if (curr.length)
result.push(curr);
return result;
},
/**
* Append obj (or an array of obj) to the tail of array and returns the index of newly pushed obj.
* If obj already inside array, also returns index of obj in array.
* @param {Array} targetArray Target array.
* @param {Variant} obj Must not be null.
* @param {Bool} reserveNestedArray
* @return {Int} Index of obj in array.
*/
pushUnique: function(targetArray, obj, reserveNestedArray)
{
var r = Kekule.ArrayUtils.pushUniqueEx(targetArray, obj, reserveNestedArray);
return r? r.index: null;
},
/**
* Append obj (or an array of obj) to the tail of array and returns the a hash of {index(Int), isPushed(Bool)}.
* If obj already inside array, returns index of obj and false.
* @param {Array} targetArray Target array.
* @param {Variant} obj Must not be null.
* @param {Bool} reserveNestedArray
* @return {Hash} {index, isPushed} hash. Index of obj in array.
*/
pushUniqueEx: function(targetArray, obj, reserveNestedArray)
{
if (DataType.isArrayValue(obj))
{
var r;
var pushFunc = (!reserveNestedArray)? Kekule.ArrayUtils.pushUniqueEx: Kekule.ArrayUtils._pushUniqueItemEx;
for (var i = 0, l = obj.length; i < l; ++i)
{
if (Kekule.ObjUtils.isUnset(r))
r = pushFunc(targetArray, obj[i], reserveNestedArray);
else
pushFunc(targetArray, obj[i], reserveNestedArray);
}
return r;
}
else
{
return Kekule.ArrayUtils._pushUniqueItemEx(targetArray, obj);
/*
if (!obj)
return {'index': -1, 'isPushed': false};
var index = targetArray.indexOf(obj);
if (index < 0) // obj not in array, push
{
return {'index': targetArray.push(obj), 'isPushed': true};
}
else // already inside, return -index of obj
return {'index': index, 'isPushed': false};
*/
}
},
/** @private */
_pushUniqueItemEx: function(targetArray, item)
{
if (!item)
return {'index': -1, 'isPushed': false};
var index = targetArray.indexOf(item);
if (index < 0) // obj not in array, push
{
return {'index': targetArray.push(item), 'isPushed': true};
}
else // already inside, return -index of obj
return {'index': index, 'isPushed': false};
},
/**
* Insert obj to index of array and returns the index of newly inserted obj.
* If obj already inside array, position of obj will be changed.
* @param {Array} targetArray Target array.
* @param {Variant} obj Must not be null.
* @return {Int} Index of obj in array.
*/
insertUnique: function(targetArray, obj, index)
{
return Kekule.ArrayUtils.insertUniqueEx(targetArray, obj, index).index;
},
/**
* Insert obj to index of array and returns the a hash of {index(Int), isInserted(Bool)}.
* If obj already inside array, position of obj will be changed.
* @param {Array} targetArray Target array.
* @param {Variant} obj Must not be null.
* @param {Int} index Index to insert. If not set or less than 0, obj will be pushed to tail of array.
* @return {Hash} {index, isPushed} hash. Index of obj in array.
*/
insertUniqueEx: function(targetArray, obj, index)
{
if (!obj)
return {'index': -1, 'isInserted': false};
if (Kekule.ObjUtils.isUnset(index) || (index < 0))
index = targetArray.length;
var i = targetArray.indexOf(obj);
if (i < 0) // obj not in array, insert
{
targetArray.splice(index, 0, obj);
return {'index': index, 'isInserted': true};
}
else if (i != index) // already inside, change position
{
targetArray.splice(i, 1);
targetArray.splice(index, 0, obj);
return {'index': index, 'isInserted': false};
}
},
/**
* Remove item at index from targetArray.
* If success, returns removed item. If index not in array, returns null.
* @param {Array} targetArray
* @param {Int} index
* @returns {Object} Object removed or null.
*/
removeAt: function(targetArray, index)
{
var obj = targetArray[index];
if (typeof(obj) != 'undefined')
{
targetArray.splice(index, 1);
return obj;
}
else
return null;
},
/**
* Remove an obj from targetArray.
* If success, returns obj. If obj not in array, returns null.
* @param {Array} targetArray
* @param {Object} obj
* @param {Bool} removeAll Whether all appearance of obj in array should be removed.
* @returns {Object} Object removed or null.
*/
remove: function(targetArray, obj, removeAll)
{
var index = targetArray.indexOf(obj);
if (index >= 0)
{
Kekule.ArrayUtils.removeAt(targetArray, index);
if (removeAll)
Kekule.ArrayUtils.remove(targetArray, obj, removeAll);
return obj;
}
else
return null;
},
/**
* Replace oldObj in array with newObj.
* @param {Array} targetArray
* @param {Variant} oldObj
* @param {Variant} newObj
* @param {Bool} replaceAll
* @returns {Variant} Object replaced or null.
*/
replace: function(targetArray, oldObj, newObj, replaceAll)
{
var index = targetArray.indexOf(oldObj);
if (index >= 0)
{
targetArray[index] = newObj;
if (replaceAll)
Kekule.ArrayUtils.replace(targetArray, oldObj, newObj, replaceAll);
return oldObj;
}
else
return null;
},
/**
* Change item at oldIndex to a new position.
* @param {Array} targetArray
* @param {Int} oldIndex
* @param {Int} newIndex
* @returns {Variant} item moved or null when oldIndex not in array.
*/
changeIndex: function(targetArray, oldIndex, newIndex)
{
if ((oldIndex >= 0) && (oldIndex <= targetArray.length))
{
var obj = targetArray[oldIndex];
targetArray.splice(oldIndex, 1);
targetArray.splice(newIndex, 0, obj);
return obj;
}
else
return null;
},
/**
* Change item in array to a new position.
* @param {Array} targetArray
* @param {Variant} item
* @param {Int} newIndex
* @returns {Variant} item or null when item not in array.
*/
changeItemIndex: function(targetArray, item, newIndex)
{
var index = targetArray.indexOf(item);
return Kekule.ArrayUtils.changeIndex(targetArray, index, newIndex);
},
/**
* Remove duplicated elements in array.
* For example, [1, 2, 3, 3] will got return value [1, 2, 3]
* @param {Array} a
* @returns {Array}
*/
toUnique: function(a)
{
var b = [];
for (var i = 0, l = a.length; i < l; ++i)
{
var e = a[i];
if (b.indexOf(e) < 0)
b.push(e);
}
return b;
},
/**
* Count elements in array.
* For example, [1, 2, 3, 3] will got return value 3
* @param {Array} a
* @returns {Int}
*/
getUniqueElemCount: function(a)
{
var b = Kekule.ArrayUtils.toUnique(a);
return b.length;
},
/**
* Returns a reversed order array.
* For example, [1,2,3] will be turned to [3,2,1] after reversing.
* @param {Array} a
* @returns {Array}
*/
reverse: function(a)
{
var result = [];
for (var i = 0, l = a.length; i < l; ++i)
{
if (Kekule.ObjUtils.notUnset(a[i]))
{
result[l - i - 1] = a[i];
}
}
return result;
},
/**
* Returns a new array that change the order of items in src array.
* @param {Array} src
*/
randomize: function(src)
{
if (!src)
return null;
if (!src.length)
return [];
var result = [];
var remaining = src.slice(0);
while (remaining.length > 1)
{
var index = Math.round(Math.random() * (remaining.length - 1));
result.push(remaining.splice(index, 1)[0]);
}
result.push(remaining[0]);
return result;
},
/**
* Subtract excluded items from source array.
* @param {Array} src
* @param {Array} excludes
* @returns {Array}
*/
exclude: function(src, excludes)
{
var result = [];
var exs = Kekule.ArrayUtils.toArray(excludes);
for (var i = 0, l = src.length; i < l; ++i)
{
var item = src[i];
if (exs.indexOf(item) < 0)
result.push(item);
}
return result;
},
/**
* Returns intersection of two arrays.
* @param {Array} a1
* @param {Array} a2
* @returns {Array}
*/
intersect: function(a1, a2)
{
var result = [];
for (var i = 0, l = a1.length; i < l; ++i)
{
if (a2.indexOf(a1[i]) >= 0)
result.push(a1[i]);
}
return result;
},
/**
* Convert a nested array to one-dimension array.
* @param {Array} src
* @returns {Array}
*/
flatten: function(src)
{
var result = [];
for (var i = 0, l = src.length; i < l; ++i)
{
var child = src[i];
if (Kekule.ArrayUtils.isArray(child))
{
var flattened = Kekule.ArrayUtils.flatten(child);
result = result.concat(flattened);
}
else
result.push(child);
}
return result;
},
/**
* Compare two arrays, from first to last items. If two items in each array is different,
* the one with the smaller item will be regarded as smaller array.
* @param {Array} a1
* @param {Array} a2
* @param {Function} itemCompareFunc
*/
compare: function(a1, a2, itemCompareFunc)
{
if (!itemCompareFunc)
itemCompareFunc = function(i1, i2)
{
return (i1 > i2)? 1:
(i1 < i2)? -1:
0;
}
var l = Math.min(a1.length, a2.length)
for (var i = 0; i < l; ++i)
{
var item1 = a1[i];
var item2 = a2[i];
var compareResult = itemCompareFunc(item1, item2);
if (compareResult !== 0)
return compareResult;
}
// all same in previous items
if (a1.length > l)
return 1;
else if (a2.length > l)
return -1;
else
return 0;
},
/**
* Compare two arrays. The array can be nested one and the nested children will also be compared.
* For instance:
* [3,2,1] > [2,3,1]
* [[2,3], 1] > [[1,2,3]]
* [[1,2],3,1] > [[1,2],3]
* @param {Array} a1
* @param {Array} a2
* @param {Func} compareFunc
* @returns {Int}
*/
compareNestedArray: function(a1, a2, compareFunc)
{
var l = Math.min(a1.length, a2.length);
if (!compareFunc)
compareFunc = function(i,j) { return i - j; };
for (var i = 0; i < l; ++i)
{
var compareValue = 0;
var item1 = a1[i];
var item2 = a2[i];
var isArray1 = DataType.isArrayValue(item1);
var isArray2 = DataType.isArrayValue(item2);
if (isArray1 && isArray2)
compareValue = Kekule.ArrayUtils.compareNestedArray(item1, item2, compareFunc);
else if (isArray1) // item2 is not array, we assum item1 > item2
compareValue = 1;
else if (isArray2)
compareValue = -1;
else // all not array
compareValue = compareFunc(item1, item2);
if (compareValue != 0)
return compareValue;
}
// still can not get result, check rest items
if (a1.length > l)
return 1;
else if (a2.length > l)
return -1;
else
return 0;
},
/**
* Compare all items in array and sort them into a new array.
* If compare result is 0 (equal), those items will be "grouped up" in a nested array.
* For example, var a = [1, 0, 1, 2, 3], the result of this method on a will be
* [0, [1, 1], 2, 3].
* @param {Array} arr
* @param {Func} compareFunc
* @returns {Array}
*/
group: function(arr, compareFunc)
{
if (!compareFunc)
compareFunc = function(a, b)
{
return (a < b)? -1:
(a > b)? 1:
0;
};
var sortedArray = Kekule.ArrayUtils.clone(arr);
sortedArray.sort(compareFunc);
var result = [];
var lastCompareItem;
for (var i = 0, l = sortedArray.length; i < l; ++i)
{
var item = sortedArray[i];
if (lastCompareItem && compareFunc(item, lastCompareItem) === 0)
{
var lastResultItem = result.length? result[result.length - 1]: null;
if (!Kekule.ArrayUtils.isArray(lastResultItem))
{
result.pop();
lastResultItem = [lastCompareItem];
result.push(lastResultItem);
}
lastResultItem.push(item);
}
else
result.push(item);
lastCompareItem = item;
}
return result;
},
/**
* Returns the index stack of elem in a nested array.
* For example, getIndexStack(2, [1, [1, 2], 3]) returns [1,1]; getIndexStack(3, [1, [1, 2], 3]) returns [2].
* If elem is not found in arr, null will be returned.
* @param {Variant} elem
* @param {Array} arr
* @returns {Array}
*/
indexStackOfElem: function(elem, arr)
{
for (var i = 0, l = arr.length; i < l; ++i)
{
var curr = arr[i];
if (curr === elem)
return [i];
else if (Kekule.ArrayUtils.isArray(curr))
{
var childResult = Kekule.ArrayUtils.indexStackOfElem(elem, curr);
if (childResult)
{
childResult.unshift(i);
return childResult;
}
}
}
return null;
},
/**
* Returns an element by indexStack from a nested array.
* @param {Array} arr
* @param {Array} indexStack
* @returns {Variant}
*/
getElemByIndexStack: function(arr, indexStack)
{
var stack = Kekule.ArrayUtils.isArray(indexStack)? Kekule.ArrayUtils.clone(indexStack): [indexStack];
var index = stack.shift();
var curr = arr[index];
if (curr)
{
if (stack.length)
{
if (Kekule.ArrayUtils.isArray(curr))
return Kekule.ArrayUtils.getElemByIndexStack(curr, stack);
else
return null;
}
else
return curr;
}
else
return null;
},
/**
* Returns median number of a numberic array.
* @param {Array} arr
* @returns {Number}
*/
getMedian: function(arr)
{
var a = [].concat(arr);
var l = a.length;
if (l === 0)
return null;
if (l <= 1)
return a[0];
else
{
// sort lengths to find the median one
a.sort(function(a, b) { return a - b;} );
return (l % 2)? a[(l + 1) >> 1]: (a[l >> 1] + a[(l >> 1) - 1]) / 2;
}
},
sortHashArray: function(arr, sortFields)
{
var PREFIX_SORT_DESC = '!';
var sortFieldInfos = [];
for (var i = 0, l = sortFields.length; i < l; ++i)
{
var info = {};
var field = sortFields[i] || '';
if (field.startsWith(PREFIX_SORT_DESC)) // sort desc
{
info.field = field.substr(1);
info.desc = true;
}
else
{
info.field = field;
info.desc = false;
}
sortFieldInfos.push(info);
}
var sortFunc = function(hash1, hash2)
{
var compareValue = 0;
for (var i = 0, l = sortFieldInfos.length; i < l; ++i)
{
var field = sortFieldInfos[i].field;
var v1 = hash1[field] || '';
var v2 = hash2[field] || '';
compareValue = (v1 > v2)? 1:
(v1 < v2)? -1: 0;
if (sortFieldInfos[i].desc)
compareValue = -compareValue;
if (compareValue !== 0)
break;
}
return compareValue;
};
arr.sort(sortFunc);
return arr;
}
};
/**
* A class with some help methods to manipulate string.
* @class
*/
Kekule.StrUtils = {
/** @private */
STR_TRUES: ['true', 'yes', 't', 'y'],
/** @private */
STR_FALSES: ['false', 'no', 'f', 'n'],
/**
* Trim leading and trailing space, tabs or line-breaks of string
* @param {String} str
* @returns {String}
*/
trim: function(str)
{
return str.replace(/^\s*|\s*$/g, "");
},
/**
* Replace repeated spaces, newlines and tabs with a single space
* @param {String} str
* @returns {String}
*/
normalizeSpace: function(str)
{
return str.replace(/^\s*|\s(?=\s)|\s*$/g, "");
},
/**
* Convert a simple value to string.
* @param {Variant} value
* @returns {String}
*/
convertToStr: function(value)
{
return '' + value;
},
/**
* Convert str to a specified typed value.
* @param {String} str
* @param {String} valueType A simple type, data from {@link DataType}.
* @returns {Variant}
*/
convertToType: function(str, valueType)
{
if (typeof(str) != 'string') // input type not string, return directly
return str;
switch (valueType)
{
case DataType.FLOAT:
case DataType.NUMBER:
return parseFloat(str); break;
case DataType.INT:
return parseInt(str); break;
case DataType.BOOL:
return Kekule.StrUtils.strToBool(str); break;
default:
return str;
}
},
/**
* Turn a boolean to a string value, use Kekule.StrUtils.STR_TRUES constants.
* @param {Bool} value
* @returns {String}
*/
boolToStr: function(value)
{
if (value)
return Kekule.StrUtils.STR_TRUES[0];
else
return Kekule.StrUtils.STR_FALSES[0];
},
/**
* Convert a string to boolean value.
* @param {String} value
* @returns {Bool}
*/
strToBool: function(value)
{
var v = value.toLowerCase();
for (var i = 0, l = Kekule.StrUtils.STR_FALSES.length; i < l; ++i)
{
if (v == Kekule.StrUtils.STR_FALSES[i])
return false;
}
return !!value;
},
/**
* If str start with leading and end with tailing, remove both of them.
* @param {String} str
* @param {String} leading
* @param {String} tailing
*/
removeAroundPair: function(str, leading, tailing)
{
if (str.startsWith(leading) && str.endsWith(tailing))
{
return str.substring(leading.length, str.length - leading.length - tailing.length + 1);
}
else // not pair
{
return str;
}
},
/**
* Remove leading and tailing ' or " of str.
* @param {String} str
* @returns {String}
*/
unquote: function(str)
{
var remove = Kekule.StrUtils.removeAroundPair;
var result = str;
result = remove(result, '\'', '\'');
result = remove(result, '"', '"');
return result;
},
/**
* Split string into tokens with separator. If separator is not provided, space will be used.
* Space around tokens will be emitted too.
* @param {String} str
* @param {String} separator
* @returns {Array}
*/
splitTokens: function(str, separator)
{
if (!str)
return [];
if (DataType.isArrayValue(str))
return str;
else // assume is string
{
var reg = separator? new RegExp(separator, 'g'): /\s+/g;
//return str.replace(reg, ' ').split(' ');
return str.split(reg);
}
},
/**
* Check if token already inside str.
* @param str
* @param token
* @param separator
*/
hasToken: function(str, token, separator)
{
var tokens = Kekule.StrUtils.splitTokens(str, separator);
return (tokens.indexOf(token) >= 0);
},
/*
* Add token(s) to str. Tokens can be a string or separator split string or array.
* If separator is not provided, space will be used.
* If token already inside str, nothing will be done.
* @param {String} str
* @param {String} token A single token
* @param {String} separator
*/
/*
addToken: function(str, token, separator)
{
var result;
if (!Kekule.StrUtils.hasToken(str, token, separator))
result = str + (separator || ' ') + token;
else
result = str;
return result;
},
*/
/**
* Add token(s) to str. Tokens can be a string or separator split string or array.
* If separator is not provided, space will be used.
* @param {String} str
* @param {Variant} tokens
* @param {String} separator
*/
addTokens: function(str, tokens, separator)
{
var ts = Kekule.StrUtils.splitTokens(str, separator);
var adds = Kekule.StrUtils.splitTokens(tokens, separator);
for (var i = 0, l = adds.length; i < l; ++i)
{
if (ts.indexOf(adds[i]) < 0)
ts.push(adds[i]);
}
return ts.join(separator || ' ');
},
/**
* Remove token(s) to str. Tokens can be a string or separator split string or array.
* If separator is not provided, space will be used.
* @param {String} str
* @param {Variant} tokens
* @param {String} separator
*/
removeTokens: function(str, tokens, separator)
{
var ts = Kekule.StrUtils.splitTokens(str, separator);
var removes = Kekule.StrUtils.splitTokens(tokens, separator);
for (var i = 0, l = removes.length; i < l; ++i)
{
var index = ts.indexOf(removes[i]);
if (index >= 0)
ts.splice(index, 1);
}
return ts.join(separator || ' ');
},
/**
* Token token(s) in str. Tokens can be a string or separator split string or array.
* If separator is not provided, space will be used.
* @param {String} str
* @param {Variant} tokens
* @param {String} separator
*/
toggleTokens: function(str, tokens, separator)
{
var ts = Kekule.StrUtils.splitTokens(str, separator);
var modifies = Kekule.StrUtils.splitTokens(tokens, separator);
var added = [];
for (var i = 0, l = modifies.length; i < l; ++i)
{
var index = ts.indexOf(modifies[i]);
if (index >= 0)
ts.splice(index, 1);
else
added.push(modifies[i]);
}
ts = ts.concat(added);
return ts.join(separator || ' ');
},
/**
* Returns the total line count of string.
* @param {String} str
* @param {String} lineDelimiter Default is "\n".
* @returns {Int}
*/
getLineCount: function(str, lineDelimiter)
{
if (!lineDelimiter)
lineDelimiter = '\n';
var lines = str.split(lineDelimiter);
return lines.length;
},
/**
* Returns maxium char count in each line of str.
* @param {String} str
* @param {String} lineDelimiter Default is "\n".
* @returns {Int}
*/
getMaxLineCharCount: function(str, lineDelimiter)
{
if (!lineDelimiter)
lineDelimiter = '\n';
var lines = str.split(lineDelimiter);
var result = 0;
for (var i = 0, l = lines.length; i < l; ++i)
{
var line = lines[i];
result = Math.max(line.length, result);
}
return result;
},
/**
* Check if str is in number format.
* @param {String} str
* @returns {Bool}
*/
isNumbericStr: function(str)
{
var a = Number(str);
return !isNaN(a);
},
/**
* Split a number ending string (e.g. 'str3') to two part, a prefix and an index.
* If the str is not ending with number, null will be returned.
* @param {String} str
* @returns {Object} A object of {prefix, index}
*/
splitIndexEndingStr: function(str)
{
var pos = str.length - 1;
var c = str.charAt(pos);
var indexStr = '';
while (c && Kekule.StrUtils.isNumbericStr(c))
{
--pos;
indexStr = c + indexStr;
c = str.charAt(pos);
}
return indexStr? {'prefix': str.substring(0, pos + 1), 'index': parseInt(indexStr)}: null;
}
};
/**
* Util methods to manipulate {role, item} hash.
* @class
*/
Kekule.RoleMapUtils = {
/** @private */
KEY_ITEM: 'item',
/** @private */
KEY_ROLE: 'role',
/** Indicate a role is not explicited set. */
NON_EXPLICIT_ROLE: null,
/**
* Create a role map.
* @param {Variant} item
* @param {String} role If not set, {@link Kekule.RoleMapUtils.NON_EXPLICIT_ROLE} will be used.
* @returns {Hash}
*/
createMap: function(item, role)
{
var r = {};
role = role || Kekule.RoleMapUtils.NON_EXPLICIT_ROLE;
r[Kekule.RoleMapUtils.KEY_ITEM] = item;
r[Kekule.RoleMapUtils.KEY_ROLE] = role;
return r;
},
/**
* Get item in map.
* @param {Hash} map
* @returns {Variant}
*/
getItem: function(map)
{
return map[Kekule.RoleMapUtils.KEY_ITEM];
},
/**
* Set item of map.
* @param {Hash} map
* @param {Variant} item
*/
setItem: function(map, item)
{
map[Kekule.RoleMapUtils.KEY_ITEM] = item;
},
/**
* Get role name in map.
* @param {Hash} map
* @returns {String}
*/
getRole: function(map)
{
return map[Kekule.RoleMapUtils.KEY_ROLE];
},
/**
* Set role name of map.
* @param {Hash} map
* @param {String} role
*/
setRole: function(map, role)
{
map[Kekule.RoleMapUtils.KEY_ROLE] = role;
}
};
/**
* A class with some help methods for factory method pattern.
* @class
*/
Kekule.FactoryUtils = {
// match methods
MATCH_EXACTLY: 0,
MATCH_BY_CLASS: 1,
/** @private */
DEF_ITEM_STORE_FIELD_NAME: '_items',
/**
* Create a simple factory.
* @param {Int} matchMethod
* @returns {Object}
*/
createSimpleFactory: function(matchMethod)
{
var FU = Kekule.FactoryUtils;
var result = {
_items: new Kekule.MapEx(true),
_matchMethod: matchMethod || Kekule.FactoryUtils.MATCH_EXACTLY,
register: function(key, aClass)
{
result._items.set(key, aClass);
},
unregister: function(key)
{
result._items.remove(key);
},
getClass: function(key)
{
if (result._matchMethod === FU.MATCH_EXACTLY)
return result._items.get(key);
else if (result._matchMethod === FU.MATCH_BY_CLASS)
{
var r = result._items.get(key);
if (!r)
{
var parent = key && ClassEx.getSuperClass(key);
if (parent)
r = result.getClass(parent);
}
return r;
}
},
getInstance: function(key, p1, p2, p3, p4, p5, p6, p7, p8, p9)
{
var aClass = result.getClass(key);
if (aClass)
return new aClass(p1, p2, p3, p4, p5, p6, p7, p8, p9);
}
};
return result;
}
};
/**
* Class to help to manipulate URL and file names
*/
Kekule.UrlUtils = {
/** @private */
PROTOCAL_DELIMITER: '://',
/** @private */
EXT_DELIMITER: '.',
/** @private */
PATH_DELIMITER: '/',
/** @private */
SEARCH_DELIMITER: '?',
/** @private */
SEARCH_PAIR_DELIMITER: '&',
/** @private */
KEY_VALUE_DELIMITER: '=',
/** @private */
HASH_DELIMITER: '#',
/**
* change all path demiliter from '\' to '/'
* @param {String} path
* @returns {String}
*/
normalizePath: function(path) // change path sep from '\' to '/' in windows env
{
return path.replace(/\\/g, '/');
},
/**
* Extract protocal name from url.
* @param {String} url
* @returns {String}
*/
extractProtocal: function(url)
{
if (!url)
return null;
var p = url.indexOf(Kekule.UrlUtils.PROTOCAL_DELIMITER);
if (p >= 0)
return url.substr(0, p);
else
return '';
},
/**
* Extract file name from url.
* @param {String} url
* @returns {String}
*/
extractFileName: function(url)
{
if (!url)
return null;
var p = url.lastIndexOf(Kekule.UrlUtils.PATH_DELIMITER);
if (p >= 0)
return url.substr(p + 1);
else
return url;
},
/**
* Get file extension of a url based file name
* @param {String} url
* @returns {String}
*/
extractFileExt: function(url)
{
var fileName = Kekule.UrlUtils.extractFileName(url);
if (fileName)
{
var p = fileName.lastIndexOf(Kekule.UrlUtils.EXT_DELIMITER);
if (p >= 0)
return fileName.substr(p + 1);
else
return '';
}
else
return null;
},
/**
* Get core file name without extension from a url based file name
* @param {String} url
* @returns {String}
*/
extractFileCoreName: function(url)
{
var fileName = Kekule.UrlUtils.extractFileName(url);
if (fileName)
{
var p = fileName.lastIndexOf(Kekule.UrlUtils.EXT_DELIMITER);
if (p >= 0)
return fileName.substr(0, p);
else
return '';
}
else
return null;
},
/**
* Get search part (e.g. http://127.0.0.1/url?key=value, part after "?") of URL.
* @param {String} url
* @returns {String} Search part of URL with "?".
*/
extractSearch: function(url)
{
if (!url)
return null;
var p = url.lastIndexOf(Kekule.UrlUtils.SEARCH_DELIMITER);
if (p >= 0)
return url.substr(p);
else
return null;
},
/**
* Returns key-value pair of search part.
* @param {String} url
* @param {Bool} returnHash If true, the return value is a hash rather than array.
* @returns {Variant} If returnHash is false, returns a array while each item in array is a hash with {key, value},
* if returnHash is true, returns a direct hash of key-value pairs.
*/
analysisSearch: function(url, returnHash)
{
var s = Kekule.UrlUtils.extractSearch(url) || '';
s = s.substr(1); // eliminate "?"
var pairs = s.split(Kekule.UrlUtils.SEARCH_PAIR_DELIMITER);
var result = returnHash? {}: [];
for (var i = 0, l = pairs.length; i < l; ++i)
{
var pair = pairs[i];
var a = pair.split(Kekule.UrlUtils.KEY_VALUE_DELIMITER);
if (a[0])
{
if (returnHash)
result[a[0]] = a[1];
else
result.push({'key': a[0], 'value': a[1]});
}
}
return result;
},
/**
* Returns concated search part string based on search params.
* @param {Hash} params Search params (key: value pairs).
* @returns {String}
*/
generateSearchString: function(params)
{
var U = Kekule.UrlUtils;
var parts = [];
var keys = Kekule.ObjUtils.getOwnedFieldNames(params);
for (var i = 0, l = keys.length; i < l; ++i)
{
var key = keys[i];
var value = params[key];
if (Kekule.ObjUtils.notUnset(value))
{
var value = encodeURIComponent('' + value);
parts.push(key + U.KEY_VALUE_DELIMITER + value);
}
}
return parts.join(U.SEARCH_PAIR_DELIMITER)
},
/**
* Generate a whole url with search and hash part.
* @param {String} baseUrl Url without search and hash part.
* @param {Hash} searchParams Key-value pairs of search.
* @param {String} hash Hash part of Url.
* @returns {String}
*/
generateUrl: function(baseUrl, searchParams, hash)
{
var U = Kekule.UrlUtils;
var result = baseUrl;
if (searchParams)
{
var ssearch = U.generateSearchString(searchParams);
result += U.SEARCH_DELIMITER + ssearch;
}
if (hash)
result += U.HASH_DELIMITER + hash;
return result;
}
};
/**
* Utility methods about matrix.
* @class
*/
Kekule.MatrixUtils = {
/**
* Create a matrix with row and col and fill it with prefilledValue.
* @param {Int} rowCount
* @param {Int} colCount
* @param {Float} prefilledValue
* @returns {Array}
*/
create: function(rowCount, colCount, prefilledValue)
{
var preValueSet = false;
var preValueIsArray = false;
var OU = Kekule.ObjUtils;
if (OU.notUnset(prefilledValue))
{
preValueSet = true;
if (Kekule.ArrayUtils.isArray(prefilledValue)) // is array
preValueIsArray = true;
}
var index = 0;
var m = new Array(rowCount);
for (var i = 0, l = m.length; i < l; ++i)
{
var r = new Array(colCount);
if (preValueSet)
{
for (var j = 0, k = r.length; j < k; ++j)
{
if (preValueIsArray && OU.notUnset(prefilledValue[index]))
{
r[j] = prefilledValue[index];
++index;
}
else
r[j] = prefilledValue;
}
}
m[i] = r;
}
return m;
},
/**
* Create a identity matrix with row and col.
* @param {Int} rowColCount
* @returns {Array}
*/
createIdentity: function(rowColCount)
{
var result = Kekule.MatrixUtils.create(rowColCount, rowColCount, 0);
for (var i = 0; i < rowColCount - 1; ++i)
result[i][i] = 1;
return result;
},
/**
* Get row count of a matrix
* @param {Array} matrix
* @returns {Int}
*/
getRowCount: function(matrix)
{
return matrix.length;
},
/**
* Get col count of a matrix
* @param {Array} matrix
* @returns {Int}
*/
getColCount: function(matrix)
{
return (Kekule.MatrixUtils.getRowCount(matrix) > 0)? matrix[0].length: 0;
},
/**
* Get value in matrix.
* @param {Array} matrix
* @param {Int} row
* @param {Int} col
* @returns {Float}
*/
getValue: function(matrix, row, col)
{
return matrix[row - 1][col - 1] || 0;
},
/**
* Set value in matrix.
* @param {Array} matrix
* @param {Int} row
* @param {Int} col
* @param {Float} value
*/
setValue: function(matrix, row, col, value)
{
matrix[row - 1][col - 1] = value;
},
/**
* Transpose a matrix.
* @param {Array} m
* @returns {Array}
*/
transpose: function(m)
{
var M = Kekule.MatrixUtils;
var rowCount = M.getColCount(m);
var colCount = M.getRowCount(m);
var result = M.create(rowCount, colCount);
for (var i = 1; i <= rowCount; ++i)
{
var r = result[i - 1];
for (var j = 1; j <= colCount; ++j)
r[j - 1] = M.getValue(m, j, i);
}
/*
console.log('origin: ');
console.dir(m);
console.log('transposed: ');
console.dir(result);
*/
return result;
},
/**
* Turn all values in matrix to minus ones.
* @param {Array} matrix
*/
minus: function(matrix)
{
var M = Kekule.MatrixUtils;
var rowCount = M.getRowCount(matrix);
var colCount = M.getColCount(matrix);
var result = this.create(rowCount, colCount);
for (var i = 0; i < rowCount; ++i)
{
var r = result[i];
var m = matrix[i];
for (var j = 0; j < colCount; ++j)
r[j] = -(m[j] || 0);
}
return result;
},
/**
* Add two matrix.
* @param {Array} m1
* @param {Array} m2
* @returns {Array}
*/
add: function(m1, m2)
{
var M = Kekule.MatrixUtils;
var rowCount = M.getRowCount(m1);
var colCount = M.getColCount(m1);
var result = this.create(rowCount, colCount);
for (var i = 1; i <= rowCount; ++i)
{
//var r = result[i];
for (var j = 1; j <= colCount; ++j)
M.setValue(result, i, j, M.getValue(m1, i, j) + M.getValue(m2, i, j));
}
return result;
},
/**
* Substract two matrix.
* @param {Array} m1
* @param {Array} m2
* @returns {Array}
*/
substract: function(m1, m2)
{
var M = Kekule.MatrixUtils;
return M.add(m1, M.minus(m2));
},
/**
* Mutiply two matrix.
* @param {Array} m1
* @param {Array} m2
* @returns {Array}
*/
multiply: function(m1, m2)
{
var M = Kekule.MatrixUtils;
var rowCount = M.getRowCount(m1);
var colCount = M.getColCount(m2);
var rowCount2 = M.getRowCount(m2);
var result = M.create(rowCount, colCount);
for (var i = 1; i <= rowCount; ++i)
{
//var r = result[i];
for (var j = 1; j <= colCount; ++j)
{
var sum = 0;
for (var k = 1; k <= rowCount2; ++k)
{
sum += M.getValue(m1, i, k) * M.getValue(m2, k, j);
}
//r[j] = sum;
M.setValue(result, i, j, sum);
}
}
//console.log(m1, m2, result);
return result;
}
};
/**
* Utility methods about coordinates (2D or 3D).
* @class
*/
Kekule.CoordUtils = {
/**
* Create a coordinate object by params.
* If two params provided, a 2D (x/y) coordinate will be created, else 3D (x/y/z) one will be created.
* @returns {Hash}
*/
create: function(x, y, z)
{
var result = {'x': x, 'y': y};
if (z || (z === 0))
result.z = z;
return result;
},
/**
* Clone a coord.
* @param {Hash} coord
* @returns {Hash}
*/
clone: function(coord)
{
var result = {'x': coord.x, 'y': coord.y};
if (coord.z || (coord.z === 0))
result.z = coord.z;
return result;
},
/**
* Check if the coord is a 3D one (has z value)
* @param {Object} coord
* @returns {Bool}
*/
is3D: function(coord)
{
return (coord.z || (coord.z === 0));
},
/**
* Check if two coords are same.
* @param {Hash} coord1
* @param {Hash} coord2
* @returns {Bool}
*/
isEqual: function(coord1, coord2)
{
var r = (coord1.x === coord2.x) && (coord1.y === coord2.y);
var O = Kekule.ObjUtils;
if (r && (O.notUnset(coord1.z) || O.notUnset(coord2.z)))
r = (coord1.z === coord2.z);
return r;
},
/**
* Check if a coord is a zero one (x/y/z all equals to 0).
* @param {Hash} coord
* @param {Float} allowedError
* @returns {Bool}
*/
isZero: function(coord, allowedError)
{
// TODO: now calculation error is fixed
var error = Kekule.ObjUtils.notUnset(allowedError)? allowedError: 0; /*1e-5;*/
return (Math.abs(coord.x || 0) <= error)
&& (Math.abs(coord.y || 0) <= error) && (Math.abs(coord.z || 0) <= error);
},
/**
* Returns the absolute value of each coord axises.
* @param {Hash} coord
* @returns {Hash}
*/
absValue: function(coord)
{
if (coord)
{
var result = {};
if (!Kekule.ObjUtils.isUnset(coord.x))
result.x = Math.abs(coord.x);
if (!Kekule.ObjUtils.isUnset(coord.y))
result.y = Math.abs(coord.y);
if (!Kekule.ObjU