UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,980 lines (1,949 loc) 118 kB
/** * @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