/********************************************************************** * * * **********************************************************************/ define(function(require){ var module = {} //var DEBUG = DEBUG != null ? DEBUG : true var sha1 = require('./ext-lib/sha1') var object = require('lib/object') /*********************************************************************/ // decide to use a hashing function... if(typeof(sha1) != 'undefined'){ var hash = sha1.hash.bind(sha1) } else { var hash = function(g){ return g } } /*********************************************************************/ // A stub image, also here for documentation... var IMAGE_DATA = module.IMAGE_DATA = { // Entity GID... id: 'GID', // Entity type type: 'image', // Entity state // // can be: // - 'single' // - 'grouped' // - 'hidden' // - ... state: 'single', // Creation time... ctime: 0, // Original path... path: './images/900px/SIZE.jpg', // Previews... // NOTE: the actual values depend on specific image and can be // any size... preview: { '150px': './images/150px/SIZE.jpg', '350px': './images/350px/SIZE.jpg', '900px': './images/900px/SIZE.jpg', }, // Classes // XXX currently unused... //classes: '', // Image orientation (optional) // // can be: // - null/undefined - same as 0 // - 0 (default) - load as-is // - 90 - rotate 90deg CW // - 180 - rotate 180deg CW // - 270 - rotate 270deg CW (90deg CCW) // // NOTE: use orientationExif2ImageGrid(..) to convert from EXIF // orientation format to ImageGrid format... //orientation: 0, // Image flip state (optional) // // can be: // - null/undefined // - array // // can contain: // - 'vertical' // - 'horizontal' // // NOTE: use orientationExif2ImageGrid(..) to convert from EXIF // orientation format to ImageGrid format... //flipped: null, // Image comment (optional) // // can be: // - null/undefined // - string //comment: null, // List of image tags (optional) // // can be: // - null/undefined // - array //tags: null, } var GROUP_DATA = module.GROUP_DATA = { // Entity GID... id: 'GID', // Entity type type: 'group', // Entity state // // can be: // - 'single' // - 'grouped' // - 'hidden' // - ... state: 'single', // image used to represent/display group... cover: 'GID', // list of group contents, including .cover items: [ 'GID', ], // Classes // XXX currently unused... //classes: '', // Image comment (optional) // // can be: // - null/undefined // - string //comment: null, // List of image tags (optional) // // can be: // - null/undefined // - array //tags: null, } // Calculate relative rotation angle... // // Calculate rotation angle relative to from: // calcRelativeRotation(from, 'cw') // calcRelativeRotation(from, 'ccw') // -> 0 | 90 | 180 | 270 // // Validate an angle: // calcRelativeRotation(angle) // calcRelativeRotation(from, angle) // -> 0 | 90 | 180 | 270 // -> null // // module.calcRelativeRotation = function(from, to){ if(to == null){ to = from from = 0 } to = to == 'cw' ? 1 : to == 'ccw' ? -1 : [0, 90, 180, 270].indexOf(to*1) >= 0 ? to*1 : [-90, -180, -270].indexOf(to*1) >= 0 ? 360+(to*1) : null // relative rotation... if(to == 1 || to == -1){ var res = from res = res == null ? 0 : res*1 res += 90*to res = res < 0 ? 270 : res > 270 ? 0 : res // explicit direction... } else { var res = to } return res } /*********************************************************************/ // cmp functions... // XXX is this the right way to seporate these??? module.makeImageDateCmp = function(data, get){ return function(a, b){ if(get != null){ a = get(a) b = get(b) } b = data[b].ctime a = data[a].ctime if(a == b){ return 0 } else if(a < b){ return -1 } else { return +1 } } } // NOTE: this expects gids... module.makeImageNameCmp = function(data, get){ return function(a, b){ if(get != null){ a = get(a) b = get(b) } a = data.getImageFileName(a) b = data.getImageFileName(b) if(a == b){ return 0 } else if(a < b){ return -1 } else { return +1 } } } module.makeImageSeqOrNameCmp = function(data, get, seq){ seq = seq == null ? data.getImageNameSeq : seq return function(a, b){ // XXX this is ugly and non-generic... if(get != null){ a = get(a) b = get(b) } // XXX this is ugly and non-generic... var aa = seq.call(data, a) var bb = seq.call(data, b) // special case: seq, name if(typeof(aa) == typeof(123) && typeof(bb) == typeof('str')){ return -1 } // special case: name, seq if(typeof(aa) == typeof('str') && typeof(bb) == typeof(123)){ return +1 } // get the names if there are no sequence numbers... // NOTE: at this point both a and b are either numbers or NaN's... a = isNaN(aa) ? data.getImageFileName(a) : aa b = isNaN(bb) ? data.getImageFileName(b) : bb // do the actual comparison if(a == b){ return 0 } else if(a < b){ return -1 } else { return +1 } } } /*********************************************************************/ // XXX Image base class... // ...not sure if we need this... (???) var ImageClassPrototype = module.ImageClassPrototype = { } var ImagePrototype = module.ImagePrototype = { } var Image = module.Image = object.makeConstructor('Image', ImageClassPrototype, ImagePrototype) /*********************************************************************/ // XXX depends on jli.quoteRegExp(..) var ImagesClassPrototype = module.ImagesClassPrototype = { // XXX populate the image doc better... // NOTE: if base is given then it will be set as .base_path and // removed from each url if present... fromArray: function(data, base){ var images = new this() // XXX stub... var i = 0 //var base_pattern = base ? RegExp('^' + base) : null var base_pattern = base ? RegExp('^' + RegExp.quoteRegExp(base)) : null data.forEach(function(path){ // XXX need to normalize path... var p = (base_pattern ? path.replace(base_pattern, './') : path) .replace(/([\/\\])\1+/g, '/') // XXXX var gid = hash('I'+i+':'+p) var name = (p // basename... .split(/[\\\/]/g).pop() || '') // ext... .split(/(\.[^\.]*$)/) // XXX populate the image doc better... images[gid] = { id: gid, path: p, // basename... name: name[0], // ext with leading '.' ext: name[1], } // remove only if base path is given and in path... if(base && base_pattern.test(path)){ images[gid].base_path = base } i += 1 }) return images }, fromJSON: function(data){ return new this().loadJSON(data) }, } var ImagesPrototype = module.ImagesPrototype = { // XXX is this a good name for this??? (see: object.js) __init__: function(json){ // load initial state... if(json != null){ this.loadJSON(json) } else { this._reset() } return this }, get length(){ return Object.keys(this).length }, // Generic iterators... // // function format: // function(key, value, index, object) // // reduce function format: // function(value1, value2, key, index, object) // // // this will be set to the value... // // XXX revise... // XXX are these slower than doing it manualy via Object.keys(..) forEach: function(func){ var i = 0 for(var key in this){ // reject non images... // XXX make this cleaner... if(key == 'length' || typeof(this[key]) == typeof(function(){})){ continue } func.call(this[key], key, this[key], i++, this) } return this }, filter: function(func){ var res = new this.constructor() var i = 0 for(var key in this){ // reject non images... // XXX make this cleaner... if(key == 'length' || typeof(this[key]) == typeof(function(){})){ continue } if(func.call(this[key], key, this[key], i++, this)){ res[key] = this[key] } } return res }, // NOTE: .map(..) and .reduce(..) will not return Images objects... map: function(func){ //var res = this.constructor() var res = [] var i = 0 for(var key in this){ // reject non images... // XXX make this cleaner... if(key == 'length' || typeof(this[key]) == typeof(function(){})){ continue } //res[key] = func.call(this[key], key, this[key], i++, this) res.push(func.call(this[key], key, this[key], i++, this)) } return res }, reduce: function(func, initial){ var res = initial for(var key in this){ // reject non images... // XXX make this cleaner... if(key == 'length' || typeof(this[key]) == typeof(function(){})){ continue } res = func.call(this[key], res, this[key], key, i++, this) } return res }, keys: function(){ return Object.keys(this) }, // Build an image index relative to an attribute... // // Format: // { // : [ // , // ... // ], // ... // } // // XXX test out the attr list functionality... makeIndex: function(attr){ var res = {} attr = attr.constructor !== Array ? [attr] : attr // buld the index... var that = this this.forEach(function(key){ var n = attr.map(function(n){ return that[n] }) n = JSON.stringify(n.length == 1 ? n[0] : n) // XXX is this the right way to go? .replace(/^"(.*)"$/g, '$1') res[n] = n in res ? res[n].concat(key) : [key] }) return res }, // Image data helpers... // XXX see: ribbons.js for details... getBestPreview: function(gid, size, img_data, full_path){ if(img_data === true){ full_path = true img_data = null } //gid = gid == null ? getImageGID(): gid //size = size == null ? getVisibleImageSize('max') : size img_data = img_data == null ? this[gid] : img_data // if no usable images are available use STUB data... if((img_data.preview == null || Object.keys(img_data.preview).length == 0) && img_data.path == null){ img_data = IMAGE_DATA } var s var url = img_data.path var preview_size = 'Original' var p = Infinity var previews = img_data.preview || {} for(var k in previews){ s = parseInt(k) if(s < p && s > size){ preview_size = k p = s url = previews[k] } } return { //url: normalizePath(url), url: (full_path && img_data.base_path ? img_data.base_path + '/' : '') + url, size: preview_size } }, // Get image filename... getImageFileName: function(gid, do_unescape){ do_unescape = do_unescape == null ? true : do_unescape if(do_unescape){ return unescape(this[gid].path.split('/').pop()) } else { return this[gid].path.split('/').pop() } }, // Get the first sequence of numbers in the file name... getImageNameSeq: function(gid){ var n = this.getImageFileName(gid) var r = /([0-9]+)/m.exec(n) return r == null ? n : parseInt(r[1]) }, // Get the sequence of numbers in the file name but only if it is // at the filename start... getImageNameLeadingSeq: function(gid){ var n = this.getImageFileName(gid) var r = /^([0-9]+)/g.exec(n) return r == null ? n : parseInt(r[1]) }, // Replace image gid... // replaceGid: function(from, to){ var img = this[from] // XXX is the test needed here??? if(img != null){ delete this[from] this[to] = img } return this }, // Gid sorters... // XXX might be a good idea to add caching... // XXX chainCmp(..) is loaded from lib/jli.js sortImages: function(gids, cmp, reverse){ gids = gids == null ? Object.keys(this) : gids cmp = cmp == null ? module.makeImageDateCmp(this) : cmp cmp = cmp.constructor === Array ? chainCmp(cmp) : cmp gids = gids.sort(cmp) gids = reverse ? gids.reverse() : gids return gids }, // Shorthands... // XXX sortedImagesByFileNameSeqWithOverflow: function(gids, reverse){ gids = gids == null ? Object.keys(this) : gids // XXX see ../ui/sort.js }, // Actions... // Rotate image... // // Rotate image clockwise: // .rotateImage(target, 'cw') // -> images // // Rotate image counterclockwise: // .rotateImage(target, 'ccw') // -> images // // Set explicit image rotation angle: // .rotateImage(target, 0|90|180|270) // .rotateImage(target, -90|-180|-270) // -> images // // NOTE: target can be a gid or a list of gids... rotateImage: function(gids, direction){ gids = gids.constructor !== Array ? [gids] : gids // validate direction... if(module.calcRelativeRotation(direction) == null){ return this } var that = this gids.forEach(function(key){ var img = that[key] if(img == null){ img = that[key] = {} } var o = direction == 'cw' || direction == 'ccw' ? module.calcRelativeRotation(img.orientation, direction) : direction*1 if(o == 0){ delete img.orientation } else { img.orientation = o } // account for proportions... //that.correctImageProportionsForRotation(img) // XXX this is a bit of an overkill but it will update the // preview if needed... //that.updateImage(img) }) return this }, // Flip image... // // .flipImage(target, 'horizontal') // .flipImage(target, 'vertical') // -> images // flipImage: function(gids, direction, reference){ gids = gids.constructor !== Array ? [gids] : gids reference = reference || 'view' var that = this gids.forEach(function(key){ var img = that[key] var o = img.orientation var d = direction // flip relative to if(reference == 'view' && (o == 90 || o == 270)){ d = d == 'horizontal' ? 'vertical' : 'horizontal' } if(img == null){ img = that[key] = {} } var state = img.flipped state = state == null ? [] : state // toggle the specific state... var i = state.indexOf(d) if(i >= 0){ state.splice(i, 1) } else { state.push(d) } if(state.length == 0){ delete img.flipped } else { img.flipped = state } }) return this }, clone: function(){ return (new Images()).join(this) }, // NOTE: this will join the other data into the current object in-place, // use .clone() to preserve current data... join: function(other){ var that = this other.forEach(function(gid, img){ that[gid] = img }) return this }, // serialization... loadJSON: function(data){ data = typeof(data) == typeof('str') ? JSON.parse(data) : JSON.parse(JSON.stringify(data)) for(var k in data){ this[k] = data[k] } return this }, // XXX this is really odd: renaming this to 'toJSON' breaks JavasCript // making chrome/node just say: "" and a filename... dumpJSON: function(data){ return JSON.parse(JSON.stringify(this)) }, _reset: function(){ }, } /*********************************************************************/ // Main Images object... var Images = module.Images = object.makeConstructor('Images', ImagesClassPrototype, ImagesPrototype) /********************************************************************** * vim:set ts=4 sw=4 : */ return module })