diff --git a/ui/bookmarks.js b/ui/bookmarks.js index 19fb995f..4e63717b 100755 --- a/ui/bookmarks.js +++ b/ui/bookmarks.js @@ -147,7 +147,7 @@ function setupBookmarks(viewer){ return viewer .on('sortedImages', function(){ - BOOKMARKS.sort(imageOrderCmp) + BOOKMARKS = fastSortGIDsByOrder(BOOKMARKS) }) } SETUP_BINDINGS.push(setupBookmarks) diff --git a/ui/data.js b/ui/data.js index f246c0a2..723d1841 100755 --- a/ui/data.js +++ b/ui/data.js @@ -536,6 +536,54 @@ Array.prototype.binSearch = function(target, cmp, get){ */ +// This is a cheating fast sort... +// +// By cheating we might use more memory -- this is both not in-place +// and may use quite a bit of memory... +// +// The gain is that this is SIGNIFICANTLY faster than using +// .sort(imageOrderCmp)... +// +// The complexity here is O(N) where N is DATA.order.length rather than +// gids.length vs. O(n nog n) for the .sort(..), but the processing overhead +// is significantly smaller... +// +// Here are a couple of test runs: +// +// var t0 = Date.now() +// getRibbonGIDs() +// .slice(0, 2000) +// .sort(imageOrderCmp) +// console.log('T:', Date.now()-t0) +// >>> T: 4126 +// +// var t0 = Date.now() +// fastSortGIDsByOrder( +// getRibbonGIDs() +// .slice(0,2000)) +// console.log('T:', Date.now()-t0) +// >>> T: 171 +// +// On the down side, this has some memory overhead -- ~ N - n * ref +// +function fastSortGIDsByOrder(gids, data){ + data = data == null ? DATA : data + + var order = data.order + var res = [] + + // insert the gids to their order positions... + gids.forEach(function(gid){ + res[order.indexOf(gid)] = gid + }) + + // clear out the nulls... + return res.filter(function(e){ + return e != null + }) +} + + // Base URL interface... // // NOTE: changing a base URL will trigger a baseURLChanged event... @@ -638,12 +686,16 @@ function getGIDRibbonIndex(gid, data){ // - number - ribbon index // - gid // - image -function getRibbonGIDs(a, data){ +function getRibbonGIDs(a, no_clone, data){ data = data == null ? DATA : data if(typeof(a) == typeof(123)){ return data.ribbons[a].slice() } - return data.ribbons[getGIDRibbonIndex(a, data)].slice() + var res = data.ribbons[getGIDRibbonIndex(a, data)] + if(no_clone){ + return res + } + return res.slice() } @@ -1286,6 +1338,10 @@ function makeNextFromListAction(get_closest, get_list, restrict_to_ribbon){ // see makeNextFromListAction(..) above for documentation... +// +// XXX try a new technique: +// - before calling getImageBefore(..) remove the curent gid form +// target list... function makePrevFromListAction(get_closest, get_list, restrict_to_ribbon){ get_closest = get_closest == null ? getGIDBefore : get_closest get_list = get_list == null ? getRibbonGIDs : get_list @@ -2113,7 +2169,8 @@ function showImage(gid){ // NOTE: if no_reload_viewer is true, then no re-rendering is triggered. function updateRibbonOrder(no_reload_viewer){ for(var i=0; i < DATA.ribbons.length; i++){ - DATA.ribbons[i].sort(imageOrderCmp) + //DATA.ribbons[i].sort(imageOrderCmp) + DATA.ribbons[i] = fastSortGIDsByOrder(DATA.ribbons[i]) } if(!no_reload_viewer){ reloadViewer(true) diff --git a/ui/keybindings.js b/ui/keybindings.js index 8de2d865..8c732b07 100755 --- a/ui/keybindings.js +++ b/ui/keybindings.js @@ -588,6 +588,10 @@ var KEYBOARD_CONFIG = { function(){ prevBookmark() }), ']': doc('Next bookmarked image', function(){ nextBookmark() }), + '{': doc('Previous unsorted section edge', + function(){ prevUnsortedSection() }), + '}': doc('Next unsorted section edge', + function(){ nextUnsortedSection() }), S: { default: doc('Start slideshow', diff --git a/ui/marks.js b/ui/marks.js index c5e4894c..5e647953 100755 --- a/ui/marks.js +++ b/ui/marks.js @@ -164,7 +164,7 @@ var updateSelectedImageMark = makeMarkUpdater( // not exist, as there is no way to distinguish between the two // situations the cleanup is optional... function cropMarkedImages(keep_ribbons, keep_unloaded_gids){ - var marked = MARKED.slice()//.sort(imageOrderCmp) + var marked = MARKED.slice() cropDataTo(marked, keep_ribbons, keep_unloaded_gids) @@ -249,7 +249,8 @@ function toggleAllMarks(action, mode){ if(action == 'on'){ var _update = function(e){ if(MARKED.indexOf(e) < 0){ - insertGIDToPosition(e, MARKED) + //insertGIDToPosition(e, MARKED) + MARKED.push(e) updated.push(e) } } @@ -274,6 +275,8 @@ function toggleAllMarks(action, mode){ res.forEach(_update) + MARKED = fastSortGIDsByOrder(MARKED) + updateImages(updated) $('.viewer').trigger('togglingMarks', [updated, action]) @@ -310,12 +313,14 @@ function invertImageMarks(){ var i = MARKED.indexOf(e) if(i == -1){ on.push(e) - insertGIDToPosition(e, MARKED) + MARKED.push(e) + //insertGIDToPosition(e, MARKED) } else { off.push(e) MARKED.splice(i, 1) } }) + MARKED = fastSortGIDsByOrder(MARKED) updateImages(ribbon) $('.viewer') @@ -348,7 +353,8 @@ function toggleMarkBlock(image){ } // do the toggle... if(state){ - insertGIDToPosition(e, MARKED) + //insertGIDToPosition(e, MARKED) + MARKED.push(e) } else { MARKED.splice(MARKED.indexOf(e), 1) } @@ -364,6 +370,8 @@ function toggleMarkBlock(image){ var right = ribbon.slice(i+1) $.each(right, _convert) + MARKED = fastSortGIDsByOrder(MARKED) + updateImages(updated) $('.viewer') @@ -396,7 +404,7 @@ function shiftMarkedImages(direction, mode, new_ribbon){ // shift all marked images... } else { var marked = MARKED.slice() - // remove all the marked images form all the ribbons... + // remove all the marked images form all other ribbons... $.each(DATA.ribbons, function(ribbon){ $.each(marked, function(e){ var i = ribbon.indexOf(e) @@ -420,7 +428,7 @@ function shiftMarkedImages(direction, mode, new_ribbon){ // add marked to existing ribbon... } else { cur += direction == 'next' ? 1 : -1 - DATA.ribbons[cur] = DATA.ribbons[cur].concat(marked).sort(cmp) + DATA.ribbons[cur] = fastSortGIDsByOrder(DATA.ribbons[cur].concat(marked)) } // remove empty ribbons... @@ -557,7 +565,7 @@ var loadFileMarks = makeFileLoader( if(DATA.version == '2.0'){ setTimeout(function(){ var t0 = Date.now() - MARKED.sort(imageOrderCmp) + MARKED = fastSortGIDsByOrder(MARKED) var t1 = Date.now() // XXX is this the correct way to do this??? diff --git a/ui/sort.js b/ui/sort.js index a9c9c1f3..f56622c0 100755 --- a/ui/sort.js +++ b/ui/sort.js @@ -177,7 +177,10 @@ function getClosestGIDs(gid){ function reverseImageOrder(){ DATA.order.reverse() - updateRibbonOrder() + DATA.ribbons.forEach(function(r){ + r.reverse() + }) + reloadViewer(true) } diff --git a/ui/tags.js b/ui/tags.js index 301f0898..2e16f5ae 100755 --- a/ui/tags.js +++ b/ui/tags.js @@ -20,6 +20,10 @@ var UNSORTED_TAG = 'unsorted' var TAGS = {} +var TAGS_FILE_DEFAULT = 'tags.json' +var TAGS_FILE_PATTERN = /^[0-9]*-tags.json$/ + + /*********************************************************************/ @@ -27,6 +31,8 @@ function buildTagsFromImages(tagset, images){ tagset = tagset == null ? TAGS : tagset images = images == null ? IMAGES : images + var order = DATA.order + for(var gid in images){ var tags = images[gid].tags // no tags in this image... @@ -40,10 +46,19 @@ function buildTagsFromImages(tagset, images){ } // only update if not tagged... if(tagset[tag].indexOf(gid) < 0){ - tagset[tag].push(gid) + // NOTE: this is cheating, but it's ~5x faster than + // insertGIDToPosition(..) but still 10^2 slower + // than .push(..) (unsorted tags) + tagset[tag][order.indexOf(gid)] = gid } }) } + + // cleanup... + for(var tag in tagset){ + tagset[tag] = tagset[tag].filter(function(e){ return e != null }) + } + return tagset } @@ -93,8 +108,6 @@ function addTag(tags, gid, tagset, images){ tagset[tag] = set } if(set.indexOf(gid) < 0){ - //set.push(gid) - //set.sort() insertGIDToPosition(gid, set) } @@ -193,7 +206,7 @@ function tagSelectAND(tags, from, no_sort, tagset){ return res == null ? [] : res.filter(function(gid){ - // skip unloaded from... + // skip unloaded... return from.indexOf(gid) >= 0 }) } @@ -231,10 +244,15 @@ function tagSelectAND(tags, from, no_sort, tagset){ } // populate res... if(gid != null && from.indexOf(gid) >= 0){ - no_sort ? res.push(gid) : insertGIDToPosition(gid, res) + //no_sort == true ? res.push(gid) : insertGIDToPosition(gid, res) + res.push(gid) } }) + if(!no_sort){ + fastSortGIDsByOrder(res) + } + return res } @@ -271,16 +289,11 @@ function tagSelectOR(tags, from, no_sort, tagset){ } -/* -// XXX don't remember the semantics... -function getRelatedTags(){ -} + +/********************************************************************** +* List oriented tag operations... */ - - -/*********************************************************************/ - function tagList(list, tags){ list.forEach(function(gid){ addTag(tags, gid) @@ -353,18 +366,22 @@ function unmarkTagged(tags){ // // Essentially this will list tag block borders. // +// NOTE: this will consider each gids's ribbon context rather than the +// straight order context... +// XXX this is slow... function listTagsAtGapsFrom(tags, gids){ gids = gids == null ? getLoadedGIDs() : gids var list = tagSelectAND(tags, gids) var res = [] list.forEach(function(gid){ - var i = gids.indexOf(gid) + var ribbon = DATA.ribbons[getGIDRibbonIndex(gid)] + var i = ribbon.indexOf(gid) // add the current gid to the result iff one or both gids // adjacent to it are not in the list... - if(list.indexOf(gids[i-1]) < 0 - || list.indexOf(gids[i+1]) < 0){ + if(list.indexOf(ribbon[i-1]) < 0 + || list.indexOf(ribbon[i+1]) < 0){ res.push(gid) } }) @@ -373,6 +390,44 @@ function listTagsAtGapsFrom(tags, gids){ } +// XXX these are still slow -- there is no need to re-index the whole +// data on each jump... +function nextGapEdge(tags, gids){ + var res = getGIDAfter(getImageGID(), listTagsAtGapsFrom(tags, gids)) + + if(res == null){ + flashIndicator('end') + return getImage() + } + return showImage(res) +} +function prevGapEdge(tags, gids){ + var cur = getImageGID() + var targets = listTagsAtGapsFrom(tags, gids) + + // drop the current elem if it's in the list... + var i = targets.indexOf(cur) + if(i >= 0){ + targets.splice(i, 1) + } + + var res = getGIDBefore(cur, targets) + + if(res == null){ + flashIndicator('start') + return getImage() + } + return showImage(res) +} + + +function nextUnsortedSection(){ + return nextGapEdge('unsorted') +} +function prevUnsortedSection(){ + return prevGapEdge('unsorted') +} + /*********************************************************************/ @@ -389,6 +444,30 @@ function cropTagged(tags, keep_ribbons, keep_unloaded_gids){ +/********************************************************************** +* Files... +*/ + +// XXX need to detect if we loaded the tags and if so not call +// buildTagsFromImages(..)... +var loadFileTags = makeFileLoader( + 'Tags', + TAGS_FILE_DEFAULT, + TAGS_FILE_PATTERN, + function(data){ + TAGS = data + }) + + +// Save image marks to file +var saveFileMarks = makeFileSaver( + TAGS_FILE_DEFAULT, + function(){ + return TAGS + }) + + + /********************************************************************** * Setup... */ @@ -411,7 +490,7 @@ function setupTags(viewer){ }) } -SETUP_BINDINGS.push(setupTags) +//SETUP_BINDINGS.push(setupTags) // Setup the unsorted image state managers...