/**
* @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 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();
},
/**
* 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))
threshold = 1e-100;
return Math.abs(f1 - f2) <= threshold;
},
/**
* 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__);
},
/**
* 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)
{
for (var fname in src)
{
if (excludingFields && (excludingFields.indexOf(fname) >= 0))
continue;
if (src[fname] !== dest[fname])
return false;
}
return true;
},
/**
* Check if fields in condition has the same value with src.
* @param {Object} src
* @param {Object} condition
* @return {Bool}
*/
match: function(src, condition)
{
for (var fname in condition)
{
if (condition.hasOwnProperty(fname))
{
if (src[fname] !== 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);
},
/**
* 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.
* @return {Int} Index of obj in array.
*/
pushUnique: function(targetArray, obj)
{
var r = Kekule.ArrayUtils.pushUniqueEx(targetArray, obj);
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.
* @return {Hash} {index, isPushed} hash. Index of obj in array.
*/
pushUniqueEx: function(targetArray, obj)
{
if (DataType.isArrayValue(obj))
{
var r;
for (var i = 0, l = obj.length; i < l; ++i)
{
if (Kekule.ObjUtils.isUnset(r))
r = Kekule.ArrayUtils.pushUniqueEx(targetArray, obj[i]);
else
Kekule.ArrayUtils.pushUniqueEx(targetArray, obj[i]);
}
return r;
}
else
{
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};
}
},
/**
* 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;
},
/**
* 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 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 = 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 */
EXT_DELIMITER: '.',
/** @private */
PATH_DELIMITER: '/',
/** @private */
SEARCH_DELIMITER: '?',
/** @private */
SEARCH_PAIR_DELIMITER: '&',
/** @private */
KEY_VALUE_DELIMITER: '=',
/** @private */
HASH_DELIMITER: '#',
/**
* 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 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 <= rowCount; ++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.ObjUtils.isUnset(coord.z))
result.z = Math.abs(coord.z);
return result;
}
},
/**
* Add value of two coordinates.
* @param {Hash} coord1
* @param {Hash} coord2
* @returns {Hash}
*/
add: function(coord1, coord2)
{
var r = {};
if (!(Kekule.ObjUtils.isUnset(coord1.x) && (Kekule.ObjUtils.isUnset(coord2.x))))
r.x = (coord1.x || 0) + (coord2.x || 0);
if (!(Kekule.ObjUtils.isUnset(coord1.y) && (Kekule.ObjUtils.isUnset(coord2.y))))
r.y = (coord1.y || 0) + (coord2.y || 0);
if (!(Kekule.ObjUtils.isUnset(coord1.z) && (Kekule.ObjUtils.isUnset(coord2.z))))
r.z = (coord1.z || 0) + (coord2.z || 0);
return r;
},
/**
* Substract values of two coordinates, coord1 - coord2.
* @param {Hash} coord1
* @param {Hash} coord2
* @returns {Hash}
*/
substract: function(coord1, coord2)
{
var r = {};
if (!(Kekule.ObjUtils.isUnset(coord1.x) && (Kekule.ObjUtils.isUnset(coord2.x))))
r.x = (coord1.x || 0) - (coord2.x || 0);
if (!(Kekule.ObjUtils.isUnset(coord1.y) && (Kekule.ObjUtils.isUnset(coord2.y))))
r.y = (coord1.y || 0) - (coord2.y || 0);
if (!(Kekule.ObjUtils.isUnset(coord1.z) && (Kekule.ObjUtils.isUnset(coord2.z))))
r.z = (coord1.z || 0) - (coord2.z || 0);
return r;
},
/**
* Coord * value.
* @param {Hash} coord
* @param {Num} value
* @returns {Hash}
*/
multiply: function(coord, value)
{
var r = {};
if (!Kekule.ObjUtils.isUnset(coord.x))
r.x = coord.x * value;
if (!Kekule.ObjUtils.isUnset(coord.y))
r.y = coord.y * value;
if (!Kekule.ObjUtils.isUnset(coord.z))
r.z = coord.z * value;
return r;
},
/**
* Coord / value.
* @param {Hash} coord
* @param {Num} value
* @returns {Hash}
*/
divide: function(coord, value)
{
return Kekule.CoordUtils.multiply(coord, 1/value);
},
/**
* Standardize coord to a length 1 vector.
* @param {Hash} coord
* @returns {Hash}
*/
standardize: function(coord)
{
var len = Math.sqrt(Math.sqr(coord.x || 0) + Math.sqr(coord.y || 0) + Math.sqr(coord.z || 0));
return Kekule.CoordUtils.divide(coord, len || 1); // if len is 0, returns {0, 0, 0}
},
/**
* Convert coord to a 2D or 3D array of values
* @param {Hash} coord
* @returns {Array}
*/
toArray: function(coord)
{
var result = [];
result.push(coord.x, coord.y);
if (Kekule.CoordUtils.is3D(coord))
result.push(coord.z);
},
/**
* Do a 2d transform to a coord.
* The tranform will be performed in the following order:
* rotate, scale, translate.
* @param {Hash} coord 2D coordinate.
* @param {Hash} options Transform options, can has the following fields:
* scale, scaleX, scaleY
* translateX, translateY,
* rotateAngle,
* center: {x, y} (center of rotate and scale).
* @returns {Hash} 2D coord after transform.
*/
transform2D: function(coord, options)
{
/*
var op = options || {};
var center = op.center || {'x': 0, 'y': 0};
var delta = {'x': coord.x - center.x, 'y': coord.y - center.y};
var x = delta.x, y = delta.y;
if (op.rotateAngle)
{
var a = op.rotateAngle;
var newX = x * Math.cos(a) - y * Math.sin(a);
var newY = x * Math.sin(a) + y * Math.cos(a);
x = newX;
y = newY;
}
if (op.scale || op.scaleX || op.scaleY)
{
x *= op.scaleX || op.scale;
y *= op.scaleY || op.scale;
}
x += op.translateX || 0;
y += op.translateY || 0;
x += center.x;
y += center.y;
return {'x': x, 'y': y};
*/
var matrix = Kekule.CoordUtils.calcTransform2DMatrix(options);
var newCoord = Kekule.CoordUtils.transform2DByMatrix(coord, matrix);
return newCoord;
/*
var newCoord = {'x': coord.x, 'y': coord.y, 'z': 0};
var newOp = Object.extend({}, options);
newOp.rotateZ = newOp.rotateAngle;
newCoord = Kekule.CoordUtils.transform3D(newCoord, newOp);
return {'x': newCoord.x, 'y': newCoord.y};
*/
},
/**
* Do a reversed 2d transform to a coord.
* @param {Hash} coord 2D coordinate.
* @param {Hash} options Transform options, can has the following fields:
* scale, scaleX, scaleY
* translateX, translateY,
* rotateAngle,
* center: {x, y} (center of rotate and scale).
* @returns {Hash} 2D coord after reversed transform.
*/
inverseTransform2D: function(coord, options)
{
var matrix = Kekule.CoordUtils.calcInverseTransform2DMatrix(options);
var newCoord = Kekule.CoordUtils.transform2DByMatrix(coord, matrix);
return newCoord;
},
/**
* Do a 2D transform to a coord by a transformer matrix.
* @param {Hash} coord
* @param {Array} transformMatrix A 3X3 matrix.
*/
transform2DByMatrix: function(coord, transformMatrix)
{
var M = Kekule.MatrixUtils;
var coordV = M.create(3, 1, [coord.x || 0, coord.y || 0, 1]);
coordV = M.multiply(transformMatrix, coordV);
return {'x': coordV[0][0], 'y': coordV[1][0]};
},
/**
* Calculate the 2D transfrom matrix.
* The tranform will be performed in the following order:
* rotate, scale, translate.
* @param {Hash} options Transform options, can has the following fields:
* scale, scaleX, scaleY
* translateX, translateY,
* rotateAngle,
* center: {x, y} (center of rotate and scale).
* @returns {Array} A 4X4 matrix.
*/
calcTransform2DMatrix: function(options, reverseOrder)
{
var M = Kekule.MatrixUtils;
var op = options || {};
var center = op.center;
if (center) // center point set, need translate before rotate and scale
{
var preMatrix = M.create(3, 3, [
1, 0, -(center.x || 0),
0, 1, -(center.y || 0),
0, 0, 1
]);
var postMatrix = M.create(3, 3, [
1, 0, (center.x || 0),
0, 1, (center.y || 0),
0, 0, 1
]);
}
// rotate matrix
if (op.rotateAngle)
{
var a = op.rotateAngle;
var cosA = Math.cos(a);
var sinA = Math.sin(a);
var rotateMatrix = M.create(3, 3, [
cosA, -sinA, 0,
sinA, cosA, 0,
0, 0, 1
]);
}
// scale matrix
var scale = op.scale;
var scaleX = op.scaleX;
var scaleY = op.scaleY;
var defScale = scale || 1;
if (scale || scaleX || scaleY)
{
var scaleMatrix = M.create(3, 3);
M.setValue(scaleMatrix, 1, 1, scaleX || defScale);
M.setValue(scaleMatrix, 2, 2, scaleY || defScale);
M.setValue(scaleMatrix, 3, 3, 1);
}
// translate matrix
var translateX = op.translateX;
var translateY = op.translateY;
if (translateX || translateY)
{
var translateMatrix = M.create(3, 3);
M.setValue(translateMatrix, 1, 1, 1);
M.setValue(translateMatrix, 2, 2, 1);
M.setValue(translateMatrix, 3, 3, 1);
M.setValue(translateMatrix, 1, 3, (translateX || 0));
M.setValue(translateMatrix, 2, 3, (translateY || 0));
}
// sum up
var transformMatrix = M.create(3, 3, [
1,0,0,
0,1,0,
0,0,1
]);
if (!reverseOrder)
{
if (preMatrix)
transformMatrix = M.multiply(preMatrix, transformMatrix); //M.multiply(transformMatrix, preMatrix);
if (rotateMatrix)
transformMatrix = M.multiply(rotateMatrix, transformMatrix); // M.multiply(transformMatrix, rotateMatrix);
if (scaleMatrix)
transformMatrix = M.multiply(scaleMatrix, transformMatrix); // M.multiply(transformMatrix, scaleMatrix);
if (translateMatrix)
transformMatrix = M.multiply(translateMatrix, transformMatrix); // M.multiply(transformMatrix, translateMatrix);
if (postMatrix)
transformMatrix = M.multiply(postMatrix, transformMatrix); // M.multiply(transformMatrix, postMatrix);
}
else
{
/*
if (preMatrix)
transformMatrix = M.multiply(transformMatrix, preMatrix);
//transformMatrix = M.multiply(preMatrix, transformMatrix);
if (rotateMatrix)
transformMatrix = M.multiply(transformMatrix, rotateMatrix);
if (scaleMatrix)
transformMatrix = M.multiply(transformMatrix, scaleMatrix);
if (translateMatrix)
transformMatrix = M.multiply(transformMatrix, translateMatrix);
if (postMatrix)
transformMatrix = M.multiply(transformMatrix, postMatrix);
//transformMatrix = M.multiply(postMatrix, transformMatrix);
*/
if (preMatrix)
transformMatrix = M.multiply(preMatrix, transformMatrix); //M.multiply(transformMatrix, preMatrix);
if (translateMatrix)
transformMatrix = M.multiply(translateMatrix, transformMatrix); // M.multiply(transformMatrix, translateMatrix);
if (scaleMatrix)
transformMatrix = M.multiply(scaleMatrix, transformMatrix); // M.multiply(transformMatrix, scaleMatrix);
if (rotateMatrix)
transformMatrix = M.multiply(rotateMatrix, transformMatrix); // M.multiply(transformMatrix, rotateMatrix);
if (postMatrix)
transformMatrix = M.multiply(postMatrix, transformMatrix); // M.multiply(transformMatrix, postMatrix);
}
//console.log(transformMatrix);
return transformMatrix;
},
/**
* Calculate inversed transform matrix of options.
* @param {Hash} options
* @returns {Array}
*/
calcInverseTransform2DMatrix: function(options)
{
var op = Object.create(options);
if (options.center)
{
op.center = {x: options.center.x, y: options.center.y};
}
if (op.scale)
op.scale = 1 / op.scale;
if (op.scaleX)
op.scaleX = 1 / op.scaleX;
if (op.scaleY)
op.scaleY = 1 / op.scaleY;
if (op.translateX)
op.translateX = -op.translateX;
if (op.translateY)
op.translateY = -op.translateY;
/*
if (op.center)
{
op.center.x *= (op.scaleX || op.scale || 1);
op.center.y *= (op.scaleY || op.scale || 1);
}
*/
if (op.rotateAngle)
op.rotateAngle = -op.rotateAngle;
return Kekule.CoordUtils.calcTransform2DMatrix(op, true);
},
/**
* Do a 3d transform to a coord.
* The tranform will be performed in the following order:
* rotate, scale, translate.
* @param {Hash} coord 3D coordinate.
* @param {Hash} options Transform options, can has the following fields:
* scale, scaleX, scaleY, scaleZ, // all scale from center or zero point
* translateX, translateY, translateZ,
* rotateX, rotateY, rotateZ,
* rotateAngle, rotateAxisVector // rotate around a vector start from center or zero point
* rotateMatrix // use matrix to rotate directly
* center: {x, y, z} (center of rotate and scale).
* @returns {Hash} 3D coord after transform.
*/
transform3D: function(coord, options)
{
var matrix = Kekule.CoordUtils.calcTransform3DMatrix(options);
return Kekule.CoordUtils.transform3DByMatrix(coord, matrix);
},
/**
* Do a 3d transform to a coord by a transformer matrix.
* @param {Hash} coord
* @param {Array} transformMatrix A 4X4 matrix.
*/
transform3DByMatrix: function(coord, transformMatrix)
{
var M = Kekule.MatrixUtils;
var coordV = M.create(4, 1, [coord.x || 0, coord.y || 0, coord.z || 0, 1]);
coordV = M.multiply(transformMatrix, coordV);
return {'x': coordV[0][0], 'y': coordV[1][0], 'z': coordV[2][0]};
},
/** @private */
_calcRotateXYZ3DMatrixCore: function(options)
{
var M = Kekule.MatrixUtils;
var result = M.create(4, 4, [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
// the following calculation is based on left-hand principle,
// generally Kekule applies right hand principle, so reverse the angle
var rotatePairs = [];
if (options.rotateAngle && options.rotateAxisVector)
{
var srotateAxisV = Kekule.CoordUtils.standardize(options.rotateAxisVector);
rotatePairs.push({
'vector': [srotateAxisV.x, srotateAxisV.y, srotateAxisV.z],
'angle': -options.rotateAngle
});
}
else
{
var rx = options.rotateX;
var ry = options.rotateY;
var rz = options.rotateZ;
if (rx)
rotatePairs.push({'vector': [1, 0, 0], 'angle': -rx});
if (ry)
rotatePairs.push({'vector': [0, 1, 0], 'angle': -ry});
if (rz)
rotatePairs.push({'vector': [0, 0, 1], 'angle': -rz});
}
for (var i = 0, l = rotatePairs.length; i < l; ++i)
{
var rotateMatrix = M.create(4, 4);
var angle = rotatePairs[i].angle;
var rotateVectorArray = rotatePairs[i].vector;
var cosAngle = Math.cos(angle);
var sinAngle = Math.sin(angle);
var v;
for (var i = 1; i <= 4; ++i)
{
for (var j = 1; j <= 4; ++j)
{
if ((i < 4) && (j < 4))
{
for (var k = 1; k <= 3; ++k)
{
if ((k !== i) && (k !== j))
break;
}
var nij = rotateVectorArray[i - 1] * rotateVectorArray[j - 1];
v =
(i < j)?
nij * (1 - cosAngle)
- Math.pow(-1, i + j) * rotateVectorArray[k - 1] * sinAngle:
(i > j)?
nij * (1 - cosAngle)
+ Math.pow(-1, i + j) * rotateVectorArray[k - 1] * sinAngle:
// i === j
nij + (1 - nij) * cosAngle;
}
else if (i === j) // i == j == 4
v = 1;
else
v = 0;
M.setValue(rotateMatrix, i, j, v);
}
}
result = M.multiply(rotateMatrix, result);
}
//console.log(result);
return result;
},
/**
* Calculate 3D rotation matrix from options.
* @param {Object} options Rotation options, can has the following fields:
* rotateX, rotateY, rotateZ, // if Y is set, Z will be ignored; if X is set, Y/Z will be ignored
* rotateAngle, rotateAxisVector // rotate around a vector start from center or zero point, if this value is set, rotateX-Z will be ignored
* rotateMatrix // use matrix to rotate directly
* center: {x, y, z} (center of rotate).
* @returns {Array} A 4X4 matrix.
*/
calcRotate3DMatrix: function(options)
{
var M = Kekule.MatrixUtils;
//var op = Object.extend({}, options || {});
var op = Object.create(options || {});
var rotateMatrix;
if (op.rotateMatrix)
{
rotateMatrix = op.rotateMatrix;
}
else
{
var center = op.center;
if (center) // center point set, need translate before rotate and scale
{
var preMatrix = M.create(4, 4, [
1, 0, 0, -(center.x || 0),
0, 1, 0, -(center.y || 0),
0, 0, 1, -(center.z || 0),
0, 0, 0, 1
]);
var postMatrix = M.create(4, 4, [
1, 0, 0, (center.x || 0),
0, 1, 0, (center.y || 0),
0, 0, 1, (center.z || 0),
0, 0, 0, 1
]);
}
/*
var rotateX = op.rotateX;
var rotateY = op.rotateY;
var rotateZ = op.rotateZ;
var rotateAngle = op.rotateAngle;
var rotateAxisVector = op.rotateAxisVector;
if (rotateX || rotateY || rotateZ || rotateAngle)
{
//if (rotateAngle && rotateAxisVector)
var srotateAxisV = (rotateAngle && rotateAxisVector)? Kekule.CoordUtils.standardize(rotateAxisVector): null;
var rotateVectorArray =
srotateAxisV? [srotateAxisV.x, srotateAxisV.y, srotateAxisV.z]:
rotateX? [1, 0, 0]:
rotateY? [0, 1, 0]:
rotateZ? [0, 0, 1]:
null;
rotateMatrix = M.create(4, 4);
var angle = rotateX || rotateY || rotateZ || rotateAngle;
// the following calculation is based on left-hand principle,
// generally Kekule applies right hand principle, so reverse the angle
angle = -angle;
var cosAngle = Math.cos(angle);
var sinAngle = Math.sin(angle);
var v;
for (var i = 1; i <= 4; ++i)
{
for (var j = 1; j <= 4; ++j)
{
if ((i < 4) && (j < 4))
{
for (var k = 1; k <= 3; ++k)
{
if ((k !== i) && (k !== j))
break;
}
var nij = rotateVectorArray[i - 1] * rotateVectorArray[j - 1];
v =
(i < j)?
nij * (1 - cosAngle)
- Math.pow(-1, i + j) * rotateVectorArray[k - 1] * sinAngle:
(i > j)?
nij * (1 - cosAngle)
+ Math.pow(-1, i + j) * rotateVectorArray[k - 1] * sinAngle:
// i === j
nij + (1 - nij) * cosAngle;
}
else if (i === j) // i == j == 4
v = 1;
else
v = 0;
M.setValue(rotateMatrix, i, j, v);
}
}
}
*/
var rotateMatrix = Kekule.CoordUtils._calcRotateXYZ3DMatrixCore(options);
if (preMatrix)
rotateMatrix = M.multiply(preMatrix, rotateMatrix);
if (postMatrix)
rotateMatrix = M.multiply(postMatrix, rotateMatrix); // M.multiply(transformMatrix, postMatrix);
}
return rotateMatrix;
},
/**
* Calculate the 3D transfrom matrix.
* The tranform will be performed in the following order:
* rotate, scale, translate.
* @param {Object} options Transform options, can has the following fields:
* scale, scaleX, scaleY, scaleZ, // all scale from center or zero point
* translateX, translateY, translateZ,
* rotateX, rotateY, rotateZ, // if Y is set, Z will be ignored; if X is set, Y/Z will be ignored
* rotateAngle, rotateAxisVector // rotate around a vector start from center or zero point, if this value is set, rotateX-Z will be ignored
* rotateMatrix // use matrix to rotate directly
* center: {x, y, z} (center of rotate and scale).
* @returns {Array} A 4X4 matrix.
*/
calcTransform3DMatrix: function(options)
{
var M = Kekule.MatrixUtils;
var op = options || {};
var center = op.center;
if (center) // center point set, need translate before rotate and scale
{
var preMatrix = M.create(4, 4, [
1, 0, 0, -(center.x || 0),
0, 1, 0, -(center.y || 0),
0, 0, 1, -(center.z || 0),
0, 0, 0, 1
]);
var postMatrix = M.create(4, 4, [
1, 0, 0, (center.x || 0),
0, 1, 0, (center.y || 0),
0, 0, 1, (center.z || 0),
0, 0, 0, 1
]);
}
// rotate matrix
var rotateOptionFields = ['rotateX', 'rotateY', 'rotateZ', 'rotateAngle', 'rotateAxisVector', 'rotateMatrix'];
var rotateOptions = Object.copyValues({}, op, rotateOptionFields);
var rotateMatrix = Kekule.CoordUtils.calcRotate3DMatrix(rotateOptions);
/*
if (op.rotateMatrix)
rotateMatrix = op.rotateMatrix;
else
{
var rotateX = op.rotateX;
var rotateY = op.rotateY;
var rotateZ = op.rotateZ;
var rotateAngle = op.rotateAngle;
var rotateAxisVector = op.rotateAxisVector;
if (rotateX || rotateY || rotateZ || rotateAngle)
{
//if (rotateAngle && rotateAxisVector)
var srotateAxisV = (rotateAngle && rotateAxisVector)? Kekule.CoordUtils.standardize(rotateAxisVector): null;
var rotateVectorArray =
srotateAxisV? [srotateAxisV.x, srotateAxisV.y, srotateAxisV.z]:
rotateX? [1, 0, 0]:
rotateY? [0, 1, 0]:
rotateZ? [0, 0, 1]:
null;
rotateMatrix = M.create(4, 4);
var angle = rotateX || rotateY || rotateZ || rotateAngle;
var cosAngle = Math.cos(angle);
var sinAngle = Math.sin(angle);
var v;
for (var i = 1; i <= 4; ++i)
{
for (var j = 1; j <= 4; ++j)
{
if ((i < 4) && (j < 4))
{
for (var k = 1; k <= 3; ++k)
{
if ((k !== i) && (k !== j))
break;
}
var nij = rotateVectorArray[i - 1] * rotateVectorArray[j - 1];
v =
(i < j)?
nij * (1 - cosAngle)
- Math.pow(-1, i + j) * rotateVectorArray[k - 1] * sinAngle:
(i > j)?
nij * (1 - cosAngle)
+ Math.pow(-1, i + j) * rotateVectorArray[k - 1] * sinAngle:
// i === j
nij + (1 - nij) * cosAngle;
}
else if (i === j) // i == j == 4
v = 1;
else
v = 0;
M.setValue(rotateMatrix, i, j, v);
}
}
}
}
*/
// scale matrix
var scale = op.scale;
var scaleX = op.scaleX;
var scaleY = op.scaleY;
var scaleZ = op.scaleZ;
if (scale || scaleX || scaleY || scaleZ)
{
var defScale = scale || 1;
var scaleMatrix = M.create(4, 4);
M.setValue(scaleMatrix, 1, 1, scaleX || defScale);
M.setValue(scaleMatrix, 2, 2, scaleY || defScale);
M.setValue(scaleMatrix, 3, 3, scaleZ || defScale);
M.setValue(scaleMatrix, 4, 4, 1);
}
// translate matrix
var translateX = op.translateX;
var translateY = op.translateY;
var translateZ = op.translateZ;
if (translateX || translateY || translateZ)
{
var translateMatrix = M.create(4, 4);
M.setValue(translateMatrix, 1, 1, 1);
M.setValue(translateMatrix, 2, 2, 1);
M.setValue(translateMatrix, 3, 3, 1);
M.setValue(translateMatrix, 4, 4, 1);
M.setValue(translateMatrix, 1, 4, translateX || 0);
M.setValue(translateMatrix, 2, 4, translateY || 0);
M.setValue(translateMatrix, 3, 4, translateZ || 0);
}
// sum up
var transformMatrix = M.create(4, 4, [
1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
]);
//console.log('----------------------------------------');
//console.log(transformMatrix);
if (preMatrix)
transformMatrix = M.multiply(preMatrix, transformMatrix); // M.multiply(transformMatrix, preMatrix);
//console.log(transformMatrix);
if (rotateMatrix)
transformMatrix = M.multiply(rotateMatrix, transformMatrix); // M.multiply(transformMatrix, rotateMatrix);
//console.log(transformMatrix);
if (scaleMatrix)
transformMatrix = M.multiply(scaleMatrix, transformMatrix); // M.multiply(transformMatrix, scaleMatrix);
//console.log(scaleMatrix, transformMatrix);
if (translateMatrix)
transformMatrix = M.multiply(translateMatrix, transformMatrix); // M.multiply(transformMatrix, translateMatrix);
//console.log(transformMatrix);
if (postMatrix)
transformMatrix = M.multiply(postMatrix, transformMatrix); // M.multiply(transformMatrix, postMatrix);
return transformMatrix;
},
/**
* Calculate inversed transform matrix of options.
* @param {Hash} options
* @returns {Array}
*/
calcInverseTransform3DMatrix: function(options)
{
var op = Object.create(options);
if (options.center)
{
op.center = {x: options.center.x, y: options.center.y, z: options.center.z};
}
if (op.scale)
op.scale = 1 / op.scale;
if (op.scaleX)
op.scaleX = 1 / op.scaleX;
if (op.scaleY)
op.scaleY = 1 / op.scaleY;
if (op.scaleZ)
op.scaleZ = 1 / op.scaleZ;
if (op.translateX)
op.translateX = -op.translateX;
if (op.translateY)
op.translateY = -op.translateY;
if (op.translateZ)
op.translateZ = -op.translateZ;
if (op.rotateAngle)
op.rotateAngle = -op.rotateAngle;
if (op.rotateX)
op.rotateX = -op.rotateX;
if (op.rotateX)
op.rotateY = -op.rotateY;
if (op.rotateZ)
op.rotateZ = -op.rotateZ;
return Kekule.CoordUtils.calcTransform3DMatrix(op, true);
},
/**
* Returns suitable 2D transform params that can transform a line between targetCoord1/targetCoord2
* to destCoord1, destCoord2.
* @param {Hash} targetCoord1
* @param {Hash} targetCoord2
* @param {Hash} destCoord1
* @param {Hash} destCoord2
* @returns {Hash}
*/
calcCoordGroup2DTransformParams: function(targetCoord1, targetCoord2, destCoord1, destCoord2)
{
var CU = Kekule.CoordUtils;
var distanceTarget = CU.getDistance(targetCoord1, targetCoord2);
var deltaTarget = CU.substract(targetCoord2, targetCoord1);
var angleTarget = Math.atan2(deltaTarget.y, deltaTarget.x);
var distanceDest = CU.getDistance(destCoord1, destCoord2);
var deltaDest = CU.substract(destCoord2, destCoord1);
var angleDest = Math.atan2(deltaDest.y, deltaDest.x);
var coordDelta = CU.substract(destCoord1, targetCoord1);
var transParam = {
'translateX': coordDelta.x,
'translateY': coordDelta.y,
'scale': distanceDest / distanceTarget,
'rotateAngle': angleDest - angleTarget,
'center': targetCoord1
}
return transParam;
},
/**
* Get distance between two coords. If coord2 is not set, this function will calc distance between coord1 and zero point.
* @param {Hash} coord1
* @param {Hash} coord2
* @returns {Float}
*/
getDistance: function(coord1, coord2)
{
if (!coord2)
coord2 = {'x': 0, 'y': 0, 'z': 0};
var l = Math.sqr(coord2.x - coord1.x) + Math.sqr(coord2.y - coord1.y);
if (Kekule.ObjUtils.notUnset(coord1.z) && Kekule.ObjUtils.notUnset(coord2.z))
l += Math.sqr(coord2.z - coord1.z);
return Math.sqrt(l);
},
/**
* Calculate center coord of a set of coords.
* @param {Array} coords
* @returns {Hash}
*/
getCenter: function(coords)
{
var length = coords.length;
if (length <= 0)
return null;
var sum = Object.extend(coords[0]);
for (var i = 1; i < length; ++i)
{
sum = Kekule.CoordUtils.add(sum, coords[i]);
}
return Kekule.CoordUtils.divide(sum, length);
},
/**
* Check if coord inside 2D rect.
* @param {Object} coord
* @param {Object} rectCoord1
* @param {Object} rectCoord2
* @returns {Bool}
* @deprecated
*/
insideRect: function(coord, rectCoord1, rectCoord2)
{
return ((coord.x - rectCoord1.x) * (coord.x - rectCoord2.x) <= 0)
&& ((coord.y - rectCoord1.y) * (coord.y - rectCoord2.y) <= 0);
},
/**
* Check if coord is inside a box.
* @param {Hash} coord
* @param {Hash} box
* @returns {Bool}
*/
isInsideBox: function(coord, box)
{
//var b = Kekule.BoxUtils.normalize(box);
var b = box;
var result = (Math.sign(coord.x - b.x1) * Math.sign(coord.x - b.x2) <= 0)
&& (Math.sign(coord.y - b.y1) * Math.sign(coord.y - b.y2) <= 0);
if (Kekule.ObjUtils.notUnset(coord.z))
result = result && (Math.sign(coord.z - b.z1) * Math.sign(coord.z - b.z2) <= 0);
return result;
},
/**
* Returns a minial box that contains all coords.
* @param {Array} coords Array of coords.
* @returns {Hash}
*/
getContainerBox: function(coords)
{
var minCoord = {};
var maxCoord = {};
var l = coords.length;
if (l > 0)
{
minCoord = Kekule.CoordUtils.clone(coords[0]); //Object.extend({}, coords[0]);
maxCoord = Kekule.CoordUtils.clone(coords[0]); // Object.extend({}, coords[0]);
}
else
return null;
for (var i = 1; i < l; ++i)
{
var coord = coords[i];
minCoord.x = Math.min(minCoord.x, coord.x);
minCoord.y = Math.min(minCoord.y, coord.y);
maxCoord.x = Math.max(maxCoord.x, coord.x);
maxCoord.y = Math.max(maxCoord.y, coord.y);
if (Kekule.ObjUtils.notUnset(minCoord.z)) // 3D
{
minCoord.z = Math.min(minCoord.z, coord.z);
maxCoord.z = Math.max(maxCoord.z, coord.z);
}
}
return Kekule.BoxUtils.createBox(minCoord, maxCoord);
}
};
/**
* Utility methods of some 2D or 3D geometry tasks.
* @class
*/
Kekule.GeometryUtils = {
/**
* Add or substract 2Pi to angle, ensure the angle in range rangeMin to rangeMin + 2Pi
* @param {Float} angle
* @param {Float} rangeMin Default is 0.
*/
standardizeAngle: function(angle, rangeMin, rangeMax)
{
rangeMin = rangeMin || 0;
var rangeMax = rangeMin + Math.PI * 2;
var times = Math.floor((angle - rangeMin) / Math.PI / 2);
return angle - times * Math.PI * 2;
},
/**
* Convert a polar coord to cartesian one.
* @param {Float} centerX
* @param {Float} centerY
* @param {Float} radius
* @param {Float} angle
* @returns {Hash}
*/
polarToCartesian: function(centerX, centerY, radius, angle)
{
return {
x: centerX + (radius * Math.cos(angle)),
y: centerY + (radius * Math.sin(angle))
};
},
/**
* Returns the cross product (v1.v2) of two vectors represented by coord1 and coord2.
* @param {Hash} coord1
* @param {Hash} coord2
* @returns {Float}
*/
getVectorScalarProduct: function(coord1, coord2)
{
var result = (coord1.x || 0) * (coord2.x || 0) + (coord1.y || 0) * (coord2.y || 0)
+ (coord1.z || 0) * (coord2.z || 0);
return result;
},
/**
* Returns the cross product (v1Xv2) of two vectors represented by coord1 and coord2.
* @param {Hash} coord1
* @param {Hash} coord2
* @returns {Hash}
*/
getVectorCrossProduct: function(coord1, coord2)
{
var result = {
'x': (coord1.y || 0) * (coord2.z || 0) - (coord1.z || 0) * (coord2.y || 0),
'y': (coord1.z || 0) * (coord2.x || 0) - (coord1.x || 0) * (coord2.z || 0),
'z': (coord1.x || 0) * (coord2.y || 0) - (coord1.y || 0) * (coord2.x || 0)
};
return result;
},
/**
* Returns included angle of two vectors represented by vector1 and vector2.
* Note this method always returns result less than Math.PI / 2.
* @param {Hash} vector1
* @param {Hash} vector2
* @returns {Number}
*/
getVectorIncludedAngle: function(vector1, vector2)
{
var CU = Kekule.CoordUtils;
var cross = Kekule.GeometryUtils.getVectorCrossProduct(vector1, vector2);
var zeroCoord = {'x': 0, 'y': 0, 'z': 0};
var divV = (CU.getDistance(vector1, zeroCoord) * CU.getDistance(vector2, zeroCoord));
var sinAngleV = CU.getDistance(cross, zeroCoord) / divV;
//var cosAngleV = Kekule.GeometryUtils.getVectorScalarProduct(coord1, coord2) / divV;
var result = Math.asin(sinAngleV); // asin always returns value between -Pi/2 ~ Pi / 2
//var result = Math.acos(cosAngleV);
/*
if (cross.z < 0)
result = Math.PI - result;
*/
return result;
},
/**
* Returns included angle of two vectors represented by vector1 and vector2.
* Note this method always returns result between 0 to 2pi.
* @param {Hash} vector1
* @param {Hash} vector2
* @returns {Number}
*/
getVectorIncludedAngle2: function(vector1, vector2)
{
return Kekule.GeometryUtils.standardizeAngle(Math.atan2(vector2.y, vector2.x) - Math.atan2(vector1.y, vector1.x));
},
/*
* Returns sin and cos value of dihedral angle of plane (c1, c2, c3) and (c2, c3, c4) while c1-4 are coords of 3D.
* This method is based on formula (when A1, A2, A3 and B1, B2, B3 are three points in each plane):
* alpha = arcsin(|Ua × Ub| / |Ua||Ub|)
* alpha = arccos(Ua.Ub) / |Ua||Ub|)
* where Ua = (A2−A1) × (A3−A1) and Ub = (B2−B1) × (B3−B1).
* Here we appoint A1(B1) / A2(B2) to c2 / c3, A3 to c1 and B3 to c4.
* @param {Hash} c1
* @param {Hash} c2
* @param {Hash} c3
* @param {Hash} c4
* @returns {Hash} A hash contains {'sinValue': sinValue, 'cosValue': cosValue}.
*/
/*
getSinCosValueOfDihedralAngleOfCoords: function(c1, c2, c3, c4)
{
var CU = Kekule.CoordUtils;
var GU = Kekule.GeometryUtils;
var v = CU.substract(c3, c2);
var ua = GU.getVectorCrossProduct(v, CU.substract(c1, c2));
var ub = GU.getVectorCrossProduct(v, CU.substract(c4, c2));
var zero = {'x': 0, 'y': 0, 'z': 0};
var divV = (CU.getDistance(ua, zero) * CU.getDistance(ub, zero));
var sinValue = CU.getDistance(GU.getVectorCrossProduct(ua, ub), zero) / divV;
var cosValue = GU.getVectorScalarProduct(ua, ub) / divV;
return {
'sinValue': sinValue, 'cosValue': cosValue
};
},
*/
/*
* Returns dihedral angle of plane (c1, c2, c3) and (c2, c3, c4) while c1-4 are coords of 3D.
* This method is based on formula (when A1, A2, A3 and B1, B2, B3 are three points in each plane):
* alpha = arcsin(|Ua × Ub| / |Ua||Ub|)
* where Ua = (A2−A1) × (A3−A1) and Ub = (B2−B1) × (B3−B1).
* @param {Hash} c1
* @param {Hash} c2
* @param {Hash} c3
* @param {Hash} c4
* @returns {Float}
*/
/*
getDihedralAngleOfCoords: function(c1, c2, c3, c4)
{
var sinCosValue = Kekule.GeometryUtils.getSinValueOfDihedralAngleOfCoords(c1, c2, c3, c4);
return Math.asin(sinCosValue.sinValue);
},
*/
/**
* Returns dihedral angle defined by three vectors (e.g., three bonds in molecule).
* v1-3 is formed by {x, y, z} coords. The calculation is based on formula:
* alpha = atan2([v1×v2]×[v2×v3].[v2/|v2|], [v1×v2].[v2×v3])
* (http://en.wikipedia.org/wiki/Dihedral_angle)
* @param {Hash} v1
* @param {Hash} v2
* @param {Hash} v3
* @returns {Float} An angle of 0-2Pi is returned.
* If angle can not be calculated (e.g., v1/v2 or v2/v3 on same line, not forms a plane),
* a negtive value will be returned instead.
*/
getDihedralAngleOfVectors: function(v1, v2, v3)
{
var CU = Kekule.CoordUtils;
var GU = Kekule.GeometryUtils;
var v1Xv2 = GU.getVectorCrossProduct(v1, v2);
var v2Xv3 = GU.getVectorCrossProduct(v2, v3);
var error = CU.getDistance(v2) / 1e5; // TODO: currently error is fixed
if (CU.isZero(v1Xv2, error) || CU.isZero(v2Xv3, error)) // can not calculate
return -1;
var d = CU.standardize(v2);
var y = GU.getVectorScalarProduct(GU.getVectorCrossProduct(v1Xv2, v2Xv3), d);
var x = GU.getVectorScalarProduct(v1Xv2, v2Xv3);
var result = Math.atan2(y, x);
if (result < 0)
result += Math.PI * 2;
return result;
},
/**
* Returns dihedral angle of plane (c1, c2, c3) and (c2, c3, c4) while c1-4 are coords of 3D.
* @param {Hash} c1
* @param {Hash} c2
* @param {Hash} c3
* @param {Hash} c4
* @returns {Float}
*/
getDihedralAngleOfPoints: function(c1, c2, c3, c4)
{
var CU = Kekule.CoordUtils;
var v1 = CU.substract(c2, c1);
var v2 = CU.substract(c3, c2);
var v3 = CU.substract(c4, c3);
return Kekule.GeometryUtils.getDihedralAngleOfVectors(v1, v2, v3);
},
/**
* Returns the cross point of a vertical line from coord to a existing line (lineCoord1-lineCoord2)
* @param {Hash} coord
* @param {Hash} lineCoord1
* @param {Hash} lineCoord2
* @param {Bool} isUnlimitedLine If true, the line length will not be considered.
* @returns {Hash}
*/
getPerpendicularCrossPointFromCoordToLine: function(coord, lineCoord1, lineCoord2, isUnlimitedLine)
{
// method from http://blog.sina.com.cn/s/blog_4bf793ad0100gudn.html
var result = {};
if (Math.abs(lineCoord1.x - lineCoord2.x) < 1e-10) // a vertical line or near vertical line
{
result.x = lineCoord1.x;
result.y = coord.y;
}
else
{
var k = (lineCoord2.y - lineCoord1.y) / (lineCoord2.x - lineCoord1.x);
// 垂线的斜率为 - 1 / k,垂线方程为:y = (-1/k) * (x - coord.x) + coord.y
// calc cross point
// x = ( k^2 * pt1.x + k * (point.y - pt1.y ) + point.x ) / ( k^2 + 1) ,y = k * ( x - pt1.x) + pt1.y
result.x = (k * k * lineCoord1.x + k * (coord.y - lineCoord1.y) + coord.x) / (k * k + 1);
result.y = k * (result.x - lineCoord1.x) + lineCoord1.y;
}
// check if cross point is between two coords of line
var betweenFlag;
if (isUnlimitedLine)
betweenFlag = true;
else
{
var vector = {'x': lineCoord1.x - lineCoord2.x, 'y': lineCoord1.y - lineCoord2.y};
if (Math.abs(vector.y) > Math.abs(vector.x))
betweenFlag = (Math.sign(result.y - lineCoord1.y) * Math.sign(result.y - lineCoord2.y) <= 0);
else
betweenFlag = (Math.sign(result.x - lineCoord1.x) * Math.sign(result.x - lineCoord2.x) <= 0);
}
if (!betweenFlag) // not inside with limited line
return null;
else
return result;
},
/**
* Returns the minial distance from point (coord) to line (lineCoord1, lineCoord2)
* @param {Hash} coord
* @param {Hash} lineCoord1
* @param {Hash}lineCoord2
* @param {Bool} isUnlimitedLine If true, the line length will not be considered.
* @returns {Hash}
*/
getDistanceFromPointToLine: function(coord, lineCoord1, lineCoord2, isUnlimitedLine)
{
var crossPoint = Kekule.GeometryUtils.getPerpendicularCrossPointFromCoordToLine(coord, lineCoord1, lineCoord2, isUnlimitedLine);
/*
// check if cross point is between two coords of line
var betweenFlag;
var vector = {'x': lineCoord1.x - lineCoord2.x, 'y': lineCoord1.y - lineCoord2.y};
if (isUnlimitedLine)
betweenFlag = true;
else
{
if (Math.abs(vector.y) > Math.abs(vector.x))
betweenFlag = (Math.sign(crossPoint.y - lineCoord1.y) * Math.sign(crossPoint.y - lineCoord2.y) <= 0);
else
betweenFlag = (Math.sign(crossPoint.x - lineCoord1.x) * Math.sign(crossPoint.x - lineCoord2.x) <= 0);
}
*/
if (!crossPoint) // not inside
{
var d0 = Kekule.CoordUtils.getDistance(coord, lineCoord1);
var d1 = Kekule.CoordUtils.getDistance(coord, lineCoord2);
return Math.min(d0, d1);
}
else // inside line
{
var distance = Kekule.CoordUtils.getDistance(coord, crossPoint);
return distance;
}
},
/**
* Returns the cross point coord of line1 and line2. If no cross point or the point out of lines, null will be returned.
* The algorithm is explained in https://www.jb51.net/article/90104.htm.
* @param {Hash} line1Coord1
* @param {Hash} line1Coord2
* @param {Hash} line2Coord1
* @param {Hash} line2Coord2
* @returns {Hash}
*/
getCrossPointOfLines: function(line1Coord1, line1Coord2, line2Coord1, line2Coord2)
{
// 三角形abc 面积的2倍
var area_abc = (line1Coord1.x - line2Coord1.x) * (line1Coord2.y - line2Coord1.y) - (line1Coord1.y - line2Coord1.y) * (line1Coord2.x - line2Coord1.x);
// 三角形abd 面积的2倍
var area_abd = (line1Coord1.x - line2Coord2.x) * (line1Coord2.y - line2Coord2.y) - (line1Coord1.y - line2Coord2.y) * (line1Coord2.x - line2Coord2.x);
// 面积符号相同则两点在线段同侧,不相交 (对点在线段上的情况,本例当作不相交处理);
if ( area_abc*area_abd>=0 ) {
return null;
}
// 三角形cda 面积的2倍
var area_cda = (line2Coord1.x - line1Coord1.x) * (line2Coord2.y - line1Coord1.y) - (line2Coord1.y - line1Coord1.y) * (line2Coord2.x - line1Coord1.x);
// 三角形cdb 面积的2倍
// 注意: 这里有一个小优化.不需要再用公式计算面积,而是通过已知的三个面积加减得出.
var area_cdb = area_cda + area_abc - area_abd ;
if ( area_cda * area_cdb >= 0 ) {
return null;
}
//计算交点坐标
var t = area_cda / ( area_abd - area_abc );
var dx= t*(line1Coord2.x - line1Coord1.x),
dy= t*(line1Coord2.y - line1Coord1.y);
return { x: line1Coord1.x + dx , y: line1Coord1.y + dy };
},
/**
* Simplify curve to line segments using Ramer–Douglas–Peucker algorithm.
* @param {Array} curvePoints Array of {x, y} coords to define a curve.
* @param {Float} distanceThreshold
* @returns {Array} Array of {x, y} coords, every two points define a line segment.
* @private
*/
simplifyCurveToLineSegments: function(curvePoints, distanceThreshold)
{
//distanceThreshold = distanceThreshold || this.getLineSimplificationDistanceThreshold();
if (!distanceThreshold)
return curvePoints;
if (curvePoints.length <= 2)
return Kekule.ArrayUtils.clone(curvePoints);
return Kekule.GeometryUtils._simplifyCurvePartToLineSegments(curvePoints, 0, curvePoints.length - 1, distanceThreshold);
},
/** @private */
_simplifyCurvePartToLineSegments: function(curvePoints, startIndex, endIndex, distanceThreshold)
{
var GU = Kekule.GeometryUtils;
var startCoord = curvePoints[startIndex];
var endCoord = curvePoints[endIndex];
if (endIndex - startIndex <= 1)
{
return [startCoord, endCoord];
}
var maxDistance = distanceThreshold, maxIndex = null;
for (var i = startIndex + 1; i < endIndex; ++i)
{
var d = Kekule.GeometryUtils.getDistanceFromPointToLine(curvePoints[i], startCoord, endCoord);
if (d > maxDistance)
{
maxDistance = d;
maxIndex = i;
}
}
if (maxIndex === null) // no max distance point
{
return [startCoord, endCoord];
}
else // split track to two
{
var lines1 = GU._simplifyCurvePartToLineSegments(curvePoints, startIndex, maxIndex, distanceThreshold);
var lines2 = GU._simplifyCurvePartToLineSegments(curvePoints, maxIndex, endIndex, distanceThreshold);
lines2.shift(); // remove the common point between lines1 and lines2
var result = [].concat(lines1).concat(lines2);
return result;
}
}
};
/**
* Utility methods about geometry box (2D or 3D).
* Box is a region defined by two coord values.
* @class
*/
Kekule.BoxUtils = {
/**
* Create a box with two coord at top-left and bottom-right.
* @param {Hash} coord1
* @param {Hash} coord2
* @returns {Hash}
*/
createBox: function(coord1, coord2)
{
var result = {};
var x1 = coord1.x || 0;
var y1 = coord1.y || 0;
var x2 = coord2.x || 0;
var y2 = coord2.y || 0;
(x1 <= x2)?
(result.x1 = x1, result.x2 = x2):
(result.x1 = x2, result.x2 = x1);
(y1 <= y2)?
(result.y1 = y1, result.y2 = y2):
(result.y1 = y2, result.y2 = y1);
if (Kekule.ObjUtils.notUnset(coord1.z) && Kekule.ObjUtils.notUnset(coord2.z))
{
(coord1.z <= coord2.z)?
(result.z1 = coord1.z, result.z2 = coord2.z):
(result.z1 = coord2.z, result.z2 = coord1.z);
}
return result;
},
/**
* Clone a box.
* @param {Hash} box
* @returns {Hash}
*/
clone: function(box)
{
var result = {
'x1': box.x1, 'y1': box.y1,
'x2': box.x2, 'y2': box.y2
};
if (Kekule.ObjUtils.notUnset(box.z1) && Kekule.ObjUtils.notUnset(box.z2))
{
result.z1 = box.z1;
result.z2 = box.z2;
}
return result;
},
/**
* Convert a box to a rect defined by left, top, width and height.
* @param {Hash} box
* @returns {Hash}
*/
convertToRect: function(box)
{
var b = Kekule.BoxUtils.normalize(box);
return Kekule.RectUtils.createRect(b.x1, b.y1, b.x2 - b.x1, b.y2 - b.y1);
},
/**
* Normalize the x/y values in box.
* Some times x1 > x2 or y1 > y2, normalize will ensure x1/y1 is always no larger then x2/y2.
* @param {Hash} box
* @returns {Hash}
*/
normalize: function(box)
{
var result = {'x1': Math.min(box.x1, box.x2), 'y1': Math.min(box.y1, box.y2), 'x2': Math.max(box.x1, box.x2), 'y2': Math.max(box.y1, box.y2)};
if (Kekule.ObjUtils.notUnset(box.z1) && Kekule.ObjUtils.notUnset(box.z2))
{
result.z1 = Math.min(box.z1, box.z2);
result.z2 = Math.max(box.z1, box.z2);
}
return result;
},
/**
* Shift box to a new location.
* @param {Float} deltaX
* @param {Float} deltaY
* @param {Float} deltaZ
* @returns {Hash}
*/
shiftBox: function(box, deltaX, deltaY, deltaZ)
{
var dx = deltaX || 0;
var dy = deltaY || 0;
var dz = deltaZ || 0;
var coord1 = {'x': box.x1 + dx, 'y': box.y1 + dy};
var coord2 = {'x': box.x2 + dx, 'y': box.y2 + dy};
if (Kekule.ObjUtils.notUnset(box.z1) && Kekule.ObjUtils.notUnset(box.z2))
{
coord1.z = box.z1 + dz;
coord2.z = box.z2 + dz;
}
var result = Kekule.BoxUtils.createBox(coord1, coord2);
return result;
},
/**
* Inflate the size of box. topleft - inflation and bottomright + inflation.
* X/Y/Z may have different inflation. If only one inflation value is provided, the box
* will be use this value on both width and height (and depth).
* @param {Hash} box
* @param {Float} inflationX
* @param {Float} inflationY
* @returns {Hash}
*/
inflateBox: function(box, inflationX, inflationY, inflationZ)
{
if (Kekule.ObjUtils.isUnset(inflationY))
inflationY = inflationX;
if (Kekule.ObjUtils.isUnset(inflationZ))
inflationZ = inflationX;
var result = Kekule.BoxUtils.normalize(box);
var result = {
x1: result.x1 - inflationX,
y1: result.y1 - inflationY,
x2: result.x2 + inflationX,
y2: result.y2 + inflationY
}
if (Kekule.ObjUtils.notUnset(result.z1) && Kekule.ObjUtils.notUnset(result.z2))
{
result.z1 = result.z1 - inflationZ;
result.z2 = result.z2 + inflationZ;
}
return result;
},
/**
* Get the smallest big box that contains box1 and box2.
* @param {Hash} box1
* @param {Hash} box2
* @returns {Hash}
*/
getContainerBox: function(box1, box2)
{
if (!box1)
return Kekule.BoxUtils.clone(box2); //Object.extend({}, box2);
else if (!box2)
return Kekule.BoxUtils.clone(box1); //Object.extend({}, box1);
var b1 = Kekule.BoxUtils.normalize(box1);
var b2 = Kekule.BoxUtils.normalize(box2);
var result = {
'x1': Math.min(b1.x1, b2.x1),
'y1': Math.min(b1.y1, b2.y1),
'x2': Math.max(b1.x2, b2.x2),
'y2': Math.max(b1.y2, b2.y2)
};
if (Kekule.ObjUtils.notUnset(b1.z1) && Kekule.ObjUtils.notUnset(b1.z2)
&& Kekule.ObjUtils.notUnset(b2.z1) && Kekule.ObjUtils.notUnset(b2.z2))
{
result.z1 = Math.min(b1.z1, b2.z1);
result.z2 = Math.max(b1.z2, b2.z2);
}
return result;
},
/**
* Returns intersection of two boxes.
* @param {Hash} box1
* @param {Hash} box2
* @returns {Hash}
*/
getIntersection: function(box1, box2)
{
var b1 = Kekule.BoxUtils.normalize(box1);
var b2 = Kekule.BoxUtils.normalize(box2);
var r = {
'x1': Math.max(b1.x1, b2.x1),
'y1': Math.max(b1.y1, b2.y1),
'x2': Math.min(b1.x2, b2.x2),
'y2': Math.min(b1.y2, b2.y2)
};
if ((r.x1 > r.x2) || (r.y1 > r.y2))
return null;
if (Kekule.ObjUtils.notUnset(b1.z1) && Kekule.ObjUtils.notUnset(b1.z2)
&& Kekule.ObjUtils.notUnset(b2.z1) && Kekule.ObjUtils.notUnset(b2.z2))
{
r.z1 = Math.max(b1.z1, b2.z1);
r.z2 = Math.min(b1.z2, b2.z2);
if (r.z1 > r.z2)
return null;
}
return r;
},
/**
* Check if two box has intersection.
* @param {Hash} box1
* @param {Hash} box2
* @returns {Bool}
*/
hasIntersection: function(box1, box2)
{
return !!Kekule.BoxUtils.getIntersection(box1, box2);
},
/**
* Check if two boxes are the same.
* @param {Hash} b1
* @param {Hash} b2
* @returns {Bool}
*/
isEqual: function(b1, b2)
{
return (Math.min(b1.x1, b1.x2) === Math.min(b2.x1, b2.x2))
&& (Math.max(b1.x1, b1.x2) === Math.max(b2.x1, b2.x2))
&& (Math.min(b1.y1, b1.y2) === Math.min(b2.y1, b2.y2))
&& (Math.max(b1.y1, b1.y2) === Math.max(b2.y1, b2.y2));
},
/**
* Check if innerBox is inside outerBox.
* @param {Hash} innerBox
* @param {Hash} outerBox
* @returns {Bool}
*/
isInside: function(innerBox, outerBox)
{
var b1 = innerBox;
var b2 = outerBox;
return (Math.min(b1.x1, b1.x2) >= Math.min(b2.x1, b2.x2))
&& (Math.max(b1.x1, b1.x2) <= Math.max(b2.x1, b2.x2))
&& (Math.min(b1.y1, b1.y2) >= Math.min(b2.y1, b2.y2))
&& (Math.max(b1.y1, b1.y2) <= Math.max(b2.y1, b2.y2))
},
/**
* Returns center coord of 2D or 3D box.
* @param {Hash} box
* @returns {Hash}
*/
getCenterCoord: function(box)
{
var result = {
'x': (box.x1 || 0 + box.x2 || 0) / 2,
'y': (box.y1 || 0 + box.y2 || 0) / 2
};
if (Kekule.ObjUtils.notUnset(box.z1) || Kekule.ObjUtils.notUnset(box.z2))
result.z = (box.z1 || 0 + box.z2 || 0) / 2;
return result;
},
/**
* Returns the min and max corner coords of box.
* @param {Hash} box
* @return {Hash} {min: coord, max: coord}
*/
getMinMaxCoords: function(box)
{
var b = Kekule.BoxUtils.normalize(box);
var result = {
'min': {'x': b.x1, 'y': b.y1},
'max': {'x': b.x2, 'y': b.y2}
};
if (Kekule.ObjUtils.notUnset(b.z1)) // 2D box
{
result.min.z = b.z1;
result.max.z = b.z2;
}
return result;
},
/**
* To a 2D box, returns four coords of box top-left, top-right, bottom-left and bottom-right.
* To a 3D box, returns coords of 8 corners.
* @param {Hash} box
* @returns {Array} Array of coords.
*/
getCornerCoords: function(box)
{
var b = Kekule.BoxUtils.normalize(box);
if (Kekule.ObjUtils.isUnset(b.z1)) // 2D box
{
return [
{'x': b.x1, 'y': b.y1},
{'x': b.x1, 'y': b.y2},
{'x': b.x2, 'y': b.y1},
{'x': b.x2, 'y': b.y2}
];
}
else // 3D box
{
return [
{'x': b.x1, 'y': b.y1, 'z': b.z1},
{'x': b.x1, 'y': b.y2, 'z': b.z1},
{'x': b.x2, 'y': b.y1, 'z': b.z1},
{'x': b.x2, 'y': b.y2, 'z': b.z1},
{'x': b.x1, 'y': b.y1, 'z': b.z2},
{'x': b.x1, 'y': b.y2, 'z': b.z2},
{'x': b.x2, 'y': b.y1, 'z': b.z2},
{'x': b.x2, 'y': b.y2, 'z': b.z2}
];
}
},
/**
* Do a 2d transform to a box.
* The tranform will be performed in the following order:
* rotate, scale, translate.
* Note that there may be rotation in transform, so box may be larger than expected.
* @param {Hash} box
* @param {Hash} options Transform options, can has the following fields:
* scale, scaleX, scaleY
* translateX, translateY,
* rotateAngle,
* center: {x, y} (center of rotate and scale).
* @returns {Hash} 2D box after transform.
*/
transform2D: function(box, options)
{
var coords = Kekule.BoxUtils.getCornerCoords(box);
var transformedCoords = [];
for (var i = 0, l = coords.length; i < l; ++i)
{
transformedCoords.push(Kekule.CoordUtils.transform2D(coords[i], options));
}
return Kekule.CoordUtils.getContainerBox(transformedCoords);
},
/**
* Do a 3d transform to a box.
* The tranform will be performed in the following order:
* rotate, scale, translate.
* @param {Hash} box 3D box.
* @param {Hash} options Transform options, can has the following fields:
* scale, scaleX, scaleY, scaleZ, // all scale from center or zero point
* translateX, translateY, translateZ,
* rotateX, rotateY, rotateZ,
* rotateAngle, rotateAxisVector // rotate around a vector start from center or zero point
* center: {x, y, z} (center of rotate and scale).
* @returns {Hash} 3D box after transform.
*/
transform3D: function(box, options)
{
var coords = Kekule.BoxUtils.getCornerCoords(box);
var transformedCoords = [];
for (var i = 0, l = coords.length; i < l; ++i)
{
transformedCoords.push(Kekule.CoordUtils.transform3D(coords[i], options));
}
return Kekule.CoordUtils.getContainerBox(transformedCoords);
}
};
/**
* Utility methods about 2D rectangle.
* Rectangle is a region defined by left, top, width and height.
* @class
*/
Kekule.RectUtils = {
/**
* Create a rect object.
* @returns {Hash}
*/
createRect: function(left, top, width, height)
{
var result = {'left': left, 'top': top, 'width': width, 'height': height};
return result;
},
/**
* Clone a rect object.
* @param {Hash} rect
* @returns {Hash}
*/
clone: function(rect)
{
return {
'left': rect.left,
'top': rect.top,
'width': rect.width,
'height': rect.height
};
},
/**
* Returns if the width/height of rect is zero
* @param rect
*/
isZero: function(rect)
{
return (rect.width === 0) && (rect.height === 0);
},
/**
* Convert a rect to a box defined by two coords.
* @param {Hash} rect
* @returns {Hash}
*/
convertToBox: function(rect)
{
var coord1 = Kekule.CoordUtils.create(rect.left, rect.top);
var coord2 = Kekule.CoordUtils.create(rect.left + rect.width, rect.top + rect.height);
return Kekule.BoxUtils.createBox(coord1, coord2);
},
/**
* Inflate the size of rect. Top left coord of rect will not change.
* X/Y may have different inflation. If only one inflation value is provided, the rect
* will be use this value on both width and height.
* @param {Hash} rect
* @param {Float} inflateX
* @param {Float} inflateY
* @returns {Hash}
*/
inflateRect: function(rect, inflateX, inflateY)
{
if (Kekule.ObjUtils.isUnset(inflateY))
inflateY = inflateX;
var result = Kekule.RectUtils.createRect(rect.left, rect.top, rect.width + inflateX, rect.height + inflateY);
return result;
},
/**
* Change the top and left value of rect.
* @param {Hash} rect
* @param {Float} deltaX
* @param {Float} deltaY
* @returns {Hash}
*/
shiftRect: function(rect, deltaX, deltaY)
{
var dx = deltaX || 0;
var dy = deltaY || 0;
var result = Kekule.RectUtils.createRect(rect.left + dx, rect.top + dy, rect.width, rect.height);
return result;
},
/**
* Get the smallest big rect that contains rect1 and rect2.
* @param {Hash} rect1
* @param {Hash} rect2
* @returns {Hash}
*/
getContainerRect: function(rect1, rect2)
{
var b1 = Kekule.RectUtils.convertToBox(rect1);
var b2 = Kekule.RectUtils.convertToBox(rect2);
var b3 = Kekule.BoxUtils.getContainerBox(b1, b2);
return Kekule.BoxUtils.convertToRect(b3);
},
/**
* Returns intersection of two rects.
* @param {Hash} rect1
* @param {Hash} rect2
* @returns {Hash}
*/
getIntersection: function(rect1, rect2)
{
var b1 = Kekule.RectUtils.convertToBox(rect1);
var b2 = Kekule.RectUtils.convertToBox(rect2);
var b3 = Kekule.BoxUtils.getIntersection(b1, b2);
return b3? Kekule.BoxUtils.convertToRect(b3): null;
},
/**
* Check if two rects has intersection.
* @param {Hash} rect1
* @param {Hash} rect2
* @returns {Bool}
*/
hasIntersection: function(rect1, rect2)
{
return !!Kekule.RectUtils.getIntersection(rect1, rect2);
}
};
/**
* Utility methods for zoom in/out functions.
* @class
*/
Kekule.ZoomUtils = {
/** @private */
PREDEFINED_ZOOM_RATIOS: [0.1, 0.3, 0.5, 0.66, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5, 7, 10, 15, 20],
/**
* Returns nearest zoom level to current ratio.
* @param {Float} currRatio
* @param {Array} constraintZoomLevels
* @returns {Float}
*/
getNearestZoomLevel: function(currRatio, constraintZoomLevels)
{
var rs = constraintZoomLevels || Kekule.ZoomUtils.PREDEFINED_ZOOM_RATIOS;
var len = rs.length;
if (currRatio > rs[len - 1]) // larger than the max one
return rs[len - 1];
else if (currRatio < rs[0]) // smaller than one of predefined ones
return rs[0];
else
{
for (var i = 0; i < len - 1; ++i)
{
var ps0 = rs[i];
var ps1 = rs[i + 1];
if (currRatio >= ps0 && currRatio <= ps1)
{
if (ps0 === 1 || ps1 === 1)
return 1;
var r = (currRatio - ps0) / (ps1 - ps0);
return (r <= 0.5)? ps0: ps1;
}
}
}
},
/**
* Get a predefined ratio that bigger than currRatio, which can be used in usual zoom in function.
* @param {Float} currRatio
* @param {Int} step
* @param {Array} constraintZoomLevels
* @returns {Float}
*/
getNextZoomInRatio: function(currRatio, step, constraintZoomLevels)
{
if (!step)
step = 1;
var rs = constraintZoomLevels || Kekule.ZoomUtils.PREDEFINED_ZOOM_RATIOS;
var len = rs.length;
if (currRatio < rs[len - 1]) // smaller than one of predefined ones
{
for (var i = 0; i < len; ++i)
{
if (rs[i] > currRatio)
{
return rs[Math.min(i + step, len) - 1];
}
}
}
//else
return currRatio; // bigger than all, return original value
},
/**
* Get a predefined ratio that smaller than currRatio, which can be used in usual zoom out function.
* @param {Float} currRatio
* @param {Int} step
* @param {Array} constraintZoomLevels
* @returns {Float}
*/
getNextZoomOutRatio: function(currRatio, step, constraintZoomLevels)
{
if (!step)
step = 1;
var rs = constraintZoomLevels || Kekule.ZoomUtils.PREDEFINED_ZOOM_RATIOS;
var len = rs.length;
if (currRatio > rs[0]) // smaller than one of predefined ones
{
for (var i = len - 1; i >= 0; --i)
{
if (rs[i] < currRatio)
return rs[Math.max(i - step + 1, 0)];
}
}
//else
return currRatio; // bigger than all, return original value
}
};