mirror of
				https://github.com/flynx/ImageGrid.git
				synced 2025-10-29 18:30:09 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1484 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			1484 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| /**********************************************************************
 | |
| * 
 | |
| * Data generation 4 implementation.
 | |
| *
 | |
| *
 | |
| **********************************************************************/
 | |
| 
 | |
| // General format info...
 | |
| //
 | |
| // Version format:
 | |
| // 	<major>.<minor>
 | |
| //
 | |
| // Major version changes mean a significant incompatibility.
 | |
| //
 | |
| // Minor version changes mean some detail changed and can be handled
 | |
| // by it's specific handler seamlessly. Backwards compatible.
 | |
| //
 | |
| //
 | |
| // For more info see:
 | |
| // 	DATA			- main data
 | |
| // 	IMAGES			- image data
 | |
| // 	MARKED			- marks data
 | |
| // 	BOOKMARKS		- bookmarks data
 | |
| // 	BOOKMARKS_DATA	- bookmarks metadata
 | |
| // 	TAGS			- tag data
 | |
| //
 | |
| //
 | |
| // Data format change history:
 | |
| // 	3.0	- Gen4 DATA format, introduced several backwards incompatible 
 | |
| // 		changes:
 | |
| // 			- added ribbon GIDs, .ribbons now is a gid indexed object
 | |
| // 			- added .ribbon_order
 | |
| // 			- added .base ribbon
 | |
| // 			- ribbons are now sparse in memory but can be compact when
 | |
| // 			  serialized.
 | |
| // 			- auto-convert from gen1 (no version) and gen3 (2.*) on load
 | |
| // 	2.3 - Minor update to sorting restrictions
 | |
| // 			- now MARKED and BOOKMARKS do not need to be sorted 
 | |
| // 			  explicitly in json, they are now sorted as a side-effect 
 | |
| // 			  of being sparse.
 | |
| // 			  This negates some restrictions posed in 2.1, including 
 | |
| // 			  conversion of 2.0 data.
 | |
| // 			  NOTE: TAGS gid sets are still compact lists, thus are 
 | |
| // 			  		actively maintained sorted.
 | |
| // 			  		...still thinking of whether making them sparse is
 | |
| // 			  		worth the work...
 | |
| // 	2.2 - Minor update to how data is handled and saved
 | |
| // 			- now DATA.current is saved separately in current.json,
 | |
| // 			  loading is done from current.json and if not found from
 | |
| // 			  data.json.
 | |
| // 			  the file is optional.
 | |
| // 			- data, marks, bookmarks, tags are now saved only if updated
 | |
| // 	2.1 - Minor update to format spec,	
 | |
| // 			- MARKED now maintained sorted, live,
 | |
| // 			- will auto-sort marks on load of 2.0 data and change 
 | |
| // 			  data version to 2.1, will need a re-save,
 | |
| // 	2.0 - Gen3 data format, still experimental,
 | |
| // 			- completely new and incompatible structure,
 | |
| // 			- use convertDataGen1(..) to convert Gen1 to 2.0 
 | |
| // 			- used for my archive, not public,
 | |
| // 			- auto-convert form gen1 on load...
 | |
| // 	none - Gen1 data format, mostly experimental,
 | |
| // 			- has no explicit version set,
 | |
| // 			- not used for real data.
 | |
| //
 | |
| //
 | |
| // NOTE: Gen1 and Gen3 refer to code generations rather than data format
 | |
| // 		iterations, Gen2 is skipped here as it is a different project 
 | |
| // 		(PortableMag) started on the same code base as ImageGrid.Viewer
 | |
| // 		generation 1 and advanced from there, back-porting some of the 
 | |
| // 		results eventually formed Gen3...
 | |
| //
 | |
| //
 | |
| /*********************************************************************/
 | |
| //
 | |
| // TODO save current crop/state as JSON (named)...
 | |
| // TODO save current order (named)...
 | |
| // TODO auto-save manual sort -- on re-sort...
 | |
| //
 | |
| //
 | |
| /*********************************************************************/
 | |
| 
 | |
| // Data class methods and API...
 | |
| //
 | |
