02646d7ea3d38d8e6dfd0584894dcf77b334447c
tdreszer
  Tue Nov 19 10:57:26 2013 -0800
Adding back-button suppoirt through jquery.history.js API. At this time back-button support is only for position.  At all times the image should agree with the cart.  This means that moving forward, the image must be stacked in window.History.state and moving back, the image must be refreshed using the old location.  Note that full page refreshes complicate the picure.  Back-button will recall a client side cached page which may be greatly out of sync with cart.  Stretegy is to use image only update, but if all else fails, a full page reload is used.  This means, that image only update is preferred going forward.  To that end, several previous places that required full page refresh can now be done with image only rtefresh.  At this time, anytime the chrom changes, the page will be fully refreshed.  Redmine #7473.
diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index e138b69..db5d5d6 100644
--- src/hg/js/hgTracks.js
+++ src/hg/js/hgTracks.js
@@ -172,30 +172,31 @@
                     $('#wormbaseLink').attr('href', a[1] + pos.start + "-" + pos.end);
                 }
             }
             // Fixup DNA link; e.g.: hgc?hgsid=2999470&o=114385768&g=getDna&i=mixed&c=chr7&l=114385768&r=114651696&db=panTro2&hgsid=2999470
             if($('#dnaLink').length) {
                 var link = $('#dnaLink').attr('href');
                 var reg = new RegExp("(.+&o=)[0-9]+.+&db=[^&]+(.*)");
                 var a = reg.exec(link);
                 if(a && a[1]) {
                     var url = a[1] + (pos.start - 1) + "&g=getDna&i=mixed&c=" + pos.chrom;
                     url += "&l=" + (pos.start - 1) + "&r=" + pos.end + "&db=" + getDb() + a[2];
                     $('#dnaLink').attr('href', url);
                 }
             }
         }
