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>" +
+                "&nbsp;" + 
+                "<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);
+    }
+    
 });