1 /**
  2  * @author Gillis Haasnoot <gillis.haasnoot@gmail.com>
  3  * @package Banana.Util
  4  * @summary Various utils
  5  */
  6 
  7 /**
  8 @class Banana.Util
  9 @name Banana.Util
 10 */
 11 
 12 goog.provide('Banana.Util.Utils');
 13 
 14 /**
 15  * serializes a mixed object
 16  * 
 17  * @param {object} data
 18  */
 19 Banana.Util.serialize = function(data)
 20 {
 21 	return JSON._stringify(data);
 22 };
 23 
 24 /**
 25  * unserializes a serialized string
 26  * 
 27  * @param {object} data
 28  */
 29 Banana.Util.unserialize = function(data)
 30 {
 31 	return JSON.parse(data);
 32 };
 33 
 34 /**
 35  * @returns Function based on namespace
 36  */
 37 Banana.Util.NamespaceToFunction = function(ns)
 38 {
 39 	var s = ns.split('.');
 40 
 41 	var fn = window;
 42 
 43 	for (var i = 0, len = s.length; i < len; i++)
 44 	{
 45 		fn = fn[s[i]];
 46 		if (!fn)
 47 		{
 48 			return null;
 49 		}
 50 	}
 51 	return fn;
 52 };
 53 
 54 /**
 55  *	@returns data by path name
 56  *
 57  */
 58 Banana.Util.getDataByPath = function(data,path)
 59 {
 60 	var path = path.split('.');
 61 
 62 	var d = data;
 63 
 64 	for(var i=0,len = path.length; i < len; i++)
 65 	{
 66 		d = d[path[i]];
 67 		if (d == undefined)
 68 		{
 69 			return null;
 70 		}
 71 	}
 72 
 73 	return d;
 74 };
 75 
 76 // Use the OWL library for the cloning
 77 goog.require('Banana.thirdParty.OWLClone');
 78 /**
 79  * Clone an object. Standard it wil do a shallow copy, when a deep clone is requested
 80  * it will copy everything by value
 81  * 
 82  * @param {mixed} data The data to clone
 83  * @param {Boolean} deep Perform a deep clone
 84  * 
 85  * @return {mixed} The cloned data
 86  * @constructor
 87  */
 88 Banana.Util.Clone = function(data, deep) {
 89 
 90 	if (deep)
 91 	{
 92 		return owl.deepCopy(data);
 93 	}
 94 	return owl.clone(data);
 95 };
 96 
 97 /**
 98  * Find an object by a field value
 99  *
100  * @param {Array} data  Dataset to search
101  * @param {mixed} field Field to search
102  * @param {mixed} value Value to find
103  *
104  * @return Object|null Object found
105  * @constructor
106  */
107 Banana.Util.FindByField = function(data, field, value)
108 {
109 	for (var x = 0, dataSize = data.length; x < dataSize; x++) {
110         if (data[x][field] == value) {
111         	return data[x];
112         }
113     }
114     return null;
115 };
116 
117 /**
118  * Combine an array collection by a field
119  *
120  * @param Array  arr   Array to combine
121  * @param String field Field to combine on
122  *
123  * @returns Array containing arrays with the items
124  * @constructor
125  */
126 Banana.Util.CombineArrayByField = function(arr, field)
127 {
128 	if (!(arr instanceof Array))
129 	{
130 		log.error('Banana.Util.CombineArrayByField - List should be of type Array');
131 		return arr;
132 	}
133 	else if (!field)
134 	{
135 		log.error('Banana.Util.CombineArrayByField - No field given');
136 		return arr;
137 	}
138 
139 	var list = {};
140 
141 	for (var x = 0; x < arr.length; x++)
142 	{
143 		if (!list[arr[x][field]])
144 		{
145 			list[arr[x][field]] = [];
146 		}
147 		list[arr[x][field]].push(arr[x]);
148 	}
149 
150 	var result = [];
151 	for (var i in list)
152 		result.push(list[i]);
153 
154 	return result;
155 };
156 
157 /**
158  * Copies all new properties from newdata to olddata
159  * All references in olddata stay intact
160  * 
161  * NOTE: arrays should contain objects with an identifier, or objects
162  * identified by their position in the array. Mixing them will lead to 
163  * problems.
164  * 
165  * NOTE2: olddata will be identical to newdata in the end. Properties
166  * of olddata that are not in newdata will be removed.
167  * 
168  * example  
169  * old [a,b,c]
170  * new [a,x,d] with x having identifier
171  * result [a,b,d] cause x is overwritten by d
172  * 
173  * 
174  * @param {Object} newdata
175  * @param {Object} olddata
176  * @param {String} identifier
177  * @param {Object} reference to parent object. used by framework. usefull to keep references intact
178  * @return {Object} instance of olddata with newdata recursively copied inside
179  * 
180  * @constructor
181  */
182 Banana.Util.CopyTo = function(newdata,olddata,identifier,refObj)
183 {
184   	if (newdata instanceof Array && olddata instanceof Array)
185 	{
186   		// Empty existing items and back them up
187   		var backup = olddata.splice(0, olddata.length);
188   		
189 		for (var i = 0, len = newdata.length; i < len; i++)
190 		{
191 			var valueA = newdata[i];
192 			if (typeof(valueA) == 'object') //arrays and objects
193 			{
194 				var objectAIdentifier = valueA[identifier];
195 				
196 				//if our object A has an identifier
197 				if (objectAIdentifier)
198 				{
199 					//check if we can find objectt with same id in the backup of B 
200 					var match = false;
201 					for (var j = 0, olen = backup.length; j < olen; j++)
202 					{
203 						var valueB = backup[j];
204 						
205 						var objectBIdentifier = valueB[identifier];
206     					if (objectBIdentifier && objectBIdentifier == objectAIdentifier)
207     					{
208 							match = true;
209 							// Let's restore from backup
210 							olddata.push(valueB);
211 							
212 							Banana.Util.CopyTo(valueA,valueB,identifier,{ref:olddata,prop:i});
213 							break;
214     					}
215 					}
216 
217 					if (!match)
218 					{
219 						// Item not found in backup, we'll use new one
220 						olddata.push(valueA);
221 					}
222 				}
223 				else
224 				{
225 					// Items is doesn't have and ID, we'll use new one
226 					olddata.push(valueA);
227 				}
228 			}
229 			else // not array/object: primitive value
230 			{
231 				olddata[i] = valueA; 
232 			}
233 		}
234 	}
235 	
236 	// seems that only new data contains the array. we just copy the array to b. 
237 	else if (newdata instanceof Array)
238 	{
239 		if (refObj)
240 		{	
241 			refObj.ref[refObj.prop] = newdata;
242 		}
243 		else
244 		{
245 			throw "Unable to assign property to object (missing reference)";
246 		}
247 	}
248 	
249 	// if its an object we also loop over it. now keys are our matches
250 	else if (typeof(newdata)== 'object')
251 	{
252 		var deadkeys = [];
253 		
254 		for(var prop in olddata)
255 		{
256 			if (typeof(olddata[prop]) == 'function') continue;
257 			
258 			deadkeys.push(prop);	
259 		}	
260 		
261 		for(var prop in newdata)
262 		{
263 			var valueA = newdata[prop];
264 
265 			if (typeof(valueA) == 'function') continue;
266 
267 			// Remove from dead keys array
268 			var index = deadkeys.indexOf(prop);
269 			if (index >= 0)
270 			{
271 				deadkeys.splice(index,1);
272 			}
273 
274 			if (typeof(newdata[prop]) == 'object' && olddata[prop])
275 			{
276 				Banana.Util.CopyTo(newdata[prop],olddata[prop],identifier,{ref:olddata,prop:prop})
277 			}
278 			else
279 			{
280 				olddata[prop] = valueA;
281 			}			
282 		}	
283 		
284 		for (var i=0;i<deadkeys.length;i++)
285 		{
286 			delete olddata[deadkeys[i]];
287 		}
288 	} 
289 
290 	return olddata;
291 }
292 
293 /**
294  * Compares 2 objects. returns true when equal.
295  * Compares recursively 
296  *
297  * @param {Object} A
298  * @param {Object} B
299  * @param {Array} list of property names which should not be compared. i.e timestamps
300  * 
301  * @return {boolean} true when equal
302  * @constructor
303  */
304 Banana.Util.ObjectsAreEqual = function(a,b,ignores)
305 {
306 	return Banana.Util.ObjectPropsSameTo(a,b,ignores) && Banana.Util.ObjectPropsSameTo(b,a,ignores);
307 }
308 
309 /**
310  * checks if A's properties are the same on B
311  *
312  * @param {object} a
313  * @param {object} b
314  * @param {Array} list of property names which should not be compared. i.e timestamps
315  * @return {boolean} true when equal
316  * @constructor
317  */
318 Banana.Util.ObjectPropsSameTo = function(a,b,ignores)
319 {
320 	//i we dont give either a a or b then not equal
321 	if (!b || !a)
322 	{
323 		return false;
324 	}
325 
326 	if (!ignores)
327 	{
328 		ignores = [];
329 	}
330 
331 	//we are going to check all property in A
332 	for (var prop in a)
333 	{
334 		if (typeof(a[prop]) == 'function') continue;
335 		
336 		if (typeof(a[prop]) == 'object' && b[prop] !== null)
337 		{
338 			if (typeof b[prop] == 'object')
339 			{
340 				//both A and B have an object assigned to their property. we need to check if those objects are the same
341 				//if not objects not the same
342 				if (!Banana.Util.ObjectsAreEqual(a[prop],b[prop],ignores))
343 				{
344 					return false;
345 				}
346 			}
347 			else
348 			{
349 				///b doesnt have an object in this property
350 				return false;
351 			}
352 		}
353 
354 		//compare string, number and bools
355 		//dont compare when prop is in the ignore list, for example datestamps
356 		else if (typeof(a[prop]) == 'string' || typeof(a[prop]) == 'number' || typeof(a[prop]) == 'boolean')
357 		{
358 			if (ignores.indexOf(prop) == -1 && b[prop] != a[prop])
359 			{
360 				return false;
361 			}
362 		}
363 	}
364 	return true;
365 };
366 
367 /**
368  * Sorts objects by key
369  * 
370  * @param {Object} obj members
371  * @param {Function} sortFunc custom sort 
372  * @return {Object} newObj members
373  * @constructor
374  */
375 Banana.Util.sortObjectKeys = function(obj,sortFunc)
376 {
377 	var formArray =[];
378 
379 	for (var key in obj)
380 	{
381 		formArray.push(key);
382 	}
383 
384 	if (sortFunc)
385 	{
386 		formArray.sort(sortFunc);
387 	}
388 	else
389 	{
390 		formArray.sort();	
391 	}
392 	
393 	var newObj = {};
394 
395 	for (var i = 0; i < formArray.length; i++)
396 	{
397 		var key = formArray[i];
398 		newObj[key] = obj[key];
399 	}
400 
401 	return newObj;		
402 };
403 
404 /**
405  * Gives a random string. Useful for UIDs
406  * @return {String}
407  * @constructor
408  */
409 Banana.Util.generateUniqueId = function()
410 {
411 	return  (((1+Math.random())*0x1000000000)|0).toString(16).substring(1);
412 }
413 
414 
415 /**
416  * Flattens tree structure
417  * @Param {Array} array
418  * @param {String} childkey the key holding the children
419  * @constructor
420  */
421 Banana.Util.flattenTreeDepthFirst = function(object,childKey,reversed)
422 {
423 	if (!reversed)
424 	{
425 		reversed = [];
426 	}
427 	
428 	if (!object)
429 	{
430 		return reversed;
431 	}
432 	
433 	if (object[childKey])
434 	{
435 		var i,len
436 		for (i =0,len=object[childKey].length; i < len; i++)
437 	    {
438 			Banana.Util.flattenTreeDepthFirst(object[childKey][i],childKey,reversed);
439 	    }
440 	}
441 	
442 	reversed.push(object);
443 	return reversed;	
444 };
445 
446 /**
447  * Iterates over array asynchronized
448  * iteration is devided into chunks where after each chunk the cpu gets time to do other tasks
449  * 
450  * TODO: let the user specify the chunksize
451  * @param {Array} array to iterate over
452  * @param {Function} cb callback called every iteration
453  * @param {int} timeout between chunks, default 0
454  * @param {Function} completeCb callback. Invoked when iteration is complete
455  * @param {Function} complete chunk callback. Invoked after each chunk completion
456  * @constructor
457  */
458 Banana.Util.arrayInteratorAsync = function(array,cb,timeout,completeCb,completeChunkCb)
459 {	
460 	if (typeof(cb) != 'function')
461 	{
462 		return log.error("No callback specified for async iterator");
463 	}
464 
465 	timeout = timeout || 0;
466 	var chunkSize = array.length > 10 ? array.length/10  : 1;
467 	
468 	function executeWait(loop)
469 	{
470 		setTimeout(function(){
471 			
472 			loop();
473 			
474 		},timeout)
475 	}	
476 	
477 	var i = -1;
478 	var alength = array.length;
479 	
480 	function loop()
481 	{
482 		i++;
483 		
484 		if (alength > i)
485 		{
486 			for (var j =0; j <= chunkSize;j++)
487 			{
488 				i++;
489 				
490 				if (i > alength)
491 				{
492 					return completeCb();
493 				}
494 				
495 				cb(array[i],i);
496 			}
497 			
498 			if (completeChunkCb)
499 			{
500 				completeChunkCb();
501 			}
502 			
503 			executeWait(loop)
504 		}
505 		else if (completeCb)
506 		{
507 			return completeCb();
508 		}
509 	};
510 	
511 	loop();
512 };