+        if (!imageV2.backSupport)
             imageV2.markAsDirtyPage();
     },
 
     check: function (img, selection)
     {   // return true if user's selection is still w/n the img (including some slop).
         var imgWidth = jQuery(img).width();
         var imgHeight = jQuery(img).height();
         var imgOfs = jQuery(img).offset();
         var slop = 10;
 
         // We ignore clicks in the gray tab and track title column
         // (we really should suppress all drag activity there,
         // but I don't know how to do that with imgAreaSelect).
         var leftX = hgTracks.revCmplDisp ?  imgOfs.left - slop :
                                             imgOfs.left + hgTracks.insideX - slop;
@@ -495,30 +496,58 @@
         imageV2.markAsDirtyPage();
         if(arguments.length > 2)
             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;
+    },
+    
+    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();
+                $(this).attr('class', 'hiddenText');
+            } else
+                $(this).attr('class', 'normalText');
+            
+            setCartVar(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');
+
     }
 
 }
   ////////////////////////////////////////////////////////////
  // dragSelect is also known as dragZoom or shift-dragZoom //
 ////////////////////////////////////////////////////////////
 var dragSelect = {
 
     areaSelector:    null, // formerly "imgAreaSelect". jQuery element used for imgAreaSelect
     autoHideSetting: true, // Current state of imgAreaSelect autoHide setting
     originalCursor:  null,
     startTime:       null,
 
     rulerModeToggle: function (ele) // UNUSED?
     {
@@ -620,43 +649,44 @@
 jQuery.fn.chromDrag = function(){
 this.each(function(){
     // Plan:
     // mouseDown: determine where in map: convert to img location: pxDown
     // mouseMove: flag drag
     // mouseUp: if no drag, then create href centered on bpDown loc with current span
     //          if drag, then create href from bpDown to bpUp
     //          if ctrlKey then expand selection to containing cytoBand(s)
     var img = { top: -1, scrolledTop: -1, height: -1, left: -1, scrolledLeft: -1, width: -1 };  // Image dimensions all in pix
     var chr = { name: "", reverse: false, beg: -1, end: -1, size: -1,
                 top: -1, bottom: -1, left: -1, right: -1, width: -1 };   // chrom Dimenaions beg,end,size in bases, rest in pix
     var pxDown = 0;     // pix X location of mouseDown
     var chrImg = $(this);
     var mouseIsDown   = false;
     var mouseHasMoved = false;
-    var hilite = jQuery('<div></div>');
+    var hilite = null;
 
     initialize();
 
     function initialize(){
 
         findDimensions();
 
         if(chr.top == -1)
             warn("chromIdeo(): failed to register "+this.id);
         else {
             hiliteSetup();
 
+            $('area.cytoBand').unbind('mousedown');  // Make sure this is only bound once
             $('area.cytoBand').mousedown( function(e)
             {   // mousedown on chrom portion of image only (map items)
                 updateImgOffsets();
                 pxDown = e.clientX - img.scrolledLeft;
                 var pxY = e.clientY - img.scrolledTop;
                 if(mouseIsDown == false
                 && isWithin(chr.left,pxDown,chr.right) && isWithin(chr.top,pxY,chr.bottom)) {
                     mouseIsDown = true;
                     mouseHasMoved = false;
 
                     $(document).bind('mousemove',chromMove);
                     $(document).bind( 'mouseup', chromUp);
                     hiliteShow(pxDown,pxDown);
                     return false;
                 }
@@ -735,30 +765,35 @@
                 if(selRange.end > -1) {
                     // prompt, then submit for new position
                     selRange = rangeNormalizeToChrom(selRange,chr);
                     if(mouseHasMoved == false) { // Update highlight by converting bp back to pix
                         pxDown = convertFromBases(selRange.beg)
                         pxUp = convertFromBases(selRange.end)
                         hiliteShow(pxDown,pxUp);
                     }
                     //if ((selRange.end - selRange.beg) < 50000)
                     //    dontAsk = true;
                     if (dontAsk
                     || confirm("Jump to new position:\n\n"+chr.name+":"+commify(selRange.beg)+
                                "-"+commify(selRange.end)+" size:"+commify(selRange.width)) ) {
                         genomePos.setByCoordinates(chr.name, selRange.beg, selRange.end)
                         $('area.cytoBand').mousedown( function(e) { return false; }); // Stop the presses :0)
+                        if (imageV2.backSupport) {
+                            imageV2.navigateInPlace("position=" +  
+                                    encodeURIComponent(genomePos.get().replace(/,/g,'')), null, true);
+                            hiliteCancel();
+                        } else
                             document.TrackHeaderForm.submit();
                         return true; // Make sure the setTimeout below is not called.
                     }
                 }
             }
             //else warn("chromIdeo("+chr.name+") NOT WITHIN VERTICAL RANGE\n selected range (pix):"+pxDown+"-"+pxUp+" chrom range (pix):"+chr.left+"-"+chr.right+"\n cytoTop-Bottom:"+chr.top +"-"+chr.bottom);
             hiliteCancel();
             setTimeout('posting.allowMapClicks();',50);
         }
         mouseIsDown = false;
         mouseHasMoved = false;
     }
 
     function isWithin(beg,here,end)
     {   // Simple utility
@@ -898,34 +933,37 @@
             begX = down + img.left;
             wide = (cur - down);
         }
         $(hilite).css({ left: begX + 'px', width: wide + 'px', top: topY + 'px',
                         height: high + 'px', display:'' });
         $(hilite).show();
     }
     function hiliteCancel(left,width,top,height)
     {   // Called on mouseup: Make green drag hilite disappear when no longer wanted
         $(hilite).hide();
         $(hilite).css({ left: '0px', width: '0px', top: '0px', height: '0px' });
     }
 
     function hiliteSetup()
     {   // Called on init: setup of drag region hilite (but don't show yet)
+        if (hilite == null) {  // setup only once
+            hilite = jQuery("<div id='chrHi'></div>");
             $(hilite).css({ backgroundColor: 'green', opacity: 0.4, borderStyle: 'solid',
                             borderWidth: '1px', bordercolor: '#0000FF' });
             $(hilite).css({ display: 'none', position: 'absolute', overflow: 'hidden', zIndex: 1 });
             jQuery($(chrImg).parents('body')).append($(hilite));
+        }
         return hilite;
     }
 
     function updateImgOffsets()
     {   // Called on mousedown: Gets the current offsets
         var offs = $(chrImg).offset();
         img.top  = Math.round(offs.top );
         img.left = Math.round(offs.left);
         img.scrolledTop  = img.top  - $("body").scrollTop();
         img.scrolledLeft = img.left - $("body").scrollLeft();
         if($.browser.msie) {
             img.height = $(chrImg).outerHeight();
             img.width  = $(chrImg).outerWidth();
         } else {
             img.height = $(chrImg).height();
@@ -947,30 +985,64 @@
         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);
             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) {
+            if ($(this).attr('abbr') != $(this).attr('rowIndex').toString()) {
+                needToSort = true;
+                return false;  // break for each() loops
+            }
+        });
+        if (!needToSort)
+            return false;
+            
+        // Create array of tr holders to sort
+        var ary = new Array();
+        $(trs).each(function(ix) {  // using sortTable found in utils.js
+            ary.push(new sortTable.field($(this).attr('abbr'),false,this));
+        });
+
+        // Sort the array
+        ary.sort(sortTable.fieldCmp);
+
+        // 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');
         if(show == seen)
             return;
 
         var centerHeight = $(center).height();
 
@@ -2508,36 +2580,43 @@
     }
 }
 
   ///////////////////////////////
  //// imageV2  (aka imgTbl) ////
 ///////////////////////////////
 var imageV2 = {
 
     enabled:        false,    // Will be set to true unless advancedJavascriptFeatures is turned off OR if track search of config page
     imgTbl:         null,     // formerly "trackImgTbl"  The imgTbl or null if non-imageV2.
     inPlaceUpdate:  false,    // modified based on value of hgTracks.inPlaceUpdate and mapIsUpdateable
     mapIsUpdateable:true,
     lastTrack:      null,     // formerly (lastMapItem) this is used to try to keep what the last track the cursor passed.
 
     markAsDirtyPage: function ()
-    {   // Page is marked as dirty so that the backbutton can be overridden
+    {   // Page is marked as dirty so that the back-button knows page doesn't match cart
         var dirty = $('#dirty');
         if (dirty != undefined && dirty.length != 0)
             $(dirty).val('true');
     },
 
+    markAsCleanPage: function ()
+    {   // Clears signal that history may be out of sync with cart.
+        var dirty = $('#dirty');
+        if (dirty != undefined && dirty.length != 0)
+            $(dirty).val('false');
+    },
+
     isDirtyPage: function ()
     { // returns true if page was marked as dirty
     // This will allow the backbutton to be overridden
 
         var dirty = $('#dirty');
         if (dirty != undefined && dirty.length > 0) {
             if ($(dirty).val() == 'true')
                 return true;
         }
         return false;
     },
 
     updateTiming: function (response)
     {   // update measureTiming text on current page based on what's in the response
         var reg = new RegExp("(<span class='timing'>.+?</span>)", "g");
@@ -2567,50 +2646,62 @@
                             },
                             function (position) {
                                 genomePos.set(position, commify(getSizeFromCoordinates(position)));
                             });
             // Make sure suggestTrack is visible when user chooses something via gene select (#3484).
             if($("#suggestTrack").length) {
                 $(document.TrackForm || document.TrackHeaderForm).submit(function(event) {
                                                        if($('#hgFindMatches').length) {
                                                            vis.makeTrackVisible($("#suggestTrack").val());
                                                        }
                                                    });
             }
         }
     },
     
-    afterReload: function ()
+    afterReload: function (id)
     {   // Reload various UI widgets after updating imgTbl map.
         dragReorder.init();
         dragSelect.load(false);
         // Do NOT reload context menu (otherwise we get the "context menu sticks" problem).
         // rightClick.load($('#tr_' + id));
         if(imageV2.imgTbl.tableDnDUpdate)
             imageV2.imgTbl.tableDnDUpdate();
         rightClick.reloadFloatingItem();
         // Turn on drag scrolling.
         if(hgTracks.imgBoxPortal) {
             $("div.scroller").panImages();
         }
+        if (imageV2.backSupport) {
+            if (id) { // The remainder is only needed for full reload
+                imageV2.markAsDirtyPage(); // vis of cfg change
+                return;
+            }
+        }
+        
         imageV2.loadRemoteTracks();
         makeItemsByDrag.load();
-        imageV2.markAsDirtyPage();
         imageV2.loadSuggestBox();
+
+        if (imageV2.backSupport) {
+            imageV2.setInHistory(false);    // Set this new position into History stack
+        } else {
+            imageV2.markAsDirtyPage();
+        }
     },
 
-    updateImgForId: function (html, id)
+    updateImgForId: function (html, id, fullImageReload, newJsonRec)
     {   // update row in imgTbl for given id.
         // return true if we successfully pull slice for id and update it in imgTrack.
         var str = "<TR id='tr_" + id + "'[^>]*>([\\s\\S]+?)</TR>";
         var reg = new RegExp(str);
         var a = reg.exec(html);
         if(a && a[1]) {
             var tr = $(document.getElementById("tr_" + id));
             if (tr.length > 0) {
                 $(tr).html(a[1]);
 
                 // Need to update tr class list too
                 str = "<TR id='tr_" + id + "[^>]* class='(.*)'>";
                 reg = new RegExp(str);
                 var classes = reg.exec(html);
                 if(classes && classes[1] && classes[1].length > 0) {
@@ -2618,36 +2709,113 @@
                     $(tr).addClass(classes[1]);
                 }
 
                 // NOTE: Want to examine the png? Uncomment:
                 //var img = $('#tr_' + id).find("img[id^='img_data_']").attr('src');
                 //warn("Just parsed image:<BR>"+img);
 
                 // >1x dragScrolling needs some extra care.
                 if(hgTracks.imgBoxPortal && (hgTracks.imgBoxWidth > hgTracks.imgBoxPortalWidth)) {
                     if (hgTracks.imgBoxPortalLeft != undefined) {
                         $(tr).find('.panImg').css({'left': hgTracks.imgBoxPortalLeft });
                         $(tr).find('.tdData').css(
                                 {'backgroundPosition': hgTracks.imgBoxPortalLeft});
                     }
                 }
+
+                // Need to update vis box (in case this is reached via back-button)
+                if (imageV2.backSupport && fullImageReload) {
+                    // Update abbr so that rows can be resorted properly
+                    str = "<TR id='tr_" + id + "[^>]* abbr='(.*)' class";
+                    reg = new RegExp(str);
+                    var abbr = reg.exec(html);
+                    if(abbr && abbr[1] && abbr[1].length > 0) {
+                        $(tr).attr('abbr',abbr[1]);
+                    }
+
+                    if (newJsonRec)
+                        vis.update(id, vis.enumOrder[newJsonRec.visibility]);
+                }
+
                 return true;
             }
         }
         return false;
     },
 
+    updateImgForAllIds: function (response, oldJson, newJson)
+    {   // update all rows in imgTbl based upon navigateInPlace response.
+        var imgTbl = $('#imgTbl');
+
+        // We update rows one at a time 
+        // (b/c updating the whole imgTbl at one time doesn't work in IE).
+        for (var id in newJson.trackDb) {
+            var newJsonRec = newJson.trackDb[id];
+            var oldJsonRec = oldJson.trackDb[id];
+            
+            if (newJsonRec.type == "remote")
+                continue;
+            if (oldJsonRec != undefined &&  oldJsonRec.visibility != 0) {
+                // New track replacing old:
+                if (!imageV2.updateImgForId(response, id, true, newJsonRec))
+                    warn("Couldn't parse out new image for id: " + id);
+            } else { //if (oldJsonRec == undefined || oldJsonRec.visibility == 0)
+                // New track seen for the first time
+                if (imageV2.backSupport) {
+                    $(imgTbl).append("<tr id='tr_" + id + "' abbr='0'" + // abbr gets filled in
+                                        " class='imgOrd trDraggable'></tr>");
+                    if (!imageV2.updateImgForId(response, id, true, newJsonRec))
+                        warn("Couldn't insert new image for id: " + id);
+                }
+            }
+        }
+        if (imageV2.backSupport) {
+            // Removes OLD: those in oldJson but not in newJson
+            for (var id in oldJson.trackDb) {
+                if(newJson.trackDb[id] == undefined)
+                    $(document.getElementById('tr_' + id)).remove();
+            }
+            
+            // Need to reorder the rows based upon abbr
+            dragReorder.sort($(imgTbl));
+        }
+    },
+    
+    updateChromImg: function (response)
+    {   // Parse out new chrom 'ideoGram' (if available)
+        // e.g.: <IMG SRC = "../trash/hgtIdeo/hgtIdeo_hgwdev_larrym_61d1_8b4a80.gif"
+        //                BORDER=1 WIDTH=1039 HEIGHT=21 USEMAP=#ideoMap id='chrom'>
+        // Larry's regex voodoo:
+        var a = /<IMG([^>]+SRC[^>]+id='chrom'[^>]*)>/.exec(response);
+        if (a && a[1]) {
+            var b = /SRC\s*=\s*"([^")]+)"/.exec(a[1]);
+            if (b && b[1]) {
+                $('#chrom').attr('src', b[1]);
+                // ideoMap?  Not needed if the chrom has not changed.
+                //a = /<MAP Name=ideoMap>([\s\S]+)<\/MAP>/.exec(response);
+                //if (a && a[1])
+                //    $("map[name='idelMap']").html('src', a[1]);
+                if (imageV2.backSupport) {
+                    // Reinit chrom dragging.
+                    if ($('area.cytoBand').length >= 1) {
+                        $('img#chrom').chromDrag();
+                    }
+                }
+            }
+        }
+    },
+        
     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,