| var DataClassPrototype = {
 | |
| 	// NOTE: we consider the input list sorted...
 | |
| 	fromList: function(list){
 | |
| 		var res = new Data()
 | |
| 		// XXX make a real ribbon gid...
 | |
| 		var gid = res.newGid('R')
 | |
| 		res.order = list
 | |
| 		res.ribbon_order.push(gid)
 | |
| 		res.ribbons[gid] = list.slice()
 | |
| 		return res
 | |
| 	},
 | |
| 	// XXX is this the right way to construct data???
 | |
| 	fromJSON: function(data){
 | |
| 		return new Data().loadJSON(data)
 | |
| 	},
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| /*********************************************************************/
 | |
| 
 | |
| // Data object methods and API...
 | |
| //
 | |
| var DataPrototype = {
 | |
| 
 | |
| 	/*****************************************************************/
 | |
| 	//
 | |
| 	// Base Terminology:
 | |
| 	// 	- gen1 methods
 | |
| 	// 		- use the data API/Format
 | |
| 	// 		- use other gen1 methods
 | |
| 	// 	- gen2 methods
 | |
| 	// 		- do NOT use the data API/Format
 | |
| 	// 		- use other methods from any of gen1 and gen2
 | |
| 	//
 | |
| 	// NOTE: only gen2 methods are marked.
 | |
| 	//
 | |
| 	//
 | |
| 	/****************************************************** Format ***/
 | |
| 	//
 | |
| 	// 	.current (gid)
 | |
| 	// 		gid of the current image
 | |
| 	//
 | |
| 	// 	.base (gid)
 | |
| 	// 		gid of the base ribbon
 | |
| 	//
 | |
| 	// 	.order
 | |
| 	// 		List of image gids setting the image order
 | |
| 	//
 | |
| 	// 		format:
 | |
| 	//	 		[ gid, .. ]
 | |
| 	//
 | |
| 	//	 	NOTE: this list may contain gids not loaded at the moment, 
 | |
| 	//	 		a common case for this is when data is cropped.
 | |
| 	//
 | |
| 	// 	.ribbon_order
 | |
| 	// 		List of ribbon gids setting the ribbon order.
 | |
| 	//
 | |
| 	// 		format:
 | |
| 	// 			[ gid, .. ]
 | |
| 	//
 | |
| 	// 	.ribbons
 | |
| 	// 		Dict of ribbons, indexed by ribbon gid, each ribbon is a 
 | |
| 	// 		sparse list of image gids.
 | |
| 	//
 | |
| 	// 		format:
 | |
| 	// 			{ gid: [ gid, .. ], .. }
 | |
| 	//
 | |
| 	// 		NOTE: ribbons are sparse...
 | |
| 	// 		NOTE: ribbons can be compact when serialized...
 | |
| 	//
 | |
| 	//
 | |
| 	/******************************************************* Utils ***/
 | |
| 	
 | |
| 	// Make a sparse list of image gids...
 | |
| 	//
 | |
| 	// This uses .order as the base for ordering the list.
 | |
| 	//
 | |
| 	// If target is given then it will get updated with the input gids.
 | |
| 	//
 | |
| 	// NOTE: this can be used to re-sort sections of a target ribbon, 
 | |
| 	// 		but care must be taken not to overwrite existing data...
 | |
| 	// NOTE: if target is given some items in it might get pushed out
 | |
| 	// 		by the new gids, especially if target is out of sync with 
 | |
| 	// 		.order, this can be avoided by setting keep_target_items 
 | |
| 	// 		(see next for more info).
 | |
| 	// 		Another way to deal with this is to .makeSparseImages(target)
 | |
| 	// 		before using it as a target.
 | |
| 	// NOTE: if keep_target_items is set items that are overwritten in 
 | |
| 	// 		the target will get pushed to gids.
 | |
| 	// 		This flag has no effect if target is an empty list (default).
 | |
| 	makeSparseImages: function(gids, target, keep_target_items){
 | |
| 		target = target == null ? [] : target
 | |
| 		keep_target_items = keep_target_items == null ? false : keep_target_items
 | |
| 		order = this.order
 | |
| 
 | |
| 		// avoid directly updating self...
 | |
| 		if(gids === target){
 | |
| 			gids = gids.slice()
 | |
| 		}
 | |
| 
 | |
| 		gids.forEach(function(e){
 | |
| 			i = order.indexOf(e)
 | |
| 			if(i >= 0){
 | |
| 				var o = target[i]
 | |
| 				// save overwritten target items if keep_target_items 
 | |
| 				// is set...
 | |
| 				if(keep_target_items 
 | |
| 						&& o != null 
 | |
| 						// if the items is already in gids, forget it...
 | |
| 						// NOTE: this is to avoid juggling loops...
 | |
| 						&& gids.indexOf(o) < 0){
 | |
| 					gids.push(o)
 | |
| 				}
 | |
| 
 | |
| 				target[i] = e
 | |
| 			}
 | |
| 		})
 | |
| 
 | |
| 		return target
 | |
| 	},
 | |
| 
 | |
| 	// Remove duplicate items from list in-place...
 | |
| 	//
 | |
| 	// NOTE: only the first occurrence is kept...
 | |
| 	// NOTE: this is slow-ish...
 | |
| 	removeDuplicates: function(lst, skip_undefined){
 | |
| 		skip_undefined = skip_undefined == null ? true : skip_undefined
 | |
| 		for(var i=0; i < lst.length; i++){
 | |
| 			if(skip_undefined && lst[i] == null){
 | |
| 				continue
 | |
| 			}
 | |
| 			if(lst.indexOf(lst[i]) != i){
 | |
| 				lst.splice(i, 1)
 | |
| 				i -= 1
 | |
| 			}
 | |
| 		}
 | |
| 		return lst
 | |
| 	},
 | |
| 
 | |
| 	// Generate a unique GID...
 | |
| 	//
 | |
| 	// XXX generate a real gid...
 | |
| 	newGid: function(prefix){
 | |
| 		prefix = prefix == null ? 'G' : prefix
 | |
| 		var gid = prefix + Date.now()
 | |
| 		while(this.ribbon_order.indexOf(gid) >= 0 
 | |
| 				|| this.order.indexOf(gid) >= 0
 | |
| 				|| this._gid_cache.indexOf(gid) >= 0){
 | |
| 			gid = prefix + Date.now()
 | |
| 		}
 | |
| 		// XXX not sure if this is a good idea...
 | |
| 		this._gid_cache.push(gid)
 | |
| 		return gid
 | |
| 	},
 | |
| 	
 | |
| 
 | |
| 
 | |
| 	/*********************************************** Introspection ***/
 | |
| 
 | |
| 	// Get image
 | |
| 	//
 | |
| 	//	Get current image:
 | |
| 	//	.getImage()
 | |
| 	//	.getImage('current')
 | |
| 	// 		-> gid
 | |
| 	//
 | |
| 	// 	Check if image is loaded/exists:
 | |
| 	// 	.getImage(gid|order)
 | |
| 	// 	.getImage(gid|order, list|ribbon)
 | |
| 	// 		-> gid
 | |
| 	// 		-> null
 | |
| 	// 		NOTE: null is returned if image does not exist.
 | |
| 	// 		NOTE: the second argument must not be an int (ribbon order)
 | |
| 	// 				to avoid conflict with the offset case below.
 | |
| 	//		NOTE: image order can be negative, thus getting an image 
 | |
| 	//				from the tail.
 | |
| 	//
 | |
| 	// 	Get first or last image in ribbon:
 | |
| 	// 	.getImage('first'[, ribbon])
 | |
| 	// 	.getImage('last'[, ribbon])
 | |
| 	// 		-> gid
 | |
| 	// 		-> null (XXX ???)
 | |
| 	// 		NOTE: the second argument must be .getRibbon(..) compatible.
 | |
| 	// 		NOTE: to get global first/last image use the index, e.g.:
 | |
| 	// 			.getImage(0) / .getImage(-1)
 | |
| 	//
 | |
| 	// 	Get image closest to current in list/ribbon:
 | |
| 	// 	.getImage(list|ribbon[, 'before'|'after'])
 | |
| 	// 		-> gid
 | |
| 	// 		-> null
 | |
| 	// 		NOTE: null is returned if there is no image before/after the
 | |
| 	// 				current image in the given list/ribbon, e.g. the 
 | |
| 	// 				current image is first/last resp.
 | |
| 	// 		NOTE: 'before' is default.
 | |
| 	// 		NOTE: the first argument must not be a number.
 | |
| 	//
 | |
| 	// 	Get image closest to current or a specific image:
 | |
| 	// 	.getImage('before'[, list|ribbon])
 | |
| 	// 	.getImage(gid|order, 'before'[, list|ribbon])
 | |
| 	// 		-> gid
 | |
| 	// 		-> null
 | |
| 	// 	.getImage('after'[, list|ribbon])
 | |
| 	// 	.getImage(gid|order, 'after'[, list|ribbon])
 | |
| 	// 		-> gid
 | |
| 	// 		-> null
 | |
| 	// 		NOTE: null is returned if there is no image before/after the
 | |
| 	// 				current image in the given list/ribbon, e.g. the 
 | |
| 	// 				current image is first/last resp.
 | |
| 	// 		NOTE: in both the above cases if gid|order is found explicitly
 | |
| 	// 			it will be returned.
 | |
| 	//
 | |
| 	// 	Get image at an offset from a given image:
 | |
| 	// 	.getImage(gid|order, offset[, list|ribbon])
 | |
| 	// 		-> gid
 | |
| 	// 		-> null
 | |
| 	// 		NOTE: null is returned if there is no image at given offset.
 | |
| 	//		NOTE: offset is calculated within the same ribbon...
 | |
| 	//
 | |
| 	// NOTE: If gid|order is not given, current image is assumed.
 | |
| 	// 		Similarly, if list|ribbon is not given then the current 
 | |
| 	// 		ribbon is used.
 | |
| 	// NOTE: if input gid is invalid this will return -1 (XXX is this good???)
 | |
| 	// NOTE: the folowing are equivalent:
 | |
| 	// 			D.getImage('current', -1, R)
 | |
| 	// 			D.getImage('before', R) 
 | |
| 	// 			D.getImage('current', 'before', R)
 | |
| 	// 		where D is a Data object and R a ribbon id/index different 
 | |
| 	// 		from the current ribbon (see next note for details).
 | |
| 	// NOTE: in before/after modes, if the target image is found then it
 | |
| 	// 		will be returned, use offset to explicitly get the image 
 | |
| 	// 		before/after target.
 | |
| 	//
 | |
| 	// XXX most of the complexity here comes from argument DSL parsing,
 | |
| 	// 		might be good to revise argument syntax and handling...
 | |
| 	getImage: function(target, mode, list){
 | |
| 		// no args...
 | |
| 		if(target == null && mode == null && list == null){
 | |
| 			return this.current
 | |
| 		}
 | |
| 
 | |
| 		// current image shorthand...
 | |
| 		if(target == 'current'){
 | |
| 			target = this.current
 | |
| 		}
 | |
| 
 | |
| 		// order -> gid special case...
 | |
| 		if(typeof(target) == typeof(123)){
 | |
| 			if(target >= this.order.length){
 | |
| 				return null
 | |
| 			}
 | |
| 			target = target < 0 ? 
 | |
| 					this.order[this.order.length+target] 
 | |
| 				: this.order[target]
 | |
| 		}
 | |
| 
 | |
| 		// first/last special case...
 | |
| 		if(target == 'first'){
 | |
| 			list = this.ribbons[this.getRibbon(mode)]
 | |
| 			for(var res in list){
 | |
| 				return list[res]
 | |
| 			}
 | |
| 			return null
 | |
| 		}
 | |
| 		if(target == 'last'){
 | |
| 			list = this.ribbons[this.getRibbon(mode)]
 | |
| 			for(var i=list.length; i >= 0; i--){
 | |
| 				if(list[i] != null){
 | |
| 					return list[i]
 | |
| 				}
 | |
| 			}
 | |
| 			return null
 | |
| 		}
 | |
| 
 | |
| 		// normalize target...
 | |
| 		if(target in this.ribbons || target.constructor.name == 'Array'){
 | |
| 			list = target
 | |
| 			target = this.current
 | |
| 		} else if(target == 'before' || target == 'after'){
 | |
| 			list = mode
 | |
| 			mode = target
 | |
| 			target = this.current
 | |
| 		}
 | |
| 
 | |
| 		// normalize mode...
 | |
| 		if(mode != null 
 | |
| 				&& mode.constructor.name == 'Array' 
 | |
| 				|| mode in this.ribbons){
 | |
| 			list = mode
 | |
| 			mode = null
 | |
| 		}
 | |
| 		// relative mode and offset...
 | |
| 		if(typeof(mode) == typeof(123)){
 | |
| 			var offset = mode
 | |
| 			mode = offset < 0 ? 'before'
 | |
| 				: offset > 0 ? 'after'
 | |
| 				: mode
 | |
| 			offset = Math.abs(offset)
 | |
| 		} else {
 | |
| 			var offset = 0 
 | |
| 			mode = mode == null ? 'before' : mode
 | |
| 		}
 | |
| 
 | |
| 		var i = this.order.indexOf(target)
 | |
| 
 | |
| 		// invalid gid...
 | |
| 		// XXX need a better way to report errors...
 | |
| 		if(i == -1){
 | |
| 			return -1
 | |
| 		}
 | |
| 
 | |
| 		// normalize the list to a sparse list of gids...
 | |
| 		list = list == null ? 
 | |
| 				this.ribbons[this.getRibbon(target)]
 | |
| 			: list.constructor.name == 'Array' ? 
 | |
| 				this.makeSparseImages(list)
 | |
| 			: this.ribbons[this.getRibbon(list)]
 | |
| 
 | |
| 		var res = list[i]
 | |
| 		// we have a direct hit...
 | |
| 		if(res != null && offset == 0){
 | |
| 			return res
 | |
| 		}
 | |
| 
 | |
| 		// prepare for the search...
 | |
| 		if(mode == 'before'){
 | |
| 			var step = -1
 | |
| 
 | |
| 		} else if(mode == 'after'){
 | |
| 			var step = 1
 | |
| 
 | |
| 		// strict -- no hit means there is no point in searching...
 | |
| 		} else {
 | |
| 			return null
 | |
| 		}
 | |
| 
 | |
| 		// skip the current elem...
 | |
| 		i += step
 | |
| 		// get the first non-null, also accounting for offset...
 | |
| 		// NOTE: we are using this.order.length here as ribbons might 
 | |
| 		// 		be truncated...
 | |
| 		for(; i >= 0 && i < this.order.length; i+=step){
 | |
| 			var cur = list[i]
 | |
| 			if(cur == null){
 | |
| 				continue
 | |
| 			}
 | |
| 			offset -= 1
 | |
| 			if(offset <= 0){
 | |
| 				return cur
 | |
| 			}
 | |
| 		}
 | |
| 		// target is either first or last...
 | |
| 		return null
 | |
| 	},	
 | |
| 
 | |
| 	// Get image order...
 | |
| 	//
 | |
| 	// This is similar to .getImage(..) but adds an optional context.
 | |
| 	//
 | |
| 	// The context can be:
 | |
| 	//	'all' 		- global order (default)
 | |
| 	// 	'loaded'	- order in loaded images
 | |
| 	// 	'ribbon'	- order in ribbon
 | |
| 	//
 | |
| 	// NOTE: acquiring the gid is exactly the same as with .getImage(..)
 | |
| 	// 		next, that gid is used to get the order, in case of the 
 | |
| 	// 		'ribbon' context, the order is relative to the ribbon where
 | |
| 	// 		the image is located.
 | |
| 	// 		To get the order of an image in a different ribbon, get an 
 | |
| 	// 		appropriate before/after image in that ribbon and get it's 
 | |
| 	// 		order.
 | |
| 	getImageOrder: function(context, target, mode, list){
 | |
| 		if(context == 'loaded'){
 | |
| 			return this.getImages('loaded').indexOf(this.getImage(target, mode, list))
 | |
| 
 | |
| 		} else if(context == 'ribbon'){
 | |
| 			var gid = this.getImage(target, mode, list)
 | |
| 			return this.getImages(gid).indexOf(gid)
 | |
| 
 | |
| 		} else if(context == 'all'){
 | |
| 			return this.order.indexOf(this.getImage(target, mode, list))
 | |
| 		} 
 | |
| 
 | |
| 		return this.order.indexOf(this.getImage(context, target, mode))
 | |
| 	},	
 | |
| 
 | |
| 	// Get a list of image gids...
 | |
| 	//
 | |
| 	//	Get list of loaded images:
 | |
| 	//	.getImages()
 | |
| 	//	.getImages('loaded')
 | |
| 	//		-> list
 | |
| 	//
 | |
| 	//	Get all images, both loaded and not:
 | |
| 	//	.getImages('all')
 | |
| 	//		-> list
 | |
| 	//
 | |
| 	//	Get list of images in current ribbon:
 | |
| 	//	.getImages('current')
 | |
| 	//		-> list
 | |
| 	//
 | |
| 	//	Filter the list and return only loaded images from it:
 | |
| 	//	.getImages(list)
 | |
| 	//		-> list
 | |
| 	//
 | |
| 	//	Get loaded images from ribbon:
 | |
| 	//	.getImages(gid|order|ribbon)
 | |
| 	//		-> list 
 | |
| 	//
 | |
| 	//	Get count gids around (default) before or after the target image:
 | |
| 	//	.getImages(gid|order|ribbon, count)
 | |
| 	//	.getImages(gid|order|ribbon, count, 'around')
 | |
| 	//	.getImages(gid|order|ribbon, count, 'after')
 | |
| 	//	.getImages(gid|order|ribbon, count, 'before')
 | |
| 	//		-> list 
 | |
| 	//
 | |
| 	//
 | |
| 	// If no image is given the current image/ribbon is assumed as target.
 | |
| 	//
 | |
| 	// This will always return count images if there is enough images 
 | |
| 	// in ribbon from the requested sides of target.
 | |
| 	//
 | |
| 	// NOTE: this expects ribbon order and not image order.
 | |
| 	// NOTE: if count is even, it will return 1 more image to the left 
 | |
| 	// 		(before) the target.
 | |
| 	// NOTE: if the target is present in the image-set it will be included
 | |
| 	// 		in the result regardless of mode...
 | |
| 	// NOTE: to get a set of image around a specific (non-current) image
 | |
| 	// 		in a specific (non-current) ribbon first get an apropriate image
 | |
| 	// 		via. .getImage(..) and then get the list with this...
 | |
| 	// 			D.getImages(D.getImage(gid, ribbon_gid), N, 'around')
 | |
| 	//
 | |
| 	// XXX for some reason negative target number (ribbon number) 
 | |
| 	// 		breaks this...
 | |
| 	getImages: function(target, count, mode){
 | |
| 		target = (target == null && count == null) ? 'loaded' : target
 | |
| 		mode = mode == null ? 'around' : mode
 | |
| 		var list
 | |
| 
 | |
| 		// normalize target and build the source list...
 | |
| 
 | |
| 		// 'current' ribbon...
 | |
| 		target = target == 'current' ? this.current : target
 | |
| 
 | |
| 		// get all gids...
 | |
| 		if(target == 'all'){
 | |
| 			list = this.order
 | |
| 			target = null
 | |
| 
 | |
| 		// get loaded only gids...
 | |
| 		} else if(target == 'loaded'){
 | |
| 			var res = []
 | |
| 			var ribbons = this.ribbons
 | |
| 			for(var k in ribbons){
 | |
| 				this.makeSparseImages(ribbons[k], res)
 | |
| 			}
 | |
| 			list = res.compact()
 | |
| 			target = null 
 | |
| 
 | |
| 		// filter out the unloaded gids from given list...
 | |
| 		} else if(target != null && target.constructor.name == 'Array'){
 | |
| 			var loaded = this.getImages('loaded')
 | |
| 			list = target.filter(function(e){
 | |
| 				return loaded.indexOf(e) >= 0
 | |
| 			})
 | |
| 			target = null 
 | |
| 
 | |
| 		// target is ribbon gid...
 | |
| 		} else if(target in this.ribbons){
 | |
| 			list = this.ribbons[target]
 | |
| 		}
 | |
| 
 | |
| 		// NOTE: list can be null if we got an image gid or ribbon order...
 | |
| 		// get the ribbon gids...
 | |
| 		if(list == null){
 | |
| 			list = this.ribbons[this.getRibbon(target)] 
 | |
| 			list = list != null ? list.compact() : []
 | |
| 		}
 | |
| 
 | |
| 		if(count == null){
 | |
| 			return list.slice()
 | |
| 		}
 | |
| 
 | |
| 		target = this.getImage(target)
 | |
| 		var i = list.indexOf(target)
 | |
| 
 | |
| 		// prepare to slice the list...
 | |
| 		if(mode == 'around'){
 | |
| 			count = count/2
 | |
| 			var from = i - Math.floor(count)
 | |
| 			var to = i + Math.ceil(count)
 | |
| 
 | |
| 		} else if(mode == 'before'){
 | |
| 			// NOTE: we are shifting by 1 to include the target...
 | |
| 			var from = (i - count) + 1
 | |
| 			var to = i + 1
 | |
| 
 | |
| 		} else if(mode == 'after'){
 | |
| 			var from = i
 | |
| 			var to = i + count
 | |
| 
 | |
| 		} else {
 | |
| 			// XXX bad mode....
 | |
| 			return null
 | |
| 		}
 | |
| 
 | |
| 		// get the actual bounds...
 | |
| 		from = Math.max(0, from)
 | |
| 		to = Math.min(list.length-1, to)
 | |
| 
 | |
| 		return list.slice(from, to)
 | |
| 	},
 | |
| 
 | |
| 	// Get ribbon...
 | |
| 	// 
 | |
| 	//	Get current ribbon:
 | |
| 	//	.getRibbon()
 | |
| 	//	.getRibbon('current')
 | |
| 	//		-> ribbon gid
 | |
| 	//
 | |
| 	//	Get base ribbon:
 | |
| 	//	.getRibbon('base')
 | |
| 	//		-> base ribbon gid
 | |
| 	//
 | |
| 	//	Get ribbon by target image/ribbon:
 | |
| 	//	.getRibbon(ribbon|order|gid)
 | |
| 	//		-> ribbon gid
 | |
| 	//		-> null -- invalid target
 | |
| 	//		NOTE: if ribbon gid is given this will return it as-is.
 | |
| 	//
 | |
| 	//	Get ribbon before/after target:
 | |
| 	//	.getRibbon(ribbon|order|gid, 'before')
 | |
| 	//	.getRibbon(ribbon|order|gid, 'after')
 | |
| 	//		-> ribbon gid
 | |
| 	//		-> null -- invalid target
 | |
| 	//
 | |
| 	//	Get ribbon at offset from target:
 | |
| 	//	.getRibbon(ribbon|order|gid, offset)
 | |
| 	//		-> ribbon gid
 | |
| 	//		-> null -- invalid target
 | |
| 	//
 | |
| 	//
 | |
| 	// NOTE: this expects ribbon order and not image order.
 | |
| 	getRibbon: function(target, offset){
 | |
| 		target = target == null ? this.current : target
 | |
| 		offset = offset == null ? 0 : offset
 | |
| 		offset = offset == 'before' ? -1 : offset
 | |
| 		offset = offset == 'after' ? 1 : offset
 | |
| 
 | |
| 		// special keywords...
 | |
| 		if(target == 'base'){
 | |
| 			return this.base
 | |
| 		} else if(target == 'current'){
 | |
| 			target = this.current
 | |
| 		}
 | |
| 
 | |
| 		var ribbons = this.ribbons
 | |
| 		var o
 | |
| 
 | |
| 		// we got a ribbon gid...
 | |
| 		if(target in ribbons){
 | |
| 			o = this.ribbon_order.indexOf(target)
 | |
| 
 | |
| 		// we got a ribbon order...
 | |
| 		} else if(typeof(target) == typeof(123)){
 | |
| 			o = target
 | |
| 
 | |
| 		// image gid...
 | |
| 		} else {
 | |
| 			var i = this.order.indexOf(target)
 | |
| 			var k
 | |
| 			for(k in ribbons){
 | |
| 				if(ribbons[k][i] != null){
 | |
| 					o = this.ribbon_order.indexOf(k)
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if(o != null){
 | |
| 			o += offset
 | |
| 			if(o < 0 || o > this.ribbon_order.length){
 | |
| 				// ERROR: offset out of bounds...
 | |
| 				return null
 | |
| 			}
 | |
| 			return this.ribbon_order[o]
 | |
| 		}
 | |
| 
 | |
| 		// ERROR: invalid target...
 | |
| 		return null
 | |
| 	},
 | |
| 	// same as .getRibbon(..) but returns ribbon order...
 | |
| 	getRibbonOrder: function(target, offset){
 | |
| 		return this.ribbon_order.indexOf(this.getRibbon(target, offset))
 | |
| 	},
 | |
| 
 | |
| 
 | |
| 
 | |
| 	/******************************************************** Edit ***/
 | |
| 
 | |
| 	// Focus an image -- make it current...
 | |
| 	//
 | |
| 	// This is signature compatible with .getImage(..), see it for more
 | |
| 	// info...
 | |
| 	focusImage: function(target, mode, list){
 | |
| 		var current = this.getImage(target, mode, list)
 | |
| 		if(this.order.indexOf(current) >= 0){
 | |
| 			this.current = current
 | |
| 		}
 | |
| 		return this
 | |
| 	},	
 | |
| 
 | |
| 	// Shorthand methods...
 | |
| 	//
 | |
| 	// XXX should these be here???
 | |
| 	focusBaseRibbon: function(){ return this.focusImage(this.base) },
 | |
| 	focusImageOffset: function(offset){
 | |
| 		offset = offset == null ? 0 : offset
 | |
| 
 | |
| 		var min = -this.getImageOrder('ribbon')
 | |
| 		var max = this.getImages('current').length-1
 | |
| 
 | |
| 		offset = Math.max(min, Math.min(max, offset))
 | |
| 
 | |
| 		return this.focusImage('current', offset)
 | |
| 	},
 | |
| 	nextImage: function(){ return this.focusImageOffset(1) }, // Gen2
 | |
| 	prevImage: function(){ return this.focusImageOffset(-1) }, // Gen2
 | |
| 	firstImage: function(){ return this.focusImage('first') }, // Gen2
 | |
| 	lastImage: function(){ return this.focusImage('last') }, // Gen2
 | |
| 	focusRibbonOffset: function(offset){
 | |
| 		var c = this.getRibbonOrder()
 | |
| 		var t = c+offset
 | |
| 		t = Math.max(0, Math.min(this.ribbon_order.length-1, t))
 | |
| 
 | |
| 		// NOTE: the modes here are different for directions to balance
 | |
| 		// 		up/down navigation...
 | |
| 		return this.focusImage('current', (t < c ? 'after' : 'before'), t)
 | |
| 	},
 | |
| 	nextRibbon: function(){ return this.focusRibbonOffset(1) }, // Gen2
 | |
| 	prevRibbon: function(){ return this.focusRibbonOffset(-1) }, // Gen2
 | |
| 
 | |
| 	// Set base ribbon...
 | |
| 	//
 | |
| 	// This is signature compatible with .getRibbon(..), see it for more
 | |
| 	// info...
 | |
| 	setBase: function(target, offset){
 | |
| 		var base = this.getRibbon(target, offset)
 | |
| 		if(base in this.ribbons){
 | |
| 			this.base = base
 | |
| 		}
 | |
| 		return this
 | |
| 	},
 | |
| 
 | |
| 	// Create empty ribbon...
 | |
| 	//
 | |
| 	// If mode is 'below' this will create a new ribbon below the target,
 | |
| 	// otherwise the new ribbon will be created above.
 | |
| 	newRibbon: function(target, mode){
 | |
| 		var gid = this.newGid('R')
 | |
| 		var i = this.getRibbonOrder(target)
 | |
| 
 | |
| 		i = mode == 'below' ? i+1 : i
 | |
| 
 | |
| 		this.ribbon_order.splice(i, 0, gid)
 | |
| 		this.ribbons[gid] = []
 | |
| 
 | |
| 		return gid
 | |
| 	},
 | |
| 
 | |
| 	// Merge ribbons
 | |
| 	//
 | |
| 	//	.mergeRibbons('all')
 | |
| 	//	.mergeRibbons(ribbon, ribbon, ...)
 | |
| 	//		-> data
 | |
| 	//
 | |
| 	// If 'all' is the first argument, this will merge all the ribbons.
 | |
| 	//
 | |
| 	// This will merge the ribbons into the first.
 | |
| 	mergeRibbons: function(target){
 | |
| 		var targets = target == 'all' ? this.ribbon_order.slice() : arguments
 | |
| 		var base = targets[0]
 | |
| 
 | |
| 		for(var i=1; i < targets.length; i++){
 | |
| 			var r = targets[i]
 | |
| 			this.makeSparseImages(this.ribbons[r], this.ribbons[base])
 | |
| 
 | |
| 			delete this.ribbons[r]
 | |
| 			this.ribbon_order.splice(this.ribbon_order.indexOf(r), 1)
 | |
| 		}
 | |
| 
 | |
| 		// update .base if that gid got merged in...
 | |
| 		if(this.ribbon_order.indexOf(this.base) < 0){
 | |
| 			this.base = base
 | |
| 		}
 | |
| 
 | |
| 		return this
 | |
| 	},
 | |
| 
 | |
| 	// Sort images in ribbons via .order...
 | |
| 	//
 | |
| 	// NOTE: this sorts in-place
 | |
| 	sortImages: function(){
 | |
| 		var ribbons = this.ribbons
 | |
| 		for(k in ribbons){
 | |
| 			ribbons[k] = this.makeSparseImages(ribbons[k])
 | |
| 		}
 | |
| 		return this
 | |
| 	},
 | |
| 
 | |
| 	// Reverse .order and all the ribbons...
 | |
| 	//
 | |
| 	// NOTE: this sorts in-place
 | |
| 	//
 | |
| 	// NOTE: this depends on setting length of an array, it works in 
 | |
| 	// 		Chrome but will it work the same in other systems???
 | |
| 	reverseImages: function(){
 | |
| 		this.order.reverse()
 | |
| 		var l = this.order.length
 | |
| 		for(k in ribbons){
 | |
| 			// XXX will this work everywhere???
 | |
| 			// NOTE: ribbons may be truncated, so we need to explicitly 
 | |
| 			// 		set their length...
 | |
| 			ribbons[k].length = l
 | |
| 			ribbons[k].reverse()
 | |
| 		}
 | |
| 		return this
 | |
| 	},
 | |
| 
 | |
| 	// Shift image...
 | |
| 	//
 | |
| 	//	Shift image to target position:
 | |
| 	//	.shiftImage(from, gid|order|ribbon)
 | |
| 	//	.shiftImage(from, gid|order|ribbon, 'before')
 | |
| 	//	.shiftImage(from, gid|order|ribbon, 'after')
 | |
| 	//		-> data
 | |
| 	//
 | |
| 	//	Shift image by offset:
 | |
| 	//	.shiftImage(from, offset, 'offset')
 | |
| 	//		-> data
 | |
| 	//
 | |
| 	//
 | |
| 	// order is expected to be ribbon order.
 | |
| 	//
 | |
| 	// from must be one of:
 | |
| 	// 	- a .getImage(..) compatible object. usually an image gid, order,
 | |
| 	// 	  or null, see .getImage(..) for more info.
 | |
| 	// 	- a list of .getImage(..) compatible objects.
 | |
| 	//
 | |
| 	// When shifting a set of gids horizontally this will pack them 
 | |
| 	// together in order.
 | |
| 	//
 | |
| 	//
 | |
| 	// NOTE: this will not create new ribbons.
 | |
| 	// NOTE: .getImage(..) defaults to 'before' thus this to defaults
 | |
| 	// 		to 'after'
 | |
| 	//
 | |
| 	// XXX check for corner cases:
 | |
| 	// 		- first/last in ribbon offset
 | |
| 	// 		- first/last in order offset
 | |
| 	// 		- first/last ribbon up/down
 | |
| 	// 			do we create new ribbons and round???
 | |
| 	// XXX when shifting groups of images we are using the first as a 
 | |
| 	// 		base, should we use last as a base for right shifting???
 | |
| 	// 		...another way to go could be using current as a reference
 | |
| 	// XXX test vertical..
 | |
| 	shiftImage: function(from, target, mode){
 | |
| 		from = from == null ? this.current : from
 | |
| 		from = from == 'current' ? this.current : from
 | |
| 		from = from.constructor.name != 'Array' ? [from] : from
 | |
| 		mode = mode == null ? 'after' : mode
 | |
| 		var ribbons = this.ribbons
 | |
| 		var order = this.order
 | |
| 
 | |
| 		first = this.getImage(from[0])
 | |
| 		var f = order.indexOf(first)
 | |
| 
 | |
| 		// target is an offset...
 | |
| 		if(mode == 'offset'){
 | |
| 			// XXX check that we can place an elem at first and last positions...
 | |
| 			var t = this.getImageOrder(this.getImage(first, target))
 | |
| 
 | |
| 			var ribbon = this.getRibbon(first)
 | |
| 
 | |
| 		// target is ribbon order...
 | |
| 		// XXX range checking???
 | |
| 		} else if(typeof(target) == typeof(123)){
 | |
| 			var t = f
 | |
| 
 | |
| 			// normalize the target...
 | |
| 			// XXX is this the correct way to go???
 | |
| 			target = Math.max(0, Math.min(this.ribbon_order.length-1, target))
 | |
| 
 | |
| 			var ribbon = this.ribbon_order[target]
 | |
| 
 | |
| 		// target is a ribbon gid...
 | |
| 		} else if(target in this.ribbons){
 | |
| 			var t = f
 | |
| 
 | |
| 			var ribbon = target
 | |
| 
 | |
| 		// target is a gid or order...
 | |
| 		} else {
 | |
| 			target = this.getImage(target)
 | |
| 			var t = order.indexOf(target)
 | |
| 			t = mode == 'after' ? t+1 : t
 | |
| 
 | |
| 			var ribbon = this.getRibbon(target)
 | |
| 		}
 | |
| 
 | |
| 		var from_ribbon = this.getRibbon(first)
 | |
| 
 | |
| 		// do vertical shift...
 | |
| 		// NOTE: image order here is not changed...
 | |
| 		if(ribbon != from_ribbon || from.length > 1){
 | |
| 			var that = this
 | |
| 			from.forEach(function(e){
 | |
| 				var i = order.indexOf(e)
 | |
| 				var from_ribbon = that.getRibbon(e)
 | |
| 
 | |
| 				that.ribbons[ribbon][i] = e
 | |
| 				delete that.ribbons[from_ribbon][i]
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		// do horizontal shift...
 | |
| 		// NOTE: images are packed horizontally together...
 | |
| 		if(f != t){
 | |
| 			for(var i=0; i<from.length; i++){
 | |
| 				f = order.indexOf(from[i])
 | |
| 
 | |
| 				// update order...
 | |
| 				order.splice(t+i, 0, this.order.splice(f, 1)[0])
 | |
| 
 | |
| 				// update ribbons...
 | |
| 				for(k in ribbons){
 | |
| 					ribbons[k].splice(t+i, 0, ribbons[k].splice(f, 1)[0])
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return this 
 | |
| 	},
 | |
| 
 | |
| 	// Shorthand actions...
 | |
| 	//
 | |
| 	// NOTE: shiftImageUp/shiftImageDown will create new ribbons when 
 | |
| 	// 		shifting from first/last ribbons respectively.
 | |
| 	// NOTE: none of these change .current
 | |
| 	//
 | |
| 	// XXX should these be here??
 | |
| 	shiftImageLeft: function(gid){ return this.shiftImage(gid, -1, 'offset') }, // Gen2
 | |
| 	shiftImageRight: function(gid){ return this.shiftImage(gid, 1, 'offset') }, // Gen2
 | |
| 	// XXX test...
 | |
| 	shiftImageUp: function(gid){ 
 | |
| 		var g = gid.constructor.name == 'Array' ? gid[0] : gid
 | |
| 		// check if we need to create a ribbon here...
 | |
| 		if(this.getRibbonOrder(g) == 0){
 | |
| 			this.newRibbon(g)
 | |
| 		}
 | |
| 		return this.shiftImage(gid, this.getRibbonOrder(g)-1) 
 | |
| 	},
 | |
| 	// XXX test...
 | |
| 	shiftImageDown: function(gid){ 
 | |
| 		var g = gid.constructor.name == 'Array' ? gid[0] : gid
 | |
| 		// check if we need to create a ribbon here...
 | |
| 		if(this.getRibbonOrder(g) == this.ribbon_order.length-1){
 | |
| 			this.newRibbon(g, 'below')
 | |
| 		}
 | |
| 		return this.shiftImage(gid, this.getRibbonOrder(g)+1) 
 | |
| 	},
 | |
| 
 | |
| 	// Shift ribbon vertically...
 | |
| 	//
 | |
| 	// 	Shift ribbon to position...
 | |
| 	// 	.shiftRibbon(gid, gid)
 | |
| 	// 	.shiftRibbon(gid, gid, 'before')
 | |
| 	// 	.shiftRibbon(gid, gid, 'after')
 | |
| 	// 		-> data
 | |
| 	// 		NOTE: 'before' is default.
 | |
| 	//
 | |
| 	// 	Shift ribbon by offset...
 | |
| 	// 	.shiftRibbon(gid, offset, 'offset')
 | |
| 	// 		-> data
 | |
| 	//
 | |
| 	//
 | |
| 	// XXX test...
 | |
| 	shiftRibbon: function(gid, to, mode){
 | |
| 		var i = this.getRibbonOrder(gid)
 | |
| 
 | |
| 		// to is an offset...
 | |
| 		if(mode == 'offset'){
 | |
| 			to = i + to
 | |
| 
 | |
| 		// to is a gid...
 | |
| 		} else {
 | |
| 			to = this.getRibbonOrder(to)
 | |
| 			if(mode == 'after'){
 | |
| 				to += 1
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// normalize to...
 | |
| 		to = Math.max(0, Math.min(this.ribbon_order.length-1, to))
 | |
| 
 | |
| 		this.ribbon_order.splice(to, 0, this.ribbon_order.splice(i, 1)[0])
 | |
| 
 | |
| 		return this
 | |
| 	},
 | |
| 
 | |
| 	// Shorthand actions...
 | |
| 	//
 | |
| 	// XXX should these be here??
 | |
| 	shiftRibbonUp: function(gid){ return this.shiftRibbon(gid, -1, 'offset') }, // Gen2
 | |
| 	shiftRibbonDown: function(gid){ return this.shiftRibbon(gid, 1, 'offset') }, // Gen2
 | |
| 
 | |
| 
 | |
| 
 | |
| 	/********************************************* Data-level edit ***/
 | |
| 
 | |
| 	// Split data into sections...
 | |
| 	//
 | |
| 	// 	.split(target, ..)
 | |
| 	// 	.split([target, ..])
 | |
| 	// 		-> list
 | |
| 	//
 | |
| 	//
 | |
| 	// This will "split" the data just before each target, i.e. target N
 | |
| 	// will get the head of N+1 section.
 | |
| 	//
 | |
| 	// 		 Data							 Data		 Data
 | |
| 	// 		[...oooooXooooo...]		->		[...ooooo]	[Xooooo...]
 | |
| 	// 				 ^									 ^
 | |
| 	// 			  target							  target
 | |
| 	//
 | |
| 	//
 | |
| 	// Special case: target is .order.length
 | |
| 	// This will indicate that the last data section will be empty.
 | |
| 	//
 | |
| 	// 		 Data							 Data		 		 Data
 | |
| 	// 		[...oooooooooooo]		->		[...oooooooooooo]	[]
 | |
| 	// 						^								^
 | |
| 	//					 target							 target
 | |
| 	//
 | |
| 	//
 | |
| 	// Targets MUST be listed in order of occurrence.
 | |
| 	//
 | |
| 	// NOTE: this will not affect the original data object...
 | |
| 	// NOTE: this might result in empty ribbons, if no images are in a 
 | |
| 	// 		given ribbon in the section to be split...
 | |
| 	// NOTE: target must be a .getImage(..) compatible value.
 | |
| 	// NOTE: if no target is given this will assume the current image.
 | |
| 	split: function(target){
 | |
| 		if(arguments.length > 1){
 | |
| 			target = Array.apply(null, arguments)
 | |
| 		} else if(target == null 
 | |
| 				|| target.constructor.name != 'Array'){
 | |
| 			target = [ target ]
 | |
| 		}
 | |
| 		var res = []
 | |
| 		var tail = this.clone()
 | |
| 		var that = this
 | |
| 
 | |
| 		// NOTE: we modify tail here on each iteration...
 | |
| 		target.forEach(function(i){
 | |
| 			i = i >= that.order.length 
 | |
| 				? tail.order.length
 | |
| 				: tail.getImageOrder(that.getImage(i))
 | |
| 			var n = new Data()
 | |
| 			n.base = tail.base
 | |
| 			n.ribbon_order = tail.ribbon_order.slice()
 | |
| 			n.order = tail.order.splice(0, i)
 | |
| 			for(var k in tail.ribbons){
 | |
| 				n.ribbons[k] = tail.ribbons[k].splice(0, i)
 | |
| 			}
 | |
| 			n.current = n.order.indexOf(tail.current) >= 0 ? tail.current : n.order[0]
 | |
| 			
 | |
| 			res.push(n)
 | |
| 		})
 | |
| 
 | |
| 		// update .current of the last element...
 | |
| 		tail.current = tail.order.indexOf(tail.current) >= 0 ? tail.current : tail.order[0]
 | |
| 
 | |
| 		res.push(tail)
 | |
| 		return res
 | |
| 	},
 | |
| 
 | |
| 	// Join data objects into the current object...
 | |
| 	//
 | |
| 	//	.join(data, ..)
 | |
| 	//	.join([ data, .. ])
 | |
| 	//		-> data with all the other data objects merged in, aligned 
 | |
| 	//			via base ribbon.
 | |
| 	//
 | |
| 	//	.join(align, data, ..)
 | |
| 	//	.join(align, [ data, .. ])
 | |
| 	//		-> data with all the other data objects merged in, via align
 | |
| 	//
 | |
| 	//
 | |
| 	// align can be:
 | |
| 	// 	'base'		- base ribbons (default)
 | |
| 	// 	'top'		- top ribbons
 | |
| 	// 	'bottom'	- bottom ribbons
 | |
| 	//
 | |
| 	// NOTE: data can be both a list of arguments or an array.
 | |
| 	// NOTE: this will merge the items in-place, into the method's object;
 | |
| 	// 		if it is needed to keep the original intact, just .clone() it...
 | |
| 	//
 | |
| 	// XXX test more complex cases...
 | |
| 	join: function(){
 | |
| 		var args = Array.apply(null, arguments)
 | |
| 		var align = typeof(args[0]) == typeof('str') ? args.splice(0, 1)[0] : 'base'
 | |
| 		args = args[0].constructor.name == 'Array' ? args[0] : args
 | |
| 
 | |
| 		var base = this
 | |
| 
 | |
| 		args.forEach(function(data){
 | |
| 			// calculate align offset...
 | |
| 			if(align == 'base'){
 | |
| 				var d = base.getRibbonOrder('base') - data.getRibbonOrder('base')
 | |
| 
 | |
| 			} else if(align == 'top'){
 | |
| 				var d = 0
 | |
| 
 | |
| 			} else if(align == 'bottom'){
 | |
| 				var d = base.ribbon_order.length - data.ribbon_order.length
 | |
| 			}
 | |
| 
 | |
| 			var t = 0
 | |
| 
 | |
| 			// merge order...
 | |
| 			base.order = base.order.concat(data.order)
 | |
| 
 | |
| 			// merge ribbons...
 | |
| 			for(var i=0; i < data.ribbon_order.length; i++){
 | |
| 				var g = data.ribbon_order[i]
 | |
| 				var r = data.ribbons[g]
 | |
| 
 | |
| 				// push the new ribbon just before the base...
 | |
| 				if(d < 0){
 | |
| 					// see if g is unique...
 | |
| 					if(g in base.ribbons || base.order.indexOf(g) >= 0){
 | |
| 						g = base.newGid('R')
 | |
| 					}
 | |
| 					base.ribbon_order.splice(t, 0, g)
 | |
| 					base.ribbons[g] = r
 | |
| 					t += 1
 | |
| 					d -= 1
 | |
| 
 | |
| 				// append ribbons...
 | |
| 				} else if(d < base.ribbon_order.length){
 | |
| 					var tg = base.ribbon_order[d]
 | |
| 					base.ribbons[tg] = base.ribbons[tg].concat(r)
 | |
| 
 | |
| 				// push the new ribbon to the end...
 | |
| 				} else {
 | |
| 					// see if g is unique...
 | |
| 					if(g in base.ribbons || base.order.indexOf(g) >= 0){
 | |
| 						g = base.newGid('R')
 | |
| 					}
 | |
| 					base.ribbon_order.push(g)
 | |
| 					base.ribbons[g] = r
 | |
| 				}
 | |
| 
 | |
| 				d += 1
 | |
| 			}
 | |
| 		})
 | |
| 
 | |
| 		// XXX this is slow-ish...
 | |
| 		base.removeDuplicateGIDs()
 | |
| 
 | |
| 		return base
 | |
| 	},
 | |
| 
 | |
| 	// Align data to ribbon...
 | |
| 	//
 | |
| 	// NOTE: if either start or end is not given this will infer the 
 | |
| 	// 		missing values via the ribbon above.
 | |
| 	// NOTE: if either start or end is not given this can only align 
 | |
| 	// 		downward, needing a ribbon above the target to infer the 
 | |
| 	// 		values.
 | |
| 	//
 | |
| 	// XXX test
 | |
| 	alignToRibbon: function(ribbon, start, end){
 | |
| 		ribbon = ribbon == null ? this.base : this.getRibbon(ribbon)
 | |
| 
 | |
| 		if(start == null || end == null){
 | |
| 			var r = this.getRibbonOrder(ribbon)
 | |
| 			// ribbon is top ribbon, nothing to do...
 | |
| 			if(r <= 0){
 | |
| 				return this
 | |
| 			}
 | |
| 
 | |
| 			var above = this.getRibbon(r-1)
 | |
| 		}
 | |
| 
 | |
| 		start = start == null 
 | |
| 			? this.getImageOrder('first', above)
 | |
| 			: this.getImageOrder(start)
 | |
| 		end = end == null 
 | |
| 			// NOTE: we need to exclude the last image in ribbon from 
 | |
| 			// 		the next section, this the offset.
 | |
| 			? this.getImageOrder('last', above)+1
 | |
| 			: this.getImageOrder(end)
 | |
| 
 | |
| 		// split the data into three sections...
 | |
| 		var res = this.split(start, end)
 | |
| 		var rest = res.splice(1)
 | |
| 
 | |
| 		// set the base ribbon on the middle section...
 | |
| 		rest[0].setBase(0)
 | |
| 
 | |
| 		// join the resulting data to the base ribbon...
 | |
| 		res = res.join(rest)
 | |
| 
 | |
| 		// transfer data to new data object...
 | |
| 		res.current = this.current
 | |
| 		res.base = this.base
 | |
| 
 | |
| 		return res
 | |
| 	},
 | |
| 
 | |
| 	// Crop the data...
 | |
| 	//
 | |
| 	// NOTE: this will not affect the original data object...
 | |
| 	// NOTE: this may result in empty ribbons...
 | |
| 	// NOTE: this will not crop the .order...
 | |
| 	//
 | |
| 	// XXX should this link to .root and .parent data???
 | |
| 	crop: function(list){
 | |
| 		var crop = this.clone()
 | |
| 		list = crop.makeSparseImages(list)
 | |
| 		for(var k in crop.ribbons){
 | |
| 			crop.ribbons[k] = crop.makeSparseImages(crop.ribbons[k].filter(function(_, i){
 | |
| 				return list[i] != null
 | |
| 			}))
 | |
| 		}
 | |
| 
 | |
| 		// XXX ???
 | |
| 		//crop.parent = this
 | |
| 		//crop.root = this.root == null ? this : this.root
 | |
| 
 | |
| 		return crop
 | |
| 	},
 | |
| 
 | |
| 	// Merge changes from crop into data...
 | |
| 	//
 | |
| 	// NOTE: this may result in empty ribbons...
 | |
| 	//
 | |
| 	// XXX sync ribbon order???
 | |
| 	// XXX should we be able to align a merged crop???
 | |
| 	// XXX test
 | |
| 	mergeCrop: function(crop){
 | |
| 		var that = this
 | |
| 
 | |
| 		this.order = crop.order.slice()
 | |
| 		// XXX sync these???
 | |
| 		this.ribbon_order = crop.ribbon_order.slice()
 | |
| 		this.sortImages()
 | |
| 
 | |
| 		// 
 | |
| 		for(var k in crop.ribbons){
 | |
| 			var local = k in this.ribbons ? this.ribbons[k] : []
 | |
| 			var remote = crop.ribbons[k]
 | |
| 
 | |
| 			this.ribbons[k] = local
 | |
| 
 | |
| 			remote.forEach(function(e){
 | |
| 				// add gid to local ribbon...
 | |
| 				if(local.indexOf(e) < 0){
 | |
| 					this.shiftImage(e, k)
 | |
| 				}
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		return this
 | |
| 	},
 | |
| 
 | |
| 	// Create a sortable ribbon representation...
 | |
| 	//
 | |
| 	// 	.cropRibbons()
 | |
| 	// 	.cropRibbons(mode)
 | |
| 	// 		-> Data
 | |
| 	//
 | |
| 	// mode controls which images represent each ribbon, it can be:
 | |
| 	// 	'current'	- the closest to current image (default)
 | |
| 	// 	'first'		- first image in ribbon
 | |
| 	// 	'last'		- last ribbon in image
 | |
| 	// 	func(ribbon) -> gid
 | |
| 	// 				- a function that will get a ribbon gid and return 
 | |
| 	// 				  an apropriate image gid
 | |
| 	//
 | |
| 	// NOTE: the images used with a given string mode are the same as 
 | |
| 	// 		the returned via .getImage(mode, ribbon)
 | |
| 	//
 | |
| 	// 				v
 | |
| 	// 		 oooooo|a|ooooooo
 | |
| 	// 		ooooooo|A|ooooooooooooo		->		aAa
 | |
| 	// 			ooo|a|ooooo
 | |
| 	//
 | |
| 	//
 | |
| 	// The resulting data will contain a single ribbon, each image in 
 | |
| 	// which represents a ribbon in the source data.
 | |
| 	// This view allows convenient sorting of ribbons as images.
 | |
| 	//
 | |
| 	// The crop can be merged back into the source ribbon via the 
 | |
| 	// .mergeRibbonCrop(..) method.
 | |
| 	//
 | |
| 	// XXX should there be a way to set the base ribbon???
 | |
| 	// XXX should this link to .root and .parent data???
 | |
| 	// XXX do these belong here???
 | |
| 	cropRibbons: function(mode){
 | |
| 		mode = mode == null ? 'current' : mode
 | |
| 		var crop = new Data()
 | |
| 
 | |
| 		// get image representations from each ribbon...
 | |
| 		var that = this
 | |
| 		var images = this.ribbon_order.map(
 | |
| 			typeof(mode) == typeof('str') 
 | |
| 				? function(e){ return that.getImage(mode, e) }
 | |
| 				: mode)
 | |
| 
 | |
| 		var r = crop.newRibbon()
 | |
| 
 | |
| 		crop.ribbons[r] = images
 | |
| 		crop.order = images.slice()
 | |
| 		crop.base = r
 | |
| 		crop.current = images[0]
 | |
| 
 | |
| 		// XXX ???
 | |
| 		//crop.parent = this
 | |
| 		//crop.root = this.root == null ? this : this.root
 | |
| 
 | |
| 		return crop
 | |
| 	},
 | |
| 
 | |
| 	// Merge the sortable ribbon representation into data...
 | |
| 	//
 | |
| 	// This will take the image order from the crop and merge it into 
 | |
| 	// the .ribbon_order of this, essentially sorting ribbons...
 | |
| 	//
 | |
| 	// NOTE: see .cropRibbons(..) for more details...
 | |
| 	// NOTE: this will set the base to the top ribbon, but only if base
 | |
| 	// 		was the top ribbon (default) in the first place...
 | |
| 	// 		XXX is this correct???
 | |
| 	//
 | |
| 	// XXX should there be a way to set the base ribbon???
 | |
| 	mergeRibbonCrop: function(crop){
 | |
| 		var b = this.ribbon_order.indexOf(this.base)
 | |
| 		var that = this
 | |
| 		this.ribbon_order = crop.order.map(function(e){
 | |
| 			return that.getRibbon(e)
 | |
| 		})
 | |
| 		// set the base to the first/top ribbon...
 | |
| 		// XXX is this the correct way???
 | |
| 		if(b == 0){
 | |
| 			this.base = this.ribbon_order[0]
 | |
| 		}
 | |
| 		return this
 | |
| 	},
 | |
| 
 | |
| 
 | |
| 	// Clone/copy the data object...
 | |
| 	//
 | |
| 	clone: function(){
 | |
| 		var res = new Data()
 | |
| 		res.base = this.base
 | |
| 		res.current = this.current
 | |
| 		res.order = this.order.slice()
 | |
| 		res.ribbon_order = this.ribbon_order.slice()
 | |
| 		res.ribbons = {}
 | |
| 		// make ribbons sparse...
 | |
| 		for(var k in this.ribbons){
 | |
| 			res.ribbons[k] = this.ribbons[k].slice()
 | |
| 		}
 | |
| 		return res
 | |
| 	},
 | |
| 
 | |
| 	// Reset the state to empty...
 | |
| 	//
 | |
| 	_reset: function(){
 | |
| 		this.base = null
 | |
| 		this.current = null
 | |
| 		this.order = [] 
 | |
| 		this.ribbon_order = [] 
 | |
| 		this.ribbons = {}
 | |
| 
 | |
| 		this._gid_cache = []
 | |
| 
 | |
| 		return this
 | |
| 	},
 | |
| 
 | |
| 	// Remove duplicate gids...
 | |
| 	//
 | |
| 	// NOTE: this is slow-ish...
 | |
| 	removeDuplicateGIDs: function(){
 | |
| 		this.removeDuplicates(this.order)
 | |
| 		this.sortImages()
 | |
| 		return this
 | |
| 	},
 | |
| 
 | |
| 
 | |
| 
 | |
| 	/****************************************** JSON serialization ***/
 | |
| 
 | |
| 	// Convert gen3 data to gen4 json...
 | |
| 	convertDataGen3: function(data){
 | |
| 		data = data.version == null ? convertDataGen1(data) : data
 | |
| 		var that = this
 | |
| 		var res = {}
 | |
| 		res.version = '3.0'
 | |
| 		res.current = data.current
 | |
| 		res.order = data.order.slice()
 | |
| 		res.ribbon_order = []
 | |
| 		res.ribbons = {}
 | |
| 		// generate gids...
 | |
| 		data.ribbons.forEach(function(e){
 | |
| 			var gid = that.newGid('R')
 | |
| 			res.ribbon_order.push(gid)
 | |
| 			res.ribbons[gid] = e.slice()
 | |
| 		})
 | |
| 		// we set the base to the first ribbon...
 | |
| 		res.base = res.ribbon_order[0]
 | |
| 		return res
 | |
| 	},
 | |
| 
 | |
| 	// Load data from JSON...
 | |
| 	//
 | |
| 	// NOTE: this loads in-place, use .fromJSON(..) to create new data...
 | |
| 	loadJSON: function(data){
 | |
| 		if(typeof(data) == typeof('str')){
 | |
| 			data = JSON.parse(data)
 | |
| 		}
 | |
| 		data = data.version < '3.0' ? this.convertDataGen3(data) : data
 | |
| 		this.base = data.base
 | |
| 		this.current = data.current
 | |
| 		this.order = data.order.slice()
 | |
| 		this.ribbon_order = data.ribbon_order.slice()
 | |
| 		this.ribbons = {}
 | |
| 		// make ribbons sparse...
 | |
| 		for(var k in data.ribbons){
 | |
| 			this.ribbons[k] = this.makeSparseImages(data.ribbons[k])
 | |
| 		}
 | |
| 		return this
 | |
| 	},
 | |
| 
 | |
| 	// Generate JSON from data...
 | |
| 	//
 | |
| 	// NOTE: if mode is either 'str' or 'string' then this will stringify
 | |
| 	// 		the result...
 | |
| 	dumpJSON: function(mode){
 | |
| 		var res = {
 | |
| 			varsion: '3.0',
 | |
| 			base: this.base,
 | |
| 			current: this.current,
 | |
| 			order: this.order.slice(),
 | |
| 			ribbon_order: this.ribbon_order.slice(),
 | |
| 			ribbons: {},
 | |
| 		}
 | |
| 		// compact ribbons...
 | |
| 		for(var k in this.ribbons){
 | |
| 			res.ribbons[k] = this.ribbons[k].compact()
 | |
| 		}
 | |
| 		if(mode == 'string' || mode == 'str'){
 | |
| 			res = JSON.stringify(res)
 | |
| 		}
 | |
| 		return res
 | |
| 	},
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| /*********************************************************************/
 | |
| 
 | |
| // Main Data object...
 | |
| //
 | |
| function Data(json){
 | |
| 	// in case this is called as a function (without new)...
 | |
| 	if(this.constructor.name != 'Data'){
 | |
| 		return new Data(json)
 | |
| 	}
 | |
| 
 | |
| 	// load initial state...
 | |
| 	if(json != null){
 | |
| 		this.loadJSON(json)
 | |
| 	} else {
 | |
| 		this._reset()
 | |
| 	}
 | |
| 
 | |
| 	return this
 | |
| }
 | |
| Data.__proto__ = DataClassPrototype
 | |
| Data.prototype = DataPrototype
 | |
| Data.prototype.constructor = Data
 | |
| 
 | |
| 
 | |
| 
 | |
| /**********************************************************************
 | |
| * vim:set ts=4 sw=4 :                                                */
 |