/********************************************************************** * * * **********************************************************************/ // General format info... // // version format: // . // // Major version change 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 // // // Changes: // none - Gen1 data format, mostly experimental, // - has no explicit version set, // - not used for real data. // 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... // 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.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.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... // 3.0 - Gen4 DATA format, introduced several backwards incompatible // cahnges: // - 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 // // // 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... /*********************************************************************/ // 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 = { // DATA structure: // .current // gid // .base // gid // .order // [ gid, .. ] // .ribbon_order // [ gid, .. ] // .ribbons // { gid: [ gid, .. ] } // NOTE: ribbons are sparse... // util methods... compactSparseList: function(list){ return list.filter(function(){return true}) }, makeSparseImages: function(gids, target){ target = target == null ? [] : target order = this.order gids.forEach(function(e){ i = order.indexOf(e) if(i >= 0){ target[i] = e } }) return target }, // 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 }, // This is signature compatible with .getImage(..), see it for more // info... // // XXX do we need more specific focus operations like: // .focusImageOffset(offset) // XXX reference impelementation: // return this.focusImage('current', offset) // .focusRibbonOffset(offset) // XXX reference impelementation: // var c = this.getRibbonOrder() // var t = c+offset // return this.focusImage('current', (t < c ? 'after' : 'before'), t) focusImage: function(target, mode, list){ var current = this.getImage(target, mode, list) if(this.order.indexOf(current) >= 0){ this.current = current } return this }, // 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... // // XXX above/below/at... // XXX do we remove ribbons and how... // XXX test newRibbon: function(target, mode){ var gid = this.newGid('R') var i = this.getRibbonOrder(target) this.ribbon_order.splice(i, 0, gid) this.ribbons[gid] = [] // XXX should we return this or gid??? return this }, // Merge ribbons // // .mergeRibbons('all') // .mergeRibbons(ribbon, ribbon, ...) // -> data // If 'all' is the first argumet, this will merge all the ribbons. // // This will merge the ribbons into the first. // // XXX we should update .base // XXX test mergeRibbons: function(target, other){ var targets = target == 'all' ? this.ribbon_order : arguments var base = arguments[0] for(var i=1; i < arguments.length; i++){ var r = arguments[i] this.makeSparseImages(this.ribbons[r], this.ribbons[base]) delete this.ribbons[r] this.ribbon_order.splice(this.ribbon_order.indexOf(r), 1) } // XXX we should update .base return this }, // Get image // // .getImage() // .getImage('current') // -> current image gid // // .getImage(gid|order) // .getImage(gid|order, list|ribbon) // -> gid if the image is loaded/exists // -> null if the image is not loaded or 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. // // .getImage('first'[, ribbon]) // .getImage('last'[, ribbon]) // -> gid of first/last image in ribbon // -> null // NOTE: the second argument must be .getRibbon(..) compatible. // // .getImage(list|ribbon[, 'before'|'after']) // -> gid of image closest to current in list|ribbon // -> null // NOTE: 'before' is default. // NOTE: the first argument must not be a number. // // .getImage('before'[, list|ribbon]) // .getImage(gid|order, 'before'[, list|ribbon]) // -> gid of the image before gid|order // -> null of nothing is before // .getImage('after'[, list|ribbon]) // .getImage(gid|order, 'after'[, list|ribbon]) // -> gid of the image after git|order // -> null of nothing is after // NOTE: in both the above cases if gid|order is found explicitly // it will be returned. // // .getImage(gid|order, offset[, list|ribbon]) // -> gid if the image at offset from current // -> null if there is no image at that offset // NOTE: the 'relative' string is required as there is no way to // destinguish between order and offset... // // 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)){ 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 } // nirmalize 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 } else { var offset = 0 mode = mode == null ? 'before' : mode } offset = Math.abs(offset) 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 }, // same as .getImage(..) but return image order. // // XXX should be able to get order in the following contexts: // - all (default) // - loaded // - ribbon // - list getImageOrder: function(target, mode, list){ return this.order.indexOf(this.getImage(target, mode, list)) }, // Get a list of gids... // // .getImages() // .getImages('loaded') // -> list of currently loaded images, from all ribbons // // .getImages('all') // -> list of all images, both loaded and not // // .getImages('current') // -> list of images, in current ribbon // // .getImages(list) // -> return only loaded images from list // // .getImages(gid|order|ribbon) // -> get loaded images from ribbon // // .getImages(gid|order|ribbon, count) // .getImages(gid|order|ribbon, count, 'around') // .getImages(gid|order|ribbon, count, 'after') // .getImages(gid|order|ribbon, count, 'before') // -> get a fixed number of gids around (default) before or // after the target // // If no image is given the current image/ribbon is assumed as target. // // This will allways 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... // // 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 = this.compactSparseList(res) 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 ? this.compactSparseList(list) : [] } 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... // // .getRibbon() // -> current ribbon gid // // .getRibbon(ribbon|order|gid) // -> ribbon gid // -> null -- invalid target // // .getRibbon(ribbon|order|gid, 'before') // .getRibbon(ribbon|order|gid, 'after') // -> ribbon gid // -> null -- invalid 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 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)) }, // 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 // // XXX 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... // // .shiftImage(from, gid|ribbon) // .shiftImage(from, gid|ribbon, 'before') // .shiftImage(from, gid|ribbon, 'after') // -> data // // .shiftImage(from, offset, 'offset') // -> data // // from must be a .getImage(..) compatible object. usually an image // gid, order, or null, see .getImage(..) for more info. // // NOTE: this will not create new ribbons. // // XXX process from as a list of gids... // XXX test shiftImage: function(from, target, mode){ from = from.constructor.name != 'Array' ? [from] : from mode = mode == null ? 'before' : 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'){ target = target == null ? 0 : target var t = f + target t = t > order.length ? order.length-1 : t < 0 ? 0 : t target = order[t] var ribbon = this.getRibbon(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... 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... if(f != t){ for(var i=0; i= 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 }, // align can be: // 'gid' // 'base' // 'top' // 'bottom' // // NOTE: this will merge the items into the first list element... // // XXX should this join to this or create a new data??? // XXX test join: function(data, align){ var res = data.pop() data.forEach(function(d){ }) // XXX return res }, // NOTE: this may result in empty ribbons... // NOTE: this will not crop the .order... 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 })) } return crop }, // Merge changes from crop into data... // // NOTE: this may result in empty ribbons... // // 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.sortRibbons() // 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 }, 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 }, // JSON serialization... // // NOTE: this loads in-place, use .fromJSON(..) to create new data... // XXX check if we need more version checking... convertDataGen3: function(data){ // XXX check for earlier versions... // XXX do we need to do more here??? 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 }, 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 }, // 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.compactSparseList(this.ribbons[k]) } if(mode == 'string' || mode == 'str'){ res = JSON.stringify(res) } return res }, } // Main Data object... // function Data(json){ if(this.constructor.name != 'Data'){ obj = new Data } else { obj = this } obj.base = null obj.current = null obj.order = [] obj.ribbon_order = [] obj.ribbons = {} obj._gid_cache = [] // load initial state... if(json != null){ obj.loadJSON(json) } return obj } Data.__proto__ = DataClassPrototype Data.prototype = DataPrototype Data.prototype.constructor = Data /********************************************************************** * vim:set ts=4 sw=4 : */