c8fdb2541c9758c0db06e4cf7fff3e4a0160d54b
tdreszer
  Fri May 2 16:48:41 2014 -0700
Took this one step further: moved methods to their own cart object and now append queued cart updates to any hgTracks ajax update.  This will provide greater flexibilty for future cart update queuing.  Redmone #13164.
diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index 1174e25..151344b 100644
--- src/hg/js/hgTracks.js
+++ src/hg/js/hgTracks.js
@@ -259,31 +259,31 @@
                                                             pos.chromStart+1, pos.chromEnd);
         return newPosition;
     },
 
     handleChange: function (response, status)
     {
         var json = eval("(" + response + ")");
         genomePos.set(json.pos);
     },
 
     changeAssemblies: function (ele)  // UNUSED?  Larry's experimental code
     {   // code to update page when user changes assembly select list.
         $.ajax({
                 type: "GET",
                 url: "../cgi-bin/hgApi",
-                data: "cmd=defaultPos&db=" + getDb(),
+                data: cart.addUpdatesToUrl("cmd=defaultPos&db=" + getDb()),
                 dataType: "html",
                 trueSuccess: genomePos.handleChange,
                 success: catchErrorOrDispatch,
                 error: errorHandler,
                 cache: true
             });
         return false;
     }
 }
 
   /////////////////////////////////////
  //// Creating items by dragging /////
 /////////////////////////////////////
 var makeItemsByDrag = {
 
@@ -441,37 +441,113 @@
 
         if ($(obj).hasClass('noLink'))  // TITLE_BUT_NO_LINK
             return false;
 
         if (obj.href.match('#') || obj.target.length > 0) {
             //alert("Matched # ["+obj.href+"] or has target:"+obj.target);
             return true;
         }
         var thisForm=$(obj).parents('form');
         if(thisForm == undefined || $(thisForm).length == 0)
             thisForm=$("FORM");
         if($(thisForm).length > 1 )
             thisForm=$(thisForm)[0];
         if(thisForm != undefined && $(thisForm).length == 1) {
             //alert("posting form:"+$(thisForm).attr('name'));
-            var href = vis.cartUpdatesAddToUrl(obj.href);
+            var href = cart.addUpdatesToUrl(obj.href);
             return postTheForm($(thisForm).attr('name'),href);
         }
         return true;
     }
 }
 
