diff --git a/public/help/help.md b/public/help/help.md index a4b014ed..295a6d29 100644 --- a/public/help/help.md +++ b/public/help/help.md @@ -27,5 +27,6 @@ The following procedure describes adding and updating settings of custom tracks. * A start position and a distance (e.g. "chr1:1+100") * A node ID anchor and a distance (e.g. "node:100+10") ![Region Input Options](helpGuideImages/img8.png) -9. Click Go to see the selected tracks render in the visualization area. -![Go Button](helpGuideImages/img9.png) \ No newline at end of file +4. If simplifying the BED file chunk or graph is possible, users will see a "Simplify Off" button, which when clicked with toggle to "Simplify On". This option enables vg simplify, which would remove small snarls. This option will only appear when there aren't any reads to be displayed. +5. Click Go to see the selected tracks render in the visualization area. +![Go Button](helpGuideImages/img9.png) diff --git a/src/common.mjs b/src/common.mjs index ae001cc8..233a9260 100644 --- a/src/common.mjs +++ b/src/common.mjs @@ -111,6 +111,16 @@ export function defaultTrackColors(trackType){ } } +/* Function to determine if any of the tracks are reads, where the tracks parameter is an object of track types */ +export function readsExist(tracks){ + for (let key in tracks){ + if (tracks[key].trackType === "read"){ + return true; + } + } + return false; +} + // Accepts a string, returns whether or not the input is a valid http URL we can call fetch on export function isValidURL(string) { if (!string) { @@ -130,3 +140,4 @@ export function isValidURL(string) { export function isEmpty(obj) { return Object.keys(obj).length === 0; } + diff --git a/src/components/CopyLink.js b/src/components/CopyLink.js index 30505a43..a9f38b6d 100644 --- a/src/components/CopyLink.js +++ b/src/components/CopyLink.js @@ -81,9 +81,15 @@ export const urlParamsToViewTarget = (url) => { result = qs.parse(s[1]); } - // TODO: qs can't tell the difference between false and "false", and "false" - // is truthy. So we need to go through and coerce things to real booleans at - // some point. + // Ensures that the simplify field is a boolean, as the qs module can't tell + // the difference between false and "false" + if (result != null){ + if (result.simplify === "true"){ + result.simplify = true; + } else if (result.simplify === "false"){ + result.simplify = false; + } + } return result; }; diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 3f4d6f09..2f408728 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -1,6 +1,6 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import { Container, Row, Col, Label, Alert } from "reactstrap"; +import { Container, Row, Col, Label, Alert, Button } from "reactstrap"; import { dataOriginTypes } from "../enums"; import { fetchAndParse } from "../fetchAndParse"; import "../config-client.js"; @@ -10,7 +10,7 @@ import ExampleSelectButtons from "./ExampleSelectButtons"; import RegionInput from "./RegionInput"; import TrackPicker from "./TrackPicker"; import BedFileDropdown from "./BedFileDropdown"; -import { parseRegion, stringifyRegion, isEmpty } from "../common.mjs"; +import { parseRegion, stringifyRegion, isEmpty, readsExist } from "../common.mjs"; // See src/Types.ts @@ -108,7 +108,7 @@ function tracksEqual(curr, next) { return false; } } - //count falsy file names as the same + // count falsy file names as the same if ((!curr_file && !next_file) || curr_file === next_file) { return true; } @@ -155,11 +155,14 @@ function viewTargetsEqual(currViewTarget, nextViewTarget) { return false; } + if (currViewTarget.simplify !== nextViewTarget.simplify){ + return false; + } + return true; } - class HeaderForm extends Component { state = EMPTY_STATE; componentDidMount() { @@ -208,6 +211,7 @@ class HeaderForm extends Component { region: ds.region, dataType: ds.dataType, name: ds.name, + simplify: ds.simplify }; return stateVals; }); @@ -465,6 +469,7 @@ class HeaderForm extends Component { name: this.state.name, region: this.state.region, dataType: this.state.dataType, + simplify: this.state.simplify && !(readsExist(this.state.tracks)) }); handleGoButton = () => { @@ -713,6 +718,11 @@ class HeaderForm extends Component { }; }; +/* Function for toggling simplify button, enabling vg simplify to be turned on or off */ +toggleSimplify = () => { + this.setState( { simplify: !this.state.simplify }); + } + render() { let errorDiv = null; if (this.state.error) { @@ -752,6 +762,7 @@ class HeaderForm extends Component { const examplesFlag = this.state.dataType === dataTypes.EXAMPLES; const viewTargetHasChange = !viewTargetsEqual(this.getNextViewTarget(), this.props.getCurrentViewTarget()); + console.log( "Rendering header form with fileSelectOptions: ", this.state.fileSelectOptions @@ -841,7 +852,12 @@ class HeaderForm extends Component { onChange={this.handleInputChange} handleFileUpload={this.handleFileUpload} > - + {/* Button for simplify */} + { + !(readsExist(this.state.tracks)) && + + } + } diff --git a/src/config.json b/src/config.json index 20128fa0..a7f10d20 100644 --- a/src/config.json +++ b/src/config.json @@ -10,7 +10,8 @@ "dataPath": "default", "region": "17:1-100", "bedFile": "exampleData/internal/snp1kg-BRCA1.bed", - "dataType": "built-in" + "dataType": "built-in", + "simplify": false }, { "name": "vg \"small\" example", @@ -91,4 +92,5 @@ "pickerTypeOptions": ["mounted", "upload"], "fileExpirationTime": 86400 + } diff --git a/src/end-to-end.test.js b/src/end-to-end.test.js index a7f08345..bee3457d 100644 --- a/src/end-to-end.test.js +++ b/src/end-to-end.test.js @@ -344,7 +344,7 @@ describe("When we wait for it to load", () => { it("produces correct link for view before & after go is pressed", async () => { // First test that after pressing go, the link reflects the dat form const expectedLinkBRCA1 = - "http://localhost?name=snp1kg-BRCA1&tracks[0][trackFile]=exampleData%2Finternal%2Fsnp1kg-BRCA1.vg.xg&tracks[0][trackType]=graph&tracks[0][trackColorSettings][mainPalette]=greys&tracks[0][trackColorSettings][auxPalette]=ygreys&tracks[1][trackFile]=exampleData%2Finternal%2FNA12878-BRCA1.sorted.gam&tracks[1][trackType]=read&dataPath=default®ion=17%3A1-100&bedFile=exampleData%2Finternal%2Fsnp1kg-BRCA1.bed&dataType=built-in"; + "http://localhost?name=snp1kg-BRCA1&tracks[0][trackFile]=exampleData%2Finternal%2Fsnp1kg-BRCA1.vg.xg&tracks[0][trackType]=graph&tracks[0][trackColorSettings][mainPalette]=greys&tracks[0][trackColorSettings][auxPalette]=ygreys&tracks[1][trackFile]=exampleData%2Finternal%2FNA12878-BRCA1.sorted.gam&tracks[1][trackType]=read&dataPath=default®ion=17%3A1-100&bedFile=exampleData%2Finternal%2Fsnp1kg-BRCA1.bed&dataType=built-in&simplify=false"; // Set up dropdown await act(async () => { let dropdown = document.getElementById("dataSourceSelect"); @@ -378,7 +378,7 @@ it("produces correct link for view before & after go is pressed", async () => { await clickCopyLink(); const expectedLinkCactus = - "http://localhost?tracks[0][trackFile]=exampleData%2Fcactus.vg.xg&tracks[0][trackType]=graph&tracks[1][trackFile]=exampleData%2Fcactus-NA12879.sorted.gam&tracks[1][trackType]=read&bedFile=exampleData%2Fcactus.bed&name=cactus®ion=ref%3A1-100&dataType=built-in"; + "http://localhost?tracks[0][trackFile]=exampleData%2Fcactus.vg.xg&tracks[0][trackType]=graph&tracks[1][trackFile]=exampleData%2Fcactus-NA12879.sorted.gam&tracks[1][trackType]=read&bedFile=exampleData%2Fcactus.bed&name=cactus®ion=ref%3A1-100&dataType=built-in&simplify=false"; // Make sure link has changed after pressing go expect(fakeClipboard).toEqual(expectedLinkCactus); }, 20000); diff --git a/src/server.mjs b/src/server.mjs index a6fd896f..065dea55 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -21,7 +21,7 @@ import { server as WebSocketServer } from "websocket"; import dotenv from "dotenv"; import dirname from "es-dirname"; import { readFileSync, writeFile } from 'fs'; -import { parseRegion, convertRegionToRangeRegion, stringifyRangeRegion, stringifyRegion, isValidURL } from "./common.mjs"; +import { parseRegion, convertRegionToRangeRegion, stringifyRangeRegion, stringifyRegion, isValidURL, readsExist } from "./common.mjs"; import { Readable } from "stream"; import { finished } from "stream/promises"; import sanitize from "sanitize-filename"; @@ -478,6 +478,14 @@ async function getChunkedData(req, res, next) { req.withBed = false; console.log("no BED file provided."); } + // client is going to send simplify = true if they want to simplify view + req.simplify = false; + if (req.body.simplify){ + if (readsExist(req.body.tracks)){ + throw new BadRequestError("Simplify cannot be used on read tracks."); + } + req.simplify = true; + } // check the bed file if this region has been pre-fetched let chunkPath = ""; @@ -585,6 +593,13 @@ async function getChunkedData(req, res, next) { console.time("vg chunk"); const vgChunkCall = spawn(`${VG_PATH}vg`, vgChunkParams); + // vg simplify for gam files + let vgSimplifyCall = null; + if (req.simplify){ + vgSimplifyCall = spawn(`${VG_PATH}vg`, ["simplify", "-"]); + console.log("Spawning vg simplify call"); + } + const vgViewCall = spawn(`${VG_PATH}vg`, ["view", "-j", "-"]); let graphAsString = ""; req.error = Buffer.alloc(0); @@ -611,12 +626,20 @@ async function getChunkedData(req, res, next) { }); vgChunkCall.stdout.on("data", function (data) { - vgViewCall.stdin.write(data); + if (req.simplify){ + vgSimplifyCall.stdin.write(data); + } else { + vgViewCall.stdin.write(data); + } }); vgChunkCall.on("close", (code) => { console.log(`vg chunk exited with code ${code}`); - vgViewCall.stdin.end(); + if (req.simplify){ + vgSimplifyCall.stdin.end(); + } else { + vgViewCall.stdin.end(); + } if (code !== 0) { console.log("Error from " + VG_PATH + "vg " + vgChunkParams.join(" ")); // Execution failed @@ -627,6 +650,49 @@ async function getChunkedData(req, res, next) { } }); + // vg simplify + if (req.simplify){ + vgSimplifyCall.on("error", function (err) { + console.log( + "Error executing " + + VG_PATH + + "vg " + + "simplify " + + "- " + + ": " + + err + ); + if (!sentResponse) { + sentResponse = true; + return next(new VgExecutionError("vg simplify failed")); + } + return; + }); + + vgSimplifyCall.stderr.on("data", (data) => { + console.log(`vg simplify err data: ${data}`); + req.error += data; + }); + + vgSimplifyCall.stdout.on("data", function (data) { + vgViewCall.stdin.write(data); + }); + + vgSimplifyCall.on("close", (code) => { + console.log(`vg simplify exited with code ${code}`); + vgViewCall.stdin.end(); + if (code !== 0) { + console.log("Error from " + VG_PATH + "vg " + "simplify - "); + // Execution failed + if (!sentResponse) { + sentResponse = true; + return next(new VgExecutionError("vg simplify failed")); + } + } + }); + } + + // vg view vgViewCall.on("error", function (err) { console.log('Error executing "vg view": ' + err); if (!sentResponse) { @@ -676,13 +742,65 @@ async function getChunkedData(req, res, next) { // We're using a shared directory for this request, so leave it in place // when the request finishes. req.rmChunk = false; - const vgViewCall = spawn(`${VG_PATH}vg`, [ - "view", - "-j", - `${req.chunkDir}/chunk.vg`, - ]); + let filename = `${req.chunkDir}/chunk.vg`; + // vg simplify for bed files + let vgSimplifyCall = null; + let vgViewArguments = ["view", "-j"]; + if (req.simplify){ + vgSimplifyCall = spawn(`${VG_PATH}vg`, ["simplify", filename,]); + vgViewArguments.push("-"); + console.log("Spawning vg simplify call"); + } else { + vgViewArguments.push(filename); + } + + let vgViewCall = spawn(`${VG_PATH}vg`, vgViewArguments); + let graphAsString = ""; req.error = Buffer.alloc(0); + + // vg simplify + if (req.simplify){ + vgSimplifyCall.on("error", function (err) { + console.log( + "Error executing " + + VG_PATH + + "vg " + + "simplify " + + filename + + ": " + + err + ); + if (!sentResponse) { + sentResponse = true; + return next(new VgExecutionError("vg simplify failed")); + } + return; + }); + + vgSimplifyCall.stderr.on("data", (data) => { + console.log(`vg simplify err data: ${data}`); + req.error += data; + }); + + vgSimplifyCall.stdout.on("data", function (data) { + vgViewCall.stdin.write(data); + }); + + vgSimplifyCall.on("close", (code) => { + console.log(`vg simplify exited with code ${code}`); + vgViewCall.stdin.end(); + if (code !== 0) { + console.log("Error from " + VG_PATH + "vg " + "simplify " + filename); + // Execution failed + if (!sentResponse) { + sentResponse = true; + return next(new VgExecutionError("vg simplify failed")); + } + } + }); + } + vgViewCall.on("error", function (err) { console.log('Error executing "vg view": ' + err); if (!sentResponse) {