b508259e8ce75a2aa1f919b8152befea99a2e1df chmalee Fri Jan 27 10:31:59 2023 -0800 Prototype download track data button. An hg.conf controlled button that allows users to download all of the track data in the current window without leaving hgTracks, refs #30024 diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js index cd11a07..5eb2718 100644 --- src/hg/js/hgTracks.js +++ src/hg/js/hgTracks.js @@ -5101,30 +5101,233 @@ }); $('#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"]); + // 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(elem.id); + }); + chrom = hgTracks.chromName; + start = hgTracks.winStart; + end = hgTracks.winEnd; + db = 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(','); + // strip off final comma: + 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"; + 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 !== "ruler") { + 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; + document.body.append(downloadDialog); + $("#checkAllDownloadTracks").click( function() { + $(".downloadTrackName").each(function(i, elem) { + elem.checked = true; + }); + }); + $("#uncheckAllDownloadTracks").click( function() { + $(".downloadTrackName").each(function(i, elem) { + elem.checked = false; + }); + }); + 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 + }); + } + $(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) { @@ -5257,16 +5460,31 @@ } } // 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) + 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); + $("#hgTracksDownload").click(downloadCurrentTrackData.showDownloadUi); + } + });