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 + " " + 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 + " " + (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 > Highlight operations, click 'Save Color' below" + "</ul></p>" + "<p>Highlight color: <input type='text' style='width:70px' id='hlColorInput' value='"+dragSelect.loadHlColor()+"'>" + " <input id='hlColorPicker'>" + " <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 < "+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. " + "<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 data", // message for no data at this position noDataSize: 0, // will be set to size of text 'no data' noAverageString: " zoom in to see values ", // "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 = " " + mouseOver.items[trackName][foundIdx].v + " "; } $('#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 = " ~ " + longestNumber + " "; } else { mouseOverValue = " " + longestNumber + " "; } 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>" + " " + "<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); } });