3fe32a26217a69602da98b5747ab182e1e5460dd
tdreszer
  Thu May 8 11:59:28 2014 -0700
Rewrote the car variable queuing routines, fixing one bug and ensuring the vars are updated at window unload event as a last resort.  Also changed a couple stray undeclared vars that use strict found.  Redmine 13164.
diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index 151344b..00e041d 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: cart.addUpdatesToUrl("cmd=defaultPos&db=" + getDb()),
+                data: cart.varsToUrlData({ 'cmd': 'defaultPos', 'db': getDb() }),
                 dataType: "html",
                 trueSuccess: genomePos.handleChange,
                 success: catchErrorOrDispatch,
                 error: errorHandler,
                 cache: true
             });
         return false;
     }
 }
 
   /////////////////////////////////////
  //// Creating items by dragging /////
 /////////////////////////////////////
 var makeItemsByDrag = {
 
@@ -441,120 +441,153 @@
 
         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 = cart.addUpdatesToUrl(obj.href);
-            return postTheForm($(thisForm).attr('name'),href);
+            return postTheForm($(thisForm).attr('name'),cart.addUpdatesToUrl(obj.href));
         }
         return true;
     }
 }
 
 /////////////////////////
 //// cart updating /////
 ///////////////////////
 var cart = {
+    // Controls queuing and ultimately updating cart variables vis ajax or submit. Queued vars
+    // are held in an object with unique keys preventing duplicate updates and ensuring last update 
+    // takes precedence.  WARNING: be careful creating an object with variables on the fly:
+    // cart.setVarsObj({track: vis}) is invalid but cart.setVarsObj({'knownGene': vis}) is ok! 
 
     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];
-            }
+            //console.log('cart.addUpdatesToUrl: '+Object.keys(cart.updateQueue).length+' vars');
+            var updates = cart.varsToUrlData(); // clears the queue
+            if (typeof url === 'undefined' || url.length === 0)
+                return updates;
+
             if (updates.length > 0) {
-                if(url.length > 0 && url.lastIndexOf("?") == -1 && url.lastIndexOf("&") == -1)
-                    url += "?" + updates.substring(1);
+                var dataOnly = (url.indexOf("cgi-bin") === -1); // all urls should be to our cgis
+                if (!dataOnly && url.lastIndexOf("?") === -1)
+                    url += "?" + updates;
                 else
-                    url += updates
+                    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
-    // },
+    beforeUnload: function ()
+    {   // named function that can be bound and unbound to beforeunload event
+        // Makes sure any outstanding queued updates are sent before leaving the page.
+        //console.log('cart.beforeUnload: '+Object.keys(cart.updateQueue).length+' vars');
+        cart.setVarsObj( {}, null, false ); // synchronous
+    },
     
-    addUpdateToQueue: function (track,newVis)
-    {   // creates a string of updates to save for ajax batch or a submit
-        cart.updateQueue[track] = newVis;
+    varsToUrlData: function (varsObj)
+    {   // creates a url data (var1=val1&var2=val2...) string from vars object and queue
+        // The queue will be emptied by this call.
+        cart.queueVarsObj(varsObj); // lay ontop of queue, to give new values precedence
         
-        // 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);
+        // Now convert to url data and clear queue
+        var urlData = '';
+        if (cart.updatesWaiting()) {
+            //console.log('cart.varsToUrlData: '+Object.keys(cart.updateQueue).length+' vars');
+            urlData = varHashToQueryString(cart.updateQueue);
+            cart.updateQueue = {};
+        }
+        return urlData;
     },
     
-    setVars: function (names, values, errFunc, async)
-    {   // ajax updates the cart, and includes any queued updates.
+    setVarsObj: function (varsObj, errFunc, async)
+    {   // Set all vars in a var hash, appending any queued updates
+        //console.log('cart.setVarsObj: were:'+Object.keys(cart.updateQueue).length + 
+        //            ' new:'+Object.keys(varsObj).length);
+        cart.queueVarsObj(varsObj); // lay ontop of queue, to give new values precedence
+        
+        // Now ajax update all in queue and clear queue
         if (cart.updatesWaiting()) {
-            for (var track in cart.updateQueue) {
-                names.push(track);
-                values.push(cart.updateQueue[track]);
-            }
+            setVarsFromHash(cart.updateQueue, errFunc, async);
             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 );
+    arysToObj: function (names,values)
+    {   // Make hash type obj with two parallel arrays. (should be moved to utils.js).
+        var obj = {};
+        for(var ix=0; ix<names.length; ix++) {
+            obj[names[ix]] = values[ix]; 
+        }
+        return obj;
+    },
+    
+    setVars: function (names, values, errFunc, async)
+    {   // ajax updates the cart, and includes any queued updates.
+        cart.setVarsObj(cart.arysToObj(names, values), errFunc, async);
+    },
+
+    queueVarsObj: function (varsObj)
+    {   // Add object worth of cart updates to the 'to be updated' queue, so they can be sent to
+        // the server later. Note: hash allows overwriting previous updates to the same variable.
+        if (typeof varsObj !== 'undefined' && Object.keys(varsObj).length !== 0) {
+            //console.log('cart.queueVarsObj: were:'+Object.keys(cart.updateQueue).length + 
+            //            ' new:'+Object.keys(varsObj).length);
+            for (var name in varsObj) {
+                cart.updateQueue[name] = varsObj[name];
+                
+                // 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) {
+                    // By unbind/bind, we assure that there is only one instance bound
+                    $(window).unbind('beforeunload', cart.beforeUnload); 
+                    $(window).bind(  'beforeunload', cart.beforeUnload); 
                 }
             }
+        }
+    },
+    
+    addVarsToQueue: function (names,values)
+    {   // creates a string of updates to save for ajax batch or a submit
+        cart.queueVarsObj(cart.arysToObj(names,values));
+    },
+    
+}
 
   ///////////////////////////////////////////////
  //// 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');
             $(this).val(visibility);
             selectUpdated = true;
         });
         if(rec) {
             rec.localVisibility = visibility;
         }
         return selectUpdated;