+/////////////////////////
+//// cart updating /////
+///////////////////////
+var cart = {
+
+    updateQueue: {},
+    
+    updatesWaiting: function ()
+    {   // returns TRUE if updates are waiting.
+        return (Object.keys(cart.updateQueue).length !== 0);
+    },
+    
+    addUpdatesToUrl: function (url)
+    {   // adds any outstanding cart updates to the url, then clears the queue
+        if (cart.updatesWaiting()) {
+            var updates = "";
+            for (var track in cart.updateQueue) {
+                updates += "&" + track + "=" + cart.updateQueue[track];
+            }
+            if (updates.length > 0) {
+                if(url.length > 0 && url.lastIndexOf("?") == -1 && url.lastIndexOf("&") == -1)
+                    url += "?" + updates.substring(1);
+                else
+                    url += updates
+            }
+            cart.updateQueue = {};
+        }
+        return url;
+    },
+    
+    // NOTE: could update in background, however, failing to hit "refresh" is a user choice
+    // updatesViaAjax: function ()
+    // {   // Called via timer: updates the cart via setVars.
+    // if (!cart.updatesWaiting())
+    //     return;
+    // 
+    //     var tracks = [];
+    //     var newVis = [];
+    //     for (var track in cart.updateQueue) {
+    //         tracks.push(track)
+    //         newVis.push(cart.updateQueue[track]);
+    //     }
+    //     if (tracks.length === 0)
+    //         return;
+    //     cart.updateQueue = {};
+    //     setCartVars(tracks,newVis,async=false); // sync to avoid another race condition mess
+    // },
+    
+    addUpdateToQueue: function (track,newVis)
+    {   // creates a string of updates to save for ajax batch or a submit
+        cart.updateQueue[track] = newVis;
+        
+        // NOTE: could update in background, however, failing to hit "refresh" is a user choice
+        // first in queue, schedule background update
+        // if (Object.keys(cart.updateQueue).length === 1)
+        //     setTimeout("cart.updatesViaAjax();",10000);
+    },
+
+    setVars: function (names, values, errFunc, async)
+    {   // ajax updates the cart, and includes any queued updates.
+        if (cart.updatesWaiting()) {
+            for (var track in cart.updateQueue) {
+                names.push(track);
+                values.push(cart.updateQueue[track]);
+            }
+            cart.updateQueue = {};
+        }
+        setCartVars(names, values, errFunc, async);
+    },
+    
+    setVar: function (names, values, errFunc, async)
+    {   // wraps single var update to expected arrays
+        vis.setVars( [ name ], [ value ], errFunc, async );
+    }
+}
+
   ///////////////////////////////////////////////
  //// visibility (mixed with group toggle) /////
 ///////////////////////////////////////////////
 var vis = {
 
     enumOrder: new Array("hide", "dense", "full", "pack", "squish"),  // map cgi enum visibility codes to strings
     cartUpdateQueue: {},
 
     update: function (track, visibility)
     {   // Updates visibility state in hgTracks.trackDb and any visible elements on the page.
         // returns true if we modify at least one select in the group list
         var rec = hgTracks.trackDb[track];
         var selectUpdated = false;
         $("select[name=" + escapeJQuerySelectorChars(track) + "]").each(function(t) {
             $(this).attr('class', visibility == 'hide' ? 'hiddenText' : 'normalText');
@@ -515,97 +591,48 @@
             return setTableRowVisibility(button, prefix, "hgtgroup", "group",false,arguments[2]);
         else
             return setTableRowVisibility(button, prefix, "hgtgroup", "group",false);
     },
 
     expandAllGroups: function (newState)
     {   // Set visibility of all track groups to newState (true means expanded).
         // This code also modifies the corresponding hidden fields and the gif's of the +/- img tag.
         imageV2.markAsDirtyPage();
         $(".toggleButton[id$='_button']").each( function (i) {  // works for old img type AND new BUTTONS_BY_CSS
             vis.toggleForGroup(this,this.id.substring(0,this.id.length - 7),newState); // clip '_button' suffix
         });
         return false;
     },
     
-    cartUpdatesWaiting: function ()
-    {   // returns TRUE if updates are waiting.
-        return (Object.keys(vis.cartUpdateQueue).length !== 0);
-    },
-    
-    cartUpdatesAddToUrl: function (url)
-    {   // adds any outstanding cart updates to the url, then clears the queue
-        var updates = "";
-        for (var track in vis.cartUpdateQueue) {
-            updates += "&" + track + "=" + vis.cartUpdateQueue[track];
-        }
-        if (updates.length > 0) {
-            if(url.length > 0 && url.lastIndexOf("?") == -1 && url.lastIndexOf("&") == -1)
-                url += "?" + updates.substring(1);
-            else
-                url += updates
-        }
-        vis.cartUpdateQueue = {};
-        return url;
-    },
-
-    // NOTE: could update in background, however, failing to hit "refresh" is a user choice
-    // cartUpdatesViaAjax: function ()
-    // {   // Called via timer: updates the cart via setVars.
-    // if (!vis.cartUpdatesWaiting())
-    //     return;
-    // 
-    //     var tracks = [];
-    //     var newVis = [];
-    //     for (var track in vis.cartUpdateQueue) {
-    //         tracks.push(track)
-    //         newVis.push(vis.cartUpdateQueue[track]);
-    //     }
-    //     if (tracks.length === 0)
-    //         return;
-    //     vis.cartUpdateQueue = {};
-    //     setCartVars(tracks,newVis,async=false); // sync to avoid another race condition mess
-    // },
-
-    cartUpdateAddToQueue: function (track,newVis)
-    {   // creates a string of updates to save for ajax batch or a submit
-        vis.cartUpdateQueue[track] = newVis;
-        
-        // NOTE: could update in background, however, failing to hit "refresh" is a user choice
-        // first in queue, schedule background update
-        // if (Object.keys(vis.cartUpdateQueue).length === 1)
-        //     setTimeout("vis.cartUpdatesViaAjax();",10000);
-    },
-
     initForAjax: function()
     {   // To better support the back-button, it is good to eliminate extraneous form puts
         // Towards that end, we support visBoxes making ajax calls to update cart.
         var sels = $('select.normalText,select.hiddenText');
         $(sels).change(function() {
             var track = $(this).attr('name');
             if ($(this).val() == 'hide') {
                 var rec = hgTracks.trackDb[track];
                 if(rec)
                     rec.visibility = 0;
                 // else Would be nice to hide subtracks as well but that may be overkill
                 $(document.getElementById('tr_' + track)).remove();
                 imageV2.highlightRegion();
                 $(this).attr('class', 'hiddenText');
             } else
                 $(this).attr('class', 'normalText');
             
-            vis.cartUpdateAddToQueue(track,$(this).val());
+            cart.addUpdateToQueue(track,$(this).val());
             return false;
         });
         // Now we can rid the submt of the burden of all those vis boxes
         var form = $('form#TrackForm');
         $(form).submit(function () {
             $('select.normalText,select.hiddenText').attr('disabled',true);
         });
         $(form).attr('method','get');
     },
 
     restoreFromBackButton: function()
     // Re-enabling vis dropdowns is necessarty because intiForAjax() disables them on submit.
     {
         $('select.normalText,select.hiddenText').attr('disabled',false);
     }
@@ -647,31 +674,31 @@
     highlightThisRegion: function(newPosition)
     // set highlighting newPosition in server-side cart and apply the highlighting in local UI.
     {
         var start, end;
         if (arguments.length == 2) {
             start = arguments[0];
             end = arguments[1];
         } else {
             var pos = parsePosition(newPosition);
             start = pos.start;
             end = pos.end;
         }
         hgTracks.highlight  = getDb() + "." + hgTracks.chromName + ":" + start + "-" + end;
         hgTracks.highlight += '#AAFFFF'; // Also include highlight color
         // we include enableHighlightingDialog because it may have been changed by the dialog
-        setCartVars(['highlight', 'enableHighlightingDialog'], 
+        cart.setVars(['highlight', 'enableHighlightingDialog'], 
                     [hgTracks.highlight, hgTracks.enableHighlightingDialog ? 1 : 0]);
         imageV2.highlightRegion();
     },
 
     selectionEndDialog: function (newPosition)
     // Let user choose between zoom-in and highlighting.
     {   
         var dragSelectDialog = $("#dragSelectDialog")[0];
         if (!dragSelectDialog) {
             $("body").append("<div id='dragSelectDialog'>" + newPosition + 
                              "<p><input type='checkbox' id='disableDragHighlight'>" + 
                              "Don't show this dialog again and always zoom.<BR>" + 
                              "(Re-enable highlight via the 'configure' menu at any time.)</p>");
             dragSelectDialog = $("#dragSelectDialog")[0];
         }
@@ -685,31 +712,31 @@
                 minWidth: 400,
                 buttons: {  
                     "Zoom In": function() {
                         // Zoom to selection
                         $(this).dialog("option", "revertToOriginalPos", false);
                         if ($("#disableDragHighlight").attr('checked'))
                             hgTracks.enableHighlightingDialog = false;
                         if (imageV2.inPlaceUpdate) {
                             var params = "position=" + newPosition;
                             if (!hgTracks.enableHighlightingDialog)
                                 params += "&enableHighlightingDialog=0"
                             imageV2.navigateInPlace(params, null, true);
                         } else {
                             $('body').css('cursor', 'wait');
                             if (!hgTracks.enableHighlightingDialog)
-                                setCartVars(['enableHighlightingDialog'],[0]);
+                                cart.setVars(['enableHighlightingDialog'],[0]);
                             document.TrackHeaderForm.submit();
                         }
                         $(this).dialog("close");
                     },
                     "Highlight": function() {
                         // Highlight selection
                         $(imageV2.imgTbl).imgAreaSelect({hide:true});
                         if ($("#disableDragHighlight").attr('checked'))
                             hgTracks.enableHighlightingDialog = false;
                         dragSelect.highlightThisRegion(newPosition);
                         $(this).dialog("close");
                     },
                     "Cancel": function() {
                         $(this).dialog("close");
                     }
@@ -1141,31 +1168,31 @@
 var dragReorder = {
 
     setOrder: function (table)
     {   // Sets the 'order' value for the image table after a drag reorder
         var names = [];
         var values = [];
         $("tr.imgOrd").each(function (i) {
             if ($(this).attr('abbr') != $(this).attr('rowIndex').toString()) {
                 $(this).attr('abbr',$(this).attr('rowIndex').toString());
                 var name = this.id.substring('tr_'.length) + '_imgOrd';
                 names.push(name);
                 values.push($(this).attr('abbr'));
             }
         });
         if(names.length > 0) {
-            setCartVars(names,values);
+            cart.setVars(names,values);
             imageV2.markAsDirtyPage();
         }
     },
 
     sort: function (table)
     {   // Sets the table row order to match the order of the abbr attribute.
         // This is needed for back-button, and for visBox changes combined with refresh.
         var tbody = $(table).find('tbody')[0];
         if(tbody == undefined)
             tbody = table;
         
         // Do we need to sort?
         var trs = tbody.rows;
         var needToSort = false;
         $(trs).each(function(ix) {
@@ -1990,31 +2017,31 @@
                             }
                         }
                         if(imageV2.inPlaceUpdate) {
                             // XXXX This attempt to "update whole track image in place" didn't work for a variety of reasons
                             // (e.g. safari doesn't parse map when we update on the client side), so this is currently dead code.
                             // However, this now works in all other browsers, so we may turn this on for non-safari browsers
                             // (see redmine #4667).
                             jQuery('body').css('cursor', '');
                             var data = "hgt.trackImgOnly=1&hgt.ideogramToo=1&position=" +
                                        newPosition + "&hgsid=" + getHgsid();
                             if(name)
                                 data += "&hgFind.matches=" + name;
                             $.ajax({
                                     type: "GET",
                                     url: "../cgi-bin/hgTracks",
-                                    data: data,
+                                    data: cart.addUpdatesToUrl(data),
                                     dataType: "html",
                                     trueSuccess: imageV2.updateImgAndMap,
                                     success: catchErrorOrDispatch,
                                     error: errorHandler,
                                     cmd: cmd,
                                     loadingId: showLoadingImage("imgTbl"),
                                     cache: false
                                 });
                         } else {
                             // do a full page refresh to update hgTracks image
                             jQuery('body').css('cursor', 'wait');
                             var ele;
                             if(document.TrackForm)
                                 ele = document.TrackForm;
                             else
@@ -2027,32 +2054,32 @@
                     }
                 }
         } else if (cmd == 'zoomCodon' || cmd == 'zoomExon') {
             var num, ajaxCmd, msg;
             if(cmd == 'zoomCodon') {
                 msg = "Please enter the codon number to jump to:";
                 ajaxCmd = 'codonToPos';
             } else {
                 msg = "Please enter the exon number to jump to:";
                 ajaxCmd = 'exonToPos';
             }
             rightClick.myPrompt(msg, function(results) {
                 $.ajax({
                         type: "GET",
                         url: "../cgi-bin/hgApi",
-                        data: "db=" + getDb() +  "&cmd=" + ajaxCmd + "&num=" + results +
-                              "&table=" + args.table + "&name=" + args.name,
+                        data: cart.addUpdatesToUrl("db=" + getDb() +  "&cmd=" + ajaxCmd + "&num=" +
+                              results + "&table=" + args.table + "&name=" + args.name),
                         trueSuccess: rightClick.handleZoomCodon,
                         success: catchErrorOrDispatch,
                         error: errorHandler,
                         cache: true
                     });
                     });
         } else if (cmd == 'hgTrackUi_popup') {
 
             popUp.hgTrackUi( rightClick.selectedMenuItem.id, false );  // Launches the popup but shields the ajax with a waitOnFunction
 
         } else if (cmd == 'hgTrackUi_follow') {
 
             var url = "hgTrackUi?hgsid=" + getHgsid() + "&g=";
             var rec = hgTracks.trackDb[id];
             if (tdbHasParent(rec) && tdbIsLeaf(rec))
@@ -2068,31 +2095,31 @@
 
         } else if (cmd == 'viewImg') {
             // Fetch a new copy of track img and show it to the user in another window. This code assume we have updated
             // remote cart with all relevant chages (e.g. drag-reorder).
 /* Here's how to do this more directly with hgRenderTracks:
             if(window.open("../cgi-bin/hgRenderTracks?hgt.internal=1&hgsid=" + getHgsid()) == null) {
                 rightClick.windowOpenFailedMsg();
             }
             return;
 */
             var data = "hgt.imageV1=1&hgt.trackImgOnly=1&hgsid=" + getHgsid();
             jQuery('body').css('cursor', 'wait');
             $.ajax({
                     type: "GET",
                     url: "../cgi-bin/hgTracks",
-                    data: data,
+                    data: cart.addUpdatesToUrl(data),
                     dataType: "html",
                     trueSuccess: rightClick.handleViewImg,
                     success: catchErrorOrDispatch,
                     error: errorHandler,
                     cmd: cmd,
                     cache: false
                 });
         } else if (cmd == 'openLink' || cmd == 'followLink') {
             var href = rightClick.selectedMenuItem.href;
             var vars = new Array("c", "l", "r", "db");
             var valNames = new Array("chromName", "winStart", "winEnd");
             for (var i in vars) {
                 // make sure the link contains chrom and window width info (necessary b/c we are stripping hgsid and/or the cart may be empty);
                 // but don't add chrom to wikiTrack links (see redmine #2476).
                 var v = vars[i];
@@ -2138,92 +2165,92 @@
             var row = $( 'tr#tr_' + id );
             var rows = dragReorder.getContiguousRowSet(row);
             if (rows && rows.length > 0) {
                 var vars = new Array();
                 var vals = new Array();
                 for (var ix=rows.length - 1; ix >= 0; ix--) { // from bottom, just in case remove screws with us
                     var rowId = $(rows[ix]).attr('id').substring('tr_'.length);
                     //if (tdbIsSubtrack(hgTracks.trackDb[rowId]) == false)
                     //    warn('What went wrong?');
 
                     vars.push(rowId, rowId+'_sel'); // Remove subtrack level vis and explicitly uncheck.
                     vals.push('[]', 0);
                     $(rows[ix]).remove();
                 }
                 if (vars.length > 0) {
-                    setCartVars( vars, vals );
+                    cart.setVars( vars, vals );
                 }
                 imageV2.afterImgChange(true);
             }
         } else if (cmd == 'hideComposite') {
             var rec = hgTracks.trackDb[id];
             if (tdbIsSubtrack(rec)) {
                 var row = $( 'tr#tr_' + id );
                 var rows = dragReorder.getCompositeSet(row);
                 if (rows && rows.length > 0) {
                     for (var ix=rows.length - 1; ix >= 0; ix--) { // from bottom, just in case remove screws with us
                         $(rows[ix]).remove();
                     }
                 var selectUpdated = vis.update(rec.parentTrack, 'hide');
-                setCartVar(rec.parentTrack, 'hide' );
+                cart.setVar(rec.parentTrack, 'hide' );
                 imageV2.afterImgChange(true);
                 }
             }
         } else if (cmd == 'jumpToHighlight') { // If highlight exists for this assembly, jump to it
             if (hgTracks.highlight) {
                 var newPos = parsePositionWithDb(hgTracks.highlight);
                 if (newPos && newPos.db == getDb()) {
                     if ( $('#highlightItem').length == 0) { // not visible? jump to it
                         var curPos = parsePosition(genomePos.get());
                         var diff = ((curPos.end - curPos.start) - (newPos.end - newPos.start));
                         if (diff > 0) { // new position is smaller then current, then center it
                             newPos.start = Math.max( Math.floor(newPos.start - (diff/2) ), 0 );
                             newPos.end   = newPos.start + (curPos.end - curPos.start);
                         }
                     }
                     if (imageV2.inPlaceUpdate) {
                         var params = "position=" + newPos.chrom+':'+newPos.start+'-'+newPos.end;
                         imageV2.navigateInPlace(params, null, true);
                     } else {
                         genomePos.setByCoordinates(newPos.chrom, newPos.start, newPos.end);
                         jQuery('body').css('cursor', 'wait');
                         document.TrackHeaderForm.submit();
                     }
                 }
             }
         } else if (cmd == 'removeHighlight') {
             hgTracks.highlight = null;
-            setCartVars(['highlight'], ['[]']);
+            cart.setVars(['highlight'], ['[]']);
             imageV2.highlightRegion();
         } else {   // if ( cmd in 'hide','dense','squish','pack','full','show' )
             // Change visibility settings:
             //
             // First change the select on our form:
             var rec = hgTracks.trackDb[id];
             var selectUpdated = vis.update(id, cmd);
 
             // Now change the track image
             if(imageV2.enabled && cmd == 'hide')
             {
                 // Hide local display of this track and update server side cart.
                 // Subtracks controlled by 2 settings so del vis and set sel=0.  Others, just set vis hide.
                 if(tdbIsSubtrack(rec))
-                    setCartVars( [ id, id+"_sel" ], [ '[]', 0 ] ); // Remove subtrack level vis and explicitly uncheck.
+                    cart.setVars( [ id, id+"_sel" ], [ '[]', 0 ] ); // Remove subtrack level vis and explicitly uncheck.
                 else if(tdbIsFolderContent(rec))
-                    setCartVars( [ id, id+"_sel" ], [ 'hide', 0 ] ); // supertrack children need to have _sel set to trigger superttrack reshaping
+                    cart.setVars( [ id, id+"_sel" ], [ 'hide', 0 ] ); // supertrack children need to have _sel set to trigger superttrack reshaping
                 else
-                    setCartVar(id, 'hide' );
+                    cart.setVar(id, 'hide' );
                 $(document.getElementById('tr_' + id)).remove();
                 imageV2.afterImgChange(true);
             } else if (!imageV2.mapIsUpdateable) {
                 jQuery('body').css('cursor', 'wait');
                 if(selectUpdated) {
                     // assert(document.TrackForm);
                     document.TrackForm.submit();
                 } else {
                         // add a hidden with new visibility value
                         var form = $(document.TrackHeaderForm);
                         $("<input type='hidden' name='"+id+"'value='"+cmd+"'>").appendTo(form);
                         document.TrackHeaderForm.submit();
                 }
             } else {
                 imageV2.requestImgUpdate(id, id + "=" + cmd, "", cmd);
@@ -2614,31 +2641,31 @@
             myLink += "&descriptionOnly=1";
 
         var rec = hgTracks.trackDb[trackName];
         if (!descriptionOnly && rec != null && rec["configureBy"] != null) {
             if (rec["configureBy"] == 'none')
                 return;
             else if (rec["configureBy"] == 'clickThrough') {
                 jQuery('body').css('cursor', 'wait');
                 window.location = myLink;
                 return;
             }  // default falls through to configureBy popup
         }
         myLink += "&ajax=1";
         $.ajax({
                     type: "GET",
-                    url: myLink,
+                    url: cart.addUpdatesToUrl(myLink),
                     dataType: "html",
                     trueSuccess: popUp.uiDialog,
                     success: catchErrorOrDispatch,
                     error: errorHandler,
                     cmd: rightClick.selectedMenuItem,
                     cache: false
                 });
     },
 
     hgTrackUi: function (trackName,descriptionOnly)
     {
         waitOnFunction( popUp._uiDialigRequest, trackName, descriptionOnly );  // Launches the popup but shields the ajax with a waitOnFunction
     },
 
     uiDialogOk: function (popObj, trackName)
@@ -3008,31 +3035,31 @@
         
     requestImgUpdate: function (trackName,extraData,loadingId,newVisibility)
     {
         // extraData, loadingId and newVisibility are optional
         var data = "hgt.trackImgOnly=1&hgsid=" + getHgsid() + "&hgt.trackNameFilter=" + trackName;
         if(extraData != undefined && extraData != "")
             data += "&" + extraData;
         if(loadingId == undefined || loadingId == "")
             loadingId = showLoadingImage("tr_" + trackName);
         var getOrPost = "GET";
         if ((data.length) > 2000) // extraData could contain a bunch of changes from the cfg dialog
             getOrPost = "POST";
         $.ajax({
                     type: getOrPost,
                     url: "../cgi-bin/hgTracks",
-                    data: data,
+                    data: cart.addUpdatesToUrl(data),
                     dataType: "html",
                     trueSuccess: imageV2.updateImgAndMap,
                     success: catchErrorOrDispatch,
                     error: errorHandler,
                     cmd: 'refresh',
                     loadingId: loadingId,
                     id: trackName,
                     newVisibility: newVisibility,
                     cache: false
                 });
     },
 
     fullReload: function()
     {
         // force reload of whole page via trackform submit
@@ -3255,31 +3282,31 @@
         if(keepCurrentTrackVisible) {
             var item = rightClick.currentMapItem || imageV2.lastTrack;
             if(item) {
                 var top = $(document.getElementById("tr_" + item.id)).position().top;
                 if(top >= $(window).scrollTop()
                 || top < $(window).scrollTop() + $(window).height()) {
                     // don't bother if the item is not currently visible.
                     currentId = item.id;
                     currentIdYOffset = top - $(window).scrollTop();
                 }
             }
         }
         $.ajax({
                 type: "GET",
                 url: "../cgi-bin/hgTracks",
-                data: vis.cartUpdatesAddToUrl(params + 
+                data: cart.addUpdatesToUrl(params + 
                                               "&hgt.trackImgOnly=1&hgt.ideogramToo=1&hgsid=" + 
                                               getHgsid()),
                 dataType: "html",
                 trueSuccess: imageV2.updateImgAndMap,
                 success: catchErrorOrDispatch,
                 error: errorHandler,
                 cmd: 'wholeImage',
                 loadingId: showLoadingImage("imgTbl"),
                 disabledEle: disabledEle,
                 currentId: currentId,
                 currentIdYOffset: currentIdYOffset,
                 cache: false
             });
     },
     
@@ -3372,33 +3399,33 @@
         // TODO: move elsewhere?
         // With history support it is best that most position changes will ajax-update the image
         // This ensures that the 'go' and 'refresh' button will do so unless the chrom changes.
         $("input[value='go'],input[value='refresh']").click(function () {
             var newPos = genomePos.get().replace(/,/g,'');
             var newChrom = newPos.split(':')[0];
             var oldChrom  = genomePos.getOriginalPos().split(':')[0];
             if (newChrom == oldChrom) {
                 imageV2.markAsDirtyPage();
                 imageV2.navigateInPlace("position="+encodeURIComponent(newPos), null, false);
                 window.scrollTo(0,0);
                 return false;
             }
             
             // If chrom changed AND there are vis updates waiting...
-            if (vis.cartUpdatesWaiting()) {
+            if (cart.updatesWaiting()) {
                 var url = "hgTracks?db=" + getDb() + "&position=" + newPos + "&hgsid="+getHgsid();
-                url = vis.cartUpdatesAddToUrl(url)
+                url = cart.addUpdatesToUrl(url)
                 window.location.assign(url)
                 return false;
             }
 
             return true;
         });
         // Have vis box changes update cart through ajax.  This helps keep page/cart in sync.
         vis.initForAjax();
 
         // We reach here from these possible paths:
         // A) Forward: Full page retrieval: hgTracks is first navigated to (or chrom change)
         // B) Back-button past a full retrieval (B in: ->A,->b,->c(full page),->d,<-c,<-B(again))
         //    B1) Dirty page: at least one non-position change (e.g. 1 track vis changed in b)
         //    B2) Clean page: only position changes from A->b->| 
         var curPos = encodeURIComponent(genomePos.get().replace(/,/g,''));