Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement image exporting #183

Merged
merged 14 commits into from
Dec 2, 2020
4 changes: 2 additions & 2 deletions metagenomescope/support_files/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
-->
<button
class="btn btn-default btn-sm disabled drawCtrl"
onclick="exportGraphView();"
id="floatingExportButton"
disabled="disabled"
>
Expand Down Expand Up @@ -594,6 +593,7 @@ <h4>Select paths ("Finishing")</h4>

<hr />
</div>
-->
<div id="exportGraphViewControls">
<h4>Export Graph View</h4>
<div
Expand Down Expand Up @@ -633,12 +633,12 @@ <h4>Export Graph View</h4>
class="btn btn-default btn-sm disabled drawCtrl"
disabled="disabled"
id="exportImageButton"
onclick="exportGraphView();"
>
<span class="glyphicon glyphicon-camera"></span> &nbsp;
Export
</button>
</div>
<!--
<div id="testLayoutsControls">
<hr />
<h4>Test Layouts</h4>
Expand Down
26 changes: 26 additions & 0 deletions metagenomescope/support_files/js/app-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ define(["jquery", "underscore", "drawer", "utils", "dom-utils"], function (
var searchFunc = this.searchForNodes.bind(this);
$("#searchButton").click(searchFunc);
domUtils.setEnterBinding("searchInput", searchFunc);

// Graph image export buttons
// (one is in the top-right of the graph display, another is in the
// node selection menu)
var exportFunc = this.exportGraphView.bind(this);
$("#floatingExportButton").click(exportFunc);
$("#exportImageButton").click(exportFunc);
}

/**
Expand Down Expand Up @@ -737,6 +744,25 @@ define(["jquery", "underscore", "drawer", "utils", "dom-utils"], function (
// the "fit graph" buttons)
domUtils.enableDrawNeededControls();
}

/**
* Exports an image of the graph, calling downloadDataURI() to prompt
* the user.
*
* The image filetype is determined using a button group in the control
* panel.
*/
exportGraphView() {
// Should be either "PNG" or "JPG"
var imgType = $("#imgTypeButtonGroup .btn.active").attr("value");
var encodedImage = this.drawer.exportImage(imgType);
var fn =
"mgsc-" +
utils.getFancyTimestamp(new Date()) +
"." +
imgType.toLowerCase();
domUtils.downloadDataURI(fn, encodedImage, false);
}
}
return { AppManager: AppManager };
});
38 changes: 38 additions & 0 deletions metagenomescope/support_files/js/dom-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,43 @@ define(["jquery", "underscore", "utils"], function ($, _, utils) {
});
}

/**
* Uses the downloadHelper <a> element to prompt the user to save a data URI
* to their system.
*
* I don't remember how I originally wrote this function, but the
* method used here is basically the same as that shown in
* https://stackoverflow.com/a/15832662.
*
* @param {String} filename Filename to save the exported file as.
* @param {String} contentToDownload Stuff to download. May be either
* plain text (in which case you
* should set isPlainText to true),
* or encoded stuff (already a valid
* data URI).
* @param {Boolean} isPlainText If true, then this will treat
* contentToDownload as text/plain data
* (appending the necessary prefixes for
* constructing a data URI, and calling
* window.btoa() on contentToDownload).
* If this is false, however, then this
* won't append any prefixes to
* contentToDownload and won't call
* window.btoa() on it.
*/
function downloadDataURI(filename, contentToDownload, isPlainText) {
$("#downloadHelper").attr("download", filename);
if (isPlainText) {
var data =
"data:text/plain;charset=utf-8;base64," +
window.btoa(contentToDownload);
$("#downloadHelper").attr("href", data);
} else {
$("#downloadHelper").attr("href", contentToDownload);
}
document.getElementById("downloadHelper").click();
}

return {
enablePersistentControls: enablePersistentControls,
disablePersistentControls: disablePersistentControls,
Expand All @@ -187,5 +224,6 @@ define(["jquery", "underscore", "utils"], function ($, _, utils) {
decrCompRank: decrCompRank,
incrCompRank: incrCompRank,
setEnterBinding: setEnterBinding,
downloadDataURI: downloadDataURI,
};
});
22 changes: 22 additions & 0 deletions metagenomescope/support_files/js/drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,28 @@ define([
}
return notFoundNames;
}

/**
* Returns a base64-encoded image of the graph.
*
* This is basically just a wrapper for cy.png() or cy.jpg().
*
* @param {String} imgType Should be either "PNG" or "JPG".
*
* @returns {String} encodedImage
*
* @throws {Error} if imgType is not "PNG" or "JPG".
*/
exportImage(imgType) {
var options = { bg: this.bgColor };
if (imgType === "PNG") {
return this.cy.png(options);
} else if (imgType === "JPG") {
return this.cy.jpg(options);
} else {
throw new Error("Unrecognized imgType: " + imgType);
}
}
}
return { Drawer: Drawer };
});
34 changes: 0 additions & 34 deletions metagenomescope/support_files/js/old.js
Original file line number Diff line number Diff line change
Expand Up @@ -2503,29 +2503,6 @@ function importColorSettings() {
}
}