@@ -608,31 +641,31 @@
         // 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');
             
-            cart.addUpdateToQueue(track,$(this).val());
+            cart.addVarsToQueue([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);
     }
@@ -674,32 +707,32 @@
     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
-        cart.setVars(['highlight', 'enableHighlightingDialog'], 
-                    [hgTracks.highlight, hgTracks.enableHighlightingDialog ? 1 : 0]);
+        cart.setVarsObj({               'highlight': hgTracks.highlight, 
+                         'enableHighlightingDialog': 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];
         }
         $(dragSelectDialog).dialog({
@@ -712,31 +745,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)
-                                cart.setVars(['enableHighlightingDialog'],[0]);
+                                cart.setVarsObj({'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");
                     }
@@ -1157,42 +1190,40 @@
             img.height = $(chrImg).height();
             img.width  = $(chrImg).width();
         }
         return img;
     }
 });
 }
 
   ///////////////////////////
  //// Drag Reorder Code ////
 ///////////////////////////
 var dragReorder = {
 
     setOrder: function (table)
     {   // Sets the 'order' value for the image table after a drag reorder
-        var names = [];
-        var values = [];
+        var varsToUpdate = {};
         $("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'));
+                varsToUpdate[name] = $(this).attr('abbr');
             }
         });
-        if(names.length > 0) {
-            cart.setVars(names,values);
+        if (Object.keys(varsToUpdate).length !== 0) {
+            cart.setVarsObj(varsToUpdate);
             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) {
@@ -1216,31 +1247,31 @@
         // most efficient reload of sorted rows I have found
         var sortedRows = jQuery.map(ary, function(ary, i) { return ary.row; });
         $(tbody).append( sortedRows ); // removes tr from current position and adds to end.
         return true;
     },
 
     showCenterLabel: function (tr, show)
     {   // Will show or hide centerlabel as requested
         // adjust button, sideLabel height, sideLabelOffset and centerlabel display
 
         if (!$(tr).hasClass('clOpt'))
             return;
         var center = $(tr).find(".sliceDiv.cntrLab");
         if($(center) == undefined)
             return;
-        seen = ($(center).css('display') != 'none');
+        var seen = ($(center).css('display') != 'none');
         if(show == seen)
             return;
 
         var centerHeight = $(center).height();
 
         var btn = $(tr).find("p.btn");
         var side = $(tr).find(".sliceDiv.sideLab");
         if($(btn) != undefined && $(side) != undefined) {
             var sideImg = $(side).find("img");
             if($(sideImg) != undefined) {
                 var top = parseInt($(sideImg).css('top'));
                 if(show) {
                     $(btn).css('height',$(btn).height() + centerHeight);
                     $(side).css('height',$(side).height() + centerHeight);
                     top += centerHeight; // top is a negative number
@@ -1495,30 +1526,31 @@
     }
 }
 
 
   //////////////////////////
  //// Drag Scroll code ////
 //////////////////////////
 jQuery.fn.panImages = function(){
     // globals across all panImages
     genomePos.original = genomePos.getOriginalPos();              // XXXX what is this for? (this already happened in initVars).
     var leftLimit   = hgTracks.imgBoxLeftLabel * -1;
     var rightLimit  = (hgTracks.imgBoxPortalWidth - hgTracks.imgBoxWidth + leftLimit);
     var only1xScrolling = ((hgTracks.imgBoxWidth - hgTracks.imgBoxPortalWidth) == 0);//< hgTracks.imgBoxLeftLabel);
     var prevX       = (hgTracks.imgBoxPortalOffsetX + hgTracks.imgBoxLeftLabel) * -1;
     var portalWidth = 0;
+    var portalAbsoluteX = 0;
     var savedPosition;
     var highlightArea  = null; // Used to ensure dragSelect highlight will scroll. 
 
     this.each(function(){
 
     var pic;
     var pan;
 
     if ( $(this).is("img") ) {
         pan = $(this).parent("div");
         pic = $(this);
     }
     else if ( $(this).is("div.scroller")  ) {
         pan = $(this);
         pic = $(this).children("img#panImg"); // Get the real pic
@@ -2054,32 +2086,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: cart.addUpdatesToUrl("db=" + getDb() +  "&cmd=" + ajaxCmd + "&num=" +
-                              results + "&table=" + args.table + "&name=" + args.name),
+                        data: cart.varsToUrlData({ '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))
@@ -2090,36 +2122,36 @@
                     url = $(link).attr('href');
                 else
                     url += rightClick.selectedMenuItem.id;
             }
             location.assign(url);
 
         } 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: cart.addUpdatesToUrl(data),
+                    data: cart.varsToUrlData({ 'hgt.imageV1': '1','hgt.trackImgOnly': '1', 
+                                               'hgsid': getHgsid() }),
                     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];
@@ -2153,104 +2185,101 @@
             } else {
                 if(rightClick.floatingMenuItem) {
                     // This doesn't work.
                     $('#img_data_' + rightClick.floatingMenuItem).parent().restartFloat();
                     // This does work
                     $.floatMgr.FOArray = new Array();
                 }
                 rightClick.floatingMenuItem = id;
                 rightClick.reloadFloatingItem();
                 imageV2.requestImgUpdate(id, "hgt.transparentImage=0", "");
             }
         } else if (cmd == 'hideSet') {
             var row = $( 'tr#tr_' + id );
             var rows = dragReorder.getContiguousRowSet(row);
             if (rows && rows.length > 0) {
-                var vars = new Array();
-                var vals = new Array();
+                var varsToUpdate = {};
                 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);
+                    // Remove subtrack level vis and explicitly uncheck.
+                    varsToUpdate[rowId]        = '[]';
+                    varsToUpdate[rowId+'_sel'] = 0;
                     $(rows[ix]).remove();
                 }
-                if (vars.length > 0) {
-                    cart.setVars( vars, vals );
+                if (Object.keys(varsToUpdate).length !== 0) {
+                    cart.setVarsObj(varsToUpdate);
                 }
                 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');
-                cart.setVar(rec.parentTrack, 'hide' );
+                cart.setVars( [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;
-            cart.setVars(['highlight'], ['[]']);
+            cart.setVarsObj({ '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))
                     cart.setVars( [ id, id+"_sel" ], [ '[]', 0 ] ); // Remove subtrack level vis and explicitly uncheck.
                 else if(tdbIsFolderContent(rec))
                     cart.setVars( [ id, id+"_sel" ], [ 'hide', 0 ] ); // supertrack children need to have _sel set to trigger superttrack reshaping
                 else
-                    cart.setVar(id, 'hide' );
+                    cart.setVars([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);
@@ -2634,31 +2663,31 @@
     _uiDialigRequest: function (trackName,descriptionOnly)
     { // popup cfg dialog
         popUp.trackName = trackName;
         var myLink = "../cgi-bin/hgTrackUi?g=" + trackName + "&hgsid=" + getHgsid() +
                      "&db=" + getDb();
         popUp.trackDescriptionOnly = descriptionOnly;
         if(popUp.trackDescriptionOnly)
             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;
+                window.location = cart.addUpdatesToUrl(myLink);
                 return;
             }  // default falls through to configureBy popup
         }
         myLink += "&ajax=1";
         $.ajax({
                     type: "GET",
                     url: cart.addUpdatesToUrl(myLink),
                     dataType: "html",
                     trueSuccess: popUp.uiDialog,
                     success: catchErrorOrDispatch,
                     error: errorHandler,
                     cmd: rightClick.selectedMenuItem,
                     cache: false
                 });
     },
@@ -2666,50 +2695,51 @@
     hgTrackUi: function (trackName,descriptionOnly)
     {
         waitOnFunction( popUp._uiDialigRequest, trackName, descriptionOnly );  // Launches the popup but shields the ajax with a waitOnFunction
     },
 
     uiDialogOk: function (popObj, trackName)
     {   // When hgTrackUi Cfg popup closes with ok, then update cart and refresh parts of page
         var rec = hgTracks.trackDb[trackName];
         var subtrack = tdbIsSubtrack(rec) ? trackName :undefined;  // If subtrack then vis rules differ
         var allVars = getAllVars($('#hgTrackUiDialog'), subtrack );// For unknown reasons IE8 fails to find $('#pop'), occasionally
         var changedVars = varHashChanges(allVars,popUp.saveAllVars);
         //warn("cfgVars:"+varHashToQueryString(changedVars));
         var newVis = changedVars[trackName];
         var hide = (newVis != null && (newVis == 'hide' || newVis == '[]'));  // subtracks do not have "hide", thus '[]'
         if($('#imgTbl') == undefined) { // On findTracks or config page
-            setVarsFromHash(changedVars);
+            if (Object.keys(changedVars).length !== 0)
+                cart.setVarsObj(changedVars);
             //if(hide) // TODO: When findTracks or config page has cfg popup, then vis change needs to be handled in page here
         }
         else {  // On image page
             if(hide) {
-                setVarsFromHash(changedVars);
+                if (Object.keys(changedVars).length !== 0)
+                    cart.setVarsObj(changedVars);
                 $(document.getElementById('tr_' + trackName)).remove();
                 imageV2.afterImgChange(true);
             } else {
                 // Keep local state in sync if user changed visibility
                 if(newVis != null) {
                     vis.update(trackName, newVis);
                 }
-                var urlData = varHashToQueryString(changedVars);
-                if(urlData.length > 0) {
+                if (Object.keys(changedVars).length !== 0) {
+                    var urlData = cart.varsToUrlData(changedVars);
                     if(imageV2.mapIsUpdateable) {
                         imageV2.requestImgUpdate(trackName,urlData,"");
                     } else {
-                        window.location = "../cgi-bin/hgTracks?" + urlData +
-                                          "&hgsid=" + getHgsid();
+                        window.location = "../cgi-bin/hgTracks?" + urlData + "&hgsid=" + getHgsid();
                     }
                 }
             }
         }
     },
 
     uiDialog: function (response, status)
     {
     // Take html from hgTrackUi and put it up as a modal dialog.
 
         // make sure all links (e.g. help links) open up in a new window
         response = response.replace(/<a /ig, "<a target='_blank' ");
 
         var cleanHtml = response;
         cleanHtml = stripJsFiles(cleanHtml,true);   // DEBUG msg with true
@@ -3283,32 +3313,31 @@
             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: cart.addUpdatesToUrl(params + 
-                                              "&hgt.trackImgOnly=1&hgt.ideogramToo=1&hgsid=" + 
-                                              getHgsid()),
+                                      "&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
             });
     },
     
     highlightRegion: function()
     // highlight vertical region in imgTbl based on hgTracks.highlight (#709).
@@ -3400,32 +3429,32 @@
         // 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 (cart.updatesWaiting()) {
-                var url = "hgTracks?db=" + getDb() + "&position=" + newPos + "&hgsid="+getHgsid();
-                url = cart.addUpdatesToUrl(url)
+                var url = "../cgi-bin/hgTracks?" + cart.varsToUrlData({ 'db': getDb(), 
+                                                        'position': newPos, 'hgsid': getHgsid() });
                 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,''));