e2be82208cc33ab43a11087601a846931e2346a5
chmalee
  Wed Mar 22 15:07:11 2023 -0700
Change downloads current track data button to a link in the downloads menu, refs #30865

diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index 970f408..405c872 100644
--- src/hg/js/hgTracks.js
+++ src/hg/js/hgTracks.js
@@ -1,5489 +1,5488 @@
 // hgTracks.js - Javascript for use in hgTracks CGI
 
 // Copyright (C) 2008 The Regents of the University of California
 
 // "use strict";
 // Don't complain about line break before '||' etc:
 /* jshint -W014 */
 
 
 var debug = false;
 
 /* Data passed in from CGI via the hgTracks object:
  *
  * string cgiVersion      // CGI_VERSION
  * string chromName       // current chromosome
  * int winStart           // genomic start coordinate (0-based, half-open)
  * int winEnd             // genomic end coordinate
  * int newWinWidth        // new width (in bps) if user clicks on the top ruler
  * boolean revCmplDisp    // true if we are in reverse display
  * int insideX            // width of side-bar (in pixels)
  * int rulerClickHeight   // height of ruler (in pixels) - zero if ruler is hidden
  * boolean inPlaceUpdate  // true if in-place-update is turned on
  * int imgBox*            // various drag-scroll values
  * boolean measureTiming  // true if measureTiming is on
  * Object trackDb         // hash of trackDb entries for tracks which are visible on current page
  * string highlight       // highlight string, in format chrom#start#end#color|chrom2#start2#end2#color2|...
  * string prevHlColor     // the last highlight color that the user picked
  */
 
 /* IE11 compatibility - IE doesn't have string startsWith and never will */
 if (!String.prototype.startsWith) {
   String.prototype.startsWith = function(searchString, position) {
     position = position || 0;
     return this.indexOf(searchString, position) === position;
   };
 }
 
 function initVars()
 {  // There are various entry points, so we call initVars in several places to make sure all is well
     if (typeof(hgTracks) !== "undefined" && !genomePos.original) {
         // remember initial position and size so we can restore it if user cancels
         genomePos.original = genomePos.getOriginalPos();
         genomePos.originalSize = $('#size').text().replace(/,/g, ""); // strip out any commas
         dragSelect.originalCursor = jQuery('body').css('cursor');
 
         imageV2.imgTbl = $('#imgTbl');
         // imageV2.enabled === true unless: advancedJavascript===false, or trackSearch, or config pg
         imageV2.enabled = (imageV2.imgTbl && imageV2.imgTbl.length > 0);
 
         // jQuery load function with stuff to support drag selection in track img
         if (theClient.isSafari()) {
             // Safari has the following bug: if we update the hgTracks map dynamically,
             // the browser ignores the changes (even though if you look in the DOM the changes
             // are there). So we have to do a full form submission when the user changes
             // visibility settings or track configuration.
             // As of 5.0.4 (7533.20.27) this is problem still exists in safari.
             // As of 5.1 (7534.50) this problem appears to have been fixed - unfortunately,
             // logs for 7/2011 show vast majority of safari users are pre-5.1 (5.0.5 is by far
             // the most common).
             //
             // Early versions of Chrome had this problem too, but this problem went away
             // as of Chrome 5.0.335.1 (or possibly earlier).
             //
             // KRR/JAT 2/2016:
             // This Safari issue is likely resolved in all current versions.  However the test
             // for version had been failing, likely for some time now.
             // (As of 9.0.9, possibly earlier, the 3rd part of the version is included in the
             // user agent string, so must be accounted for in string match)
             // Consequences were that page refresh was used instead of img update (e.g. 
             // for drag-zoom).  And UI dialog was unable to update (e.g. via Apply button).
             imageV2.mapIsUpdateable = false;
             var reg = new RegExp("Version\/([0-9]+.[0-9]+)(.[0-9]+)? Safari");
             var a = reg.exec(navigator.userAgent);
             if (a && a[1]) {
                 var version = Number(a[1]);
                 if (version >= 5.1) {
                     imageV2.mapIsUpdateable = true;
                 }
             }
         }
         imageV2.inPlaceUpdate = hgTracks.inPlaceUpdate && imageV2.mapIsUpdateable;
     }
 }
 
   /////////////////////////////////////
  ////////// Genomic position /////////
 /////////////////////////////////////
 var genomePos = {
 
     original: null,
     originalSize: 0,
 
     linkFixup: function (pos, id, reg, endParamName)
     {   // fixup external links (e.g. ensembl)
         var ele = $(document.getElementById(id));
         if (ele.length) {
             var link = ele.attr('href');
             var a = reg.exec(link);
             if (a && a[1]) {
                 ele.attr('href', a[1] + pos.start + "&" + endParamName + "=" + pos.end);
             }
         }
     },
 
     setByCoordinates: function (chrom, start, end)
     {
         var newPosition = chrom + ":" + start + "-" + end;
         genomePos.set(newPosition, end - start + 1);
         return newPosition;
     },
 
     getElement: function ()
     {
     // Return position box object
         var tags = document.getElementsByName("position");
         // There are multiple tags with name === "position" (the visible position text input
         // and a hidden with id='positionHidden'); we return value of visible element.
         for (var i = 0; i < tags.length; i++) {
                 var ele = tags[i];
                 if (ele.id !== "positionHidden") {
                     return ele;
                 }
         }
         return null;
     },
 
     get: function ()
     {
     // Return current value of position box
         var ele = genomePos.getElement();
         if (ele) {
             return ele.value;
         }
         return null;
     },
 
     getOriginalPos: function ()
     {
         return genomePos.original || genomePos.get();
     },
 
     revertToOriginalPos: function ()
     {
     // undo changes to position (i.e. after user aborts a drag-and-select).
         this.set(this.original, this.originalSize);
     },
 
     undisguisePosition: function(position) // UN-DISGUISE VMODE
     {   // find the virt position
         //  position should be real chrom span
         var pos = parsePosition(position);
         if (!pos)
             return position; // some parsing error, return original
         var start = pos.start - 1;
         var end = pos.end;
         var chromName = hgTracks.windows[0].chromName;
         if (pos.chrom !== chromName)
             return position; // return original
         var newStart = -1;
         var newEnd = -1;
         var lastW = null;
         var windows = null;
         for (j=0; j < 3; ++j) {
             if (j === 0) windows = hgTracks.windowsBefore;
             if (j === 1) windows = hgTracks.windows;
             if (j === 2) windows = hgTracks.windowsAfter;
             for (i=0,len=windows.length; i < len; ++i) {
                 var w = windows[i];
                 //  double check chrom is same thoughout all windows, otherwise warning, return original value
                 if (w.chromName != chromName) {
                     return position; // return original
                 }
                 // check that the regions are ascending and non-overlapping
                 if (lastW && w.winStart < lastW.winEnd) {
                     return position; // return original
                 }
                 // overlap with position?
                 //  if intersection, 
                 if (w.winEnd > start && end > w.winStart) {
                     var s = Math.max(start, w.winStart);
                     var e = Math.min(end, w.winEnd);
                     var cs = s - w.winStart + w.virtStart;
                     var ce = e - w.winStart + w.virtStart;
                     if (newStart === -1)
                         newStart = cs;
                     newEnd = ce;
                 }
                 lastW = w;
             }
         }
         //  return new virt undisguised position as a string
         var newPos = "multi:" + (newStart+1) + "-" + newEnd;
         return newPos;
     },
 
     disguiseSize: function(position) // DISGUISE VMODE
     {   // find the real size of the windows spanned
         //  position should be a real chrom span
         var pos = parsePosition(position);
         if (!pos)
             return 0;
         var start = pos.start - 1;
         var end = pos.end;
         var newSize = 0;
         var windows = null;
         for (j=0; j < 3; ++j) {
             if (j === 0) windows = hgTracks.windowsBefore;
             if (j === 1) windows = hgTracks.windows;
             if (j === 2) windows = hgTracks.windowsAfter;
             for (i=0,len=windows.length; i < len; ++i) {
                 var w = windows[i];
                 // overlap with position?
                 //  if intersection, 
                 if (w.winEnd > start && end > w.winStart) {
                     var s = Math.max(start, w.winStart);
                     var e = Math.min(end, w.winEnd);
                     newSize += (e - s);
                 }
             }
         }
         //  return real size of the disguised position 
         return newSize;
     },
 
     disguisePosition: function(position) // DISGUISE VMODE
     {   // find the single-chrom range spanned
         //  position should be virt
         var pos = parsePosition(position);
         if (!pos)
             return position; // some parsing error, return original
         var start = pos.start - 1;
         var end = pos.end;
         var chromName = hgTracks.windows[0].chromName;
         var newStart = -1;
         var newEnd = -1;
         var lastW = null;
         var windows = null;
         for (j=0; j < 3; ++j) {
             if (j === 0) windows = hgTracks.windowsBefore;
             if (j === 1) windows = hgTracks.windows;
             if (j === 2) windows = hgTracks.windowsAfter;
             for (i=0,len=windows.length; i < len; ++i) {
                 var w = windows[i];
                 //  double check chrom is same thoughout all windows, otherwise warning, return original value
                 if (w.chromName != chromName) {
                     return position; // return undisguised original
                 }
                 // check that the regions are ascending and non-overlapping
                 if (lastW && w.winStart < lastW.winEnd) {
                     return position; // return undisguised original
                 }
                 // overlap with position?
                 //  if intersection, 
                 if (w.virtEnd > start && end > w.virtStart) {
                     var s = Math.max(start, w.virtStart);
                     var e = Math.min(end, w.virtEnd);
                     var cs = s - w.virtStart + w.winStart;
                     var ce = e - w.virtStart + w.winStart;
                     if (newStart === -1)
                         newStart = cs;
                     newEnd = ce;
                 }
                 lastW = w;
             }
         }
         //  return new non-virt disguised position as a string
         var newPos = chromName + ":" + (newStart+1) + "-" + newEnd;
         return newPos;
     },
 
     set: function (position, size)
     {   // Set value of position and size (in hiddens and input elements).
         // We assume size has already been commified.
         // Either position or size may be null.
 
         // stack dump  // DEBUG
         //console.trace();
         // NOT work on safari
         //var obj = {};
         //Error.captureStackTrace(obj);
         //warn("genomePos.set() called "+obj.stack);
 
         position = position.replace(/,/g, ""); // strip out any commas
         position.replace("virt:", "multi:");
 
         if (position) {
             // DISGUISE VMODE
             //warn("genomePos.set() called, position = "+position);
             if (hgTracks.virtualSingleChrom && (position.search("multi:")===0)) {
                 var newPosition = genomePos.disguisePosition(position);
                 //warn("genomePos.set() position = "+position+", newPosition = "+newPosition);
                 position = newPosition;
             }
         }
         if (position) {
             // There are multiple tags with name === "position"
             // (one in TrackHeaderForm and another in TrackForm).
             var tags = document.getElementsByName("position");
             for (var i = 0; i < tags.length; i++) {
                 var ele = tags[i];
                 ele.value = position;
             }
         }
         var pos = parsePosition(position);
         if ($('#positionDisplay').length) {
             // add commas to positionDisplay
             var commaPosition = position;
             if (pos)
                 commaPosition = pos.chrom+":"+commify(pos.start)+"-"+commify(pos.end);  
             $('#positionDisplay').text(commaPosition);
         }
         if (size) {
             if (hgTracks.virtualSingleChrom && (position.search("multi:")!==0)) {
                 var newSize = genomePos.disguiseSize(position);
                 //warn("genomePos.set() position = "+position+", newSize = "+newSize);
                 if (newSize > 0)
                     size = newSize;
             }
             $('#size').text(commify(size)); // add commas
         }
         if (pos) {
             // fixup external static links on page'
 
             // Example ensembl link:
             // http://www.ensembl.org/Homo_sapiens/contigview?chr=21&start=33031934&end=33041241
             genomePos.linkFixup(pos, "ensemblLink", new RegExp("(.+start=)[0-9]+"), "end");
 
             // Example NCBI Map Viewer link (obsolete):
             // https://www.ncbi.nlm.nih.gov/mapview/maps.cgi?taxid=9606&CHR=21&BEG=33031934&END=33041241
             genomePos.linkFixup(pos, "ncbiLink", new RegExp("(.+BEG=)[0-9]+"), "END");
 
             // Example NCBI Genome Data Viewer link
             // https://www.ncbi.nlm.nih.gov/genome/gdv/browser/?id=GCF_000001405.37&chr=4&from=45985744&to=45991655&context=genome
             genomePos.linkFixup(pos, "ncbiLink", new RegExp("(.+from=)[0-9]+"), "to");
 
             // Example medaka link: 
             // http://utgenome.org/medakabrowser_ens_jump.php?revision=version1.0&chr=chromosome18&start=14435198&end=14444829
             genomePos.linkFixup(pos, "medakaLink", new RegExp("(.+start=)[0-9]+"), "end");
 
             var link;
             var reg;
             var a;
             if ($('#wormbaseLink').length) {
                 // e.g. http://www.wormbase.org/db/gb2/gbrowse/c_elegans?name=II:14646301-14667800
                 link = $('#wormbaseLink').attr('href');
                 reg = new RegExp("(.+:)[0-9]+");
                 a = reg.exec(link);
                 if (a && a[1]) {
                     $('#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) {
                 link = $('#dnaLink').attr('href');
                 reg = new RegExp("(.+&o=)[0-9]+.+&db=[^&]+(.*)");
                 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();
     },
 
     getXLimits : function(img, slop) {
         // calculate the min/max x position for drag-select, such that user cannot drag into the label area
         var imgWidth = jQuery(img).width();
         var imgOfs = jQuery(img).offset();
         var leftX = hgTracks.revCmplDisp ?  imgOfs.left - slop :
                                             imgOfs.left + hgTracks.insideX - slop;
         var rightX = hgTracks.revCmplDisp ? imgOfs.left + imgWidth - hgTracks.insideX + slop :
                                             imgOfs.left + imgWidth + slop;
 
         return [leftX, rightX];
     },
 
     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;
 
         // No need to check the x limits anymore, as imgAreaSelect is doing that now.
         return  (  (selection.event.pageY >= (imgOfs.top - slop))
                 && (selection.event.pageY <  (imgOfs.top + imgHeight + slop)));
     },
 
     pixelsToBases: function (img, selStart, selEnd, winStart, winEnd, addHalfBp)
     {   // Convert image coordinates to chromosome coordinates
         var imgWidth = jQuery(img).width() - hgTracks.insideX;
         var width = hgTracks.winEnd - hgTracks.winStart;
         var mult = width / imgWidth;   // mult is bp/pixel multiplier
  
         // where does a bp position start on the screen?
         // For things like drag-select, if the user ends just before the nucleotide itself, do not count
         // the nucleotide itself as selected. But for things like clicks onto
         // a selection, if the user right-clicks just before the middle of the
         // nucleotide, we certainly want to use this position.
         var halfBpWidth = 0;
         if (addHalfBp)
             halfBpWidth = (imgWidth / width) / 2; // how many pixels does one bp take up;
 
         var startDelta;   // startDelta is how many bp's to the right/left
         var x1;
 
         // The magic number three appear at another place in the code 
         // as LEFTADD. It was originally annotated as "borders or cgi item calc
         // ?" by Larry. It has to be used when going any time when converting 
         // between pixels and coordinates.
         selStart -= 3;
         selEnd -= 3;
 
         if (hgTracks.revCmplDisp) {
             x1 = Math.min(imgWidth, selStart);
             startDelta = Math.floor(mult * (imgWidth - x1 - halfBpWidth));
         } else {
             x1 = Math.max(hgTracks.insideX, selStart);
             startDelta = Math.floor(mult * (x1 - hgTracks.insideX + halfBpWidth));
         }
         var endDelta;
         var x2;
         if (hgTracks.revCmplDisp) {
             endDelta = startDelta;
             x2 = Math.min(imgWidth, selEnd);
             startDelta = Math.floor(mult * (imgWidth - x2 + halfBpWidth));
         } else {
             x2 = Math.max(hgTracks.insideX, selEnd);
             endDelta = Math.floor(mult * (x2 - hgTracks.insideX - halfBpWidth));
         }
         var newStart = hgTracks.winStart + startDelta;
         var newEnd = hgTracks.winStart + 1 + endDelta;
 
         // if user selects space between two bases, start>end can happen
         if (newStart >= newEnd)
             newStart = newEnd-1;
 
         if (newEnd > winEnd) {
             newEnd = winEnd;
         }
         return {chromStart : newStart, chromEnd : newEnd};
     },
 
     chromToVirtChrom: function (chrom, chromStart, chromEnd)
     {   // Convert regular chromosome position to virtual chrom coordinates using hgTracks.windows list
         // Consider the first contiguous set of overlapping regions to define the match (for now).
         // only works for regions covered by the current hgTracks.windows
         var virtStart = -1, virtEnd = -1;
         var s,e;
         var i, len;
         for (i = 0, len = hgTracks.windows.length; i < len; ++i) {
             var w = hgTracks.windows[i];
             var overlap = (chrom == w.chromName && chromEnd > w.winStart && w.winEnd > chromStart);
             if (virtStart == -1) {
                 if (overlap) {
                     // when they overlap the first time
                     s = Math.max(chromStart, w.winStart);
                     e = Math.min(chromEnd, w.winEnd);
                     virtStart = w.virtStart + (s - w.winStart);
                     virtEnd   = w.virtStart + (e - w.winStart);
                 } else {
                     // until they overlap
                     // do nothing
                 }
             } else {
                 if (overlap) {
                     // while they continue to overlap, extend
                     e = Math.min(chromEnd, w.winEnd);
                     virtEnd   = w.virtStart + (e - w.winStart);
                 } else {
                     // when they do not overlap anymore, stop
                     break;
                 }
             }
         }
         return {chromStart : virtStart, chromEnd : virtEnd};
     },
 
     selectionPixelsToBases: function (img, selection)
     {   // Convert selection x1/x2 coordinates to chromStart/chromEnd.
         return genomePos.pixelsToBases(img, selection.x1, selection.x2,
                                         hgTracks.winStart, hgTracks.winEnd, true);
     },
 
     update: function (img, selection, singleClick)
     {
         var pos = genomePos.pixelsToBases(img, selection.x1, selection.x2,
                                             hgTracks.winStart, hgTracks.winEnd, true);
         // singleClick is true when the mouse hasn't moved (or has only moved a small amount).
         if (singleClick) {
             var center = (pos.chromStart + pos.chromEnd)/2;
             pos.chromStart = Math.floor(center - hgTracks.newWinWidth/2);
             pos.chromEnd = pos.chromStart + hgTracks.newWinWidth;
             // clip
             if (pos.chromStart < hgTracks.chromStart)
                 pos.chromStart = hgTracks.chromStart; // usually  1
             if (pos.chromEnd > hgTracks.chromEnd)
                 pos.chromEnd = hgTracks.chromEnd; // usually virt chrom size
 
             // save current position so that that it may be restored after highlight or cancel.
             genomePos.original = genomePos.getOriginalPos();
             genomePos.originalSize = $('#size').text().replace(/,/g, ""); // strip out any commas
 
         }
         var newPosition = genomePos.setByCoordinates(hgTracks.chromName,
                                                             pos.chromStart+1, pos.chromEnd);
         return newPosition;
     },
 
     handleChange: function (response, status)
     {
         var json = JSON.parse(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.varsToUrlData({ 'cmd': 'defaultPos', 'db': getDb() }),
                 dataType: "html",
                 trueSuccess: genomePos.handleChange,
                 success: catchErrorOrDispatch,
                 error: errorHandler,
                 cache: true
             });
         return false;
     },
 
 
     convertedVirtCoords : {chromStart : -1, chromEnd : -1},
 
     handleConvertChromPosToVirtCoords: function (response, status)
     {
         var virtStart = -1, virtEnd = -1;
         var newJson = scrapeVariable(response, "convertChromToVirtChrom");
         if (!newJson) {
             warn("convertChromToVirtChrom object is missing from the response");
         } else {
             virtStart = newJson.virtWinStart;
             virtEnd   = newJson.virtWinEnd;
         }
         genomePos.convertedVirtCoords = {chromStart : virtStart, chromEnd : virtEnd};
     },
 
     convertChromPosToVirtCoords: function (chrom, chromStart, chromEnd)
     {   // code to convert chrom position to virt coords
         genomePos.convertedVirtCoords = {chromStart : -1, chromEnd : -1};  // reset
         var pos = chrom+":"+(chromStart+1)+"-"+chromEnd; // easier to pass 1 parameter than 3
         $.ajax({
                 type: "GET",
                 async: false, // wait for result
                 url: "../cgi-bin/hgTracks",
                 data: cart.varsToUrlData({ 'hgt.convertChromToVirtChrom': pos, 'hgt.trackImgOnly' : 1, 'hgsid': getHgsid() }),
                 dataType: "html",
                 trueSuccess: genomePos.handleConvertChromPosToVirtCoords,
                 success: catchErrorOrDispatch,
                 error: errorHandler,
                 cache: false
             });
         return genomePos.convertedVirtCoords;
     },
 
     positionDisplayDialog: function ()
     // Show the virtual and real positions of the windows
     {   
         var position = genomePos.get();
         position.replace("virt:", "multi:");
         var positionDialog = $("#positionDialog")[0];
         if (!positionDialog) {
             $("body").append("<div id='positionDialog'><span id='positionDisplayPosition'></span>");
             positionDialog = $("#positionDialog")[0];
         }
         if (hgTracks.windows) {
             var i, len, end;
             var matches = /^multi:[0-9]+-([0-9]+)/.exec(position);
             var modeType = (hgTracks.virtModeType === "customUrl" ? "Custom regions on virtual chromosome" :
                             (hgTracks.virtModeType === "exonMostly" ? "Exon view of" :
                             (hgTracks.virtModeType === "geneMostly" ? "Gene view of" :
                             (hgTracks.virtModeType === "singleAltHaplo" ? "Alternate haplotype as virtual chromosome" :
                                         "Unknown mode"))));
             var str = modeType + "&nbsp;" + position;
             if (matches) {
                 end = matches[1];
                 if (end < hgTracks.chromEnd) {
                     str += ". Full virtual region is multi:1-" + hgTracks.chromEnd + ". Zoom out to view.";
                 }
             }
             if (!(hgTracks.virtualSingleChrom && (hgTracks.windows.length === 1))) {
                 var w;
                 if (hgTracks.windows.length <= 10) {
                     str += "<p><table>\n";
                     for (i=0,len=hgTracks.windows.length; i < len; ++i) {
                         w = hgTracks.windows[i];
                         str += "<tr><td>" + w.chromName + ":" + (w.winStart+1) + "-" + w.winEnd + "</td><td>" + 
                                     (w.winEnd - w.winStart) + " bp" + "</td></tr>\n";
                     }
                     str += "</table></p>\n";
                 } else {
                     str += "<br><ul style='list-style-type:none; max-height:200px; padding:0; width:80%; overflow:hidden; overflow-y:scroll;'>\n";
                     for (i=0,len=hgTracks.windows.length; i < len; ++i) {
                         w = hgTracks.windows[i];
                         str += "<li>" + w.chromName + ":" + (w.winStart+1) + "-" + w.winEnd +
                                     "&nbsp;&nbsp;&nbsp;" + (w.winEnd - w.winStart) + " bp" + "</li>\n";
                     }
                     str += "</ul>\n";
                 }
             }
             $("#positionDisplayPosition").html(str);
         } else {
             $("#positionDisplayPosition").html(position);
         }
         $(positionDialog).dialog({
                 modal: true,
                 title: "Multi-Region Position Ranges",
                 closeOnEscape: true,
                 resizable: false,
                 autoOpen: false,
                 minWidth: 400,
                 minHeight: 40,
 
                 close: function() {
                     // All exits to dialog should go through this
                     $(imageV2.imgTbl).imgAreaSelect({hide:true});
                     $(this).hide();
                     $('body').css('cursor', ''); // Occasionally wait cursor got left behind
                 }
         });
         $(positionDialog).dialog('open');
     }
 
 };
 
   /////////////////////////////////////
  //// Creating items by dragging /////
 /////////////////////////////////////
 var makeItemsByDrag = {
 
     end: function (img, selection)
     {
         var image = $(img);
         var imageId = image.attr('id');
         var trackName = imageId.substring('img_data_'.length);
         var pos = genomePos.selectionPixelsToBases(image, selection);
         var command = document.getElementById('hgt_doJsCommand');
         command.value  = "makeItems " + trackName + " " + hgTracks.chromName;
         command.value +=  " " + pos.chromStart + " " + pos.chromEnd;
         document.TrackHeaderForm.submit();
         return true;
     },
 
     init: function (trackName)
     {
     // Set up so that they can drag out to define a new item on a makeItems track.
     var img = $("#img_data_" + trackName);
     if (img && img.length !== 0) {
         var imgHeight = imageV2.imgTbl.height();
         jQuery(img.imgAreaSelect( { selectionColor: 'green', outerColor: '',
             minHeight: imgHeight, maxHeight: imgHeight, onSelectEnd: makeItemsByDrag.end,
             autoHide: true, movable: false}));
         }
     },
 
     load: function ()
     {
         for (var id in hgTracks.trackDb) {
             var rec = hgTracks.trackDb[id];
             if (rec && rec.type && rec.type.indexOf("makeItems") === 0) {
                 this.init(id);
             }
         }
     }
 };
 
   /////////////////
  //// posting ////
 /////////////////
 var posting = {
 
     blockUseMap: false,
 
     blockMapClicks: function () 
     // Blocks clicking on map items when in effect. Drag opperations frequently call this.
     { 
         posting.blockUseMap=true;  
     },
     
     allowMapClicks:function ()  
     // reallows map clicks.  Called after operations that compete with clicks (e.g. dragging) 
     { 
         $('body').css('cursor', ''); // Explicitly remove wait cursor.
         posting.blockUseMap=false; 
     },
 
     mapClicksAllowed: function () 
     // Verify that click-competing operation (e.g. dragging) isn't currently occurring.
     { 
         return (posting.blockUseMap === false); 
     },
 
     blockTheMapOnMouseMove: function (ev)
     {
         if (!posting.blockUseMap && mouse.hasMoved(ev)) {
             posting.blockUseMap=true;
         }
     },
 
     mapClk: function ()
     {
         var done = false;
         // if we clicked on a merged item then show all the items, similar to clicking a
         // dense track to turn it to pack
         if (this && this.href && this.href.indexOf("i=mergedItem") !== -1) {
             var id = this.href.slice(this.href.indexOf("&g="));
             id = id.split(/&[^=]+=/)[1];
             updateObj={};
             updateObj[id+".doMergeItems"] = 0;
             hgTracks.trackDb[id][id+".doMergeItems"] = 0;
             cart.setVarsObj(updateObj,null,false);
             imageV2.requestImgUpdate(id, id + ".doMergeItems=0");
             return false;
         }
 
         if (false && imageV2.inPlaceUpdate) {
             // XXXX experimental and only turned on in larrym's tree.
             // Use in-place update if the map item just modifies the current position (this is nice
             // because it's faster and preserves the users relative position in the track image).
             //
             // First test handles next/prev item.
             var str = "/cgi-bin/hgTracks\\?position=([^:]+):(.+)&hgsid=(\\d+)" +
                                                                     "&(hgt.(next|prev)Item=[^&]+)";
             var reg = new RegExp(str);
             var a = reg.exec(this.href);
             if (a && a[1] && a[1] === hgTracks.chromName) {
                 imageV2.navigateInPlace("position=" + encodeURIComponent(a[1] + ":" + a[2]) + 
                                                                             "&" + a[4], null, true);
                 done = true;
             } else {
                 // handle next/prev exon
                 str = "/cgi-bin/hgTracks\\?position=([^:]+):(.+)&hgsid=(\\d+)$";
                 reg = new RegExp(str);
                 a = reg.exec(this.href);
                 if (a && a[1]) {
                     imageV2.navigateInPlace("position=" + encodeURIComponent(a[1] + ":" + a[2]), 
                                                                                         null, true);
                     done = true;
                 } else {
                     // handle toggle visibility.
                     // Request may include a track set, so we cannot use requestImgUpdate.
                     str = "/cgi-bin/hgTracks\\?(position=[^:]+:.+&hgsid=\\d+&([^=]+)=([^&]+))$";
                     reg = new RegExp(str);
                     a = reg.exec(this.href);
                     if (a && a[1]) {
                         imageV2.navigateInPlace(a[1], null, true);
                         // imageV2.requestImgUpdate(a[1], a[1] + "=" + a[2], "", a[2]);
                         done = true;
                     }
                 }
             }
         }
         if (done)
             return false;
         else
             return posting.saveSettings(this);
     },
 
     saveSettings: function (obj)
     {
         if (posting.blockUseMap === true) {
             return false;
         }
         if (!obj || !obj.href) // called directly with obj
             obj = this;                               // and from callback without obj
 
         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 = normed($(obj).parents('form').first());
         if (!thisForm)
             thisForm = normed($("FORM").first());
         if (thisForm) {
             //alert("posting form:"+$(thisForm).attr('name'));
             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 objNotEmpty(cart.updateQueue);
     },
     
     addUpdatesToUrl: function (url)
     {   // adds any outstanding cart updates to the url, then clears the queue
         if (cart.updatesWaiting()) {
             //console.log('cart.addUpdatesToUrl: '+objKeyCount(cart.updateQueue)+' vars');
             var updates = cart.varsToUrlData(); // clears the queue
             if (!url || url.length === 0)
                 return updates;
 
             if (updates.length > 0) {
                 var dataOnly = (url.indexOf("cgi-bin") === -1); // all urls should be to our cgis
                 if (!dataOnly && url.lastIndexOf("?") === -1)
                     url += "?" + updates;
                 else
                     url += '&' + updates;
             }
         }
         return url;
     },
     
     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
         
         // Now convert to url data and clear queue
         var urlData = '';
         if (cart.updatesWaiting()) {
             //console.log('cart.varsToUrlData: '+objKeyCount(cart.updateQueue)+' vars');
             urlData = varHashToQueryString(cart.updateQueue);
             cart.updateQueue = {};
         }
         return urlData;
     },
     
     setVarsObj: function (varsObj, errFunc, async)
     {   // Set all vars in a var hash, appending any queued updates
         // The default behavior is async = true
         //console.log('cart.setVarsObj: were:'+objKeyCount(cart.updateQueue) + 
         //            ' new:'+objKeyCount(varsObj);
         cart.queueVarsObj(varsObj); // lay ontop of queue, to give new values precedence
         
         // Now ajax update all in queue and clear queue
         if (cart.updatesWaiting()) {
             setVarsFromHash(cart.updateQueue, errFunc, async);
             cart.updateQueue = {};
         }
     },
     
     setVars: function (names, values, errFunc, async)
     {   // ajax updates the cart, and includes any queued updates.
         cart.updateSessionPanel();      // handles hide from left minibutton
         cart.setVarsObj(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' && objNotEmpty(varsObj)) {
             //console.log('cart.queueVarsObj: were:'+objKeyCount(cart.updateQueue) + 
             //            ' new:'+objKeyCount(varsObj));
             for (var name in varsObj) {
                 cart.updateQueue[name] = varsObj[name];
             }
         }
     },
 
     addVarsToQueue: function (names,values)
     {   // creates a string of updates to save for ajax batch or a submit
         cart.queueVarsObj(arysToObj(names,values));
     },
 
     updateSessionPanel: function()
     {
     if (typeof recTrackSetsDetectChanges === 'undefined' || recTrackSetsDetectChanges === null)
         return;
 
     // change color of text
     $('span.gbSessionChangeIndicator').addClass('gbSessionChanged');
 
     // change mouseover on the panel.  A bit fragile here inserting text in the mouseover specified in
     // hgTracks.js, so depends on match with text there, and should present same message as C code
     // (Perhaps this could be added as a script tag, so not duplicated)
     var txt = $('span.gbSessionLabelPanel').attr('title');
     if (txt && !txt.match(/with changes/)) {
         $('span.gbSessionLabelPanel').attr('title', txt.replace(
                                    "track set", 
                                    "track set, with changes (added or removed tracks) you have requested"));
         }
     }
 };
 
   ///////////////////////////////////////////////
  //// visibility (mixed with group toggle) /////
 ///////////////////////////////////////////////
 var vis = {
 
     // map cgi enum visibility codes to strings
     enumOrder: new Array("hide", "dense", "full", "pack", "squish"),  
 
     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;
     },
 
     get: function (track)
     {   // return current visibility for given track
         var rec = hgTracks.trackDb[track];
         if (rec) {
             if (rec.localVisibility) {
                 return rec.localVisibility;
             } else {
                 return vis.enumOrder[rec.visibility];
             }
         } else {
             return null;
         }
     },
 
     makeTrackVisible: function (track)
     {   // Sets the vis box to visible, and ques a cart update, but does not update the image
         // Trusts that the cart update will be submitted later.
         if (track && vis.get(track) !== "full") {
             vis.update(track, 'pack');
             cart.addVarsToQueue([track], ['pack']);
         }
     },
 
     toggleForGroup: function (button, prefix)
     {   // toggle visibility of a track group; prefix is the prefix of all the id's of tr's in the
         // relevant group. This code also modifies the corresponding hidden fields and the gif
         // of the +/- img tag.
         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      // - 7: clip '_button' suffix
             vis.toggleForGroup(this,this.id.substring(0,this.id.length - 7),newState);
         });
         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();
                 cart.updateSessionPanel();
                 imageV2.drawHighlights();
                 $(this).attr('class', 'hiddenText');
             } else
                 $(this).attr('class', 'normalText');
             
             cart.setVars([track], [$(this).val()]);
             imageV2.markAsDirtyPage();
             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 necessary because initForAjax() disables them on submit.
     {
         $('select.normalText,select.hiddenText').attr('disabled',false);
     }
 };
 
   ////////////////////////////////////////////////////////////
  // dragSelect is also known as dragZoom or shift-dragZoom //
 ////////////////////////////////////////////////////////////
 var dragSelect = {
 
     hlColorDefault: '#aaedff', // default highlight color, if nothing specified
     hlColor :       '#aaedff', // current highlight color
     areaSelector:    null, // formerly "imgAreaSelect". jQuery element used for imgAreaSelect
     originalCursor:  null,
     startTime:       null,
     escPressed :     false,  // flag is set when user presses Escape
 
     selectStart: function (img, selection)
     {
         initVars();
         dragSelect.escPressed = false;
         if (rightClick.menu) {
             rightClick.menu.hide();
         }
         var now = new Date();
         dragSelect.startTime = now.getTime();
         posting.blockMapClicks();
     },
 
     selectChange: function (img, selection)
     {
         if (selection.x1 !== selection.x2) {
             if (genomePos.check(img, selection)) {
                 genomePos.update(img, selection, false);
                 jQuery('body').css('cursor', dragSelect.originalCursor);
             } else {
                 jQuery('body').css('cursor', 'not-allowed');
             }
         }
         return true;
     },
 
     findHighlightIdxForPos : function(findPos) {
         // return the index of the highlight string e.g. hg19.chrom1:123-345#AABBDCC that includes a chrom range findPos
         // mostly copied from drawHighlights()
         var currDb = getDb();
         if (hgTracks.highlight) {
             var hlArray = hgTracks.highlight.split("|"); // support multiple highlight items
             for (var i = 0; i < hlArray.length; i++) {
                 hlString = hlArray[i];
                 pos = parsePositionWithDb(hlString);
                 imageV2.undisguiseHighlight(pos);
 
                 if (!pos) 
                     continue; // ignore invalid position strings
                 pos.start--;
 
                 if (pos.chrom === hgTracks.chromName && pos.db === currDb
                 &&  pos.start <= findPos.chromStart && pos.end >= findPos.chromEnd) {
                     return i;
                 }
             }
         }
         return null;
     },
 
     saveHlColor : function (hlColor) 
     // save the current hlColor to the object and also the cart variable hlColor and return it.
     // hlColor is a 6-character hex string prefixed by #
     {
             dragSelect.hlColor = hlColor;
             cart.setVars(["prevHlColor"], [dragSelect.hlColor], null, false);
             hgTracks.prevHlColor = hlColor; // cart.setVars does not update the hgTracks-variables. The cart-variable system is a problem.
             return hlColor;
     },
 
     loadHlColor : function () 
     // load hlColor from prevHlColor in the cart, or use default color, set and return it
     // color is a 6-char hex string prefixed by #
     {
         if (hgTracks.prevHlColor)
             dragSelect.hlColor = hgTracks.prevHlColor;
         else
             dragSelect.hlColor = dragSelect.hlColorDefault;
         return dragSelect.hlColor;
     },
 
     highlightThisRegion: function(newPosition, doAdd, hlColor)
     // set highlighting newPosition in server-side cart and apply the highlighting in local UI.
     // hlColor can be undefined, in which case it defaults to the last used color or the default light blue
     // if hlColor is set, it is also saved into the cart.
     // if doAdd is true, the highlight is added to the current list. If it is false, all old highlights are deleted.
     {
         newPosition.replace("virt:", "multi:");
         var hlColorName = null;
         if (hlColor==="" || hlColor===null || hlColor===undefined)
             hlColorName = dragSelect.loadHlColor();
         else
             hlColorName = dragSelect.saveHlColor( hlColor );
 
         var pos = parsePosition(newPosition);
         var start = pos.start;
         var end = pos.end;
         var newHighlight = makeHighlightString(getDb(), pos.chrom, start, end, hlColorName);
         newHighlight = imageV2.disguiseHighlight(newHighlight);
         var oldHighlight = hgTracks.highlight;
         if (oldHighlight===undefined || doAdd===undefined || doAdd===false || oldHighlight==="") {
             // just set/overwrite the old highlight position, this used to be the default
             hgTracks.highlight = newHighlight;
         }
         else {
             // add to the end of a |-separated list
             hgTracks.highlight = oldHighlight+"|"+newHighlight;
         }
         // we include enableHighlightingDialog because it may have been changed by the dialog
         var cartSettings = {             'highlight': hgTracks.highlight, 
                           'enableHighlightingDialog': hgTracks.enableHighlightingDialog ? 1 : 0 };
 
         if (hgTracks.windows && !hgTracks.virtualSingleChrom) {
             var nonVirtChrom = "";
             var nonVirtStart = -1; 
             var nonVirtEnd   = -1; 
             for (i=0,len=hgTracks.windows.length; i < len; ++i) {
                 var w = hgTracks.windows[i];
                 // overlap with new position?
                 if (w.virtEnd > start && end > w.virtStart) {
                     var s = Math.max(start, w.virtStart);
                     var e = Math.min(end, w.virtEnd);
                     var cs = s - w.virtStart + w.winStart;
                     var ce = e - w.virtStart + w.winStart;
                     if (nonVirtChrom === "") {
                         nonVirtChrom = w.chromName;
                         nonVirtStart = cs; 
                         nonVirtEnd   = ce;
                     } else {
                         if (w.chromName === nonVirtChrom) {
                             nonVirtEnd = Math.max(ce, nonVirtEnd);
                         } else {
                             break;
                         }
                     }
                 }
             }
             if (nonVirtChrom !== "")
                 cartSettings.nonVirtHighlight = makeHighlightString(getDb(), nonVirtChrom, nonVirtStart, (nonVirtEnd+1), hlColorName);
         } else if (hgTracks.windows && hgTracks.virtualSingleChrom) {
                 cartSettings.nonVirtHighlight = hgTracks.highlight;
         }
         // TODO if not virt, do we need to erase cart nonVirtHighlight ?
         cart.setVarsObj(cartSettings);
         imageV2.drawHighlights();
     },
 
     selectionEndDialog: function (newPosition)
     // Let user choose between zoom-in and highlighting.
     {   
         newPosition.replace("virt:", "multi:");
         // if the user hit Escape just before, do not show this dialo
         if (dragSelect.startTime===null)
             return;
         var dragSelectDialog = $("#dragSelectDialog")[0];
         if (!dragSelectDialog) {
             $("body").append("<div id='dragSelectDialog'>" + 
                              "<p><ul>"+
                              "<li>Hold <b>Shift+drag</b> to show this dialog" +
                              "<li>Hold <b>Alt+drag</b> (Windows) or <b>Option+drag</b> (Mac) to add a highlight" +
                              "<li>Hold <b>Ctrl+drag</b> (Windows) or <b>Cmd+drag</b> (Mac) to zoom" +
                              "<li>To cancel, press <tt>Esc</tt> anytime during the drag" +
                              "<li>Using the keyboard, highlight the current position with <tt>h then m</tt>" +
                              "<li>Clear all highlights with View - Clear Highlights or <tt>h then c</tt>" +
                              "<li>To merely save the color for the next keyboard or right-click &gt; Highlight operations, click 'Save Color' below" +
                              "</ul></p>" +
                              "<p>Highlight color: <input type='text' style='width:70px' id='hlColorInput' value='"+dragSelect.loadHlColor()+"'>" +
                              "&nbsp;&nbsp;<input id='hlColorPicker'>" + 
                              "&nbsp;&nbsp;<a href='#' id='hlReset'>Reset</a></p>" + 
                              "<input style='float:left' type='checkbox' id='disableDragHighlight'>" + 
                              "<span style='border:solid 1px #DDDDDD; padding:3px;display:inline-block' id='hlNotShowAgainMsg'>Don't show this again and always zoom with shift.<br>" + 
                              "Re-enable via 'View - Configure Browser' (<tt>c then f</tt>)</span></p>"+ 
                              "Selected chromosome position: <span id='dragSelectPosition'></span>");
             dragSelectDialog = $("#dragSelectDialog")[0];
             // reset value
             $('#hlReset').click(function() { 
                 var hlDefault = dragSelect.hlColorDefault;
                 $('#hlColorInput').val(hlDefault);
                 $("#hlColorPicker").spectrum("set", hlDefault);
                 dragSelect.saveHlColor(hlDefault);
             });
             // allow to click checkbox by clicking on the label
             $('#hlNotShowAgainMsg').click(function() { $('#disableDragHighlight').click();});
             // click "add highlight" when enter is pressed in color input box
             $("#hlColorInput").keyup(function(event){
                 if(event.keyCode == 13){
                     $(".ui-dialog-buttonset button:nth-child(3)").click();
                 }
             });
             // activate the color picker
             var opt = {
                 hideAfterPaletteSelect : true,
                 color : $('#hlColorInput').val(),
                 showPalette: true,
                 showInput: true,
                 preferredFormat: "hex",
                 change: function() { var color = $("#hlColorPicker").spectrum("get"); $('#hlColorInput').val(color); },
                 };
             $("#hlColorPicker").spectrum(opt);
             // update the color picker if you change the input box
             $("#hlColorInput").change(function(){ $("#hlColorPicker").spectrum("set", $('#hlColorInput').val()); });
         }
 
         $("#hlColorPicker").spectrum("set", $('#hlColorInput').val());
 
         if (hgTracks.windows) {
             var i,len;
             var newerPosition = newPosition;
             if (hgTracks.virtualSingleChrom && (newPosition.search("multi:")===0)) {
                 newerPosition = genomePos.disguisePosition(newPosition);
             }
             var str = newerPosition + "<br>\n";
             var str2 = "<br>\n";
             str2 += "<ul style='list-style-type:none; max-height:200px; padding:0; width:80%; overflow:hidden; overflow-y:scroll;'>\n";
             var pos = parsePosition(newPosition);
             var start = pos.start - 1;
             var end = pos.end;
             var selectedRegions = 0;
             for (i=0,len=hgTracks.windows.length; i < len; ++i) {
                 var w = hgTracks.windows[i];
                 // overlap with new position?
                 if (w.virtEnd > start && end > w.virtStart) {
                     var s = Math.max(start, w.virtStart);
                     var e = Math.min(end, w.virtEnd);
                     var cs = s - w.virtStart + w.winStart;
                     var ce = e - w.virtStart + w.winStart;
                     str2 += "<li>" + w.chromName + ":" + (cs+1) + "-" + ce + "</li>\n";
                     selectedRegions += 1;
                 }
             }
             str2 += "</ul>\n";
             if (!(hgTracks.virtualSingleChrom && (selectedRegions === 1))) {
                 str += str2;
             }
             $("#dragSelectPosition").html(str);
         } else {
             $("#dragSelectPosition").html(newPosition);
         }
         $(dragSelectDialog).dialog({
                 modal: true,
                 title: "Drag-and-select",
                 closeOnEscape: true,
                 resizable: false,
                 autoOpen: false,
                 revertToOriginalPos: true,
                 minWidth: 550,
                 buttons: {  
                     "Zoom In": function() {
                         // Zoom to selection
                         $(this).dialog("option", "revertToOriginalPos", false);
                         if ($("#disableDragHighlight").attr('checked'))
                             hgTracks.enableHighlightingDialog = false;
                         if (imageV2.inPlaceUpdate) {
                             if (hgTracks.virtualSingleChrom && (newPosition.search("multi:")===0)) {
                                 newPosition = genomePos.disguisePosition(newPosition); // DISGUISE
                             }
                             var params = "position=" + newPosition;
                             if (!hgTracks.enableHighlightingDialog)
                                 params += "&enableHighlightingDialog=0";
                             imageV2.navigateInPlace(params, null, true);
                         } else {
                             $('body').css('cursor', 'wait');
                             if (!hgTracks.enableHighlightingDialog)
                                 cart.setVarsObj({'enableHighlightingDialog': 0 },null,false); // async=false
                             document.TrackHeaderForm.submit();
                         }
                         $(this).dialog("close");
                     },
                     "Single Highlight": function() {
                         // Clear old highlight and Highlight selection
                         $(imageV2.imgTbl).imgAreaSelect({hide:true});
                         if ($("#disableDragHighlight").attr('checked'))
                             hgTracks.enableHighlightingDialog = false;
                         var hlColor = $("#hlColorInput").val();
                         dragSelect.highlightThisRegion(newPosition, false, hlColor);
                         $(this).dialog("close");
                     },
                     "Add Highlight": function() {
                         // Highlight selection
                         if ($("#disableDragHighlight").attr('checked'))
                             hgTracks.enableHighlightingDialog = false;
                         var hlColor = $("#hlColorInput").val();
                         dragSelect.highlightThisRegion(newPosition, true, hlColor);
                         $(this).dialog("close");
                     },
                     "Save Color": function() {
                         var hlColor = $("#hlColorInput").val();
                         dragSelect.saveHlColor( hlColor );
                         $(this).dialog("close");
                     },
                     "Cancel": function() {
                         $(this).dialog("close");
                     }
                 },
 
                 open: function () { // Make zoom the focus/default action
                    $(this).parents('.ui-dialog-buttonpane button:eq(0)').focus(); 
                 },
 
                 close: function() {
                     // All exits to dialog should go through this
                     $(imageV2.imgTbl).imgAreaSelect({hide:true});
                     if ($(this).dialog("option", "revertToOriginalPos"))
                         genomePos.revertToOriginalPos();
                     if ($("#disableDragHighlight").attr('checked'))
                         $(this).remove();
                     else
                         $(this).hide();
                     $('body').css('cursor', ''); // Occasionally wait cursor got left behind
                     $("#hlColorPicker").spectrum("hide");
                 }
         });
         $(dragSelectDialog).dialog('open');
         
         // put the cursor into the input field
         // we are not doing this for now - default behavior was to zoom when enter was pressed
         // so people may still expect that "enter" on the dialog will zoom.
         //var el = $("#hlColorInput")[0];
         //el.selectionStart = 0;
         //el.selectionEnd = el.value.length;
         //el.focus();
 
     },
 
     selectEnd: function (img, selection, event)
     {
         var now = new Date();
         var doIt = false;
         var rulerClicked = selection.y1 <= hgTracks.rulerClickHeight; // = drag on base position track (no shift)
         if (dragSelect.originalCursor)
             jQuery('body').css('cursor', dragSelect.originalCursor);
         if (dragSelect.escPressed)
             return false;
         // ignore releases outside of the image rectangle (allowing a 10 pixel slop)
         if (genomePos.check(img, selection)) {
             // ignore single clicks that aren't in the top of the image
             // (this happens b/c the clickClipHeight test in dragSelect.selectStart
             // doesn't occur when the user single clicks).
             doIt = (dragSelect.startTime !== null || rulerClicked);
         }
 
         if (doIt) {
             // dragSelect.startTime is null if mouse has never been moved
             var singleClick = (  (selection.x2 === selection.x1)
                               || dragSelect.startTime === null
                               || (now.getTime() - dragSelect.startTime) < 100);
             var newPosition = genomePos.update(img, selection, singleClick);
             newPosition.replace("virt:", "multi:");
             if (newPosition) {
                 if (event.altKey) {
                     // with the alt-key, only highlight the region, do not zoom
                     dragSelect.highlightThisRegion(newPosition, true);
                     $(imageV2.imgTbl).imgAreaSelect({hide:true});
                 } else {
                     if (hgTracks.enableHighlightingDialog && !(event.metaKey || event.ctrlKey))
                         // don't show the dialog if: clicked on ruler, if dialog deactivated or meta/ctrl was pressed
                         dragSelect.selectionEndDialog(newPosition);
                     else {
                         // in every other case, show the dialog
                         $(imageV2.imgTbl).imgAreaSelect({hide:true});
                         if (imageV2.inPlaceUpdate) {
                             if (hgTracks.virtualSingleChrom && (newPosition.search("multi:")===0)) {
                                 newPosition = genomePos.disguisePosition(newPosition); // DISGUISE
                             }
                             imageV2.navigateInPlace("position=" + newPosition, null, true);
                         } else {
                             jQuery('body').css('cursor', 'wait');
                             document.TrackHeaderForm.submit();
                         }
                     }
                 }
             } else {
                 $(imageV2.imgTbl).imgAreaSelect({hide:true});
                 genomePos.revertToOriginalPos();
             }
             dragSelect.startTime = null;
             // blockMapClicks/allowMapClicks() is necessary if selectEnd was over a map item.
             setTimeout(posting.allowMapClicks,50);
             return true;
         }
     },
 
     load: function (firstTime)
     {
         var imgHeight = 0;
         if (imageV2.enabled)
             imgHeight = imageV2.imgTbl.innerHeight() - 1; // last little bit makes border look ok
         
 
         // No longer disable without ruler, because shift-drag still works
         if (typeof(hgTracks) !== "undefined") {
 
             if (hgTracks.rulerClickHeight === undefined || hgTracks.rulerClickHeight === null)
                 hgTracks.rulerClickHeight = 0; // will be zero if no ruler track
             var heights = hgTracks.rulerClickHeight;
 
             var xLimits = genomePos.getXLimits($(imageV2.imgTbl), 0);
 
             dragSelect.areaSelector = jQuery((imageV2.imgTbl).imgAreaSelect({
                 minX : xLimits[0],
                 maxX : xLimits[1],
                 selectionColor:  'blue',
                 outerColor:      '',
                 minHeight:       imgHeight,
                 maxHeight:       imgHeight,
                 onSelectStart:   dragSelect.selectStart,
                 onSelectChange:  dragSelect.selectChange,
                 onSelectEnd:     dragSelect.selectEnd,
                 autoHide:        false, // gets hidden after possible dialog
                 movable:         false,
                 clickClipHeight: heights
             }));
 
             // remove any ongoing drag-selects when the esc key is pressed anywhere for this document
             // This allows to abort zooming / highlighting
             $(document).keyup(function(e){
                 if(e.keyCode === 27) {
                     $(imageV2.imgTbl).imgAreaSelect({hide:true});
                     dragSelect.escPressed = true;
                 }
             });
 
             // hide and redraw all current highlights when the browser window is resized
             $(window).resize(function() {
                 $(imageV2.imgTbl).imgAreaSelect({hide:true});
                 imageV2.drawHighlights();
             });
 
         }
     }
 };
 
   /////////////////////////////////////
  //// Chrom Drag/Zoom/Expand code ////
 /////////////////////////////////////
 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)
 
     // Image dimensions all in pix
     var img = { top: -1, scrolledTop: -1, height: -1, left: -1, scrolledLeft: -1, width: -1 };  
     // chrom Dimensions beg,end,size in bases, rest in pix
     var chr = { name: "", reverse: false, beg: -1, end: -1, size: -1,
                 top: -1, bottom: -1, left: -1, right: -1, width: -1 };   
     var pxDown = 0;     // pix X location of mouseDown
     var chrImg = $(this);
     var mouseIsDown   = false;
     var mouseHasMoved = false;
     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;
                 }
             });
         }
     }
 
     function chromMove(e)
     {   // If mouse was down, determine if dragged, then show hilite
         if ( mouseIsDown ) {
             var pxX = e.clientX - img.scrolledLeft;
             var relativeX = (pxX - pxDown);
             if (mouseHasMoved || (mouseHasMoved === false && Math.abs(relativeX) > 2)) {
                 mouseHasMoved = true;
                 if (isWithin(chr.left,pxX,chr.right))
                     hiliteShow(pxDown,pxX);
                 else if (pxX < chr.left)
                     hiliteShow(pxDown,chr.left);
                 else
                     hiliteShow(pxDown,chr.right);
             }
         }
     }
     function chromUp(e)
     {   // If mouse was down, handle final selection
         $(document).unbind('mousemove',chromMove);
         $(document).unbind('mouseup',chromUp);
         chromMove(e); // Just in case
         if (mouseIsDown) {
             updateImgOffsets();
             var bands;
             var pxUp = e.clientX - img.scrolledLeft;
             var pxY  = e.clientY - img.scrolledTop;
             if (isWithin(0,pxY,img.height)) {  // within vertical range or else cancel
                 var selRange = { beg: -1, end: -1, width: -1 };
                 var dontAsk = true;
 
                 if (e.ctrlKey) {
                     bands = findCytoBand(pxDown,pxUp);
                     if (bands.end > -1) {
                         pxDown = bands.left;
                         pxUp   = bands.right;
                         mouseHasMoved = true;
                         dontAsk = false;
                         selRange.beg = bands.beg;
                         selRange.end = bands.end;
                         hiliteShow(pxDown,pxUp);
                     }
                 }
                 else if (mouseHasMoved) {
                     // bounded by chrom dimensions: but must remain within image!
                     if (isWithin(-20,pxUp,chr.left)) 
                         pxUp = chr.left;
                     if (isWithin(chr.right,pxUp,img.width + 20))
                         pxUp = chr.right;
 
                     if ( isWithin(chr.left,pxUp,chr.right+1) ) {
 
                         selRange.beg = convertToBases(pxDown);
                         selRange.end = convertToBases(pxUp);
 
                         if (Math.abs(selRange.end - selRange.beg) < 20)
                             mouseHasMoved = false; // Drag so small: treat as simple click
                         else
                             dontAsk = false;
                     }
                 }
                 if (mouseHasMoved === false) { // Not else because small drag turns this off
 
                     hiliteShow(pxUp,pxUp);
                     var curWidth = hgTracks.winEnd - hgTracks.winStart;
                     // Notice that beg is based upon up position
                     selRange.beg = convertToBases(pxUp) - Math.round(curWidth/2); 
                     selRange.end  = selRange.beg + curWidth;
                 }
                 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);
                         // Stop the presses :0)
                         $('area.cytoBand').mousedown( function(e) { return false; });
                         if (imageV2.backSupport) {
                             imageV2.navigateInPlace("position=" +  
                                     encodeURIComponent(genomePos.get().replace(/,/g,'')) + 
                                     "&findNearest=1",null,true);
                             hiliteCancel();
                         } else
                             document.TrackHeaderForm.submit();
                         return true; // Make sure the setTimeout below is not called.
                     }
                 }
             }
             hiliteCancel();
             setTimeout(posting.allowMapClicks,50);
         }
         mouseIsDown = false;
         mouseHasMoved = false;
     }
 
     function isWithin(beg,here,end)
     {   // Simple utility
         return ( beg <= here && here < end );
     }
     function convertToBases(pxX)
     {   // Simple utility to convert pix to bases
         var offset = (pxX - chr.left)/chr.width;
         if (chr.reverse)
             offset = 1 - offset;
         return Math.round(offset * chr.size);
     }
     function convertFromBases(bases)
     {   // Simple utility to convert bases to pix
         var offset = bases/chr.size;
         if (chr.reverse)
             offset = 1 - offset;
         return Math.round(offset * chr.width) + chr.left;
     }
 
     function findDimensions()
     {   // Called at init: determine the dimensions of chrom from 'cytoband' map items
         var lastX = -1;
         $('area.cytoBand').each(function(ix) {
             var loc = this.coords.split(",");
             if (loc.length === 4) {
                 var myLeft  = parseInt(loc[0]);
                 var myRight = parseInt(loc[2]);
                 if (chr.top === -1) {
                     chr.left   = myLeft;
                     chr.right  = myRight;
                     chr.top    = parseInt(loc[1]);
                     chr.bottom = parseInt(loc[3]);
                 } else {
                     if (chr.left  > myLeft)
                         chr.left  = myLeft;
                     if (chr.right < parseInt(loc[2]))
                         chr.right = parseInt(loc[2]);
                 }
 
                 var range = this.title.substr(this.title.lastIndexOf(':')+1);
                 var pos = range.split('-');
                 if (pos.length === 2) {
                     if (chr.name.length === 0) {
                         chr.beg = parseInt(pos[0]);
                         //chr.end = parseInt(pos[1]);
                         chr.name = this.title.substring(this.title.lastIndexOf(' ')+1,
                                                         this.title.lastIndexOf(':'));
                     } else {
                         if (chr.beg > parseInt(pos[0]))
                             chr.beg = parseInt(pos[0]);
                     }
                     if (chr.end < parseInt(pos[1])) {
                         chr.end = parseInt(pos[1]);
                         if (lastX === -1)
                             lastX = myRight;
                         else if (lastX > myRight)
                             chr.reverse = true;  // end is advancing, but X is not, so reverse
                     } else if (lastX !== -1 && lastX < myRight)
                         chr.reverse = true;      // end is not advancing, but X is, so reverse
 
                 }
                 $(this).css( 'cursor', 'text');
                 $(this).attr("href","");
             }
         });
         chr.size  = (chr.end   - chr.beg );
         chr.width = (chr.right - chr.left);
     }
 
     function findCytoBand(pxDown,pxUp)
     {   // Called when mouseup and ctrl: Find the bounding cytoband dimensions (in pix and bases)
         var cyto = { left: -1, right: -1, beg: -1, end: -1 };
         $('area.cytoBand').each(function(ix) {
             var loc = this.coords.split(",");
             if (loc.length === 4) {
                 var myLeft  = parseInt(loc[0]);
                 var myRight = parseInt(loc[2]);
                 var range;
                 var pos;
                 if (cyto.left === -1 || cyto.left > myLeft) {
                     if ( isWithin(myLeft,pxDown,myRight) || isWithin(myLeft,pxUp,myRight) ) {
                         cyto.left  = myLeft;
                         range = this.title.substr(this.title.lastIndexOf(':')+1);
                         pos = range.split('-');
                         if (pos.length === 2) {
                             cyto.beg  = (chr.reverse ? parseInt(pos[1]) : parseInt(pos[0]));
                         }
                     }
                 }
                 if (cyto.right === -1 || cyto.right < myRight) {
                     if ( isWithin(myLeft,pxDown,myRight) || isWithin(myLeft,pxUp,myRight) ) {
                         cyto.right = myRight;
                         range = this.title.substr(this.title.lastIndexOf(':')+1);
                         pos = range.split('-');
                         if (pos.length === 2) {
                             cyto.end  = (chr.reverse ? parseInt(pos[0]) : parseInt(pos[1]));
                         }
                     }
                 }
             }
         });
         return cyto;
     }
     function rangeNormalizeToChrom(selection,chrom)
     {   // Called before presenting or using base range: make sure chrom selection
         // is within chrom range
         if (selection.end < selection.beg) {
             var tmp = selection.end;
             selection.end = selection.beg;
             selection.beg = tmp;
         }
         selection.width = (selection.end - selection.beg);
         selection.beg += 1;
         if (selection.beg < chrom.beg) {
             selection.beg = chrom.beg;
             selection.end = chrom.beg + selection.width;
         }
         if (selection.end > chrom.end) {
             selection.end = chrom.end;
             selection.beg = chrom.end - selection.width;
             if (selection.beg < chrom.beg) { // spans whole chrom
                 selection.width = (selection.end - chrom.beg);
                 selection.beg = chrom.beg + 1;
             }
         }
         return selection;
     }
 
     function hiliteShow(down,cur)
     {   // Called on mousemove, mouseup: set drag hilite dimensions
         var topY = img.top;
         var high = img.height;
         var begX = -1;
         var wide = -1;
         if (cur < down) {
             begX = cur + img.left;
             wide = (down - cur);
         } else {
             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 (theClient.isIePre11()) {
             img.height = $(chrImg).outerHeight();
             img.width  = $(chrImg).outerWidth();
         } else {
             img.height = $(chrImg).height();
             img.width  = $(chrImg).width();
         }
         return img;
     }
 });
 };
 
 
 
   //////////////////////////
  //// Drag Scroll code ////
 //////////////////////////
 jQuery.fn.panImages = function(){
     // globals across all panImages
     genomePos.original = genomePos.getOriginalPos(); // redundant but makes certain original is set.
     var leftLimit   = hgTracks.imgBoxLeftLabel * -1;
     var rightLimit  = (hgTracks.imgBoxPortalWidth - hgTracks.imgBoxWidth + leftLimit);
     var only1xScrolling = ((hgTracks.imgBoxWidth - hgTracks.imgBoxPortalWidth) === 0);
     var prevX       = (hgTracks.imgBoxPortalOffsetX + hgTracks.imgBoxLeftLabel) * -1;
     var portalWidth = 0;
     var portalAbsoluteX = 0;
     var savedPosition;
     var highlightAreas  = 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
     }
 
     if (!pan || !pic) {
         throw "Not a div with child image! 'panImages' can only be used with divs contain images.";
     }
 
     // globals across all panImages
     portalWidth     = $(pan).width();
     portalAbsoluteX = $(pan).offset().left;
     // globals to one panImage
     var newX        = 0;
     var mouseDownX  = 0;
     var mouseIsDown = false;
     var beyondImage = false;
     var atEdge      = false;
 
     initialize();
 
     function initialize(){
 
         $(pan).parents('td.tdData').mousemove(function(e) {
             if (e.shiftKey)
                 $(this).css('cursor',"crosshair");  // shift-dragZoom
             else if ( theClient.isIePre11() )     // IE will override map item cursors if this gets set
                 $(this).css('cursor',"");  // normal pointer when not over clickable item
         });
 
         panAdjustHeight(prevX);
 
         pan.mousedown(function(e){
              if (e.which > 1 || e.button > 1 || e.shiftKey || e.metaKey || e.altKey || e.ctrlKey)
                  return true;
             if (mouseIsDown === false) {
                 if (rightClick.menu) {
                     rightClick.menu.hide();
                 }
                 mouseIsDown = true;
                 mouseDownX = e.clientX;
                 highlightAreas = $('.highlightItem');
                 atEdge = (!beyondImage && (prevX >= leftLimit || prevX <= rightLimit));
                 $(document).bind('mousemove',panner);
                 $(document).bind( 'mouseup', panMouseUp);  // Will exec only once
                 return false;
             }
         });
     }
 
     function panner(e) {
         //if (!e) e = window.event;
         if ( mouseIsDown ) {
             var relativeX = (e.clientX - mouseDownX);
 
             if (relativeX !== 0) {
                 if (posting.mapClicksAllowed()) {
                     // need to throw up a z-index div.  Wait mask?
                     savedPosition = genomePos.get();
                     dragMaskShow();
                     posting.blockMapClicks();
                 }
                 var decelerator = 1;
                 //var wingSize    = 1000; // 0 stops the scroll at the edges.
                 // Remeber that offsetX (prevX) is negative
                 newX = prevX + relativeX;
                 if ( newX >= leftLimit ) { // scrolled all the way to the left
                     if (atEdge) {  // Do not drag straight off edge.  Force second drag
                         beyondImage = true;
                         newX = leftLimit + (newX - leftLimit)/decelerator;// slower
                         //if (newX >= leftLimit + wingSize) // Don't go too far over the edge!
                         //    newX =  leftLimit + wingSize;
                     } else
                         newX = leftLimit;
 
                 } else if ( newX < rightLimit ) { // scrolled all the way to the right
                     if (atEdge) {  // Do not drag straight off edge.  Force second drag
                         beyondImage = true;
                         newX = rightLimit - (rightLimit - newX)/decelerator;// slower
                         //if (newX < rightLimit - wingSize) // Don't go too far over the edge!
                         //    newX = rightLimit - wingSize;
                     } else
                         newX = rightLimit;
 
                 } else if (newX >= rightLimit && newX < leftLimit)
                     beyondImage = false; // could have scrolled back without mouse up
 
                 posStatus = panUpdatePosition(newX,true);
                 newX = posStatus.newX;
                 // do not update highlights if we are at the end of a chromsome
                 if (!posStatus.isOutsideChrom)
                     scrollHighlight(relativeX);
 
                 var nowPos = newX.toString() + "px";
                 $(".panImg").css( {'left': nowPos });
                 $('.tdData').css( {'backgroundPosition': nowPos } );
                 if (!only1xScrolling)
                     panAdjustHeight(newX);  // Will dynamically resize image while scrolling.
             }
         }
     }
     function panMouseUp(e) {  // Must be a separate function instead of pan.mouseup event.
         //if (!e) e = window.event;
         if (mouseIsDown) {
 
             dragMaskClear();
             $(document).unbind('mousemove',panner);
             $(document).unbind('mouseup',panMouseUp);
             mouseIsDown = false;
             // timeout incase the dragSelect.selectEnd was over a map item. select takes precedence.
             setTimeout(posting.allowMapClicks,50); 
 
             // Outside image?  Then abandon.
             var curY = e.pageY;
             var imgTbl = $('#imgTbl');
             var north = $(imgTbl).offset().top;
             var south = north + $(imgTbl).height();
             if (curY < north || curY > south) {
                 atEdge = false;
                 beyondImage = false;
                 if (savedPosition)
                     genomePos.set(savedPosition);
                 var oldPos = prevX.toString() + "px";
                 $(".panImg").css( {'left': oldPos });
                 $('.tdData').css( {'backgroundPosition': oldPos } );
                 if (highlightAreas)
                     imageV2.drawHighlights();
                 return true;
             }
 
             // Do we need to fetch anything?
             if (beyondImage) {
                 if (imageV2.inPlaceUpdate) {
                     var pos = parsePosition(genomePos.get());
                     imageV2.navigateInPlace("position=" +
                             encodeURIComponent(pos.chrom + ":" + pos.start + "-" + pos.end),
                             null, true);
                 } else {
                     document.TrackHeaderForm.submit();
                 }
                 return true; // Make sure the setTimeout below is not called.
             }
 
             // Just a normal scroll within a >1X image
             if (prevX !== newX) {
                 prevX = newX;
                 if (!only1xScrolling) {
                     //panAdjustHeight(newX); // Will resize image AFTER scrolling.
                     // Important, since AJAX could lead to reinit after this within bounds scroll
                     hgTracks.imgBoxPortalOffsetX = (prevX * -1) - hgTracks.imgBoxLeftLabel;
                     hgTracks.imgBoxPortalLeft = newX.toString() + "px";
                 }
             }
         }
     }
     });  // end of this.each(function(){
 
     function panUpdatePosition(newOffsetX,bounded)
     {
         // Updates the 'position/search" display with change due to panning
         var closedPortalStart = hgTracks.imgBoxPortalStart + 1;   // Correction for half open
         var portalWidthBases = hgTracks.imgBoxPortalEnd - closedPortalStart;
         var portalScrolledX  = hgTracks.imgBoxPortalOffsetX+hgTracks.imgBoxLeftLabel + newOffsetX;
         var recalculate = false;
 
         var newPortalStart = 0;
         if (hgTracks.revCmplDisp)
             newPortalStart = closedPortalStart +     // As offset goes down, so do bases seen.
                                 Math.round(portalScrolledX*hgTracks.imgBoxBasesPerPixel);
         else
             newPortalStart = closedPortalStart -     // As offset goes down, bases seen goes up!
                                 Math.round(portalScrolledX*hgTracks.imgBoxBasesPerPixel);
         if (newPortalStart < hgTracks.chromStart && bounded) {     // Stay within bounds
             newPortalStart = hgTracks.chromStart;
             recalculate = true;
         }
         var newPortalEnd = newPortalStart + portalWidthBases;
         if (newPortalEnd > hgTracks.chromEnd && bounded) {
             newPortalEnd = hgTracks.chromEnd;
             newPortalStart = newPortalEnd - portalWidthBases;
             recalculate = true;
         }
         if (newPortalStart > 0) {
             var newPos = hgTracks.chromName + ":" + newPortalStart + "-" + newPortalEnd;
             genomePos.set(newPos); // no need to change the size
         }
         if (recalculate && hgTracks.imgBoxBasesPerPixel > 0) { 
             // Need to recalculate X for bounding drag
             portalScrolledX = (closedPortalStart - newPortalStart) / hgTracks.imgBoxBasesPerPixel;
             newOffsetX = portalScrolledX - (hgTracks.imgBoxPortalOffsetX+hgTracks.imgBoxLeftLabel);
         }
 
         ret = {};
         ret.newX = newOffsetX;
         ret.isOutsideChrom = recalculate;
         return ret;
     }
     function mapTopAndBottom(mapName,east,west)
     {
     // Find the top and bottom px given left and right boundaries
         var mapPortal = { top: -10, bottom: -10 };
         var items = $("map[name='"+mapName+"']").children();
         if ($(items).length>0) {
             $(items).each(function(t) {
                 var loc = this.coords.split(",");
                 var aleft   = parseInt(loc[0]);
                 var aright  = parseInt(loc[2]);
                 if (aleft < west && aright >= east) {
                     var atop    = parseInt(loc[1]);
                     var abottom = parseInt(loc[3]);
                     if (mapPortal.top    < 0 ) {
                         mapPortal.top    = atop;
                         mapPortal.bottom = abottom;
                     } else if (mapPortal.top > atop) {
                             mapPortal.top = atop;
                     } else if (mapPortal.bottom < abottom) {
                             mapPortal.bottom = abottom;
                     }
                 }
             });
         }
         return mapPortal;
     }
     function panAdjustHeight(newOffsetX) {
         // Adjust the height of the track data images so that bed items scrolled off screen
         // do not waste vertical real estate
 
         // Is the > 1x?
         if (only1xScrolling)
             return;
 
         var east = newOffsetX * -1;
         var west = east + portalWidth;
         $(".panImg").each(function(t) {
             var mapid  = this.id.replace('img_','map_');
             var hDiv   = $(this).parent();
             var north  = parseInt($(this).css("top")) * -1;
             var south  = north + $(hDiv).height();
 
             var mapPortal = mapTopAndBottom(mapid,east,west);
             if (mapPortal.top > 0) {
                 var topdif = Math.abs(mapPortal.top - north);
                 var botdif = Math.abs(mapPortal.bottom - south);
                 if (topdif > 2 || botdif > 2) {
                     $(hDiv).height( mapPortal.bottom - mapPortal.top );
                     north = mapPortal.top * -1;
                     $(this).css( {'top': north.toString() + "px" });
 
                     // Need to adjust side label height as well!
                     var imgId = this.id.split("_");
                     var titlePx = 0;
                     var center = $("#img_center_"+imgId[2]);
                     if (center.length > 0) {
                         titlePx = $(center).parent().height();
                         north += titlePx;
                     }
                     var side = $("#img_side_"+imgId[2]);
                     if (side.length > 0) {
                         $(side).parent().height( mapPortal.bottom - mapPortal.top + titlePx);
                         $(side).css( {'top': north.toString() + "px" });
                     }
                     var btn = $("#p_btn_"+imgId[2]);
                     if (btn.length > 0) {
                         $(btn).height( mapPortal.bottom - mapPortal.top + titlePx);
                     } else {
                         btn = $("#img_btn_"+imgId[2]);
                         if (btn.length > 0) {
                             $(btn).parent().height( mapPortal.bottom - mapPortal.top + titlePx);
                             $(btn).css( {'top': top.toString() + "px" });
                         }
                     }
                 }
             }
         });
         dragMaskResize();  // Resizes the dragMask to match current image size
     }
 
     function dragMaskShow() 
     {   // Sets up the dragMask to show grabbing cursor within image
         // and not allowed north and south of image
         var imgTbl = $('#imgTbl');
         // Find or create the waitMask (which masks the whole page)
         var dragMask = normed($('div#dragMask'));
         if (!dragMask) {
             $("body").prepend("<div id='dragMask' class='waitMask'></div>");
             dragMask = $('div#dragMask');
         }
 
         $('body').css('cursor','not-allowed');
         $(dragMask).css('cursor',"url(../images/grabbing.cur),w-resize");
         $(dragMask).css({opacity:0.0,display:'block',
                         top: $(imgTbl).position().top.toString() + 'px',
                         height: $(imgTbl).height().toString() + 'px' });
     }
 
     function dragMaskResize() 
     {   // Resizes dragMask (called when image is dynamically resized in >1x scrolling)
         var imgTbl = $('#imgTbl');
         // Find or create the waitMask (which masks the whole page)
         var dragMask = normed($('div#dragMask'));
         if (dragMask) {
             $(dragMask).height( $(imgTbl).height() );
         }
     }
 
     function dragMaskClear() {        // Clears the dragMask
         $('body').css('cursor','auto');
         var  dragMask = normed($('#dragMask'));
         if (dragMask)
             $(dragMask).hide();
     }
 
     function scrollHighlight(relativeX) 
     // Scrolls the highlight region if one exists
     {        
         if (highlightAreas) {
             for (i=0; i<highlightAreas.length; i++) {
                 highlightArea = highlightAreas[i];
                 // Best to have a left and right, then min/max the edges, then set width
                 var hiOffset = $(highlightArea).offset();
                 var hiDefinedLeft  = $(highlightArea).data('leftPixels');
                 var hiDefinedWidth = $(highlightArea).data('widthPixels');
                 hiOffset.left = Math.max(hiDefinedLeft + relativeX,
                                          portalAbsoluteX);
                 var right     = Math.min(hiDefinedLeft + hiDefinedWidth + relativeX,
                                          portalAbsoluteX + portalWidth);
                 var newWidth = Math.max(right - hiOffset.left,0);
                 if (hiDefinedWidth !== newWidth)
                     $(highlightArea).width(newWidth);
                 $(highlightArea).offset(hiOffset);
             }
         }
     }
 
 };
 
   ///////////////////////////////////////
  //// rightClick (aka context menu) ////
 ///////////////////////////////////////
 var rightClick = {
 
     menu: null,
     selectedMenuItem: null,   // currently choosen context menu item (via context menu).
     floatingMenuItem: null,
     currentMapItem:   null,
     supportZoomCodon: true,  // add zoom to exon and zoom to codon to right click menu
     clickedHighlightIdx : null,  // the index (0,1,...) of the highlight item that overlaps the last right-click
 
     makeMapItem: function (id)
     {   // Create a dummy mapItem on the fly
         // (for objects that don't have corresponding entry in the map).
         if (id && id.length > 0 && hgTracks.trackDb) {
             var title;
             var rec = hgTracks.trackDb[id];
             if (rec) {
                 title = rec.shortLabel;
             } else {
                 title = id;
             }
             return {id: id, title: "configure " + title};
         } else {
             return null;
         }
     },
 
     findMapItem: function (e)
     {   // Find mapItem for given event; returns item object or null if none found.
 
         if (rightClick.currentMapItem) {
             return rightClick.currentMapItem;
         }
 
         // rightClick for non-map items that can be resolved to their parent tr and
         // then trackName (e.g. items in gray bar)
         var tr = $( e.target ).parents('tr.imgOrd');
         if ($(tr).length === 1) {
             var a = /tr_(.*)/.exec($(tr).attr('id'));  // voodoo
             if (a && a[1]) {
                 var id = a[1];
                 return rightClick.makeMapItem(id);
             }
         }
         return null;
     },
 
     windowOpenFailedMsg: function ()
     {
         warn("Your web browser prevented us from opening a new window.\n\n" +
              "Please change your browser settings to allow pop-up windows from " +
              document.domain + ".");
     },
 
     handleZoomCodon: function (response, status)
     {
         var json = JSON.parse(response);
         if (json.pos) {
             imageV2.navigateInPlace("position="+json.pos);
         } else {
             alert(json.error);
         }
     },
 
     handleViewImg: function (response, status)
     {   // handles view image response, which must get new image without imageV2 gimmickery
         jQuery('body').css('cursor', '');
         var str = "<IMG[^>]*SRC='([^']+)'";
         var reg = new RegExp(str);
         var a = reg.exec(response);
         if (a && a[1]) {
             if ( ! window.open(a[1]) ) {
                 rightClick.windowOpenFailedMsg();
             }
             return;
         }
         warn("Couldn't parse out img src");
     },
 
     myPrompt: function (msg, callback)
     {   // replacement for prompt; avoids misleading/confusing security warnings which are caused
         // by prompt in IE 7+.   Callback is called if user presses "OK".
         $("body").append("<div id = 'myPrompt'><div id='dialog' title='Basic dialog'><form>" +
                             msg + "<input id='myPromptText' value=''></form>");
         $("#myPrompt").dialog({
                                 modal: true,
                                 closeOnEscape: true,
                                 buttons: { "OK": function() {
                                                     var myPromptText = $("#myPromptText").val();
                                                     $(this).dialog("close");
                                                     callback(myPromptText);
                                                 }
                                         }
                             });
     },
 
     hit: function (menuItemClicked, menuObject, cmd, args)
     {
         setTimeout( function() {
                         rightClick.hitFinish(menuItemClicked, menuObject, cmd, args);
                     }, 1);
     },
 
     hitFinish: function (menuItemClicked, menuObject, cmd, args)
     {   // dispatcher for context menu hits
         var id = rightClick.selectedMenuItem.id;
         var url = null;  // TODO: Break this giant routine with shared vars into some sub-functions
         var href = null;
         var rec = null;
         var row = null;
         var rows = null;
         var selectUpdated = null;
                 function mySuccess() {}
         if (menuObject.shown) {
             // warn("Spinning: menu is still shown");
             setTimeout(function() { rightClick.hitFinish(menuItemClicked, menuObject, cmd); }, 10);
             return;
         }
         if (cmd === 'selectWholeGene' || cmd === 'getDna' || cmd === 'highlightItem') {
                 // bring whole gene into view or redirect to DNA screen.
                 href = rightClick.selectedMenuItem.href;
                 var chrom, chromStart, chromEnd;
                 // Many links leave out the chrom (b/c it's in the server side cart as "c")
                 // var chrom = hgTracks.chromName; // This is no longer acceptable
                 // with multi-window capability drawing multiple positions on multiple chroms.
                 var a = /hgg_chrom=(\w+)&/.exec(href);
                 if (a) {
                     if (a && a[1])
                         chrom = a[1];
                     a = /hgg_start=(\d+)/.exec(href);
                     if (a && a[1])
                         chromStart = parseInt(a[1]) + 1;
                     a = /hgg_end=(\d+)/.exec(href);
                     if (a && a[1])
                         chromEnd = parseInt(a[1]);
                 } else {
                     // a = /hgc.*\W+c=(\w+)/.exec(href);
                     a = /hgc.*\W+c=(\w+)/.exec(href);
                     if (a && a[1])
                         chrom = a[1];
                     a = /o=(\d+)/.exec(href);
                     if (a && a[1])
                         chromStart = parseInt(a[1]) + 1;
                     a = /t=(\d+)/.exec(href);
                     if (a && a[1])
                         chromEnd = parseInt(a[1]);
                 }
                 if (!chrom || chrom.length === 0 || !chromStart || !chromEnd) {// 1-based chromStart
                     warn("couldn't parse out genomic coordinates");
                 } else {
                     if (cmd === 'getDna') {
                         // NOTE: this should be shared with URL generation for getDna blue bar menu
                         url = "../cgi-bin/hgc?g=getDna&i=mixed&c=" + chrom;
                         url += "&l=" + (chromStart - 1) + "&r=" + chromEnd;
                         url += "&db=" + getDb() + "&hgsid=" + getHgsid();
                         if ( ! window.open(url) ) {
                             rightClick.windowOpenFailedMsg();
                         }
                     } else if (cmd === 'highlightItem') {
                         if (hgTracks.windows && !hgTracks.virtualSingleChrom) {
                             // orig way only worked if the entire item was visible in the windows.
                             //var result = genomePos.chromToVirtChrom(chrom, parseInt(chromStart-1), parseInt(chromEnd));
 
                             var result = genomePos.convertChromPosToVirtCoords(chrom, parseInt(chromStart-1), parseInt(chromEnd));
 
                             if (result.chromStart != -1)
                                 {
                                 var newPos2 = hgTracks.chromName+":"+(result.chromStart+1)+"-"+result.chromEnd;
                                 dragSelect.highlightThisRegion(newPos2, true);
                                 }
 
                         } else {
                             var newChrom = hgTracks.chromName;
                             if (hgTracks.windows && hgTracks.virtualSingleChrom) {
                                 newChrom = hgTracks.windows[0].chromName;
                             }
                             var newPos3 = newChrom+":"+(parseInt(chromStart))+"-"+parseInt(chromEnd);
                             dragSelect.highlightThisRegion(newPos3, true);
                         }
                     } else {
                         var newPosition = genomePos.setByCoordinates(chrom, chromStart, chromEnd);
                         var reg = new RegExp("hgg_gene=([^&]+)");
                         var b = reg.exec(href);
                         var name;
                         // pull item name out of the url so we can set hgFind.matches (redmine 3062)
                         if (b && b[1]) {
                             name = b[1];
                         } else {
                             reg = new RegExp("[&?]i=([^&]+)");
                             b = reg.exec(href);
                             if (b && b[1]) {
                                 name = b[1];
                             }
                         }
                         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: 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
                                 ele = document.TrackHeaderForm;
                             if (name)
                                 // Add or update form input with gene to highlight
                                 suggestBox.updateFindMatches(name);
                             ele.submit();
                         }
                     }
                 }
         } 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.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') {
 
             // Launches the popup but shields the ajax with a waitOnFunction
             popUp.hgTrackUi( rightClick.selectedMenuItem.id, false );  
 
         } else if (cmd === 'hgTrackUi_follow') {
 
             url = "hgTrackUi?hgsid=" + getHgsid() + "&g=";
             rec = hgTracks.trackDb[id];
             if (tdbHasParent(rec) && tdbIsLeaf(rec))
                 url += rec.parentTrack;
             else {
                 // The button already has the ref
                 var link = normed($( 'td#td_btn_'+ rightClick.selectedMenuItem.id ).children('a')); 
                 if (link)
                     url = $(link).attr('href');
                 else
                     url += rightClick.selectedMenuItem.id;
             }
             location.assign(url);
 
         } else if (cmd === 'newCollection') {
             $.ajax({
                 type: "PUT",
                 async: false,
                 url: "../cgi-bin/hgCollection",
                 data:  "cmd=newCollection&track=" + id + "&hgsid=" + getHgsid(),
                 trueSuccess: mySuccess,
                 success: catchErrorOrDispatch,
                 error: errorHandler,
             });
 
             imageV2.fullReload();
         } else if (cmd === 'addCollection') {
             var shortLabel = $(menuItemClicked).text().substring(9).slice(0,-1); 
             var ii;
             var collectionName;
             for(ii=0; ii < hgTracks.collections.length; ii++) {
                 if ( hgTracks.collections[ii].shortLabel === shortLabel) {
                     collectionName = hgTracks.collections[ii].track;
                     break;
                 }
             }
 
             $.ajax({
                 type: "PUT",
                 async: false,
                 url: "../cgi-bin/hgCollection",
                 data: "cmd=addTrack&track=" + id + "&collection=" + collectionName + "&hgsid=" + getHgsid(),
                 trueSuccess: mySuccess,
                 success: catchErrorOrDispatch,
                 error: errorHandler,
             });
 
             imageV2.fullReload();
         } else if ((cmd === 'sortExp') || (cmd === 'sortSim')) {
             url = "hgTracks?hgsid=" + getHgsid() + "&" + cmd + "=";
             rec = hgTracks.trackDb[id];
             if (tdbHasParent(rec) && tdbIsLeaf(rec))
                 url += rec.parentTrack;
             else {
                 // The button already has the ref
                 var link2 = normed($( 'td#td_btn_'+ rightClick.selectedMenuItem.id ).children('a')); 
                 if (link2)
                     url = $(link2).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).
             jQuery('body').css('cursor', 'wait');
             $.ajax({
                     type: "GET",
                     url: "../cgi-bin/hgTracks",
                     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') {
             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];
                 var val;
                 if (v === "db") {
                     val = getDb();
                 } else {
                     val = hgTracks[valNames[i]];
                 }
                 if (val
                 && id !== "wikiTrack"
                 && (href.indexOf("?" + v + "=") === -1)
                 && (href.indexOf("&" + v + "=") === -1)) {
                     href = href + "&" + v + "=" + val;
                 }
             }
             if (cmd === 'followLink') {
                 // XXXX This is blocked by Safari's popup blocker (without any warning message).
                 location.assign(href);
             } else {
                 // Remove hgsid to force a new session (see redmine ticket 1333).
                 href = removeHgsid(href);
                 if ( ! window.open(href) ) {
                     rightClick.windowOpenFailedMsg();
                 }
             }
         } else if (cmd === 'float') {
             if (rightClick.floatingMenuItem && rightClick.floatingMenuItem === id) {
                 $.floatMgr.FOArray = [];
                 rightClick.floatingMenuItem = null;
             } else {
                 if (rightClick.floatingMenuItem) {
                     // This doesn't work.
                     $('#img_data_' + rightClick.floatingMenuItem).parent().restartFloat();
                     // This does work
                     $.floatMgr.FOArray = [];
                 }
                 rightClick.floatingMenuItem = id;
                 rightClick.reloadFloatingItem();
                 imageV2.requestImgUpdate(id, "hgt.transparentImage=0", "");
             }
         } else if (cmd === 'hideSet') {
             row = $( 'tr#tr_' + id );
             rows = dragReorder.getContiguousRowSet(row);
             if (rows && rows.length > 0) {
                 var varsToUpdate = {};
                 // from bottom up, just in case remove screws with us
                 for (var ix=rows.length - 1; ix >= 0; ix--) { 
                     var rowId = $(rows[ix]).attr('id').substring('tr_'.length);
                     // Remove subtrack level vis and explicitly uncheck.
                     varsToUpdate[rowId]        = '[]';
                     varsToUpdate[rowId+'_sel'] = 0;
                     $(rows[ix]).remove();
                 }
                 if (objNotEmpty(varsToUpdate)) {
                     cart.setVarsObj(varsToUpdate);
                 }
                 imageV2.afterImgChange(true);
             }
         } else if (cmd === 'hideComposite') {
             rec = hgTracks.trackDb[id];
             if (tdbIsSubtrack(rec)) {
                 row = $( 'tr#tr_' + id );
                 rows = dragReorder.getCompositeSet(row);
                 // from bottom up, just in case remove screws with us
                 if (rows && rows.length > 0) {
                     for (var rIx=rows.length - 1; rIx >= 0; rIx--) {
                         $(rows[rIx]).remove();
                     }
                 selectUpdated = vis.update(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 && rightClick.clickedHighlightIdx!==null) {
                 var newPos = getHighlight(hgTracks.highlight, rightClick.clickedHighlightIdx);
                 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') {
 
             var highlights = hgTracks.highlight.split("|");
             highlights.splice(rightClick.clickedHighlightIdx, 1); // splice = remove element from array
             hgTracks.highlight = highlights.join("|");
             cart.setVarsObj({'highlight' : hgTracks.highlight});
             imageV2.drawHighlights();
         } else if (cmd === 'toggleMerge') {
             // toggle both the cart (if the user goes to trackUi)
             // and toggle args[key], if the user doesn't leave hgTracks
             var key = id + ".doMergeItems";
             var updateObj = {};
             if (args[key] === 1) {
                 args[key] = 0;
                 updateObj[key] = 0;
                 cart.setVarsObj(updateObj,null,false);
                 imageV2.requestImgUpdate(id, id + ".doMergeItems=0");
             } else {
                 args[key] = 1;
                 updateObj[key] = 1;
                 cart.setVarsObj(updateObj,null,false);
                 imageV2.requestImgUpdate(id, id + ".doMergeItems=1");
             }
         } else {   // if ( cmd in 'hide','dense','squish','pack','full','show' )
             // Change visibility settings:
             //
             // First change the select on our form:
             rec = hgTracks.trackDb[id];
             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.
                 if (tdbIsSubtrack(rec)) {
                     // Remove subtrack level vis and explicitly uncheck.
                     cart.setVars( [ id, id+"_sel" ], [ '[]', 0 ] ); 
                 } else if (tdbIsFolderContent(rec)) {
                     // supertrack children need to have _sel set to trigger superttrack reshaping
                     cart.setVars( [ id, id+"_sel" ], [ 'hide', 0 ] ); 
                 } else {
                     cart.setVars([id], ['hide']);  // Others, just set vis 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 vis update to queue then submit
                         cart.setVars([id], [cmd], null, false); // synchronous
                         document.TrackHeaderForm.submit();
                 }
             } else {
                 imageV2.requestImgUpdate(id, id + "=" + cmd, "", cmd);
             }
         }
     },
 
     makeHitCallback: function (title)
     {   // stub to avoid problem with a function closure w/n a loop
         return function(menuItemClicked, menuObject) {
             rightClick.hit(menuItemClicked, menuObject, title); return true;
         };
     },
 
     reloadFloatingItem: function ()
     {   // currently dead (experimental code)
         if (rightClick.floatingMenuItem) {
             $('#img_data_' + rightClick.floatingMenuItem).parent().makeFloat(
                 {x:"current",y:"current", speed: 'fast', alwaysVisible: true, alwaysTop: true});
         }
     },
 
     makeImgTag: function (img)
     {   // Return img tag with explicit dimensions for img (dimensions are currently hardwired).
         // This fixes the "weird shadow problem when first loading the right-click menu"
         // seen in FireFox 3.X, which occurred b/c FF doesn't actually fetch the image until
         // the menu is being shown.
         return "<img style='width:16px; height:16px; border-style:none;' src='../images/" +
                 img + "' />";
     },
 
 
     load: function (img)
     {
         rightClick.menu = img.contextMenu(function() {
             popUp.cleanup();   // Popup box is not getting closed properly so must do it here
             if ( ! rightClick.selectedMenuItem )  // This is literally an edge case so ignore
                 return;
 
             var o; // TODO: Break this giant routine with shared vars into some sub-functions
             var str;
             var rec = null;
             var menu = [];
             var selectedImg = rightClick.makeImgTag("greenChecksm.png");
             var blankImg    = rightClick.makeImgTag("invisible16.png");
             var done = false;
             if (rightClick.selectedMenuItem && rightClick.selectedMenuItem.id) {
                 var href = rightClick.selectedMenuItem.href;
                 var isHgc, isGene;
                 if (href) {
                     isGene = href.match("hgGene");
                     isHgc = href.match("hgc");
                 }
                 var id = rightClick.selectedMenuItem.id;
                 rec = hgTracks.trackDb[id];
                 var offerHideSubset    = false;
                 var offerHideComposite = false;
                 var offerSingles       = true;
                 var row = $( 'tr#tr_' + id );
                 if (row) {
                     var btn = $(row).find('p.btnBlue'); // btnBlue means cursor over left button
                     if (btn.length === 1) {
                         var compositeSet = dragReorder.getCompositeSet(row);
                         if (compositeSet && compositeSet.length > 0) {  // There is composite set
                             offerHideComposite = true;
                             $( compositeSet ).find('p.btn').addClass('blueButtons');// blue persists
 
                             var subSet = dragReorder.getContiguousRowSet(row);
                             if (subSet && subSet.length > 1) {
                                 offerSingles = false;
                                 if (subSet.length < compositeSet.length) {
                                     offerHideSubset = true;
                                     $( subSet ).addClass("greenRows"); // green persists
                                 }
                             }
                         }
                     }
                 }
 
                 // First option is hide sets
                 if (offerHideComposite) {
                     if (offerHideSubset) {
                         o = {};
                         o[blankImg + " hide track subset (green)"] = {
                                             onclick: rightClick.makeHitCallback('hideSet')};
                         menu.push(o);
                     }
 
                     o = {};
                     str = blankImg + " hide track set";
                     if (offerHideSubset)
                         str += " (blue)";
                     o[str] = {onclick: rightClick.makeHitCallback('hideComposite')};
                     menu.push(o);
                 }
 
                 // Second set of options: visibility for single track
                 if (offerSingles) {
                     if (offerHideComposite)
                         menu.push($.contextMenu.separator);
 
                     // XXXX what if select is not available (b/c trackControlsOnMain is off)?
                     // Move functionality to a hidden variable?
                     var select = $("select[name=" + escapeJQuerySelectorChars(id) + "]");
                     if (select.length > 1)  
                         // Not really needed if $('#hgTrackUiDialog').html(""); has worked
                         select =  [ $(select)[0] ];
                     var cur = $(select).val();
                     if (cur) {
                         $(select).children().each(function(index, o) {
                             var title = $(this).val();
                             str = blankImg + " " + title;
                             if (title === cur)
                                 str = selectedImg + " " + title;
                             o = {};
                             o[str] = {onclick: function (menuItemClicked, menuObject) {
                                         rightClick.hit(menuItemClicked, menuObject, title);
                                         return true;}};
                             menu.push(o);
                         });
                         done = true;
                     } else {
                         if (rec) {
                             // XXXX check current state from a hidden variable.
                             var visStrings = new Array("hide","dense","squish","pack","full");
                             for (var i in visStrings) {
                                 // use maxVisibility and change hgTracks so it can hide subtracks
                                 o = {};
                                 str = blankImg + " " + visStrings[i];
                                 if (rec.canPack
                                 || (visStrings[i] !== "pack" && visStrings[i] !== "squish")) {
                                     if (rec.localVisibility) {
                                         if (visStrings[i] === rec.localVisibility) {
                                             str = selectedImg + " " + visStrings[i];
                                         }
                                     } else if (visStrings[i] === vis.enumOrder[rec.visibility]) {
                                         str = selectedImg + " " + visStrings[i];
                                     }
                                     o[str] = { onclick:
                                                 rightClick.makeHitCallback(visStrings[i])
                                              };
                                     menu.push(o);
                                 }
                             }
                             done = true;
                         }
                     }
                 }
 
                 if (done) {
                     o = {};
                     var any = false;
                     var title = rightClick.selectedMenuItem.title || "feature";
                     var exonNum = 0;
                     var maxLength = 60;
                     if (title.length > maxLength) {
                         title = title.substring(0, maxLength) + "...";
                     }
 
                     if ((isGene || isHgc || id === "wikiTrack") && href.indexOf("i=mergedItem") === -1) {
                         // Add "Open details..." item
                         var displayItemFunctions = false;
                         if (rec) {
                             if (rec.type.indexOf("wig") === 0
                             ||  rec.type.indexOf("bigWig") === 0
                             ||  id === "wikiTrack") {
                                 displayItemFunctions = false;
                             } else if (rec.type.indexOf("expRatio") === 0) {
                                 displayItemFunctions = title !== "zoomInMore";
                             } else {
                                 displayItemFunctions = true;
                             }
                             // For barChart mouseovers, replace title (which may be a category 
                             // name+value) with item name
                             if (rec.type.indexOf("barChart") === 0
                             || rec.type.indexOf("bigBarChart") === 0) {
                                 a = /i=([^&]+)/.exec(href);
                                 if (a && a[1]) {
                                     title = a[1];
                                 }
                             }
                         }
 
                         // when "exonNumbers on", the mouse over text is not a good item description for the right-click menu
                         // "exonNumbers on" is the default for genePred/bigGenePred tracks but can also be actived for bigBed and others
                         // We don't have the value of "exonNumbers" here, so just use a heuristic to see if it's on
                         if (title.search(/, strand [+-], (Intron|Exon) /)!==-1) {
                             re = /(Exon) ([1-9]+) of/;
                             matches = re.exec(title);
                             if (matches !== null && matches[2].length > 0)
                                 exonNum = matches[2];
                             title = title.split(",")[0];
                         }
 
                         else if (isHgc && ( href.indexOf('g=gtexGene')!== -1 
                                             || href.indexOf('g=unip') !== -1 
                                             || href.indexOf('g=knownGene') !== -1 )) {
                             // For GTEx gene and UniProt mouseovers, replace title (which may be a tissue name) with 
                             // item (gene) name. Also need to unescape the urlencoded characters and the + sign.
                             a = /i=([^&]+)/.exec(href);
                             if (a && a[1]) {
                                 title = decodeURIComponent(a[1].replace(/\+/g, " "));
                             }
                         }
 
                         if (displayItemFunctions) {
                             o[rightClick.makeImgTag("magnify.png") + " Zoom to " +  title] = {
                                 onclick: function(menuItemClicked, menuObject) {
                                             rightClick.hit(menuItemClicked, menuObject,
                                                     "selectWholeGene"); return true;
                                           }
                                 };
                             o[rightClick.makeImgTag("highlight.png") + " Highlight " + title] = 
                                 {   onclick: function(menuItemClicked, menuObject) {
                                         rightClick.hit(menuItemClicked, menuObject,
                                                        "highlightItem"); 
                                         return true;
                                     }
                                 };
                             if (rightClick.supportZoomCodon && rec.type.indexOf("genePred") !== -1) {
                                 // http://hgwdev-larrym.gi.ucsc.edu/cgi-bin/hgGene?hgg_gene=uc003tqk.2&hgg_prot=P00533&hgg_chrom=chr7&hgg_start=55086724&hgg_end=55275030&hgg_type=knownGene&db=hg19&c=chr7
                                 var name, table;
                                 var reg = new RegExp("hgg_gene=([^&]+)");
                                 var a = reg.exec(href);
                                 if (a && a[1]) {
                                     name = a[1];
                                     reg = new RegExp("hgg_type=([^&]+)");
                                     a = reg.exec(href);
                                     if (a && a[1]) {
                                         table = a[1];
                                     }
                                 } else {
                                     // http://hgwdev-larrym.gi.ucsc.edu/cgi-bin/hgc?o=55086724&t=55275031&g=refGene&i=NM_005228&c=chr7
                                     // http://hgwdev-larrym.gi.ucsc.edu/cgi-bin/hgc?o=55086713&t=55270769&g=wgEncodeGencodeManualV4&i=ENST00000455089&c=chr7
                                     reg = new RegExp("i=([^&]+)");
                                     a = reg.exec(href);
                                     if (a && a[1]) {
                                         name = a[1];
                                         reg = new RegExp("g=([^&]+)");
                                         a = reg.exec(href);
                                         if (a && a[1]) {
                                             table = a[1];
                                         }
                                     }
                                 }
                                 if (name && table) {
                                     o[rightClick.makeImgTag("magnify.png")+" Zoom to codon"] =
                                     {   onclick: function(menuItemClicked, menuObject) {
                                             rightClick.hit(menuItemClicked, menuObject,
                                                         "zoomCodon",
                                                         {name: name, table: table});
                                             return true;}
                                     };
                                     if (exonNum > 0) {
                                         o[rightClick.makeImgTag("magnify.png")+" Zoom to exon"] = {
                                             onclick: function(menuItemClicked, menuObject) {
                                                 $.ajax({
                                                         type: "GET",
                                                         url: "../cgi-bin/hgApi",
                                                         data: cart.varsToUrlData({ 'db': getDb(),
                                                                 'cmd': "exonToPos", 'num': exonNum,
                                                                 'table': table, 'name': name}),
                                                         trueSuccess: rightClick.handleZoomCodon,
                                                         success: catchErrorOrDispatch,
                                                         error: errorHandler,
                                                         cache: true
                                                     });
                                                 return true; }
                                         };
                                     }
                                 }
                             }
                             o[rightClick.makeImgTag("dnaIcon.png")+" Get DNA for "+title] = {
                                 onclick: function(menuItemClicked, menuObject) {
                                     rightClick.hit(menuItemClicked, menuObject, "getDna");
                                     return true; }
                             };
                         }
                         o[rightClick.makeImgTag("bookOut.png")+
                                                 " Open details page in new window..."] = {
                             onclick: function(menuItemClicked, menuObject) {
                                 rightClick.hit(menuItemClicked, menuObject, "openLink");
                                 return true; }
                         };
                         any = true;
                     }
                     if (href && href.length  > 0 && href.indexOf("i=mergedItem") === -1) {
                         // Add "Show details..." item
                         if (title.indexOf("Click to alter ") === 0) {
                             // suppress the "Click to alter..." items
                         } else if (rightClick.selectedMenuItem.href.indexOf("cgi-bin/hgTracks")
                                                                                         !== -1) {
                             // suppress menu items for hgTracks links (e.g. Next/Prev map items).
                         } else {
                             var item;
                             if (title === "zoomInMore")
                                 // avoid showing menu item that says
                                 // "Show details for zoomInMore..." (redmine 2447)
                                 item = rightClick.makeImgTag("book.png") + " Show details...";
                             else
                                 item = rightClick.makeImgTag("book.png")+" Show details for "+
                                        title + "...";
                             o[item] = {onclick: function(menuItemClicked, menuObject) {
                                        rightClick.hit(menuItemClicked,menuObject,"followLink");
                                        return true; }
                             };
                             any = true;
                         }
                     }
                     if (any) {
                         menu.push($.contextMenu.separator);
                         menu.push(o);
                     }
                 }
             }
 
             if (rightClick.selectedMenuItem && rec) {
                 // Add cfg options at just shy of end...
                 o = {};
                 if (tdbIsLeaf(rec)) {
 
                     if (rec.configureBy !== 'none'
                     && (!tdbIsCompositeSubtrack(rec) || rec.configureBy !== 'clickThrough')) {
                         // Note that subtracks never do clickThrough because
                         // parentTrack cfg is the desired clickThrough
                         o[rightClick.makeImgTag("wrench.png")+" Configure "+rec.shortLabel] = {
                             onclick: function(menuItemClicked, menuObject) {
                                 rightClick.hit(menuItemClicked, menuObject, "hgTrackUi_popup");
                                 return true; }
                         };
                     }
                     if (rec.parentTrack) {
                         o[rightClick.makeImgTag("folderWrench.png")+" Configure "+
                           rec.parentLabel + " track set..."] = {
                             onclick: function(menuItemClicked, menuObject) {
                                 rightClick.hit(menuItemClicked,menuObject,"hgTrackUi_follow");
                                 return true; }
                           };
                     }
                 } else {
 
                     o[rightClick.makeImgTag("folderWrench.png")+" Configure "+rec.shortLabel +
                       " track set..."] = {
                         onclick: function(menuItemClicked, menuObject) {
                             rightClick.hit(menuItemClicked, menuObject, "hgTrackUi_follow");
                             return true; }
                       };
                 }
                 if (jQuery.floatMgr) {
                     o[(rightClick.selectedMenuItem.id === rightClick.floatingMenuItem ?
                             selectedImg : blankImg) + " float"] = {
                         onclick: function(menuItemClicked, menuObject) {
                             rightClick.hit(menuItemClicked, menuObject, "float");
                             return true; }
                     };
                 }
                 // add a toggle to hide/show the merged item(s)
                 mergeTrack = rightClick.selectedMenuItem.id + ".doMergeItems";
                 if (rec.hasOwnProperty(mergeTrack)) {
                     var hasMergedItems = rec[mergeTrack] === 1;
                     titleStr = rightClick.makeImgTag("wrench.png") + " ";
                     if (hasMergedItems) {
                         titleStr += "Show merged items";
                     } else {
                         titleStr += "Merge items that span the current region";
                     }
                     o[titleStr] = {onclick: function(menuItemClick, menuObject) {
                         rightClick.hit(menuItemClick, menuObject, "toggleMerge", rec);
                         return true; }
                     };
                 }
                 menu.push($.contextMenu.separator);
                 menu.push(o);
             }
 
             menu.push($.contextMenu.separator);
             if (hgTracks.highlight && rightClick.clickedHighlightIdx!==null) {
                 var currentlySeen = ($('#highlightItem').length > 0); 
                 o = {};
                 // Jumps to highlight when not currently seen in image
                 var text = (currentlySeen ? " Zoom" : " Jump") + " to highlight";
                 o[rightClick.makeImgTag("highlightZoom.png") + text] = {
                     onclick: rightClick.makeHitCallback('jumpToHighlight')
                 };
 
                 if ( currentlySeen ) {   // Remove only when seen
                     o[rightClick.makeImgTag("highlightRemove.png") + 
                                                                " Remove highlight"] = {
                         onclick: rightClick.makeHitCallback('removeHighlight')
                     };
                 }
                 menu.push(o);
             }
 
             if (rec.isCustomComposite)
                 {
                 // add delete from composite
                 }
             else if ((!rec.type.startsWith("wigMaf")) &&
                 (rec.type.startsWith("bigWig") || rec.type.startsWith("multiWig") || rec.type.startsWith("wig") || rec.type.startsWith("bedGraph"))) {
                 o = {};
                 o[" Make a New Collection with \"" + rec.shortLabel + "\""] = {
                     onclick: rightClick.makeHitCallback("newCollection")
                 };  
                 menu.push(o);
 
                 if (hgTracks.collections) {
                     var ii;
                     for(ii=0; ii < hgTracks.collections.length; ii++) {
                         o = {};
                         o[" Add to \"" + hgTracks.collections[ii].shortLabel + "\""] = {
                             onclick: rightClick.makeHitCallback("addCollection")
                         };  
                         menu.push(o);
                     }
                 }
                 menu.push($.contextMenu.separator);
             }
 
             // add sort options if this is a custom composite
             if (rec.isCustomComposite && tdbHasParent(rec) && tdbIsLeaf(rec)) {
 
                 o = {};
                 o[" Sort by Magnitude "] = {
                     onclick: function(menuItemClicked, menuObject) {
                         rightClick.hit(menuItemClicked, menuObject, "sortExp");
                         return true; }
                 };  
                 menu.push(o);
 
                 o = {};
                 o[" Sort by Similarity "] = {
                     onclick: function(menuItemClicked, menuObject) {
                         rightClick.hit(menuItemClicked, menuObject, "sortSim");
                         return true; }
                 };  
                 menu.push(o);
 
                 menu.push($.contextMenu.separator);
             }
 
             // Add view image at end
             o = {};
             o[rightClick.makeImgTag("eye.png") + " View image"] = {
                 onclick: function(menuItemClicked, menuObject) {
                     rightClick.hit(menuItemClicked, menuObject, "viewImg");
                     return true; }
             };
             menu.push(o);
 
             return menu;
         },
         {
             beforeShow: function(e) {
                 // console.log(mapItems[rightClick.selectedMenuItem]);
                 rightClick.selectedMenuItem = rightClick.findMapItem(e);
                 
                 // find the highlight that was clicked
                 var imageX = (imageV2.imgTbl[0].getBoundingClientRect().left) + imageV2.LEFTADD;
                 var xDiff = (e.clientX) - imageX;
                 var clickPos = genomePos.pixelsToBases(img, xDiff, xDiff+1, hgTracks.winStart, hgTracks.winEnd, false);
                 rightClick.clickedHighlightIdx = dragSelect.findHighlightIdxForPos(clickPos);
 
                 // XXXX? posting.blockUseMap = true;
                 return true;
             },
             hideTransition:'hide', // hideCallback fails if these are not defined.
             hideSpeed:10,
             hideCallback: function() {
                 $('p.btn.blueButtons').removeClass('blueButtons');
                 $('tr.trDraggable.greenRows').removeClass('greenRows');
             }
         });
         return;
     }
 };
 
   //////////////////////////////////
  //// external tools           ////
 //////////////////////////////////
 
 function showExtToolDialog() {
         /* show the 'send to external tool' dialog */
         // information about external tools is stored in the extTools global list
         // defined by a <script> at the end of the body
 
         // remove an existing dialog box
         var extToolDialog = $("#extToolDialog").remove();
 
         // construct the contents
         var htmlLines = ["<ul class='indent'>"];
         var winSize = hgTracks.winEnd - hgTracks.winStart;
         for (i = 0; i < extTools.length; i++) {
             var tool = extTools[i];
             var toolId = tool[0];
             var shortLabel = tool[1];
             var longLabel = tool[2];
             var maxSize = tool[3];
             if ((maxSize===0) || (winSize < maxSize))
                 {
                 var url = "hgTracks?hgsid="+getHgsid()+"&hgt.redirectTool="+toolId;
                 //var onclick = "$('#extToolDialog').dialog('close');";
                 //htmlLines.push("<li><a onclick="+'"'+onclick+'"'+"id='extToolLink' target='_BLANK' href='"+url+"'>"+shortLabel+"</a>: <small>"+longLabel+"</small></li>");
 		// onclick js code moved to jsInline
                 htmlLines.push("<li><a class='extToolLink2' target='_BLANK' href='"+url+"'>"+shortLabel+"</a>: <small>"+longLabel+"</small></li>");
                 }
             else
                 {
                 note = "<br><b>Needs zoom to &lt; "+maxSize/1000+" kbp.</b></small></span></li>";
                 htmlLines.push('<li><span style="color:grey">'+shortLabel+": <small>"+longLabel+note);
                 }
         }
         htmlLines.push("</ul>");
         content = htmlLines.join("");
             
         var title = hgTracks.chromName + ":" + (hgTracks.winStart+1) + "-" + hgTracks.winEnd;
         if (hgTracks.nonVirtPosition)
             title = hgTracks.nonVirtPosition;
         title += " on another website";
         $("body").append("<div id='extToolDialog' title='"+title+"'><p>" + content + "</p>");
 
 	// GALT 
 	$('a.extToolLink2').click(function(){$('#extToolDialog').dialog('close');});
 
         // copied from the hgTrackUi function below
         var popMaxHeight = ($(window).height() - 40);
         var popMaxWidth  = ($(window).width() - 40);
         var popWidth     = 600;
         if (popWidth > popMaxWidth)
             popWidth = popMaxWidth;
 
         // also copied from the hgTrackUi code below
         $('#extToolDialog').dialog({
             resizable: true,               // Let description scroll vertically
             height: popMaxHeight,
             width: popWidth,
             minHeight: 200,
             minWidth: 600,
             maxHeight: popMaxHeight,
             maxWidth: popMaxWidth,
             modal: true,
             closeOnEscape: true,
             autoOpen: false,
             buttons: { "Close": function() {
                     $(this).dialog("close");
             }},
         });
         
         $('#extToolDialog').dialog('open');
 }
 
   /////////////////////////////////////////////////////////
  //// popupHgt popup for hgTracks  (aka modal dialog) ////
 /////////////////////////////////////////////////////////
 var popUpHgt = {
 
     whichHgTracksMethod: "",
     title: "",
 
     cleanup: function ()
     {  // Clean out the popup box on close
         if ($('#hgTracksDialog').html().length > 0 ) {
             // clear out html after close to prevent problems caused by duplicate html elements
             $('#hgTracksDialog').html("");
             popUpHgt.whichHgTracksMethod = "";
             popUpHgt.title = "";
         }
     },
 
     _uiDialogRequest: function (whichHgTracksMethod)
     { // popup cfg dialog
         popUpHgt.whichHgTracksMethod = whichHgTracksMethod;
         var myLink = "../cgi-bin/hgTracks?hgsid=" + getHgsid() + "&db=" + getDb();
         if (popUpHgt.whichHgTracksMethod === "multi-region config") {
             myLink += "&hgTracksConfigMultiRegionPage=multi-region";
             popUpHgt.title = "Configure Multi-Region View";
         }
 
         $.ajax({
                     type: "GET",
                     url: cart.addUpdatesToUrl(myLink),
                     dataType: "html",
                     trueSuccess: popUpHgt.uiDialog,
                     success: catchErrorOrDispatch,
                     error: errorHandler,
                     cache: false
                 });
     },
 
     hgTracks: function (whichHgTracksMethod)
     {   // Launches the popup but shields the ajax with a waitOnFunction
         waitOnFunction( popUpHgt._uiDialogRequest, whichHgTracksMethod);
     },
 
     uiDialogOk: function (popObj)
     {   // When popup closes with ok
 
     },
 
     uiDialog: function (response, status)
     {
     // Take html from hgTracks 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 = stripCspHeader(cleanHtml,false); // DEBUG msg with true
         cleanHtml = stripJsFiles(cleanHtml,false);   // DEBUG msg with true
         cleanHtml = stripCssFiles(cleanHtml,false);  // DEBUG msg with true
         //cleanHtml = stripJsEmbedded(cleanHtml,false);// DEBUG msg with true // Obsolete by CSP2?
         var nonceJs = {};
         cleanHtml = stripCSPAndNonceJs(cleanHtml, false, nonceJs); // DEBUG msg with true
 
         cleanHtml = stripMainMenu(cleanHtml,false);  // DEBUG msg with true
 
         $('#hgTracksDialog').html("<div id='pop' style='font-size:.9em;'>"+ cleanHtml +"</div>");
 
         appendNonceJsToPage(nonceJs);
 
 
         // Strategy for popups with js:
         // - jsFiles and CSS should not be included in html.  Here they are shluped out.
         // - The resulting files ought to be loadable dynamically (with getScript()), 
         //   but this was not working nicely with the modal dialog
         //   Therefore include files must be included with hgTracks CGI !
         // - embedded js should not be in the popup box.
         // - Somethings should be in a popup.ready() function, and this is emulated below, 
         //   as soon as the cleanHtml is added
         //   Since there are many possible popup cfg dialogs, the ready should be all inclusive.
 
         // -- popup.ready() -- Here is the place to do things that might otherwise go
         //                     into a $('#pop').ready() routine!
 
         // Searching for some semblance of size suitability
         var popMaxHeight = ($(window).height() - 40);
         var popMaxWidth  = ($(window).width() - 40);
         var popWidth     = 700;
         if (popWidth > popMaxWidth)
             popWidth = popMaxWidth;
 
         $('#hgTracksDialog').dialog({
             ajaxOptions: {
                 // This doesn't work
                 cache: true
             },
             resizable: true,               // Let scroll vertically
             height: 'auto',
             width: popWidth,
             minHeight: 200,
             minWidth: 400,
             maxHeight: popMaxHeight,
             maxWidth: popMaxWidth,
             modal: true,
             closeOnEscape: true,
             autoOpen: false,
             buttons: { 
                 /* NOT NOW
                 "OK": function() {
                     popUpHgt.uiDialogOk($('#pop'));
                     $(this).dialog("close");
                 }
                 */
             },
             // popup.ready() doesn't seem to work in open.
 
             //create: function () { 
                 //$(this).siblings().find(".ui-dialog-title").html('<span style="">Test </span>'); 
                 //$(this).siblings().find(".ui-dialog-title").html('<span style="       visibility: hidden;"></span>'); 
             //},
             
             open: function () {
                 $('#hgTracksDialog').find('.filterBy,.filterComp').each(
                     function(i) {  // ddcl.js is dropdown checklist lib support
                         if ($(this).hasClass('filterComp'))
                             ddcl.setup(this);
                         else
                             ddcl.setup(this, 'noneIsAll');
                     }
                 );
 
             },
 
             close: function() {
                 popUpHgt.cleanup();
             }
         });
         
     
         $('#hgTracksDialog').dialog('option' , 'title' , popUpHgt.title);
         $('#hgTracksDialog').dialog('open');
 
         // Initialize autocomplete for alt/fix sequence names
         autocompleteCat.init($('#singleAltHaploId'),
                              { baseUrl: 'hgSuggest?db=' + getDb() + '&type=altOrPatch&prefix=',
                                enterSelectsIdentical: true });
         // Make multi-region option inputs select their associated radio buttons
         $('input[name="emPadding"]').keyup(function() {
             $('#virtModeType[value="exonMostly"]').attr('checked', true); });
         $('input[name="gmPadding"]').keyup(function() {
             $('#virtModeType[value="geneMostly"]').attr('checked', true); });
         $('#multiRegionsBedInput').keyup(function() {
             $('#virtModeType[value="customUrl"]').attr('checked', true); });
         $('#singleAltHaploId').keyup(function() {
             $('#virtModeType[value="singleAltHaplo"]').attr('checked', true); });
 
         // disable exit if not in MR mode
         if (!hgTracks.virtModeType) {
             $('#virtModeTypeDefaultLabel').addClass('disabled');
             $('#virtModeType[value="exonMostly"]').attr('checked', true);
             $('#virtModeType[value="default"]').attr('disabled', 'disabled');
         } else {
             $('#virtModeType[value="default"]').removeAttr('disabled');
         }
 
         // Customize message based on current mode
         var msg = "<em>Select a multi-region viewing mode below.</em>";  // default
         if (hgTracks.virtModeType) {
             msg = "The display is currently in <em><b> ";
             var mode = "unknown";
             if (hgTracks.virtModeType === "exonMostly") {
                 msg += "exon";
             } else if (hgTracks.virtModeType == "geneMostly") {
                 msg += "gene";
             } else if (hgTracks.virtModeType == "customUrl") {
                 msg += "custom regions";
             } else if (hgTracks.virtModeType == "singleAltHaplo") {
                 msg += "alt haplotype";
             } 
             msg += " </b></em> view. &nbsp;&nbsp;"
                 + "<em>Select a different viewing mode, or exit and return to normal view</em>.";
         }
         $('#multiRegionConfigStatusMsg').html(msg);
 
         // Make 'Cancel' button close dialog
         $('input[name="Cancel"]').click(function() {
             $('#hgTracksDialog').dialog('close');
         });
     }
 };
 
 // Show the recommended track sets popup
 function showRecTrackSetsPopup() {
     // Update links with current position
     $('a.recTrackSetLink').each(function() {
         var $this = $(this);
         var link = $this.attr("href").replace(/position=.*/, 'position=');
         $this.attr("href", link + genomePos.original);
     });
     $('#recTrackSetsPopup').dialog({width:'650'});
 }
 
 function removeSessionPanel() {
     $('#recTrackSetsPanel').remove();
     setCartVar("hgS_otherUserSessionLabel", "off", null, false);
 }
 
 // A function to show the keyboard help dialog box, bound to ? and called from the menu bar
 function showHotkeyHelp() {
     $("#hotkeyHelp").dialog({width:'600'});
 }
 
 // A function to add an entry for the keyboard help dialog box to the menubar 
 // and add text that indicates the shortcuts to many static menubar items as suggested by good old IBM CUA/SAA
 function addKeyboardHelpEntries() {
     var html = '<li><a id="keybShorts" title="List all possible keyboard shortcuts" href="#">Keyboard Shortcuts</a><span class="shortcut">?</span></li>';
     $('#help .last').before(html);
     $("#keybShorts").click( function(){showHotkeyHelp();} );
 
     html = '<span class="shortcut">s s</span>';
     $('#sessionsMenuLink').after(html);
 
     html = '<span class="shortcut">p s</span>';
     $('#publicSessionsMenuLink').after(html);
 
     html = '<span class="shortcut">c t</span>';
     $('#customTracksMenuLink').after(html);
 
     html = '<span class="shortcut">t c</span>';
     $('#customCompositeMenuLink').after(html);
 
     html = '<span class="shortcut">t h</span>';
     $('#trackHubsMenuLink').after(html);
 
     html = '<span class="shortcut">t b</span>';
     $('#blatMenuLink').after(html);
 
     html = '<span class="shortcut">t t</span>';
     $('#tableBrowserMenuLink').after(html);
 
     html = '<span class="shortcut">t i</span>';
     $('#ispMenuLink').after(html);
 
     html = '<span class="shortcut">t s</span>';
     $('#trackSearchMenuLink').after(html);
 
     html = '<span class="shortcut">c f</span>';
     $('#configureMenuLink').after(html);
 
     html = '<span class="shortcut">c r</span>';
     $('#cartResetMenuLink').after(html);
 }
 
 // A function for the keyboard shortcut:
 // View DNA
 function gotoGetDnaPage() {
     var position = hgTracks.chromName+":"+hgTracks.winStart+"-"+hgTracks.winEnd;
     if (hgTracks.virtualSingleChrom && (pos.chrom.search("multi") === 0)) {
         position = genomePos.get().replace(/,/g,'');
     } else if (hgTracks.windows && hgTracks.nonVirtPosition) {
         position = hgTracks.nonVirtPosition;
     }
     var pos = parsePosition(position);
     if (pos) {
         var url = "hgc?hgsid="+getHgsid()+"&g=getDna&i=mixed&c="+pos.chrom+"&l="+pos.start+"&r="+pos.end+"&db="+getDb();
         window.location.href = url;
     }
     return false;
 }
 
 // A function for the keyboard shortcuts "zoom to x bp"
 function zoomTo(zoomSize) {
     var flankSize = Math.floor(zoomSize/2);
     var posStr = genomePos.get();
     posStr = posStr.replace("virt:", "multi:");
     var pos = parsePosition(posStr);
     var mid = pos.start+(Math.floor((pos.end-pos.start)/2));
     var newStart = Math.max(mid - flankSize, 0);
     var newEnd = mid + flankSize - 1;
     var newPos = genomePos.setByCoordinates(pos.chrom, newStart, newEnd);
     if (hgTracks.virtualSingleChrom && (newPos.search("multi:")===0))
         newPos = genomePos.disguisePosition(newPosition); // DISGUISE?
     imageV2.navigateInPlace("position="+newPos, null, true);
 }
 
 // A function for the keyboard shortcuts "highlight add/clear/new"
 function highlightCurrentPosition(mode) {
     var pos = genomePos.get();
     if (mode=="new")
         dragSelect.highlightThisRegion(pos, false);
     else if (mode=="add")
         dragSelect.highlightThisRegion(pos, true);
     else {
         hgTracks.highlight = "";
         var cartSettings = {'highlight': ""};
         cart.setVarsObj(cartSettings);
         imageV2.drawHighlights();
     }
 }
 
   //////////////////////////////////
  //// popup (aka modal dialog) ////
 //////////////////////////////////
 var popUp = {
 
     trackName:            "",
     trackDescriptionOnly: false,
     saveAllVars:          null,
 
     cleanup: function ()
     {  // Clean out the popup box on close
         if ($('#hgTrackUiDialog').html().length > 0 ) {
             // clear out html after close to prevent problems caused by duplicate html elements
             $('#hgTrackUiDialog').html("");
             popUp.trackName = ""; //set to defaults
             popUp.trackDescriptionOnly = false;
             popUp.saveAllVars = null;
         }
     },
 
     _uiDialogRequest: 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 && rec.configureBy) {
             if (rec.configureBy === 'none')
                 return;
             else if (rec.configureBy === 'clickThrough') {
                 jQuery('body').css('cursor', 'wait');
                 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
                 });
     },
 
     hgTrackUi: function (trackName,descriptionOnly)
     {   // Launches the popup but shields the ajax with a waitOnFunction
         waitOnFunction( popUp._uiDialogRequest, trackName, descriptionOnly );  
     },
 
     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;  // subtrack vis rules differ
         // For unknown reasons IE8 fails to find $('#pop'), occasionally
         var allVars = getAllVars($('#hgTrackUiDialog'), subtrack );
         //  Since 2010, when Tim changed this to only report changed vars instead of all form vars,
         // it no longer matches the behavior of hgTrackUi when called the non-popup way.
         // A few places in the hgTracks C code have been patched to explicitly set the cart vars
         // for some default checkboxes. So now this still means that QA must explicitly test
         // both paths through the code: as a separate full hgTracksUi page, and as a popup config window.
         // hgTrackUi always sends in its form all variables causing them to be explicitly set in the cart.  
         // The popup only sends things that have changed, causing those changes to appear explicitly
         // and even then it skips over disabled form items.
         // There is some C code that was written before the popup config, 
         // and it expects all the variables are set or none are.
         // If just some are set, and not the default ones, it gets confused.
         // I fixed just such a bug in the code that handles refSeq.
         // See commit daf92c0f9eb331ea60740e6802aabd241d4be363.
         var changedVars = varHashChanges(allVars,popUp.saveAllVars);
          // DEBUG Examples:
         //debugDumpFormCollection("saveAllVars", popUp.saveAllVars);
         //debugDumpFormCollection("allVars", allVars);
         //debugDumpFormCollection("changedVars", changedVars);
         var newVis = changedVars[trackName];
         // subtracks do not have "hide", thus '[]'
         var hide = (newVis && (newVis === 'hide' || newVis === '[]'));  
         if ( ! normed($('#imgTbl')) ) { // On findTracks or config page
             if (objNotEmpty(changedVars))
                 cart.setVarsObj(changedVars);
         }
         else {  // On image page
             if (hide) {
                 if (objNotEmpty(changedVars))
                     cart.setVarsObj(changedVars);
                 $(document.getElementById('tr_' + trackName)).remove();
                 imageV2.afterImgChange(true);
                 cart.updateSessionPanel();
             } else {
                 // Keep local state in sync if user changed visibility
                 if (newVis) {
                     vis.update(trackName, newVis);
                 }
                 if (objNotEmpty(changedVars)) {
                     var urlData = cart.varsToUrlData(changedVars);
                     if (imageV2.mapIsUpdateable) {
                         imageV2.requestImgUpdate(trackName,urlData,"");
                     } else {
                         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,false);   // DEBUG msg with true
         cleanHtml = stripCssFiles(cleanHtml,false);  // DEBUG msg with true
         //cleanHtml = stripJsEmbedded(cleanHtml,false);// DEBUG msg with true // OBSOLETE BY CSP2?
 	var nonceJs = {};
 	cleanHtml = stripCSPAndNonceJs(cleanHtml, false, nonceJs); // DEBUG msg with true
 
 	//alert(cleanHtml);  // DEBUG REMOVE
         $('#hgTrackUiDialog').html("<div id='pop' style='font-size:.9em;'>"+ cleanHtml +"</div>");
 
 	appendNonceJsToPage(nonceJs);
 
         // Strategy for popups with js:
         // - jsFiles and CSS should not be included in html.  Here they are shluped out.
         // - The resulting files ought to be loadable dynamically (with getScript()), 
         //   but this was not working nicely with the modal dialog
         //   Therefore include files must be included with hgTracks CGI !
         // - embedded js should not be in the popup box.
         // - Somethings should be in a popup.ready() function, and this is emulated below, 
         //   as soon as the cleanHtml is added
         //   Since there are many possible popup cfg dialogs, the ready should be all inclusive.
 
         if ( ! popUp.trackDescriptionOnly ) {
             // If subtrack then vis rules differ
             var subtrack = tdbIsSubtrack(hgTracks.trackDb[popUp.trackName]) ? popUp.trackName :"";  
             // Saves the original vars (and vals) that may get changed by the popup cfg.
             popUp.saveAllVars = getAllVars( $('#hgTrackUiDialog'), subtrack ); 
 
             // -- popup.ready() -- Here is the place to do things that might otherwise go
             //                     into a $('#pop').ready() routine!
         }
 
         // Searching for some semblance of size suitability
         var popMaxHeight = ($(window).height() - 40);
         var popMaxWidth  = ($(window).width() - 40);
         var popWidth     = 640;
         if (popWidth > popMaxWidth)
             popWidth = popMaxWidth;
 
         // Create dialog buttons for UI popup
         var uiDialogButtons = {};
         if (popUp.trackDescriptionOnly) {
             uiDialogButtons.OK = function() {
                 $(this).dialog("close");
             };
         } else {
             uiDialogButtons.Apply = function() {
                  popUp.uiDialogOk($('#pop'), popUp.trackName);
                  // thanks to JAT for this cleverness to keep button functioning
                  popUp.saveAllVars = getAllVars( $('#hgTrackUiDialog'), popUp.trackName);
                  if (popUp.saveAllVars[popUp.trackName+"_sel"] === 0) {       // hide
                     // NOTE: once hidden, can't be unhidden by popup, so shut it down
                     $(this).dialog("close");
                 }
             };
             uiDialogButtons.OK = function() {
                 popUp.uiDialogOk($('#pop'), popUp.trackName);
                 $(this).dialog("close");
             };
         }
 
         $('#hgTrackUiDialog').dialog({
             ajaxOptions: {
                 // This doesn't work
                 cache: true
             },
             resizable: true,               // Let description scroll vertically
             height: (popUp.trackDescriptionOnly ? popMaxHeight : 'auto'),
             width: popWidth,
             minHeight: 200,
             minWidth: 400,
             maxHeight: popMaxHeight,
             maxWidth: popMaxWidth,
             modal: true,
             closeOnEscape: true,
             autoOpen: false,
             buttons: uiDialogButtons,
 
             // popup.ready() doesn't seem to work in open.
             open: function(event) {
                 // fix popup to a location -- near the top and somewhat centered on the browser image
                 $(event.target).parent().css('position', 'fixed');
                 $(event.target).parent().css('top', '18%');
                 $(event.target).parent().css('left', '30%');
                 var containerHeight = $(event.target).parent().height();
                 var offsetTop = $(event.target).parent()[0].offsetTop;
                 // from popMaxHeight calculation above:
                 var offsetBottom = 40;
                 var maxContainerHeight = $(window).height() - offsetTop - offsetBottom;
                 if (containerHeight > maxContainerHeight) {
                     $(event.target).parent().css('height', maxContainerHeight);
                     // the 100 below accounts for the buttons, and label, there is
                     // probably a better way to get the exact size of the container
                     // with no content
                     $(event.target).css('height', maxContainerHeight - 100);
                 }
 
                 if (!popUp.trackDescriptionOnly) {
                     $('#hgTrackUiDialog').find('.filterBy,.filterComp').each(
                         function(i) {
                             if ($(this).hasClass('filterComp'))
                                 ddcl.setup(this);
                             else
                                 ddcl.setup(this, 'noneIsAll');
                         }
                     );
                 }
             },
             close: function() {
                 popUp.cleanup();
             }
         });
         
         // FIXME: Why are open and close no longer working!!!
         if (popUp.trackDescriptionOnly) {
             var myWidth =  $(window).width() - 300;
             if (myWidth > 900)
                 myWidth = 900;
             $('#hgTrackUiDialog').dialog("option", "maxWidth", myWidth);
             $('#hgTrackUiDialog').dialog("option", "width", myWidth);
             $('#hgTrackUiDialog').dialog('option' , 'title' ,
                                hgTracks.trackDb[popUp.trackName].shortLabel+" Track Description");
             $('#hgTrackUiDialog').dialog('open');
         } else {
             $('#hgTrackUiDialog').dialog('option' , 'title' ,
                                   hgTracks.trackDb[popUp.trackName].shortLabel+" Track Settings");
             $('#hgTrackUiDialog').dialog('open');
         }
         var buttOk = $('button.ui-state-default');
         $(buttOk).focus();
     }
 };
 
   ///////////////////////////////
  //// 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 & mapIsUpdateable
     mapIsUpdateable:true,
     lastTrack:      null,   // formerly (lastMapItem) this is used to try to keep what the
                             // last track the cursor passed.
 
     LEFTADD: 3,             // when going from pixels to chrom coords, these 3 pixels
                             // are somehow used for "borders or cgi item calc ?" (original comment)
 
     markAsDirtyPage: function ()
     {   // Page is marked as dirty so that the back-button knows page doesn't match cart
         var dirty = normed($('#dirty'));
         if (dirty)
             $(dirty).val('true');
     },
 
     markAsCleanPage: function ()
     {   // Clears signal that history may be out of sync with cart.
         var dirty = normed($('#dirty'));
         if (dirty)
             $(dirty).val('false');
     },
 
     isDirtyPage: function ()
     { // returns true if page was marked as dirty
     // This will allow the backbutton to be overridden
 
         var dirty = normed($('#dirty'));
         if (dirty && $(dirty).val() === 'true')
             return true;
         return false;
     },
     
     manyTracks: function ()
     {   // image-reload is slower than whole page reload when there are too many tracks
         if (!hgTracks || !hgTracks.trackDb || objKeyCount(hgTracks.trackDb) > 50)
             return true;
         return false;
     },
 
     moveTiming: function() 
     {    // move measure timing messages to the end of the page
         if ($(".timing").length > 0) {
             $("body").append("<div id='timingDiv'></div>");
             $(".timing").detach().appendTo('#timingDiv');
         }
     },
 
     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");
         var strs = [];
         for (var a = reg.exec(response); a && a[1]; a = reg.exec(response)) {
             strs.push(a[1]);
         }
         if (strs.length > 0) {
             $('.timing').remove();
             for (var ix = strs.length; ix > 0; ix--) {
                 $('#timingDiv').append(strs[ix - 1]);
             }
         }
         reg = new RegExp("(<span class='trackTiming'>[\\S\\s]+?</span>)");
         a = reg.exec(response);
         if (a && a[1]) {
             $('.trackTiming').replaceWith(a[1]);
         }
     },
 
     loadSuggestBox: function ()
     {
         if ($('#positionInput').length) {
             if (!suggestBox.initialized) { // only call init once
                 suggestBox.init(getDb(),
                             $("#suggestTrack").length > 0,
                             function (item) {
                                 genomePos.set(item.id, getSizeFromCoordinates(item.id));
                                 if ($("#suggestTrack").length && $('#hgFindMatches').length) {
                                     // Set cart variables to open the hgSuggest gene track and highlight
                                     // the chosen transcript.  These variables will be submittted by the goButton
                                     // click handler.
                                     vis.makeTrackVisible($("#suggestTrack").val());
                                     cart.addVarsToQueue(["hgFind.matches"],[$('#hgFindMatches').val()]);
                                 }
                                 $("#goButton").click();
                             },
                             function (position) {
                                 genomePos.set(position, getSizeFromCoordinates(position));
                             }
                 );
             }
         }
     },
     
     afterImgChange: function (dirty)
     {   // Standard things to do when manipulations change image without ajax update.
         dragReorder.init();
         dragSelect.load(false);
         imageV2.drawHighlights();
         if (dirty)
             imageV2.markAsDirtyPage();
     },
     
     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
                 imageV2.drawHighlights();
                 return;
             }
         }
         
         imageV2.loadRemoteTracks();
         makeItemsByDrag.load();
         imageV2.loadSuggestBox();
         imageV2.drawHighlights();
 
         if (imageV2.backSupport) {
             imageV2.setInHistory(false);    // Set this new position into History stack
         } else {
             imageV2.markAsDirtyPage();
         }
     },
 
     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 newTr = $(html).find("tr[id='tr_" + id + "']");
         if (newTr.length > 0) {
             var tr = $(document.getElementById("tr_" + id));
             if (tr.length > 0) {
                 $(tr).html(newTr.children());
 
                 // Need to update tr class list too
                 var classes = $(html).find("tr[id='tr_"+ id + "']")[0].className;
                 if (classes && classes.length > 0) {
                     $(tr).removeClass();
                     $(tr).addClass(classes);
                 }
 
                 // 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
                     &&  hgTracks.imgBoxPortalLeft !== null) {
                         $(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
                     var abbr = $(newTr).attr('abbr');
 
                     if (abbr) {
                         $(tr).attr('abbr', abbr);
                     }
 
                     if (newJsonRec)
                         vis.update(id, vis.enumOrder[newJsonRec.visibility]);
                 }
                 // hg.conf will turn this on 2020-10 - Hiram
                 if (window.mouseOverEnabled) { mouseOver.updateMouseOver(id, newJsonRec); }
                 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).
         var id;
         for (id in newJson.trackDb) {
             var newJsonRec = newJson.trackDb[id];
             var oldJsonRec = oldJson.trackDb[id];
             
             if (newJsonRec.visibility === 0)  // hidden 'ruler' is in newJson.trackDb!
                 continue;
             if (newJsonRec.type === "remote")
                 continue;
             var escapedId = id.replace('.', '\\.');
             if (oldJsonRec &&  oldJsonRec.visibility !== 0 && $('tr#tr_' + escapedId).length === 1) {
                 // New track replacing old:
                 if (!imageV2.updateImgForId(response, id, true, newJsonRec))
                     warn("Couldn't parse out new image for id: " + id);
             } else { //if (!oldJsonRec || 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);
                     } else {
                         cart.updateSessionPanel();
                     }
                 }
             }
         }
         if (imageV2.backSupport) {
             // Removes OLD: those in oldJson but not in newJson
             for (id in oldJson.trackDb) {
                 if ( ! newJson.trackDb[id] )
                     $(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' style='display: inline;'>
         // If the ideo is hidden or missing, we supply a place-holder for dynamic update later.
         // e.g.: <IMG SRC = ""
         //                BORDER=1 WIDTH=1039 HEIGHT=0 USEMAP=#ideoMap id='chrom' style='display: none'>
         // 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) { // tolerate empty SRC= string when no ideo
                 $('#chrom').attr('src', b[1]);
                 var c = /style\s*=\s*'([^')]+)'/.exec(a[1]);
                 if (c && c[1]) {
                     $('#chrom').attr('style', c[1]);
                 }
                 var d = /HEIGHT\s*=\s*(\d*)/.exec(a[1]);
                 if (d && d[1]) {
                     $('#chrom').attr('HEIGHT', d[1]);
                 }
                 // Even if we're on the same chrom, ideoMap may change because the label
                 // on the left changes width depending on band name, and that changes px scaling.
                 var ideoMapMatch = /<MAP Name=ideoMap>[\s\S]+?<\/MAP>/.exec(response);
                 if (ideoMapMatch) {
                     var $oldMap = $("map[name='ideoMap']");
                     var $container = $oldMap.parent();
                     $oldMap.remove();
                     $container.append(ideoMapMatch[0]);
                 }
                 if (imageV2.backSupport) {
                     // Reinit chrom dragging.
                     if ($('area.cytoBand').length >= 1) {
                         $('img#chrom').chromDrag();
                     }
                 }
             }
         }
     },
 
     updateBackground: function (response)
     {
         // Added by galt to update window separators
         // Parse out background image url
         // background-image:url("../trash/hgt/blueLines1563-118-12_hgwdev_galt_9df9_e33b30.png")
         // background-image:url("../trash/hgt/winSeparators_hgwdev_galt_5bcb_baff60.png")
         // This will only need to update when multi-region is on and is using winSeparators.
 
         var a = /background-image:url\("(..\/trash\/hgt\/winSeparators[^"]+[.]png)"\)/.exec(response);
         if (a && a[1]) {
             $('td.tdData').css("background-image", "url("+a[1]+")");
         }
 
     },
 
         
     requestImgUpdate: function (trackName,extraData,loadingId,newVisibility)
     {
         // extraData, loadingId and newVisibility are optional
         var data = "hgt.trackImgOnly=1&hgsid=" + getHgsid() + "&hgt.trackNameFilter=" + trackName;
         if (extraData && extraData !== "")
             data += "&" + extraData;
         if (!loadingId || 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: cart.addUpdatesToUrl(data),
                     dataType: "html",
                     trueSuccess: imageV2.updateImgAndMap,
                     success: catchErrorOrDispatch,
                     error: errorHandler,
                     cmd: 'refresh',
                     loadingId: loadingId,
                     id: trackName,
                     newVisibility: newVisibility,
                     cache: false
                 });
     },
 
     fullReload: function(extraData)
     {
         // force reload of whole page via trackform submit
         // This function does not return
         jQuery('body').css('cursor', 'wait');
         if (extraData || cart.updatesWaiting()) {
             var url = cart.addUpdatesToUrl(window.location.href);
             if (extraData) {
                 if ( url.lastIndexOf("?") === -1)
                     url += "?" + extraData;
                 else
                     url += '&' + extraData;
             }
             window.location.assign(url);
             return false;
         }
         document.TrackHeaderForm.submit();
     },
 
     updateImgAndMap: function (response, status)
     {   // Handle ajax response with an updated trackMap image, map and optional ideogram. 
         //    and maybe the redLines background too.
         // 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 newJson = scrapeVariable(response, "hgTracks");
 
         //alert(JSON.stringify(newJson)); // DEBUG Example
 
         var oldJson = hgTracks;
         var valid = false;
         var stripped = {};
         if (!newJson) {
             stripJsEmbedded(response, true, stripped);
             if ( ! stripped.warnMsg )
                 warn("hgTracks object is missing from the response");
         } else {
             if (this.id) {
                 if (newJson.trackDb[this.id]) {
                     var visibility = vis.enumOrder[newJson.trackDb[this.id].visibility];
                     var 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 = oldJson.trackDb[this.id];
                     rec.limitedVis = newJson.trackDb[this.id].limitedVis;
                     vis.update(this.id, visibility);
                     if (visibility === "hide")
                         cart.updateSessionPanel(); // notify when vis change to hide track
                     valid = true;
                 } else {
                     // what got returned from the AJAX request was a different
                     // set of tracks.  Let's do a reload and hope for the best
                     imageV2.fullReload();
                 }
             } else {
                 valid = true;
             }
 
             suggestBox.restoreWatermark(getDb(), $("#suggestTrack").length > 0);
 
             // the ajax request may have generated an error or warning in the warnbox
             // so make sure those warnings still get to the user
             stripJsEmbedded(response, false, stripped);
         }
         if (valid) {
             if (imageV2.enabled
             && this.id
             && this.cmd
             && this.cmd !== 'wholeImage'
             && this.cmd !== 'selectWholeGene'
             && !newJson.virtChromChanged) {
                 // 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, false)) {
                     imageV2.afterReload(id);
                     imageV2.updateBackground(response);  // Added by galt to update window separators
                 } else {
                     warn("Couldn't parse out new image for id: " + id);
                     // Very helpful when debugging and alert doesn't render the html:
                     //alert("Couldn't parse out new image for id: " + id+"BR"+response);
                 }
             } else {
                 if (imageV2.enabled) {
                     // Implement in-place updating of hgTracks image
                     // GALT delaying this until after newJson updated in hgTracks so disguising works
                     //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 || newJson.virtChromChanged) {
                         // 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 {
                         // Will rebuild image adding new, removing old and resorting tracks
                         imageV2.updateImgForAllIds(response,oldJson,newJson);
                         imageV2.updateChromImg(response);
                         imageV2.updateBackground(response);  // Added by galt to update window separators
                         hgTracks = newJson;
                         genomePos.original = undefined;
                         genomePos.setByCoordinates(hgTracks.chromName, hgTracks.winStart + 1, hgTracks.winEnd); // MOVED HERE GALT
                         initVars();
                         imageV2.afterReload();
                     }
                 } else {
                     warn("ASSERT: Attempt to update track without advanced javascript features.");
                 }
             }
             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);
         }
     },
 
     loadRemoteTracks: function ()
     {
         if (hgTracks.trackDb) {
             for (var id in hgTracks.trackDb) {
                 var rec = hgTracks.trackDb[id];
                 if (rec.type === "remote") {
                     if ($("#img_data_" + id).length > 0) {
                         // load the remote track renderer via jsonp
                         rec.loadingId = showLoadingImage("tr_" + id);
                         var script = document.createElement('script');
                         var pos = parsePosition(genomePos.get());
                         var name = rec.remoteTrack || id;
                         script.setAttribute('src',
                                 rec.url + "?track=" + name +
                                 "&jsonp=imageV2.remoteTrackCallback&position=" +
                                 encodeURIComponent(pos.chrom + ":" + pos.start + "-" + pos.end) +
                                 "&pix=" + $('#imgTbl').width()
                         );
                         document.getElementsByTagName('head')[0].appendChild(script);
                     }
                 }
             }
         }
     },
 
     remoteTrackCallback: function (rec)
     // jsonp callback to load a remote track.
     {
         if (rec.error) {
             alert("retrieval from remote site failed with error: " + rec.error);
         } else {
             var remoteTrack = rec.track;
             for (var track in hgTracks.trackDb) {
                 if (hgTracks.trackDb[track].remoteTrack === remoteTrack) {
                     $('#img_data_' + track).attr('style', "left:-116px; top: -23px;");
                     $('#img_data_' + track).attr('height', rec.height);
                     // XXXX use width in some way?
            //       $('#img_data_' + track).attr('width', rec.width);
                     $('#img_data_' + track).attr('width', $('#img_data_ruler').width());
                     $('#img_data_' + track).attr('src', rec.img);
           /* jshint loopfunc: true */// function inside loop works and replacement is awkward.
                     $('#td_data_' + track + ' > div').each(function(index) {
                         if (index === 1) {
                             var style = $(this).attr('style');
                             style = style.replace(/height:\s*\d+/i, "height:" + rec.height);
                             $(this).attr('style', style);
                         }
                     });
                     var style = $('#p_btn_' + track).attr('style');
                     style = style.replace(/height:\s*\d+/i, "height:" + rec.height);
                     $('#p_btn_' + track).attr('style', style);
                     if (hgTracks.trackDb[track].loadingId) {
                         hideLoadingImage(hgTracks.trackDb[track].loadingId);
                     }
                 }
             }
         }
     },
 
     navigateButtonClick: function (ele) // called from hgTracks.c
     {   // code to update just the imgTbl in response to navigation buttons (zoom-out etc.).
         if (imageV2.inPlaceUpdate) {
             var params = ele.name + "=" + ele.value;
             $(ele).attr('disabled', 'disabled');
             // dinking navigation needs additional data
             if (ele.name === "hgt.dinkLL" || ele.name === "hgt.dinkLR") {
                 params += "&dinkL=" + $("input[name='dinkL']").val();
             } else if (ele.name === "hgt.dinkRL" || ele.name === "hgt.dinkRR") {
                 params += "&dinkR=" + $("input[name='dinkR']").val();
             }
             imageV2.navigateInPlace(params, $(ele), false);
             return false;
         } else {
             return true;
         }
     },
 
     updateButtonClick: function (ele) // UNUSED?
     {   // code to update the imgTbl based on changes in the track controls.
         // This is currently experimental code and is dead in the main branch.
         if (imageV2.mapIsUpdateable) {
             var data = "";
             $("select").each(function(index, o) {
                 var cmd = $(this).val();
                 if (cmd === "hide") {
                     if (hgTracks.trackDb[this.name]) {
                         alert("Need to implement hide");
                     }
                 } else {
                     if ( ! hgTracks.trackDb[this.name]
                     ||  cmd !== vis.enumOrder[hgTracks.trackDb[this.name].visibility]) {
                         if (data.length > 0) {
                             data = data + "&";
                         }
                         data = data + this.name + "=" + cmd;
                     }
             }
             });
             if (data.length > 0) {
                 imageV2.navigateInPlace(data, null, false);
             }
             return false;
         } else {
             return true;
         }
     },
 
     navigateInPlace: function (params, disabledEle, keepCurrentTrackVisible)
     {   // request an hgTracks image, using params
         // disabledEle is optional; this element will be enabled when update is complete
         // If keepCurrentTrackVisible is true, we try to maintain relative position of the item
         // under the mouse after the in-place update.
         // Tim thinks we should consider disabling all UI input while we are doing in-place update.
         // TODO: waitOnFuction?
     
         // No ajax image update if there are too many tracks!
         if (imageV2.manyTracks()) {
             imageV2.fullReload(params);
             return false;  // Shouldn't return from fullReload but I have seen it in FF
         }
     
         jQuery('body').css('cursor', 'wait');
         var currentId, currentIdYOffset;
         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: 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
             });
     },
 
     disguiseHighlight: function(position)
     // disguise highlight position
     {
         pos = parsePositionWithDb(position);
         // DISGUISE
         if (hgTracks.virtualSingleChrom && (pos.chrom.search("multi") === 0)) {
             var positionStr = pos.chrom+":"+pos.start+"-"+pos.end;
             var newPosition = genomePos.disguisePosition(positionStr);
             var newPos = parsePosition(newPosition);
             pos.chrom = newPos.chrom;
             pos.start = newPos.start;
             pos.end   = newPos.end;
         }
         return makeHighlightString(pos.db, pos.chrom, pos.start, pos.end, pos.color);
     },
 
     undisguiseHighlight: function(pos)
     // undisguise highlight pos
     {
         // UN-DISGUISE
         if (hgTracks.virtualSingleChrom && (pos.chrom.search("multi") !== 0)) {
             var position = pos.chrom+":"+pos.start+"-"+pos.end;
             var newPosition = genomePos.undisguisePosition(position);
             var newPos = parsePosition(newPosition);
             if (newPos) {
                 pos.chrom = newPos.chrom;
                 pos.start = newPos.start;
                 pos.end   = newPos.end;
             }
         }
     },
 
     drawHighlights: function()
     // highlight vertical region in imgTbl based on hgTracks.highlight (#709).
     // For PDF/hgRenderTracks output, the highlights are drawn by hgTracks.c:drawHighlights()
     {
         var pos;
         var hexColor = dragSelect.hlColorDefault;
         // if possible, re-use the color that the user picked last time
         if (hgTracks.prevHlColor)
             hexColor = hgTracks.prevHlColor;
 
         $('.highlightItem').remove();
         if (hgTracks.highlight) {
             var hlArray = hgTracks.highlight.split("|"); // support multiple highlight items
             for (var i = 0; i < hlArray.length; i++) {
                 hlString = hlArray[i];
                 pos = parsePositionWithDb(hlString);
                 // UN-DISGUISE
                 imageV2.undisguiseHighlight(pos);
                 if (pos) {
                     pos.start--;  // make start 0-based to match hgTracks.winStart
                     if (pos.color)
                         hexColor = pos.color;
                 }
 
                 if (pos && pos.chrom === hgTracks.chromName && pos.db === getDb() 
                 &&  pos.start <= hgTracks.imgBoxPortalEnd && pos.end >= hgTracks.imgBoxPortalStart) {
                     var portalWidthBases = hgTracks.imgBoxPortalEnd - hgTracks.imgBoxPortalStart;
                     var portal = $('#imgTbl td.tdData')[0];
                     var leftPixels = $(portal).offset().left + imageV2.LEFTADD;
                     var pixelsPerBase = ($(portal).width() - 2) / portalWidthBases;
                     var clippedStartBases = Math.max(pos.start, hgTracks.imgBoxPortalStart);
                     var clippedEndBases = Math.min(pos.end, hgTracks.imgBoxPortalEnd);
                     var widthPixels = (clippedEndBases - clippedStartBases) * pixelsPerBase;
                     if (hgTracks.revCmplDisp)
                         leftPixels += (hgTracks.imgBoxPortalEnd - clippedEndBases) * pixelsPerBase - 1;
                     else
                         leftPixels += (clippedStartBases - hgTracks.imgBoxPortalStart) * pixelsPerBase;
                     // Impossible to get perfect... Okay to overrun by a pixel on each side
                     leftPixels  = Math.floor(leftPixels);
                     widthPixels = Math.ceil(widthPixels);
                     if (widthPixels < 2) {
                         widthPixels = 3;
                         leftPixels -= 1;
                     }
     
                     var area = jQuery("<div id='highlightItem' class='highlightItem'></div>");
                     $(area).css({ backgroundColor: hexColor, // display: 'none'
                                 left: leftPixels + 'px', top: $('#imgTbl').offset().top + 1 + 'px',
                                 width: widthPixels + 'px',
                                 height: $('#imgTbl').css('height') });
                     $(area).data({leftPixels: leftPixels, widthPixels: widthPixels});// needed by dragScroll
     
                     // Larry originally appended to imgTbl, but discovered that doesn't work on IE 8 and 9.
                     $('body').append($(area)); 
                     // z-index is done in css class, so highlight is beneath transparent data images.
                     // NOTE: ideally highlight would be below transparent blue-lines, but THAT is a  
                     // background-image so z-index can't get below it!  PS/PDF looks better for blue-lines!
                 }
             }
         }
     },
 
     backSupport: (window.History.enabled !== undefined), // support of our 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 history 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 prevDbPos = imageV2.history.getState().data.lastDbPos;
             var prevPos = imageV2.history.getState().data.position;
             var curDbPos = hgTracks.lastDbPos;
             if (prevDbPos && prevDbPos !== curDbPos) {
                 // NOTE: this function is NOT called when backing past a full retrieval boundary
                 genomePos.set(decodeURIComponent(prevPos));
                 imageV2.navigateInPlace("" + prevDbPos, null, false);
             }
         });
         
         // 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.
         $("#goButton,input[value='refresh']").click(function () {
             var newPos = genomePos.get().replace(/,/g,'');
             if (newPos.length > 2000) {
                alert("Sorry, you cannot paste identifiers or sequences with more than 2000 characters into this box.");
                $('input[name="hgt.positionInput"]').val("");
                return false;
             }
 
             var newDbPos = hgTracks.lastDbPos;
             if ( ! imageV2.manyTracks() ) {
                 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 not just image update AND there are vis updates waiting...
             if (cart.updatesWaiting()) {
                 var url = "../cgi-bin/hgTracks?position=" + newPos + "&" + cart.varsToUrlData({ 'db': getDb(), 'hgsid': getHgsid() });
                 window.location.assign(url);
                 return false;
             }
 
             // redirect to hgBlat if the input looks like a DNA sequence
             // minimum length=19 so we do not accidentally redirect to hgBlat for a gene identifier 
             // like ATG5
             var dnaRe = new RegExp("^(>[^\n\r ]+[\n\r ]+)?(\\s*[actgnACTGN \n\r]{19,}\\s*)$");
             if (dnaRe.test(newPos)) {
                 var blatUrl = "hgBlat?type=BLAT%27s+guess&userSeq="+newPos;
                 window.location.href = blatUrl;
                 return false;
             }
 
             // helper functions for checking whether a plain chrom name was searched for
             term = encodeURIComponent(genomePos.get().replace(/^[\s]*/,'').replace(/[\s]*$/,''));
             function onSuccess(jqXHR, textStatus) {
                 if (jqXHR.chromName !== null) {
                     imageV2.markAsDirtyPage();
                     imageV2.navigateInPlace("position=" + encodeURIComponent(newPos), null, false);
                     window.scrollTo(0,0);
                 } else  {
                     window.location.assign("../cgi-bin/hgSearch?search=" + term  + "&hgsid="+ getHgsid());
                 }
             }
             function onFail(jqXHR, textStatus) {
                 window.location.assign("../cgi-bin/hgSearch?search=" + term  + "&hgsid="+ getHgsid());
             }
 
             // redirect to search disambiguation page if it looks like we didn't enter a regular position:
             var canonMatch = newPos.match(canonicalRangeExp);
             var gbrowserMatch = newPos.match(gbrowserRangeExp);
             var lengthMatch = newPos.match(lengthRangeExp);
             var bedMatch = newPos.match(bedRangeExp);
             var sqlMatch = newPos.match(sqlRangeExp);
             var singleMatch = newPos.match(singleBaseExp);
             var positionMatch = canonMatch || gbrowserMatch || lengthMatch || bedMatch || sqlMatch || singleMatch;
             if (positionMatch === null) {
                 // user may have entered a full chromosome name, check for that asynchronosly:
                 $.ajax({
                     type: "GET",
                     url: "../cgi-bin/hgSearch",
                     data: cart.varsToUrlData({ 'cjCmd': '{"getChromName": {"db": "' + getDb() + '", "searchTerm": "' + term + '"}}' }),
                     dataType: "json",
                     trueSuccess: onSuccess,
                     success: onSuccess,
                     error: onFail,
                     cache: true
                 });
                 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 curDbPos = hgTracks.lastDbPos;
         var cachedPos = imageV2.history.getState().data.position;
         var cachedDbPos = imageV2.history.getState().data.lastDbPos;
         // A) Forward: Full page retrieval: hgTracks is first navigated to (or chrom change)
         if (!cachedDbPos) { // 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("db="+getDb()+"&"+cachedDbPos, null, false);
                 } else {
                     imageV2.fullReload();
                 }
             } else {
                 // B2) Clean page: only position changes from a->b 
                 if (cachedDbPos !== curDbPos) {
                     imageV2.navigateInPlace("db="+getDb()+"&"+cachedDbPos, null, false);
                 }
             }
             // Special because FF is leaving vis drop-downs disabled
             vis.restoreFromBackButton();
         }
     },
     
     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 lastDbPos = imageV2.history.getState().data.lastDbPos;
         var newPos  = encodeURIComponent(genomePos.get().replace(/,/g,''));  // no commas
         var newDbPos  = hgTracks.lastDbPos;
         
         // 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 && lastDbPos)
             return;
 
         if (!lastDbPos || lastDbPos !== newDbPos) {
             // Swap the position into the title
             var title = $('TITLE')[0].text;
             var ttlWords = title.split(' ');
             if (ttlWords.length >= 2) {
                 for (var i=1; i < ttlWords.length; i++) {
                     if (ttlWords[i].indexOf(':') >= 0) {
                         ttlWords[i] = genomePos.get();
                     }
                 }
                 title = ttlWords.join(' ');
             } else
                 title = genomePos.get();
 
             var sid = getHgsid();
             if (fullPageLoad) { 
                 // Should only be on initial set-up: first navigation to page
                 imageV2.history.replaceState({lastDbPos: newDbPos, position: newPos, hgsid: + sid },title,
                                  "hgTracks?db="+getDb()+"&"+newDbPos+"&hgsid="+sid);
             } else {  
                 // Should be when advancing (not-back-button)
                 imageV2.history.pushState({lastDbPos: newDbPos, position: newPos, hgsid: + sid },title,
                                  "hgTracks?db="+getDb()+"&"+newDbPos+"&hgsid="+sid);
             }
         }
     }
 };
 
   ///////////////////////////////////////
  //// mouseOver data display 2020-10 ///
 ///////////////////////////////////////
 var mouseOver = {
 
     items: {},  // items[trackName][] - data for each item in this track
     visible: false,     // keeping track of popUp window visibility
     tracks: {}, // tracks[trackName] - number of data items for this track
     trackType: {},	// key is track name, value is track type from hgTracks
     mouseOverFunction: {}, // key is track name, value mouseOverFunction string
     jsonUrl: {},       // list of json files from hidden DIV elements
     maximumWidth: {},   // maximumWidth[trackName] - largest string to display
     popUpDelay: 200,   // 0.2 second delay before popUp appears
     popUpTimer: null,// handle from setTimeout to use in clearTimout(popUpTimer)
     delayDone: true,   // mouse has not left element, still receiving move evts
     delayInProgress: false,        // true while waiting for delay timer
     mostRecentMouseEvt: null,   // to use when mouse delay is finished
     browserTextSize: 12,        // default if not found otherwise
     measureTextBox: null,
     noDataString: "no&nbsp;data",	// message for no data at this position
     noDataSize: 0,	// will be set to size of text 'no data'
     noAverageString: "&nbsp;zoom&nbsp;in&nbsp;to&nbsp;see&nbsp;values&nbsp;",	// "noAverage" function
 
     // items{} - key name is track name, value is an array of data items
     //           where the format of each item can be different for different
     //           data tracks.  For example, the wiggle track is an array of:
     //                   objects: {x1, x2, v, c}
     //           where [x1..x2) is the array index where the value 'v'
     //           is found, and 'c' is the data value count in this value
     //           i.e. when c > 1 the value is a 'mean' of 'c' data values
     // visible - keep track of window visible or not, value: true|false
     //           shouldn't need to do this here, the window knows if it
     //           is visible or not, just ask it for status
     // tracks{}  - tracks that were set up initially, key is track name
     //             value is the number of items (for debugging)
     // maximumWidth{} - key is track name, value is length of longest
     //                  number string as measured when rendered
 
     // given hgt_....png file name, change to hgt_....json file name
     jsonFileName: function(imgDataId)
     {
       var jsonFile=imgDataId.src.replace(".png", ".json");
       return jsonFile;
     },
 
     // called from: updateImgForId when it has updated a track in place
     // need to refresh the event handlers and json data
     updateMouseOver: function (trackName, trackDb)
     {
       var trackType = null;
       var hasChildren = null;
       if (trackDb) {
 	trackType = trackDb.type;
 	hasChildren = trackDb.hasChildren;
       } else if (hgTracks.trackDb && hgTracks.trackDb[trackName]) {
 	trackType = hgTracks.trackDb[trackName].type;
       } else if (mouseOver.trackType[trackName]) {
 	trackType = mouseOver.trackType[trackName];
       }
       var tdData = "td_data_" + trackName;
       var tdDataId  = document.getElementById(tdData);
       var imgData = "img_data_" + trackName;
       var imgDataId  = document.getElementById(imgData);
       if (imgDataId && tdDataId) {
 	var url = mouseOver.jsonFileName(imgDataId);
         if (mouseOver.tracks[trackName]) {  // > 0 -> seen before in receiveData
             $( tdDataId ).mousemove(mouseOver.mouseMoveDelay);
             $( tdDataId ).mouseout(mouseOver.popUpDisappear);
             mouseOver.fetchJsonData(url);  // may be a refresh, don't know
         } else {
 	  if (trackType) {
             var validType = false;
             if (trackType.indexOf("wig") === 0) { validType = true; }
             if (trackType.indexOf("bigWig") === 0) { validType = true; }
             if (trackType.indexOf("wigMaf") === 0) { validType = false; }
             if (hasChildren) { validType = false; }
             if (validType) {
               $( tdDataId ).mousemove(mouseOver.mouseMoveDelay);
               $( tdDataId ).mouseout(mouseOver.popUpDisappear);
               mouseOver.fetchJsonData(url);
             }
           }
         }
       }
     },
 
     // given an X coordinate: x, find the index idx
     // in the rects[idx] array where rects[idx].x1 <= x < rects[idx].x2
     // returning -1 when not found
     // if we knew the array was sorted on x1 we could get out early
     //   when x < x1
     // Note, different track types could have different intersection
     //       procedures.  For example, the HiC track will need to intersect
     //       the mouse position within the diamond/square defined by the
     //       items in the display.
     findRange: function (x, rects)
     {
       var answer = -1;  // assmume not found
       var idx = 0;
       if (hgTracks.revCmplDisp) {
         var rectsLen = rects.length - 1;
         for ( idx in rects ) {
            if ((rects[rectsLen-idx].x1 <= x) && (x < rects[rectsLen-idx].x2)) {
              answer = rectsLen-idx;
              break;
            }
         }
       } else {
         for ( idx in rects ) {
            if ((rects[idx].x1 <= x) && (x < rects[idx].x2)) {
              answer = idx;
              break;
            }
         }
       }
       return answer;
     },
 
     popUpDisappear: function () {
       if (mouseOver.visible) {        // should *NOT* have to keep track !*!
         mouseOver.visible = false;
         $('#mouseOverText').css('display','none');
         $('#mouseOverVerticalLine').css('display','none');
       }
       if (mouseOver.popUpTimer) {
          clearTimeout(mouseOver.popUpTimer);
          mouseOver.popUpTimer = null;
       }
       mouseOver.delayDone = true;
       mouseOver.delayInProgress = false;
     },
 
     popUpVisible: function () {
       if (! mouseOver.visible) {        // should *NOT* have to keep track !*!
         mouseOver.visible = true;
         $('#mouseOverText').css('display','block');
         $('#mouseOverVerticalLine').css('display','block');
       }
     },
 
     //  the evt.currentTarget.id is the td_data_<trackName> element of
     //     the track graphic.  There doesn't seem to be a evt.target.id ?
     mouseInTrackImage: function (evt)
     {
     // the center label also events here, can't use that
     //  plus there is a one pixel line under the center label that has no
     //   id name at all, so verify we are getting the event from the correct
     //   element.
     if (! evt.currentTarget.id.includes("td_data_")) { return; }
     var trackName = evt.currentTarget.id.replace("td_data_", "");
     if (trackName.length < 1) { return; }	// verify valid trackName
 
     // location of mouse relative to the whole page
     //     even when the top of page has scolled off
     var evtX = Math.floor(evt.pageX);
 //  var evtY = Math.floor(evt.pageY);
 //  var offX = Math.floor(evt.offsetX);       // no need for evtY or offX
 
     // find location of this <td> slice in the image, this is the track
     //   image in the graphic, including left margin and center label
     //   This location follows the window scrolling, could go negative
     var tdId  = document.getElementById(evt.currentTarget.id);
     var tdRect = tdId.getBoundingClientRect();
     var tdLeft = Math.floor(tdRect.left);
     var tdTop = Math.floor(tdRect.top);
 //    if (tdTop < 0) { return; }  // track is scrolled off top of screen
     var tdWidth = Math.floor(tdRect.width);
     var tdHeight = Math.floor(tdRect.height);
     var tdRight = tdLeft + tdWidth;
     // clientX is the X coordinate of the mouse hot spot
     var clientX = Math.floor(evt.clientX);
     var clientY = Math.floor(evt.clientY);
     // the graphOffset is the index (x coordinate) into the 'items' definitions
     //  of the data value boxes for the graph.  The magic number three
     //   is used elsewhere in this code, note the comment on the constant
     //   LEFTADD.
     var graphOffset = Math.max(0, clientX - tdLeft - 3);
     if (hgTracks.revCmplDisp) {
        graphOffset = Math.max(0, tdRight - clientX);
     }
 
     var windowUp = false;     // see if window is supposed to become visible
     var foundIdx = -1;
     if (mouseOver.items[trackName]) {
        foundIdx = mouseOver.findRange(graphOffset, mouseOver.items[trackName]);
     }
     // can show 'no data' when not found
     var mouseOverValue = mouseOver.noDataString;
     if (mouseOver.mouseOverFunction[trackName] === "noAverage") {
        mouseOverValue = mouseOver.noAverageString;
     }
     if (foundIdx > -1) { // value to display
         mouseOverValue = "&nbsp;" + mouseOver.items[trackName][foundIdx].v + "&nbsp;";
     }
     $('#mouseOverText').html(mouseOverValue);
     var msgWidth = mouseOver.maximumWidth[trackName];
     $('#mouseOverText').width(msgWidth);
     var msgHeight = Math.ceil($('#mouseOverText').height());
     var lineHeight = Math.max(0, tdHeight - msgHeight);
     if (tdTop < 0) {
        lineHeight = Math.max(0, tdHeight + tdTop - msgHeight);
     }
     var msgLeft = Math.max(tdLeft, clientX - (msgWidth/2) - 3); // with magic 3
     var msgTop = Math.max(0, tdTop);
     var lineTop = Math.max(0, msgTop + msgHeight);
     var lineLeft = Math.max(0, clientX - 1);  // magic 3 +  2 for width of indicator box
     if (clientY < msgTop + msgHeight) {	// cursor overlaps with the msg box
       msgLeft = clientX - msgWidth - 6;     // to the left of the cursor
       if (msgLeft < tdLeft || msgLeft < 0) {   // hits left edge, switch
         msgLeft = clientX;         // to right of cursor
       }
     } else {	// apply limits to left and right edges, window or image
       msgLeft = Math.min(msgLeft, tdRight - msgWidth);  // image right limit
       msgLeft = Math.min(msgLeft, $(window).width() - msgWidth); // window right
       msgLeft = Math.max(0, msgLeft);  // left window edge limit
     }
     $('#mouseOverText').css('top',msgTop + "px");
     $('#mouseOverText').css('left',msgLeft + "px");
     $('#mouseOverVerticalLine').css('left',lineLeft + "px");
     $('#mouseOverVerticalLine').css('top',lineTop + "px");
     $('#mouseOverVerticalLine').css('height',lineHeight + "px");
     windowUp = true;      // yes, window is to become visible
 
     if (windowUp) {     // the window should become visible
       mouseOver.popUpVisible();
     } else {    // the window should disappear
       mouseOver.popUpDisappear();
     } //      window visible/not visible
     },  //      mouseInTrackImage function (evt)
 
     // timeout calls here upon completion
     delayCompleted: function()
     {
        mouseOver.delayDone = true;
        // mouse could just be sitting there with no events, if there
        // have been events during the timer, the evt has been recorded
        // so the popUp appears where the mouse is while it moved during the
        // time delay since mostRecentMouseEvt is up to date to now
        // If mouse has moved out of element during timeout, the
        // delayInProgress will be false and nothing happens.
        if (mouseOver.delayInProgress) {
           mouseOver.mouseInTrackImage(mouseOver.mostRecentMouseEvt);
        }
     },
 
     // all mouse move events come here even during timeout
     mouseMoveDelay: function (evt)
     {
       mouseOver.mostRecentMouseEvt = evt;   // record evt for delayCompleted
 
       if (mouseOver.delayInProgress) {
         if (mouseOver.delayDone) {
           mouseOver.mouseInTrackImage(evt);	// OK to trigger event now
           return;
         } else {
           return; // wait for delay to be done
         }
       }
       mouseOver.delayDone = false;
       mouseOver.delayInProgress = true;
       if (mouseOver.popUpTimer) {
          clearTimeout(mouseOver.popUpTimer);
          mouseOver.popUpTimer = null;
       }
       mouseOver.popUpTimer = setTimeout(mouseOver.delayCompleted, mouseOver.popUpDelay);
     },
 
     // given a string of text, return width of rendered text size
     // using an off-screen span element that is created here first time through
     getWidthOfText: function (measureThis)
     {
     if(mouseOver.measureTextBox === null){  // set up first time only
         mouseOver.measureTextBox = document.createElement('span');
         var cssText = "position: fixed; width: auto; display: block; text-align: right; left:-999px; top:-999px; font-style:normal; font-size:" + mouseOver.browserTextSize + "px; font-family:" + jQuery('body').css('font-family');
         mouseOver.measureTextBox.style.cssText = cssText;
         document.body.appendChild(mouseOver.measureTextBox);
     }
     mouseOver.measureTextBox.innerHTML = measureThis;
     return Math.ceil(mouseOver.measureTextBox.clientWidth);
     },
 
     // =======================================================================
     // receiveData() callback for successful JSON request, receives incoming
     //             JSON data and gets it into global variables for use here.
     //  The incoming 'arr' is a a set of objects where the object key name is
     //      the track name, used here as an array reference: arr[trackName]
     //        (currently only one object per json file, one file for each track,
     //           this may change to have multiple tracks in one json file.)
     //  The value associated with each track name
     //  is an array of span definitions, where each element in the array is a
     //     mapBox definition object:
     //        {x1:n, x2:n, value:s}
     //        where n is an integer in the range: 0..width,
     //        and s is the value string to display
     //     Will need to get them sorted on x1 for efficient searching as
     //     they accumulate in the local data structure here.
     //  2020-11-24 more generalized incoming data structure, don't care
     //             what the structure is for each item, this will vary
     //             depending upon the type of track.  trackType now remembered
     //             in mouseOver.trackType[trackName]
     // =======================================================================
     receiveData: function (arr)
     {
       mouseOver.popUpDisappear();
       for (var trackName in arr) {
 	// clear these variables if they existed before
       if (mouseOver.trackType[trackName]) {mouseOver.trackType[trackName] = undefined;}
       if (mouseOver.items[trackName]) {mouseOver.items[trackName] = undefined;}
       if (mouseOver.tracks[trackName]) {mouseOver.tracks[trackName] = 0;}
       mouseOver.items[trackName] = [];      // start array
       mouseOver.trackType[trackName] = arr[trackName].t;
       if (arr[trackName].hasOwnProperty('mo')) {
          mouseOver.mouseOverFunction[trackName] = arr[trackName].mo;
       } else {
          delete mouseOver.mouseOverFunction[trackName];
       }
       // add a 'mousemove' and 'mouseout' event listener to each track
       //     display object
       var tdData = "td_data_" + trackName;
       var tdDataId  = document.getElementById(tdData);
 // from jQuery doc:
 //  As the .mousemove() method is just a shorthand
 //    for .on( "mousemove", handler ), detaching is possible
 //      using .off( "mousemove" ).
       $( tdDataId ).mousemove(mouseOver.mouseMoveDelay);
       $( tdDataId ).mouseout(mouseOver.popUpDisappear);
       var itemCount = 0;	// just for monitoring purposes
       // save incoming x1,x2,v data into the mouseOver.items[trackName][] array
       var lengthLongestNumberString = 0;
       var longestNumber = 0;
       var hasMean = false;
       for (var datum in arr[trackName].d) {      // .d is the data array
          if (arr[trackName].d[datum].c > 1) { hasMean = true; }
          var lenV = arr[trackName].d[datum].v.toString().length;
          if (lenV > lengthLongestNumberString) {
 	   lengthLongestNumberString = lenV;
 	   longestNumber = arr[trackName].d[datum].v;
          }
         mouseOver.items[trackName].push(arr[trackName].d[datum]);
        ++itemCount;
       }
       mouseOver.tracks[trackName] = itemCount;	// != 0 -> indicates valid track
       var mouseOverValue = "";
       if (hasMean) {
          mouseOverValue = "&nbsp;~&nbsp;" + longestNumber + "&nbsp;";
       } else {
          mouseOverValue = "&nbsp;" + longestNumber + "&nbsp;";
       }
       if (mouseOver.mouseOverFunction[trackName] === "noAverage") {
          mouseOverValue = mouseOver.noAverageString;
       }
       $('#mouseOverText').css('fontSize',mouseOver.browserTextSize);
       var maximumWidth = mouseOver.getWidthOfText(mouseOverValue);
       if ( 0 === mouseOver.noDataSize) {  // only need to do this once
         mouseOver.noDataSize = mouseOver.getWidthOfText(mouseOver.noDataString);
       }
       if (mouseOver.noDataSize > maximumWidth) {
           maximumWidth = mouseOver.noDataSize;
       }
       mouseOver.maximumWidth[trackName] = maximumWidth;
       }
     },  //      receiveData: function (arr)
 
     failedRequest: function(url)
     {   // failed request to get json data, remove it from the URL list
       if (mouseOver.jsonUrl[url]) {
         delete mouseOver.jsonUrl[url];
       }
     },
 
     // =========================================================================
     // fetchJsonData() sends JSON request, callback to receiveData() upon return
     // =========================================================================
     fetchJsonData: function (url)
     {
        // avoid fetching the same URL multiple times.  Multiple track data
        // can be in a single json file
        if (mouseOver.jsonUrl[url]) {
           mouseOver.jsonUrl[url] += 1;
           return;
        }
        mouseOver.jsonUrl[url] = 1;     // remember already done this one
        var xmlhttp = new XMLHttpRequest();
        xmlhttp.onreadystatechange = function() {
          if (4 === this.readyState && 200 === this.status) {
             var mapData = JSON.parse(this.responseText);
             mouseOver.receiveData(mapData);
          } else {
             if (4 === this.readyState && 404 === this.status) {
                mouseOver.failedRequest(url);
             }
          }
        };
     xmlhttp.open("GET", url, true);
     xmlhttp.send();  // sends request and exits this function
                      // the onreadystatechange callback above will trigger
                      // when the data has safely arrived
     },
 
     getData: function ()
     {
       // check for the hidden div elements for mouseOverData
       // single file version can find many trackNames, but will all
       // be the same URL
       var trackList = document.getElementsByClassName("mouseOverData");
       for (var i = 0; i < trackList.length; i++) {
         var trackName = trackList[i].getAttribute('name');
         var jsonFileUrl = trackList[i].getAttribute('jsonUrl');
         if (jsonFileUrl) { mouseOver.fetchJsonData(jsonFileUrl); }
       }
     },
 
     // any scrolling turns the popUp message off
     scroll: function()
     {
     if (mouseOver.visible) { mouseOver.popUpDisappear(); }
     },
 
     addListener: function () {
         mouseOver.visible = false;
         if (window.browserTextSize) {
            mouseOver.browserTextSize = window.browserTextSize;
         }
         window.addEventListener('scroll', mouseOver.scroll, false);
         window.addEventListener('load', mouseOver.getData, false);
     }
 };	//	var mouseOver
 
   //////////////////////
  //// 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").
             // NOTE: must match TRACK_SEARCH_PAGER in hg/inc/searchTracks.h
             $("input[name=hgt_tsPage]").val(0);  
             $('#trackSearch').submit();
             // This doesn't work with IE or Safari.
             // $('#searchSubmit').click();
         }
     },
 
     init: function ()
     {
         // Track search uses tabs
         if ($("#tabs").length > 0) {
             // Search page specific code
 
             var val = $('#currentTab').val();
             $("#tabs").tabs({
                                 show: function(event, ui) {
                                     $('#currentTab').val(ui.panel.id);
                                 },
                                 select: function(event, ui) { findTracks.switchTabs(ui); }
                             });
             $('#tabs').show();
             $("#tabs").tabs('option', 'selected', '#' + val);
             if (val === 'simpleTab' && $('div#found').length < 1) {
                 $('input#simpleSearch').focus();
             }
             $("#tabs").css('font-family', jQuery('body').css('font-family'));
             $("#tabs").css('font-size', jQuery('body').css('font-size'));
             $('.submitOnEnter').keydown(trackSearch.searchKeydown);
             findTracks.normalize();
             findTracks.updateMdbHelp(0);
         }
     }
 };
 
 ////////
 // Download Current Tracks in window Dialog
 ////////
 var downloadCurrentTrackData = {
     downloadData: {}, // container for holding data while it comes in from the api
     currentRequests: {}, // pending requests
     intervalId: null, // the id of the timer that waits on the api
 
     failedTrackDataRequest: function(msg) {
         msgJson = JSON.parse(msg);
         alert("Download failed. Error message: '" + msgJson.error);
     },
 
     receiveTrackData: function(track, data) {
         downloadCurrentTrackData.downloadData[track] = data;
     },
 
     convertJson: function(data, outType) {
         if (outType !== "tsv" && outType !== "csv") {
             alert("ERROR: incorrect output format option");
             return null;
         }
         outSep = outType === "tsv" ? '\t' : ',';
         // TODO: someday we will probably want to include some of these fields
         // for each track downloaded, perhaps as an option
         ignoredKeys = new Set(["chrom", "dataTime", "dataTimeStamp", "downloadTime", "downloadTimeStamp",
             "start", "end", "track", "trackType", "genome", "itemsReturned", "columnTypes",
             "bigDataUrl", "chromSize", "hubUrl"]);
         // first get rid of top level non track object keys
         _.each(data, function(val, key) {
             if (ignoredKeys.has(key)) {delete data[key];}
         });
         // now go through each track and format it correctly
         str = "";
         _.each(data, function(val, track) {
             str += "track name=\"" + track + "\"\n";
             for (var row in val) {
                 for (var  i = 0; i < val[row].length; i++) {
                     str += JSON.stringify(val[row][i]);
                     if (i < val[row].length) { str += outSep; }
                 }
                 str += "\n";
             }
             str += "\n"; // extra new line after each track oh well
         });
         return new Blob([str], {type: "text/plain"});
     },
 
     makeDownloadFile: function(key) {
         if (_.keys(downloadCurrentTrackData.currentRequests).length === 0) {
             // first stop the timer so we don't execute again
             clearInterval(downloadCurrentTrackData.intervalId);
             outType = $("#outputFormat")[0].selectedOptions[0].value;
             var blob = null;
             if (outType === 'json') {
                 blob = new Blob([JSON.stringify(downloadCurrentTrackData.downloadData[key])], {type: "text/plain"});
             } else {
                 blob = downloadCurrentTrackData.convertJson(downloadCurrentTrackData.downloadData[key], outType);
             }
             if (blob) {
                 anchor = document.createElement("a");
                 anchor.href = URL.createObjectURL(blob);
                 fname = $("#downloadFileName")[0].value;
                 if (fname.length === 0) {
                     fname = "trackDownload.txt";
                 }
                 switch (outType) {
                     case "tsv":
                         if (!fname.endsWith(".tsv")) {fname += ".tsv";}
                         break;
                     case "csv":
                         if (!fname.endsWith(".csv")) {fname += ".csv";}
                         break;
                     default:
                         if (!fname.endsWith(".txt")) {fname += ".txt";}
                         break;
                 }
                 anchor.download = fname;
                 anchor.click();
                 window.URL.revokeObjectURL(anchor.href);
                 downloadCurrentTrackData.downloadData = {};
             }
         }
     },
 
     startDownload: function() {
         trackList = [];
         $(".downloadTrackName:checked").each(function(i, elem) {
             trackList.push(undecoratedTrack(elem.id));
         });
         chrom = hgTracks.chromName;
         start = hgTracks.winStart;
         end = hgTracks.winEnd;
         db = undecoratedDb(getDb());
         apiUrl = "../cgi-bin/hubApi/getData/track?";
         apiUrl += "chrom=" + chrom;
         apiUrl += ";start=" + start;
         apiUrl += ";end=" + end;
         apiUrl += ";genome=" + db;
         apiUrl += ";jsonOutputArrays=1";
         apiUrl += ";track=" + trackList.join(',');
         var xmlhttp = new XMLHttpRequest();
         downloadCurrentTrackData.currentRequests[apiUrl] = true;
         xmlhttp.onreadystatechange = function() {
             if (4 === this.readyState && 200 === this.status) {
                 var mapData = JSON.parse(this.responseText);
                 downloadCurrentTrackData.receiveTrackData(apiUrl, mapData);
                 delete downloadCurrentTrackData.currentRequests[apiUrl];
             } else {
                 if (4 === this.readyState && this.status >= 400) {
                     clearInterval(downloadCurrentTrackData.intervalId);
                     downloadCurrentTrackData.failedTrackDataRequest(this.responseText);
                     delete downloadCurrentTrackData.currentRequests[apiUrl];
                 }
             }
         };
         xmlhttp.open("GET", apiUrl, true);
         xmlhttp.send();  // sends request and exits this function
         // the onreadystatechange callback above will trigger
         // when the data has safely arrived
         // wait for the request to complete before making the download file
         downloadCurrentTrackData.intervalId = setInterval(downloadCurrentTrackData.makeDownloadFile, 200, apiUrl);
     },
 
 
     showDownloadUi: function() {
         // Populate the dialog with the current list of tracks
         // and allow the user to select which ones to download
         // Grey out tracks that are currently unsupported by the api
         // or are protected data
         var downloadDialog = $("#downloadDialog")[0];
         if (!downloadDialog) {
             downloadDialog = document.createElement("div");
             downloadDialog.id = "downloadDialog";
             downloadDialog.style = "display: none";
             document.body.append(downloadDialog);
             var popMaxHeight = ($(window).height() - 40);
             var popMaxWidth  = ($(window).width() - 40);
             var popWidth     = 700;
             if (popWidth > popMaxWidth)
                 popWidth = popMaxWidth;
             downloadTrackDataButtons = {};
             downloadTrackDataButtons.Download = downloadCurrentTrackData.startDownload;
             downloadTrackDataButtons.Cancel = function(){
                 $(this).dialog("close");
             };
             $(downloadDialog).dialog({
                 title: "Download track data in view",
                 resizable: true,               // Let scroll vertically
                 height: 'auto',
                 width: popWidth,
                 minHeight: 200,
                 minWidth: 400,
                 maxHeight: popMaxHeight,
                 maxWidth: popMaxWidth,
                 modal: true,
                 closeOnEscape: true,
                 autoOpen: false,
                 buttons: downloadTrackDataButtons
             });
         }
         htmlStr = "<p>Use this selection window to download track data" +
             " for the current region (" + genomePos.get() + "). Please note that large regions" +
             " may be slow to download.</p>";
         htmlStr  += "<div><button id='checkAllDownloadTracks'>Check All</button>" +
             "&nbsp;" +
             "<button id='uncheckAllDownloadTracks'>Clear All</button>" +
             "</div>";
         _.each(hgTracks.trackDb, function(track, trackName) {
             if (!trackName.includes("Squish") && trackName !== "ruler" && track.visibility > 0) {
                 htmlStr += "<input type=checkbox checked class='downloadTrackName' id='" + trackName + "'>";
                 htmlStr += "<label>" + track.shortLabel + "</label>";
                 htmlStr += "</input>";
                 htmlStr += "<br>";
             }
         });
         htmlStr += "<div ><label style='padding-right: 10px' for='downloadFileName'>Enter an output file name</label>";
         htmlStr += "<input type=text size=30 class='downloadFileName' id='downloadFileName'" +
             " value='" + getDb() + ".tracks'</input>";
         htmlStr += "<br>";
         htmlStr += "<label style='padding-right: 10px' for='outputFormat'>Choose an output format</label>";
         htmlStr += "<select name='outputFormat' id='outputFormat'>";
         htmlStr += "<option selected value='json'>JSON</option>";
         htmlStr += "<option value='csv'>CSV</option>";
         htmlStr += "<option value='tsv'>TSV</option>";
         htmlStr += "</select>";
         htmlStr += "</div>";
         downloadDialog.innerHTML = htmlStr;
         $("#checkAllDownloadTracks").click( function() {
             $(".downloadTrackName").each(function(i, elem) {
                 elem.checked = true;
             });
         });
         $("#uncheckAllDownloadTracks").click( function() {
             $(".downloadTrackName").each(function(i, elem) {
                 elem.checked = false;
             });
         });
         $(downloadDialog).dialog('open');
     }
 };
 
 
   ///////////////
  //// READY ////
 ///////////////
 $(document).ready(function()
 {
     imageV2.moveTiming();
 
     // hg.conf will turn this on 2020-10 - Hiram
     if (window.mouseOverEnabled) { mouseOver.addListener(); }
 
     // on Safari the back button doesn't call the ready function.  Reload the page if
     // the back button was pressed.
     $(window).bind("pageshow", function(event) {
         if (event.originalEvent.persisted) {
                 window.location.reload() ;
         }
     });
 
     // 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.
             imageV2.markAsCleanPage();
             jQuery('body').css('cursor', 'wait');
             window.location = "../cgi-bin/hgTracks?hgsid=" + getHgsid();
             return false;
         }
     }
 
     initVars();
     imageV2.loadSuggestBox();
     if ($('#pdfLink').length === 1) {
         $('#pdfLink').click(function(i) {
             var thisForm = normed($('#TrackForm'));
             if (thisForm) {
                 //alert("posting form:"+$(thisForm).attr('name'));
                 updateOrMakeNamedVariable($(thisForm),'hgt.psOutput','on');
                 return postTheForm($(thisForm).attr('name'),this.href);
             }
             return true;
         });
     }
 
     if (imageV2.enabled) {
 
         // Make imgTbl allow drag reorder of imgTrack rows
         dragReorder.init();
         var imgTable = $(".tableWithDragAndDrop");
         if ($(imgTable).length > 0) {
             $(imgTable).tableDnD({
                 onDragClass: "trDrag",
                 dragHandle: "dragHandle",
                 scrollAmount: 40,
                 onDragStart: function(ev, table, row) {
                     mouse.saveOffset(ev);
                     $(document).bind('mousemove',posting.blockTheMapOnMouseMove);
 
                     // Can drag a contiguous set of rows if dragging blue button
                     table.tableDnDConfig.dragObjects = [ row ]; // defaults to just the one
                     var btn = $( row ).find('p.btnBlue');  // btnBlue means cursor over left button
                     if (btn.length === 1) {
                         table.tableDnDConfig.dragObjects = dragReorder.getContiguousRowSet(row);
                         var compositeSet = dragReorder.getCompositeSet(row);
                         if (compositeSet && compositeSet.length > 0)
                             $( compositeSet ).find('p.btn').addClass('blueButtons');// blue persists
                     }
                 },
                 onDrop: function(table, row, dragStartIndex) {
                     var compositeSet = dragReorder.getCompositeSet(row);
                     if (compositeSet && compositeSet.length > 0)
                         $( compositeSet ).find('p.btn').removeClass('blueButtons');// blue persists
                     if ($(row).attr('rowIndex') !== dragStartIndex) {
                         // NOTE Even if dragging a contiguous set of rows,
                         // still only need to check the one under the cursor.
                         if (dragReorder.setOrder) {
                             dragReorder.setOrder(table);
                         }
                         dragReorder.zipButtons( table );
                     }
                     $(document).unbind('mousemove',posting.blockTheMapOnMouseMove);
                     // Timeout necessary incase the onDrop over map item. onDrop takes precedence.
                     setTimeout(posting.allowMapClicks,100);
                 }
             });
         }
 
         // Drag scroll init
         if (hgTracks.imgBoxPortal) {
             // Turn on drag scrolling.
             $("div.scroller").panImages();
         }
 
         // Retrieve tracks via AJAX that may take too long to draw initialliy (i.e. a remote bigWig)
         var retrievables = $('#imgTbl').find("tr.mustRetrieve");
         if ($(retrievables).length > 0) {
             $(retrievables).each( function (i) {
                 var trackName = $(this).attr('id').substring(3);
                 imageV2.requestImgUpdate(trackName,"","");
             });
         }
         imageV2.loadRemoteTracks();
         makeItemsByDrag.load();
         
         // Any highlighted region must be shown and warnBox must play nice with it.
         imageV2.drawHighlights();
         // When warnBox is dismissed, any image highlight needs to be redrawn.
         $('#warnOK').click(function (e) { imageV2.drawHighlights();});
         // Also extend the function that shows the warn box so that it too redraws the highlight.
         showWarnBox = (function (oldShowWarnBox) {
             function newShowWarnBox() {
                 oldShowWarnBox.apply();
                 imageV2.drawHighlights();
             }
             return newShowWarnBox;
         })(showWarnBox);
     }
 
     // Drag select in chromIdeogram
     if ($('img#chrom').length === 1) {
         if ($('area.cytoBand').length >= 1) {
             $('img#chrom').chromDrag();
         }
     }
 
     // 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);
         }
     }
 
     // jquery.history.js Back-button support
     if (imageV2.enabled && imageV2.backSupport) {
         imageV2.setupHistory();
     }
 
     // ensure clicks into hgTrackUi save the cart state
     $("td a").each( function (tda) {
         if (this.href && this.href.indexOf("hgTrackUi") !== -1) {
             this.onclick = posting.saveSettings;
         }
     });
 
 
-    // add a button to download the current track data (under hg.conf control)
+    // add a 'link' to download the current track data (under hg.conf control)
     if (typeof showDownloadButton !== 'undefined' && showDownloadButton) {
-        newButton = document.createElement("input");
-        newButton.setAttribute("id", "hgTracksDownload");
-        newButton.setAttribute("type", "button");
-        newButton.setAttribute("name", "downloadTracks");
-        newButton.setAttribute("value", "download current tracks");
-        newButton.setAttribute("title", "download track data in window");
-        // add a space to match the other buttons
-        $("#hgt\\.setWidth")[0].parentNode.appendChild(document.createTextNode(" "));
-        $("#hgt\\.setWidth")[0].parentNode.appendChild(newButton);
+        newListEl = document.createElement("li");
+        newLink = document.createElement("a");
+        newLink.setAttribute("id", "hgTracksDownload");
+        newLink.setAttribute("name", "downloadTracks");
+        newLink.textContent = "Download Current Track Data";
+        newLink.href = "#";
+        newListEl.appendChild(newLink);
+        $("#downloads > ul")[0].appendChild(newListEl);
         $("#hgTracksDownload").click(downloadCurrentTrackData.showDownloadUi);
     }
     
 });