diff --git a/ui/crop.js b/ui/crop.js new file mode 100755 index 00000000..9330b191 --- /dev/null +++ b/ui/crop.js @@ -0,0 +1,193 @@ +/********************************************************************** +* +* Ribbon Crop API +* +* +**********************************************************************/ + +var CROP_STACK = [] + +var CROP_MODES = [] + + +/******************************************************* Crop Data ***/ + +function isViewCropped(){ + return CROP_STACK.length != 0 +} + + +function getAllData(){ + if(!isViewCropped()){ + return DATA + } else { + return CROP_STACK[0] + } +} + + +// NOTE: this will not update .current state... +// NOTE: when keep_ribbons is set, this may generate empty ribbons... +// +// XXX should this set the .current to anything but null or the first elem??? +function makeCroppedData(gids, keep_ribbons){ + var res = { + varsion: '2.0', + current: null, + ribbons: [], + order: DATA.order.slice(), + } + + // flat single ribbon crop... + if(!keep_ribbons){ + res.ribbons[0] = gids + + // keep the ribbon structure... + } else { + $.each(DATA.ribbons, function(_, e){ + e = e.filter(function(ee){ return gids.indexOf(ee) >= 0 }) + // skip empty ribbons... + if(e.length != 0){ + res.ribbons.push(e) + } + }) + } + + return res +} + + +// NOTE: if keep_ribbons is not set this will ALWAYS build a single ribbon +// data-set... +function cropDataTo(gids, keep_ribbons){ + var prev_state = DATA + var cur = DATA.current + var r = getRibbonIndex() + + var new_data = makeCroppedData(gids, keep_ribbons) + + // do nothing if there is no change... + // XXX is there a better way to compare states??? + if(JSON.stringify(DATA.ribbons) == JSON.stringify(new_data.ribbons)){ + return DATA + } + + CROP_STACK.push(prev_state) + DATA = new_data + + cur = getGIDBefore(cur, keep_ribbons ? r : 0) + cur = cur == null ? gids[0] : cur + DATA.current = cur + + reloadViewer() + updateImages() + + return prev_state +} + + +function uncropData(){ + if(!isViewCropped()){ + return DATA + } + var prev_state = DATA + var cur = DATA.current + + DATA = CROP_STACK.pop() + + // check if cur exists in data being loaded... + if($.map(DATA.ribbons, + function(e, i){ return e.indexOf(cur) >= 0 }).indexOf(true) >= 0){ + // keep the current position... + DATA.current = cur + } + + reloadViewer() + updateImages() + + return prev_state +} + + +function showAllData(){ + var prev_state = DATA + var cur = DATA.current + + if(CROP_STACK.length != 0){ + DATA = getAllData() + CROP_STACK = [] + + // XXX do we need to check if this exists??? + // ...in theory, as long as there are no global destructive + // operations, no. + // keep the current position... + DATA.current = cur + + reloadViewer() + updateImages() + } + + return prev_state +} + + +// Helpers for making crop modes and using crop... + +// Make a generic crop mode toggler +// +// NOTE: This will add the toggler to CROP_MODES, for use by +// uncropLastState(...) +// NOTE: crop modes are exclusive -- it is not possible to enter one crop +// mode from a different crop mode +function makeCropModeToggler(cls, crop){ + var res = createCSSClassToggler( + '.viewer', + //cls + ' cropped-mode', + cls, + function(action){ + // prevent mixing marked-only and single-ribbon modes... + if(action == 'on' + && isViewCropped() + && res('?') != 'on'){ + return false + } + }, + function(action){ + if(action == 'on'){ + showStatusQ('Cropping current ribbon...') + crop() + } else { + showStatusQ('Uncropping to all data...') + showAllData() + } + }) + CROP_MODES.push(res) + return res +} + + +// Uncrop to last state and there is no states to uncrop then exit +// cropped mode. +// +// NOTE: this will exit all crop modes when uncropping the last step. +function uncropLastState(){ + // do nothing if we aren't in a crop mode... + if(!isViewCropped()){ + return + } + + // exit cropped all modes... + if(CROP_STACK.length == 1){ + $.each(CROP_MODES, function(_, e){ e('off') }) + + // ucrop one state... + } else { + showStatusQ('Uncropping...') + uncropData() + } +} + + + +/********************************************************************** +* vim:set ts=4 sw=4 : */ diff --git a/ui/data.js b/ui/data.js index 2fe04800..f5b8818e 100755 --- a/ui/data.js +++ b/ui/data.js @@ -1,7 +1,11 @@ /********************************************************************** * +* Data API and Data DOM connections... * * TODO move DATA to a more logical context avoiding the global vars... +* TODO try and split this into: +* - data.js -- pure DATA API +* - data-ribbons.js -- DATA and Ribbon API mashup... * **********************************************************************/ @@ -109,10 +113,6 @@ var IMAGES_CREATED = false var MARKED = [] -var CROP_STACK = [] - -var CROP_MODES = [] - // NOTE: these are named: - var SETTINGS = { 'global-theme': null, @@ -146,14 +146,6 @@ var UPDATE_SORT_ENABLED = false // XXX for some reason the sync version appears to work faster... var UPDATE_SYNC = false -// object to register all the worker queues... -var WORKERS = {} - -// Used in sortImagesByFileNameSeqWithOverflow -var PROXIMITY = 30 -var CHECK_1ST_PROXIMITY = false -var OVERFLOW_GAP = PROXIMITY * 5 - /********************************************************************** @@ -1596,221 +1588,6 @@ function getPrevLocation(){ * Actions... */ -/******************************************************* Crop Data ***/ - -function isViewCropped(){ - return CROP_STACK.length != 0 -} - - -function getAllData(){ - if(!isViewCropped()){ - return DATA - } else { - return CROP_STACK[0] - } -} - - -// NOTE: this will not update .current state... -// NOTE: when keep_ribbons is set, this may generate empty ribbons... -// -// XXX should this set the .current to anything but null or the first elem??? -function makeCroppedData(gids, keep_ribbons){ - var res = { - varsion: '2.0', - current: null, - ribbons: [], - order: DATA.order.slice(), - } - - // flat single ribbon crop... - if(!keep_ribbons){ - res.ribbons[0] = gids - - // keep the ribbon structure... - } else { - $.each(DATA.ribbons, function(_, e){ - e = e.filter(function(ee){ return gids.indexOf(ee) >= 0 }) - // skip empty ribbons... - if(e.length != 0){ - res.ribbons.push(e) - } - }) - } - - return res -} - - -// NOTE: if keep_ribbons is not set this will ALWAYS build a single ribbon -// data-set... -function cropDataTo(gids, keep_ribbons){ - var prev_state = DATA - var cur = DATA.current - var r = getRibbonIndex() - - var new_data = makeCroppedData(gids, keep_ribbons) - - // do nothing if there is no change... - // XXX is there a better way to compare states??? - if(JSON.stringify(DATA.ribbons) == JSON.stringify(new_data.ribbons)){ - return DATA - } - - CROP_STACK.push(prev_state) - DATA = new_data - - cur = getGIDBefore(cur, keep_ribbons ? r : 0) - cur = cur == null ? gids[0] : cur - DATA.current = cur - - reloadViewer() - updateImages() - - return prev_state -} - - -function uncropData(){ - if(!isViewCropped()){ - return DATA - } - var prev_state = DATA - var cur = DATA.current - - DATA = CROP_STACK.pop() - - // check if cur exists in data being loaded... - if($.map(DATA.ribbons, - function(e, i){ return e.indexOf(cur) >= 0 }).indexOf(true) >= 0){ - // keep the current position... - DATA.current = cur - } - - reloadViewer() - updateImages() - - return prev_state -} - - -function showAllData(){ - var prev_state = DATA - var cur = DATA.current - - if(CROP_STACK.length != 0){ - DATA = getAllData() - CROP_STACK = [] - - // XXX do we need to check if this exists??? - // ...in theory, as long as there are no global destructive - // operations, no. - // keep the current position... - DATA.current = cur - - reloadViewer() - updateImages() - } - - return prev_state -} - - -// Helpers for making crop modes and using crop... - -// Make a generic crop mode toggler -// -// NOTE: This will add the toggler to CROP_MODES, for use by -// uncropLastState(...) -// NOTE: crop modes are exclusive -- it is not possible to enter one crop -// mode from a different crop mode -function makeCropModeToggler(cls, crop){ - var res = createCSSClassToggler( - '.viewer', - //cls + ' cropped-mode', - cls, - function(action){ - // prevent mixing marked-only and single-ribbon modes... - if(action == 'on' - && isViewCropped() - && res('?') != 'on'){ - return false - } - }, - function(action){ - if(action == 'on'){ - showStatusQ('Cropping current ribbon...') - crop() - } else { - showStatusQ('Uncropping to all data...') - showAllData() - } - }) - CROP_MODES.push(res) - return res -} - - -// Uncrop to last state and there is no states to uncrop then exit -// cropped mode. -// -// NOTE: this will exit all crop modes when uncropping the last step. -function uncropLastState(){ - // do nothing if we aren't in a crop mode... - if(!isViewCropped()){ - return - } - - // exit cropped all modes... - if(CROP_STACK.length == 1){ - $.each(CROP_MODES, function(_, e){ e('off') }) - - // ucrop one state... - } else { - showStatusQ('Uncropping...') - uncropData() - } -} - - - -/********************************************************* Workers ***/ - -// get/create a named worker queue... -function getWorkerQueue(name, no_auto_start){ - - // create a new worker queue... - if(WORKERS[name] == null){ - var queue = makeDeferredsQ() - WORKERS[name] = queue - // start if needed... - if(!no_auto_start){ - queue.start() - } - - // return existing worker queue... - } else { - var queue = WORKERS[name] - } - - return queue -} - - -// kill all worker queues... -function killAllWorkers(){ - for(var k in WORKERS){ - if(WORKERS[k].isWorking()){ - console.log('Worker: Stopped:', k) - } - WORKERS[k].kill() - } - WORKERS = {} -} - - - /******************************************************* Extension ***/ // Open image in an external editor/viewer @@ -1833,189 +1610,5 @@ function openImageWith(prog){ -/********************************************************* Sorting ***/ - -function reverseImageOrder(){ - DATA.order.reverse() - updateRibbonOrder() -} - - -// NOTE: using imageOrderCmp as a cmp function here will yield odd -// results -- in-place sorting a list based on relative element -// positions within itself is fun ;) -function sortImages(cmp, reverse){ - cmp = cmp == null ? imageDateCmp : cmp - DATA.order.sort(cmp) - if(reverse){ - DATA.order.reverse() - } - updateRibbonOrder() -} - - -// shorthands... -function sortImagesByDate(reverse){ - return sortImages(reverse) -} -function sortImagesByFileName(reverse){ - return sortImages(imageNameCmp, reverse) -} -function sortImagesByFileSeqOrName(reverse){ - return sortImages(imageSeqOrNameCmp, reverse) -} -function sortImagesByFileNameXPStyle(reverse){ - return sortImages(imageXPStyleFileNameCmp, reverse) -} - - -// Sort images by name while taking into account sequence overflows -// -// A name sequence overflow is when file name sequence overflows over -// *9999 and then resets to *0001... -// -// For this to be applicable: -// - ALL filenames must contain a sequence number -// XXX do we need to make this more strict? -// ...for example make name.split() equal for all files -// - the total number of files in sequence is < 10K -// XXX a simplification... -// there could be more than 10K images but then we will need to -// either take dates or folder names into account... -// - the lowest filename in set must be near seq 0001 -// - the highest filename in set must be near seq 9999 -// - there must be a gap somewhere in the set -// this gap size is roughly close to 10K - N where N is the total -// number of files in set -// XXX a simplification... -// -// The gap size must be above overflow_gap, and if it is set to false -// then no limit is used (default: OVERFLOW_GAP). -// If check_1st is false then also check the lowest sequence number -// for proximity to 0001 (default: CHECK_1ST_PROXIMITY). -// -// NOTE: if any of the above conditions is not applicable this will -// essentially revert to sortImagesByFileSeqOrName(...) -// NOTE: this will cut at the largest gap between sequence numbers. -// -// XXX it would be a good idea to account for folder name sequencing... -// XXX it's also a good idea to write an image serial number sort... -// XXX is this overcomplicated??? -// -// NOTE: I like this piece if code, if it works correctly no one will -// ever know it's here, if we replace it with the thee line dumb -// sortImagesByFileName(...) then things get "annoying" every 10K -// images :) -function sortImagesByFileNameSeqWithOverflow(reverse, proximity, overflow_gap, check_1st){ - proximity = proximity == null ? PROXIMITY : proximity - overflow_gap = overflow_gap == null ? OVERFLOW_GAP : overflow_gap - check_1st = check_1st == null ? CHECK_1ST_PROXIMITY : check_1st - - // prepare to sort and check names... - // NOTE: we do not usually have a filename seq 0000... - if(DATA.order.length < 9999){ - var need_to_fix = true - - function cmp(a, b){ - if(need_to_fix){ - if(typeof(getImageNameSeq(a)) == typeof('str') - || typeof(getImageNameSeq(b)) == typeof('str')){ - need_to_fix = false - } - } - return imageSeqOrNameCmp(a, b) - } - - // revert to normal sort my name... - } else { - // XXX make this more cleaver -- split the set into 10K chunks and - // sort the chunks too... - return sortImagesByFileName(reverse) - } - - DATA.order.sort(cmp) - - // find and fix the gap... - if(need_to_fix - // check if first and last are close to 0001 and 9999 resp. - && (!check_1st || getImageNameSeq(DATA.order[0]) <= proximity) - && getImageNameSeq(DATA.order[DATA.order.length-1]) >= 9999-proximity){ - // find the largest gap position... - var pos = null - var gap = 0 - for(var i=1; i overflow_gap){ - DATA.order = DATA.order.splice(pos).concat(DATA.order) - } - } - if(reverse){ - DATA.order.reverse() - } - - updateRibbonOrder() -} - - - -/************************************************** Manual sorting ***/ - -// Ordering images... -// NOTE: this a bit more complicated than simply shifting an image -// left/right the DATA.order, we have to put it before or after -// the prev/next image... -function horizontalShiftImage(image, direction){ - image = image == null ? getImage() : $(image) - var gid = getImageGID(image) - var r = getRibbonIndex(image) - var ri = DATA.ribbons[r].indexOf(gid) - - // the image we are going to move relative to... - var target = DATA.ribbons[r][ri + (direction == 'next' ? 1 : -1)] - - // we can hit the end or start of the ribbon... - if(target == null){ - return image - } - - // update the order... - // NOTE: this is a critical section and must be done as fast as possible, - // this is why we are using the memory to first do the work and - // then push it in... - // NOTE: in a race condition this may still overwrite the order someone - // else is working on, the data will be consistent... - var order = DATA.order.slice() - order.splice(order.indexOf(gid), 1) - order.splice(order.indexOf(target) + (direction == 'next'? 1 : 0), 0, gid) - // do the dirty work... - DATA.order.splice.apply(DATA.order, [0, DATA.order.length].concat(order)) - - // just update the ribbons, no reloading needed... - updateRibbonOrder(true) - - // shift the images... - getImage(target)[direction == 'prev' ? 'before' : 'after'](image) - - // update stuff that changed, mainly order... - updateImages() - - return image -} -function shiftImageLeft(image){ - return horizontalShiftImage(image, 'prev') -} -function shiftImageRight(image){ - return horizontalShiftImage(image, 'next') -} - - - - /********************************************************************** * vim:set ts=4 sw=4 spell : */ diff --git a/ui/index.html b/ui/index.html index 642a3ed9..dcb957fb 100755 --- a/ui/index.html +++ b/ui/index.html @@ -21,8 +21,11 @@ - + + + + diff --git a/ui/base.js b/ui/ribbons.js similarity index 100% rename from ui/base.js rename to ui/ribbons.js diff --git a/ui/sort.js b/ui/sort.js new file mode 100755 index 00000000..fafb3ba3 --- /dev/null +++ b/ui/sort.js @@ -0,0 +1,208 @@ +/********************************************************************** +* +* Image Sorting API +* +* +**********************************************************************/ + +// Used in sortImagesByFileNameSeqWithOverflow +var PROXIMITY = 30 +var CHECK_1ST_PROXIMITY = false +var OVERFLOW_GAP = PROXIMITY * 5 + + + +/********************************************************* Sorting ***/ + +function reverseImageOrder(){ + DATA.order.reverse() + updateRibbonOrder() +} + + +// NOTE: using imageOrderCmp as a cmp function here will yield odd +// results -- in-place sorting a list based on relative element +// positions within itself is fun ;) +function sortImages(cmp, reverse){ + cmp = cmp == null ? imageDateCmp : cmp + DATA.order.sort(cmp) + if(reverse){ + DATA.order.reverse() + } + updateRibbonOrder() +} + + +// shorthands... +function sortImagesByDate(reverse){ + return sortImages(reverse) +} +function sortImagesByFileName(reverse){ + return sortImages(imageNameCmp, reverse) +} +function sortImagesByFileSeqOrName(reverse){ + return sortImages(imageSeqOrNameCmp, reverse) +} +function sortImagesByFileNameXPStyle(reverse){ + return sortImages(imageXPStyleFileNameCmp, reverse) +} + + +// Sort images by name while taking into account sequence overflows +// +// A name sequence overflow is when file name sequence overflows over +// *9999 and then resets to *0001... +// +// For this to be applicable: +// - ALL filenames must contain a sequence number +// XXX do we need to make this more strict? +// ...for example make name.split() equal for all files +// - the total number of files in sequence is < 10K +// XXX a simplification... +// there could be more than 10K images but then we will need to +// either take dates or folder names into account... +// - the lowest filename in set must be near seq 0001 +// - the highest filename in set must be near seq 9999 +// XXX alternatively check the difference between first and +// last elements, if it is far greater than the number +// of elements then it's likely that we need to split... +// - there must be a gap somewhere in the set +// this gap size is roughly close to 10K - N where N is the total +// number of files in set +// XXX a simplification... +// +// The gap size must be above overflow_gap, and if it is set to false +// then no limit is used (default: OVERFLOW_GAP). +// If check_1st is false then also check the lowest sequence number +// for proximity to 0001 (default: CHECK_1ST_PROXIMITY). +// +// NOTE: if any of the above conditions is not applicable this will +// essentially revert to sortImagesByFileSeqOrName(...) +// NOTE: this will cut at the largest gap between sequence numbers. +// +// XXX it would be a good idea to account for folder name sequencing... +// XXX it's also a good idea to write an image serial number sort... +// XXX is this overcomplicated??? +// +// NOTE: I like this piece if code, if it works correctly no one will +// ever know it's here, if we replace it with the thee line dumb +// sortImagesByFileName(...) then things get "annoying" every 10K +// images :) +function sortImagesByFileNameSeqWithOverflow(reverse, proximity, overflow_gap, check_1st){ + proximity = proximity == null ? PROXIMITY : proximity + overflow_gap = overflow_gap == null ? OVERFLOW_GAP : overflow_gap + check_1st = check_1st == null ? CHECK_1ST_PROXIMITY : check_1st + + // prepare to sort and check names... + // NOTE: we do not usually have a filename seq 0000... + if(DATA.order.length < 9999){ + var need_to_fix = true + + function cmp(a, b){ + if(need_to_fix){ + if(typeof(getImageNameSeq(a)) == typeof('str') + || typeof(getImageNameSeq(b)) == typeof('str')){ + need_to_fix = false + } + } + return imageSeqOrNameCmp(a, b) + } + + // revert to normal sort my name... + } else { + // XXX make this more cleaver -- split the set into 10K chunks and + // sort the chunks too... + return sortImagesByFileName(reverse) + } + + DATA.order.sort(cmp) + + // find and fix the gap... + if(need_to_fix + // check if first and last are close to 0001 and 9999 resp. + // XXX alternatively check the difference between first and + // last elements, if it is far greater than the number + // of elements then it's likely that we need to split... + && (!check_1st || getImageNameSeq(DATA.order[0]) <= proximity) + && getImageNameSeq(DATA.order[DATA.order.length-1]) >= 9999-proximity){ + // find the largest gap position... + var pos = null + var gap = 0 + for(var i=1; i overflow_gap){ + DATA.order = DATA.order.splice(pos).concat(DATA.order) + } + } + if(reverse){ + DATA.order.reverse() + } + + updateRibbonOrder() +} + + + +/************************************************** Manual sorting ***/ + +// Ordering images... +// NOTE: this a bit more complicated than simply shifting an image +// left/right the DATA.order, we have to put it before or after +// the prev/next image... +function horizontalShiftImage(image, direction){ + image = image == null ? getImage() : $(image) + var gid = getImageGID(image) + var r = getRibbonIndex(image) + var ri = DATA.ribbons[r].indexOf(gid) + + // the image we are going to move relative to... + var target = DATA.ribbons[r][ri + (direction == 'next' ? 1 : -1)] + + // we can hit the end or start of the ribbon... + if(target == null){ + return image + } + + // update the order... + // NOTE: this is a critical section and must be done as fast as possible, + // this is why we are using the memory to first do the work and + // then push it in... + // NOTE: in a race condition this may still overwrite the order someone + // else is working on, the data will be consistent... + var order = DATA.order.slice() + order.splice(order.indexOf(gid), 1) + order.splice(order.indexOf(target) + (direction == 'next'? 1 : 0), 0, gid) + // do the dirty work... + DATA.order.splice.apply(DATA.order, [0, DATA.order.length].concat(order)) + + // just update the ribbons, no reloading needed... + updateRibbonOrder(true) + + // shift the images... + getImage(target)[direction == 'prev' ? 'before' : 'after'](image) + + // update stuff that changed, mainly order... + updateImages() + + return image +} +function shiftImageLeft(image){ + return horizontalShiftImage(image, 'prev') +} +function shiftImageRight(image){ + return horizontalShiftImage(image, 'next') +} + + + + + + +/********************************************************************** +* vim:set ts=4 sw=4 : */ diff --git a/ui/workers.js b/ui/workers.js new file mode 100755 index 00000000..55780db3 --- /dev/null +++ b/ui/workers.js @@ -0,0 +1,52 @@ +/********************************************************************** +* +* Deferred worker API +* +* NOTE: at this point this only contains a worker queue... +* +* +**********************************************************************/ + +// object to register all the worker queues... +var WORKERS = {} + + + +/********************************************************* Workers ***/ + +// get/create a named worker queue... +function getWorkerQueue(name, no_auto_start){ + + // create a new worker queue... + if(WORKERS[name] == null){ + var queue = makeDeferredsQ() + WORKERS[name] = queue + // start if needed... + if(!no_auto_start){ + queue.start() + } + + // return existing worker queue... + } else { + var queue = WORKERS[name] + } + + return queue +} + + +// kill all worker queues... +function killAllWorkers(){ + for(var k in WORKERS){ + if(WORKERS[k].isWorking()){ + console.log('Worker: Stopped:', k) + } + WORKERS[k].kill() + } + WORKERS = {} +} + + + +/********************************************************************** +* vim:set ts=4 sw=4 : */