/* Uses the downloadHelper <a> element to prompt the user to save a data URI
* to their system.
*
* If the isPlainText argument is true, then this will treat contentToDownload
* as text/plain data (appending the necessary prefixes for constructing a data
* URI, and calling window.btoa() on contentToDownload).
* If isPlainText is false, however, then this won't append any prefixes to
* contentToDownload and won't call window.btoa() on it.
*/
function downloadDataURI(filename, contentToDownload, isPlainText) {
"use strict";
$("#downloadHelper").attr("download", filename);
if (isPlainText) {
var data =
"data:text/plain;charset=utf-8;base64," +
window.btoa(contentToDownload);
$("#downloadHelper").attr("href", data);
} else {
$("#downloadHelper").attr("href", contentToDownload);
}
document.getElementById("downloadHelper").click();
}

function clearSelectedInfo() {
"use strict";
$("#nodeInfoTable tr.nonheader").remove();
Expand All @@ -2542,17 +2519,6 @@ function clearSelectedInfo() {
}
}

/* Exports image of graph. */
function exportGraphView() {
"use strict";
var imgType = $("#imgTypeButtonGroup .btn.active").attr("value");
if (imgType === "PNG") {
downloadDataURI("screenshot.png", cy.png({ bg: mgsc.BG_COLOR }), false);
} else {
downloadDataURI("screenshot.jpg", cy.jpg({ bg: mgsc.BG_COLOR }), false);
}
}

/* Opens the dialog for filtering edges. */
function openEdgeFilteringDialog() {
"use strict";
Expand Down
65 changes: 65 additions & 0 deletions metagenomescope/support_files/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,69 @@ define(["underscore"], function (_) {
return s;
}

/**
* Given a number, converts to a String and makes it have two digits.
*
* e.g. leftPad(9) -> "09"
* leftPad(12) -> "12"
*
* @param {Number} x Must be an integer and in the range [0, 99], or this
* will throw an error.
*
* @return {String} xPadded
*/
function leftPad(x) {
if (Number.isInteger(x)) {
if (x >= 0 && x <= 99) {
return String(x).padStart(2, 0);
} else {
throw new Error(
"Argument to leftPad() not in the range [0, 99]: " + x
);
}
} else {
throw new Error("Argument to leftPad() not an integer: " + x);
}
}

/**
* Returns a String timestamp of the format "YYYY-MM-DDThh:mm:ss".
*
* This should be https://xkcd.com/1179 compliant. And ISO-8601
* compliant... uh, I think. Don't hold me to that :P
*
* Hours are in "military time" (American-speak for "5:00pm is 17:00")
* because that's the default of Date.prototype.getHours().
*
* Intended to produce useful, non-overlapping filenames for screenshots.
*
* This should be decently robust, but if you're reading this because it
* broke while you were in a plane across the date line on a leap year or
* something then I am sorry, please go open an issue and yell at me.
*
* FYI -- If you want a timestamp from the current time, you can just call
* getFancyTimestamp(new Date()).
*
* @param {Date} d Date object to convert to a timestamp.
*
* @returns {String} timestamp
*/
function getFancyTimestamp(d) {
var timestamp = "";
timestamp += d.getFullYear() + "-";
// Date.prototype.getMonth() is 0-indexed ._.
timestamp += leftPad(d.getMonth() + 1) + "-";
// ...but getDate() isn't. (I guess getHours(), getMinutes(), and
// getSeconds() are, though? But those are already 0-indexed sooooo...)
// Oh, also: the reason we use "T" to separate the date (YYYY-MM-DD)
// and the time (hh:mm:ss) is because that's what ISO 8601 requires.
timestamp += leftPad(d.getDate()) + "T";
timestamp += leftPad(d.getHours()) + ":";
timestamp += leftPad(d.getMinutes()) + ":";
timestamp += leftPad(d.getSeconds());
return timestamp;
}

return {
getNodeColorization: getNodeColorization,
degreesToRadians: degreesToRadians,
Expand All @@ -334,5 +397,7 @@ define(["underscore"], function (_) {
getHumanReadablePatternType: getHumanReadablePatternType,
searchNodeTextToArray: searchNodeTextToArray,
arrToHumanReadableString: arrToHumanReadableString,
getFancyTimestamp: getFancyTimestamp,
leftPad: leftPad,
};
});