@@ -2667,129 +2835,101 @@
     {
         // force reload of whole page via trackform submit
         // This function does not return
         jQuery('body').css('cursor', 'wait');
         document.TrackHeaderForm.submit();
 
     },
 
     updateImgAndMap: function (response, status)
     {   // Handle ajax response with an updated trackMap image, map and optional ideogram.
         //
         // this.cmd can be used to figure out which menu item triggered this.
         // this.id == appropriate track if we are retrieving just a single track.
 
         // update local hgTracks.trackDb to reflect possible side-effects of ajax request.
-        var json = scrapeVariable(response, "hgTracks");
-        var oldTrackDb = hgTracks.trackDb;
+        var newJson = scrapeVariable(response, "hgTracks");
+        var oldJson = hgTracks;
         var valid = false;
-        if(json == undefined) {
+        if (newJson == undefined) {
             var stripped = new Object();
             stripJsEmbedded(response, true, stripped);
             if(stripped.warnMsg == null)
                 warn("hgTracks object is missing from the response");
         } else {
             if(this.id != null) {
-                if(json.trackDb[this.id]) {
-                    var visibility = vis.enumOrder[json.trackDb[this.id].visibility];
+                if (newJson.trackDb[this.id]) {
+                    var visibility = vis.enumOrder[newJson.trackDb[this.id].visibility];
                     var limitedVis;
-                    if(json.trackDb[this.id].limitedVis)
-                        limitedVis = vis.enumOrder[json.trackDb[this.id].limitedVis];
+                    if (newJson.trackDb[this.id].limitedVis)
+                        limitedVis = vis.enumOrder[newJson.trackDb[this.id].limitedVis];
                     if(this.newVisibility && limitedVis && this.newVisibility != limitedVis)
                         // see redmine 1333#note-9
                         alert("There are too many items to display the track in " +
                                 this.newVisibility + " mode.");
-                    var rec = hgTracks.trackDb[this.id];
-                    rec.limitedVis = json.trackDb[this.id].limitedVis;
+                    var rec = oldJson.trackDb[this.id];
+                    rec.limitedVis = newJson.trackDb[this.id].limitedVis;
                     vis.update(this.id, visibility);
                     valid = true;
                 } else {
                     warn("Invalid hgTracks.trackDb received from the server");
                 }
             } else {
                 valid = true;
-                hgTracks.trackDb = json.trackDb;
             }
         }
         if(valid) {
             if(imageV2.enabled
             && this.id
             && this.cmd
             && this.cmd != 'wholeImage'
             && this.cmd != 'selectWholeGene') {
                 // Extract <TR id='tr_ID'>...</TR> and update appropriate row in imgTbl;
                 // this updates src in img_left_ID, img_center_ID and img_data_ID and map in map_data_ID
                 var id = this.id;
-                if(imageV2.updateImgForId(response, id)) {
-                    imageV2.afterReload();
+                if (imageV2.updateImgForId(response, id, false)) {
+                    imageV2.afterReload(id);
                 } else {
                     warn("Couldn't parse out new image for id: " + id);
                     //alert("Couldn't parse out new image for id: " + id+"BR"+response);  // Very helpful
                 }
             } else {
                 if(imageV2.enabled) {
                     // Implement in-place updating of hgTracks image
-                    genomePos.setByCoordinates(json.chromName, json.winStart + 1, json.winEnd);
-                    $("input[name='c']").val(json.chromName);
-                    $("input[name='l']").val(json.winStart);
-                    $("input[name='r']").val(json.winEnd);
-                    if(json.cgiVersion != hgTracks.cgiVersion) {
-                        // Must reload whole page because of a new version on the server; this should happen very rarely.
-                        // Note that we have already updated position based on the user's action.
+                    genomePos.setByCoordinates(newJson.chromName, newJson.
+                                               winStart + 1, newJson.winEnd);
+                    $("input[name='c']").val(newJson.chromName);
+                    $("input[name='l']").val(newJson.winStart);
+                    $("input[name='r']").val(newJson.winEnd);
+
+                    if (newJson.cgiVersion != oldJson.cgiVersion) {
+                        // Must reload whole page because of a new version on the server;
+                        // this should happen very rarely. Note that we have already updated 
+                        // position based on the user's action.
                         imageV2.fullReload();
                     } else {
-                        // We update rows one at a time (b/c updating the whole imgTable at one time doesn't work in IE).
-                        for (var id in hgTracks.trackDb) {
-                        // handle case where invisible items may be in the trackDb list (see redmine #5670).
-                            if(hgTracks.trackDb[id].type != "remote"
-                            && hgTracks.trackDb[id].visibility > 0 // && $('#tr_' + id).length > 0
-                            && !imageV2.updateImgForId(response, id)) {
-                                warn("Couldn't parse out new image for id: " + id);
-                            }
-                        }
-                    /* This (disabled) code handles dynamic addition of tracks:
-                        for (var id in hgTracks.trackDb) {
-                            if(oldTrackDb[id] == undefined) {
-                                // XXXX Tim, what s/d abbr attribute be?
-                                $('#imgTbl').append("<tr id='tr_" + id + "' class='imgOrd trDraggable'></tr>");
-                                imageV2.updateImgForId(response, id);
-                                vis.update(id, vis.enumOrder[hgTracks.trackDb[id].visibility]);
-                            }
-                        }
-                    */
-                        hgTracks = json;
+                        // Will rebuild image adding new, removing old and resorting tracks
+                        imageV2.updateImgForAllIds(response,oldJson,newJson);
+                        imageV2.updateChromImg(response);
+                        hgTracks = newJson;
                         genomePos.original = undefined;
                         initVars();
                         imageV2.afterReload();
                     }
                 } else {
                     warn("ASSERT: Attempt to update track without advanced javascript features.");
                 }
-                // now pull out and parse the map.
-                //a = /<MAP id='map' Name=map>([\s\S]+)<\/MAP>/.exec(response);
-                //if(!a[1])
-                //    warn("Couldn't parse out map");
-            }
-            // Parse out new ideoGram url (if available)
-            // e.g.: <IMG SRC = "../trash/hgtIdeo/hgtIdeo_hgwdev_larrym_61d1_8b4a80.gif" BORDER=1 WIDTH=1039 HEIGHT=21 USEMAP=#ideoMap id='chrom'>
-            // We do this last b/c it's least important.
-            var a = /<IMG([^>]+SRC[^>]+id='chrom'[^>]*)>/.exec(response);
-            if(a && a[1]) {
-                var b = /SRC\s*=\s*"([^")]+)"/.exec(a[1]);
-                if(b && b[1]) {
-                    $('#chrom').attr('src', b[1]);
-                }
             }
             if(hgTracks.measureTiming) {
                 imageV2.updateTiming(response);
             }
         }
         if(this.disabledEle) {
             this.disabledEle.removeAttr('disabled');
         }
         if(this.loadingId) {
             hideLoadingImage(this.loadingId);
         }
         jQuery('body').css('cursor', '');
         if(valid && this.currentId) {
             var top = $(document.getElementById("tr_" + this.currentId)).position().top;
             $(window).scrollTop(top - this.currentIdYOffset);
@@ -2925,30 +3065,149 @@
         $.ajax({
                 type: "GET",
                 url: "../cgi-bin/hgTracks",
                 data: 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
             });
+    },
+    
+    backSupport: (window.History.enabled != undefined), // support of r back button via: 
+    history: null,                                     //  jquery.history.js and HTML5 history API
+    
+    setupHistory: function ()
+    {   // Support for back-button using jquery.history.js.
+        // Sets up the history and initializes a state.
+    
+        // Since ajax updates leave the browser cached pages different from the server state, 
+        // simple back-button fails.  Using a 'dirty flag' we had forced an update from server,
+        // whenever the back button was hit, meaning there was no going back from server-state!
+        // NOW using the hitsory API, the back-button triggers a 'statechange' event which can 
+        // contain data.  We save the position in the data and ajax update the image when the
+        // back-button is pressed.  This works great for going back through ajax-updated position
+        // changes, but is a bit messier when going back past a full-page retrieved state (as
+        // described below).
+    
+        // NOTE: many things besides position could be ajax updated (e.g. track visibility). We are
+        // using the back-button to keep track of position only.  Since the image should be updated
+        // every-time the back button is pressed, all track settings should persist (not go back).
+        // What will occasionally fail is vis box state and group expansion state. This is because
+        // the back-button goes to a browser cached page and then the image alone is updated.
+    
+        imageV2.history = window.History;
+        
+        // The 'statechange' function triggerd by the back-button.
+        // Whenever the position changes, then use ajax-update to refetch the position
+        imageV2.history.Adapter.bind(window,'statechange',function(){
+            var prevPos = imageV2.history.getState().data.position;
+            var curPos = encodeURIComponent(genomePos.get().replace(/,/g,''));
+            if (prevPos != undefined && prevPos != curPos) {
+                // NOTE: this function is NOT called when backing passed a full retrieval boundary
+                genomePos.set(decodeURIComponent(prevPos));
+                imageV2.navigateInPlace("position=" + prevPos, null, false);
+            }
+        });
+        
+        // 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.navigateInPlace("position="+encodeURIComponent(newPos), null, false);
+                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,''));
+        var cachedPos = imageV2.history.getState().data.position;
+        // A) Forward: Full page retrieval: hgTracks is first navigated to (or chrom change)
+        if (cachedPos == undefined) { // Not a back-button operation
+            // set the current position into history outright (will replace). No img update needed
+            imageV2.setInHistory(true);
+        } else { // B) Back-button past a full retrieval
+            genomePos.set(decodeURIComponent(cachedPos));
+            // B1) Dirty page: at least one non-position change 
+            if (imageV2.isDirtyPage()) {
+                imageV2.markAsCleanPage();
+                // Only forcing a full page refresh if chrom changes
+                var cachedChrom = decodeURIComponent(cachedPos).split(':')[0];
+                var curChrom    = decodeURIComponent(   curPos).split(':')[0];
+                if (cachedChrom == curChrom) {
+                    imageV2.navigateInPlace("position=" + cachedPos, null, false);
+                } else {
+                    imageV2.fullReload();
+                }
+            } else {
+                // B2) Clean page: only position changes from a->b 
+                if (cachedPos != curPos) {
+                    imageV2.navigateInPlace("position=" + cachedPos, null, false);
+                }
+            }
+        }
+    },
+    
+    setInHistory: function (fullPageLoad)
+    {   // Keep a position history and allow the back-button to work (sort of)
+        // replaceState on initial page load, pushState on each advance
+        // When call triggered by back button, the lastPos==newPos, so no action.
+        var lastPos = imageV2.history.getState().data.position;
+        var newPos  = encodeURIComponent(genomePos.get().replace(/,/g,''));  // no commas
+        
+        // A full page load could be triggered by back-button, but then there will be a lastPos
+        // if this is the case then don't set the position in history again!
+        if (fullPageLoad && lastPos != undefined)
+            return;
+
+        if (lastPos == undefined || lastPos == null || lastPos != newPos) {
+            // Swap the position into the title
+            var title = $('TITLE')[0].text;
+            var ttlWords = title.split(' ');
+            if (ttlWords.length >= 2) {
+                ttlWords[1] = genomePos.get();
+                title = ttlWords.join(' ')
+            } else
+                title = genomePos.get()
+
+            if (fullPageLoad) { 
+                // Should only be on initial set-up: first navigation to page
+                imageV2.history.replaceState({position: newPos, hgsid: + getHgsid()},title,
+                                          "hgTracks?db=" + getDb() + "&position=" + newPos);
+            } else {  
+                // Should be when advancing (not-back-button)
+                imageV2.history.pushState({position: newPos, hgsid: + getHgsid()},title,
+                                          "hgTracks?db=" + getDb() + "&position=" + newPos);
+            }
+        }
     }
 
 }
 
   //////////////////////
  //// track search ////
 //////////////////////
 var trackSearch = {
 
     searchKeydown: function (event)
     {
         if (event.which == 13) {
             // Required to fix problem on IE and Safari where value of hgt_tSearch is "-" (i.e. not "Search").
             $("input[name=hgt_tsPage]").val(0);  // NOTE: must match TRACK_SEARCH_PAGER in hg/inc/searchTracks.h
             $('#trackSearch').submit();
@@ -2982,37 +3241,39 @@
             findTracks.updateMdbHelp(0);
         }
     }
 }
 
 
   ///////////////
  //// READY ////
 ///////////////
 $(document).ready(function()
 {
     // The page may be reached via browser history (back button)
     // If so, then this code should detect if the image has been changed via js/ajax
     // and will reload the image if necessary.
     // NOTE: this is needed for IE but other browsers can detect the dirty page much earlier
+    if (!imageV2.backSupport) {
         if (imageV2.isDirtyPage()) {
             // mark as non dirty to avoid infinite loop in chrome.
-        $('#dirty').val('false');
+            imageV2.markAsCleanPage();
             jQuery('body').css('cursor', 'wait');
                 window.location = "../cgi-bin/hgTracks?hgsid=" + getHgsid();
                 return false;
         }
+    }
     initVars();
     imageV2.loadSuggestBox();
     // Convert map AREA gets to post the form, ensuring that cart variables are kept up to date (but turn this off for search form).
     if($("FORM").length > 0 && $('#trackSearch').length == 0) {
         var allLinks = $('a');
         $( allLinks ).unbind('click');
         $( allLinks ).click( posting.saveSettings );
     }
     if($('#pdfLink').length == 1) {
         $('#pdfLink').click(function(i) {
             var thisForm=$('#TrackForm');
             if(thisForm != undefined && $(thisForm).length == 1) {
                 //alert("posting form:"+$(thisForm).attr('name'));
                 updateOrMakeNamedVariable($(thisForm),'hgt.psOutput','on');
                 return postTheForm($(thisForm).attr('name'),this.href);
@@ -3092,16 +3353,21 @@
     // Track search uses tabs
     trackSearch.init();
 
     // Drag select initialize
     if (imageV2.enabled) {   // moved from window.load().
         dragSelect.load(true);
 
         if($('#hgTrackUiDialog'))
             $('#hgTrackUiDialog').hide();
 
         // Don't load contextMenu if jquery.contextmenu.js hasn't been loaded
         if (jQuery.fn.contextMenu) {
             rightClick.load(imageV2.imgTbl);
         }
     }
+
+    // Experimentally trying jquery.history.js
+    if (imageV2.enabled && imageV2.backSupport) {
+        imageV2.setupHistory();
+    }
 });