Browse files

Represent chunk of logic exported to a service. Table view changed for

visualization view to one where motus align by name for easy comparison.
  • Loading branch information...
1 parent 1bd0e02 commit 7801b85f8e17d8f81f14b2e7638c784185bd4bb6 @justsz committed Aug 15, 2011
View
92 grails-app/controllers/webtax/InputController.groovy
@@ -1,45 +1,77 @@
+/*
+ *-------------------------------InputController---------------------------------
+ * This controller handles user uploaded .zips with text files of MOTUs in them.
+ * The zip is extracted into a directory, the user can review what files there are
+ * and delete or add more. After they are satisfied, a blast database can be selected
+ * from a list and files sent to megablast. An auto-refreshing status screen keeps the
+ * user informed on progress.
+ *---------------------------------------------------------------------------------
+ * Common things: the dataset String is always passed around so that it would be unique
+ * to the browser window and links could be easily shared. Also, the dataset's save path
+ * is passed around, though it could be recreated form just by knowing the dataset..
+ */
+
package webtax
import java.util.UUID
class InputController {
- def inputParserService
- def ant = new AntBuilder()
-
- //allowedMethods ?
+ def inputParserService //inject the service that does blasting an annotation
+ def ant = new AntBuilder() //AntBuilder for unzipping and folder deletion
def index = {
redirect(action: "add", params: params)
}
+ /* ------add------
+ * Add takes a .zip file (.jar and .war are also accepted by ant) and dataset.
+ * The zip is sent to uploadFiles and the dataset set to the current dataset.
+ */
def add = {
return [dataset: params.dataset]
}
+
+ /*-------uploadFiles--------
+ * Takes a zip file from add, saves it on disk and then uses the one on
+ * disk to unzip. This is done because Grais uses CommonsMultipartFile
+ * and the safest and easiest way to convert that to a plain File is by
+ * using transferTo to get it on disk. Ant cannot work with the Grails
+ * interpretation of File.
+ *
+ *
+ */
def uploadFiles = {
def dataset = params.dataset
+ //save path created by appending the dataset name to the save location defined in the config file
def destination = new File("${grailsApplication.config.userInputPath}${dataset}")
+ //tokenized input so that clicking on submit twice doesn't crash the app
withForm {
if (!dataset) {
flash.message = "Please enter a dataset to work within."
redirect (action:'add')
return
}
+ //download file that the user has submitted
def up = request.getFile("myFile")
if(up.empty) {
flash.message = "Uploaded file was empty."
redirect (action:'add', params:[dataset:dataset])
return
}
+ //create unique temporary file
UUID uuid = UUID.randomUUID()
def file = new File("${grailsApplication.config.userInputPath}temp${uuid}")
+
+ //and save user's upload there
up.transferTo(file)
- //def file = up.getFileItem().getStoreLocation()
+
+ //use AntBuilder to unzip the file and then delete the temporary file created above
try {
ant.unzip(src: file, dest: destination, overwrite:"true")
} catch(Exception e) {
@@ -55,41 +87,63 @@ class InputController {
}
}
- //Refactoring oppurtunity: pass just the dataset and rebuild destination from that.
-
+ /*-------review-------
+ * Displays a list of files that the user uploaded in the zip.
+ * Files can be deleted or more added. Deletion is done with deleteFiles action,
+ * addition is just a redirect to the add action. The user can then select the database to
+ * blast against.
+ */
def review = {
+ //create a list of all files in the dataset directory
def dir = new File(params.destination)
def files = []
dir.eachFile{ files.add(it.getName()) }
+ //create a list of available databases from the config-like databases.txt
def databaseFile = new File("${grailsApplication.config.databasePath}databases.txt")
def dbs = []
databaseFile.eachLine { dbs.add(it) }
- // def databaseDir = new File(grailsApplication.config.databasePath)
- // def dbs = []
- // databaseDir.eachFile { dbs.add(it.getName()) }
-
return [files: files, destination: params.destination, dataset: params.dataset, dbs:dbs]
}
+ /*-------deleteFiles--------
+ * Deletes the files the user ticked in the review view by reading the params map.
+ * A ticked file will look like this [someFile.fasta : on].
+ */
def deleteFiles = {
params.each { if(it.value == 'on') {
def file = new File(params.destination, it.key)
+
+ //a single file and a directory need to be deleted differently. Ant is more concise than
+ //standard Groovy for directory deletion
if (file.isFile()) file.delete()
else ant.delete(dir: "${params.destination}/${it.key}")
}
}
-
redirect (action:'review', params: [destination: params.destination, dataset: params.dataset])
}
+
+ /*-------blast--------
+ * Takes the user's uploaded files, creates a Job object for each and
+ * sends the files off to inputParserService to be megablasted and annotated.
+ * Jobs are done asyncronously (but still with only 1 job at a time) so that the
+ * progresses could be displayed. Grails won't let you go to another view while the
+ * current action is still running, so runAsync (from executor plugin) spins off a new
+ * thread for each job.
+ */
def blast = {
+ //for some reason inputParserService doesn't take values from params so I have
+ //created a variable for the needed param variables
def dataset = params.dataset
def database = params.database
def destination = params.destination
- new Dataset(name: dataset).save(flush: true) //test if dataset name is unique
+ //test if dataset name is unique
+ new Dataset(name: dataset).save(flush: true)
+
+ //create list of files to blast from upload location
def dir = new File(params.destination)
def files = []
dir.eachFile{ files.add(it) }
@@ -100,20 +154,30 @@ class InputController {
return
}
+ //a list of all the jobs begun in this batch, these will be used by statuses to display the correct progresses
def jobIds = []
files.each {
def fileName = it.getName()
+
+ //job is saved and persisted immediately so that the statuses page can access the progress value
def job = new Job(progress: 0, name: fileName).save(flush:true)
jobIds.add(job.id)
+
+ //spin off a thread for each file
runAsync {
- inputParserService.parseAndAdd(job.id, dataset, database, destination, fileName) //Refactioring opportunity: just pass a file.
+ inputParserService.parseAndAdd(job.id, dataset, database, destination, fileName)
}
}
redirect(action:'statuses', params:[jobIds: jobIds, dataset: dataset])
}
+
+ /*-------statuses---------
+ * Displays a list of running jobs and their progresses.
+ * Page reloads every 10 seconds to show changes.
+ */
def statuses = {
return [jobIds: params.jobIds, dataset: params.dataset]
}
View
141 grails-app/controllers/webtax/ShowController.groovy
@@ -1,13 +1,41 @@
+/*
+*---------------------------------ShowController-------------------------------------
+* This controller handles all the display aspects of the user's input
+* after blasting and annotating. This includes listing all MOTUs, showing an
+* individual MOTU in a table with its 10 best matches from megablast, searching
+* through motus based on sample name and cutoff value, and a summary view that
+* has a selection of criteria and chart types to produce an overview of what kinds of
+* creatures are in the user's sample.
+* Charts are drawn using Google's visualization API plugin.
+* Data can be downloaded via OutputController.
+*------------------------------------------------------------------------------------
+* Common things: the dataset String is always passed around so that it would be unique
+* to the browser window and links could be easily shared.
+*/
+
package webtax
class ShowController {
+ def visualizeService
+
def index = { }
+ /*-------repForm--------
+ * Takes user's criteria for the summary view, represent. The criteria are
+ * list of sample sites, MOTU clustering cutoff, clumping threshold (put poorly represented MOTU's in one category),
+ * a filtering phrase that applies to the MOTU's hits, minimum bitscore, minimum difference between first and next bitscore,
+ * taxonomic type to show, and chart type to display.
+ */
def repForm = {
return [dataset: params.dataset, params: params]
}
+ /*------represent--------
+ * Gets criteria from repForm, constructs and executes queries (these have many steps)
+ * and draws the results in tables and charts.
+ */
def represent = {
+ //user input validity checks
if(!params.dataset) {
flash.message = "No dataset supplied!"
redirect(action:'repForm', params: params)
@@ -25,8 +53,8 @@ class ShowController {
return
}
- if(!params.cutoff) {
- flash.message = "Please enter a cutoff."
+ if(!params.cutoff.isNumber()) {
+ flash.message = "Cutoff must be a number."
redirect(action:'repForm', params: params)
return
}
@@ -40,109 +68,28 @@ class ShowController {
redirect(action:'repForm', params: params)
return
}
+ //end of validity checks
- def properties = ['species', 'genus', 'taxOrder', 'family', 'taxClass', 'phylum']
- def reps = []
- def data = []
- def totalHits = []
+ //take user's input
def type = params.type
def cutoff = params.cutoff
- def sites = params.sites.split(",")
+ def minBitScore = params.minBitScore as Integer
+ def minBitScoreStep = params.minBitScoreStep as Integer
+ //make the sites string into a list and trim off excess whitespace
+ def sites = params.sites.split(",")
for (i in 0..<sites.size()) {
sites[i] = sites[i].trim()
}
+ //println sites.getClass()
+
+ visualizeService.processCriteria(params.dataset, sites as List, params.threshold, params.keyPhrase, cutoff, minBitScore, minBitScoreStep, type)
+ def reps = visualizeService.getReps()
+ def data = visualizeService.getData()
+ def tableData = visualizeService.getTableData(reps)
+
- def counter = 0
-
- for (site in sites) {
- reps[counter] = [:]
- data[counter] = []
-
- def motus = Motu.withCriteria {
- 'in'("id", Dataset.findByName(params.dataset).motus*.id)
- eq("site", site)
- eq("cutoff", cutoff)
- }
-
-
- // def hits = motus.collect {it.hits.max {it.bitScore}}
-
- def minBitScore = params.minBitScore as Integer
- def minBitScoreStep = params.minBitScoreStep as Integer
- def freqs = motus.collect {it.freq}
- def hits = motus.collect { it.hits } //[[h1, h2...], [h11, h12...],...]
-
-
- hits = hits*.sort { -it.bitScore }
-
- hits = hits.collect { it = it.split{ it.bitScore >= minBitScore }[0] } //change to x -> x.... format for readability
-
- //hits = hits.split { it.size() > 1 }[0] //trim out singletons
- if (minBitScoreStep != 0) {
- hits = hits.collect {
- if (it[0] != null && it[1] != null) {
- if ((it[0].bitScore - it[1].bitScore) >= minBitScoreStep) {it = it[0]}
- else it = null
- } else it = null
- }
- //hits = hits.split{it}[0] //trim out nulls
- } else {
- hits = hits.collect { it = it[0] }
- }
-
- def hitsWithFreqs = [:]
- for (i in 0..<hits.size()) {
- hitsWithFreqs.put (hits[i], 0)
- }
-
- for (i in 0..<hits.size()) {
- hitsWithFreqs[hits[i]] += freqs[i].toInteger()
- }
-
-
-
-
- for (h in hitsWithFreqs) {
-
- if (h.key) {
- for (prop in properties) {
- if (h.key[prop] =~ ".*${params.keyPhrase}.*") {
- if (reps[counter].containsKey(h.key[type])) {
- reps[counter][h.key[type]] += (h.value.toInteger())
- } else {
- reps[counter].put(h.key[type], h.value.toInteger())
- }
- break
- }
- }
- }
- }
- reps[counter] = reps[counter].sort {a, b -> b.value <=> a.value}
-
-
- totalHits[counter] = 0
- reps[counter].each {key, value -> totalHits[counter] += value}
- def others = ['others', 0]
-
- reps[counter].each {key, value ->
- if ((value / totalHits[counter]) > ((params.threshold.toDouble()) / 100)) { //Clump together under "others" chart sections for motus that represent less than threshold% of the total motu count
- def entry = [key, value]
- data[counter].add(entry)
- } else {
- others[1] += value
- }
- }
- if((params.threshold.toDouble()) != 0) {
- data[counter].add(others)
- reps[counter].put(others)
- }
-
- counter++
- }
-
-
- return [reps: reps, type: type, data: data, sites:sites, chart: params.chart, params: params, dataset:params.dataset]
+ return [reps: reps, tableData: tableData, type: type, data: data, sites:sites, chart: params.chart, params: params, dataset:params.dataset]
}
def search = {
View
163 grails-app/services/webtax/VisualizeService.groovy
@@ -0,0 +1,163 @@
+package webtax
+
+class VisualizeService {
+ static transactional = false
+ //the filter by phrase keywords will be looked for in these properties of each hit
+ def properties = ['species', 'genus', 'taxOrder', 'family', 'taxClass', 'phylum']
+
+ //a list that will contain a map for each sample site. The map will hold the MOTU summary data
+ def reps = []
+
+ //google's chart API takes a list of maps, so data will hold the same information
+ //as reps but formatted differently
+ def data = []
+ def totalHits = []
+
+
+ //sites, params.threshold, params.keyPhrase, cutoff, minBitScore, minBitScoreStep, type
+ def void processCriteria(String dataset, List sites, String threshold, String keyPhrase, String cutoff, Integer minBitScore, Integer minBitScoreStep, String type) {
+
+ reps = []
+ data = []
+ totalHits = []
+
+ //counter that keeps track of which sample site is being processed to
+ //separate out the data between samples
+ def counter = 0
+
+ for (site in sites) {
+ reps[counter] = [:]
+ data[counter] = []
+
+ //list all motus that belong to the current dataset, and have the site and cutoff specified
+ def motus = Motu.withCriteria {
+ 'in'("id", Dataset.findByName(dataset).motus*.id)
+ eq("site", site)
+ eq("cutoff", cutoff)
+ }
+
+ //make a list of frequencies and corresponding hits
+ //must not sort before the frequencies of each hit is taken into account!
+ def freqs = motus.collect {it.freq}
+ def hits = motus.collect { it.hits } //looks like [[h1, h2...], [h11, h12...],...]
+
+
+
+ //sort each list of hits within the big list
+ hits = hits*.sort { -it.bitScore }
+
+
+
+
+ //drop all the hits that have below the minimum bitscore
+ hits = hits.collect {subHitList -> subHitList = subHitList.split{hit -> hit.bitScore >= minBitScore }[0] }
+
+
+ if (minBitScoreStep != 0) {
+ hits = hits.collect {
+ if (it[0] != null && it[1] != null) {
+ if ((it[0].bitScore - it[1].bitScore) >= minBitScoreStep) {it = it[0]}
+ else it = null
+ } else it = null
+ }
+ //hits = hits.split{it}[0] //trim out nulls
+ } else {
+ hits = hits.collect { it = it[0] }
+ }
+
+ def hitsWithFreqs = [:]
+ for (i in 0..<hits.size()) {
+ hitsWithFreqs.put (hits[i], 0)
+ }
+
+ for (i in 0..<hits.size()) {
+ hitsWithFreqs[hits[i]] += freqs[i].toInteger()
+ }
+
+
+
+
+ for (h in hitsWithFreqs) {
+
+ if (h.key) {
+ for (prop in properties) {
+ if (h.key[prop] =~ ".*${keyPhrase}.*") {
+ if (reps[counter].containsKey(h.key[type])) {
+ reps[counter][h.key[type]] += (h.value.toInteger())
+ } else {
+ reps[counter].put(h.key[type], h.value.toInteger())
+ }
+ break
+ }
+ }
+ }
+ }
+ def dataForChart = reps[counter].sort {a, b -> b.value <=> a.value}
+ reps[counter] = reps[counter].sort {a, b -> -(b.key <=> a.key)}
+
+
+
+ totalHits[counter] = 0
+ reps[counter].each {key, value -> totalHits[counter] += value}
+ def others = ['others', 0]
+
+ dataForChart.each {key, value ->
+ if ((value / totalHits[counter]) > ((threshold.toDouble()) / 100)) { //Clump together under "others" chart sections for motus that represent less than threshold% of the total motu count
+ def entry = [key, value]
+ data[counter].add(entry)
+ } else {
+ others[1] += value
+ }
+ }
+ if((threshold.toDouble()) != 0) {
+ data[counter].add(others)
+ reps[counter].put(others)
+ }
+
+ counter++
+ }
+
+ }
+
+ def List getReps() {
+ return reps
+ }
+
+ def List getData() {
+ return data
+ }
+
+ def List getTableData(List repses) {
+ def repses2 = repses.clone()
+
+ def allNames = []
+
+ repses.each { map ->
+ map.each { entry ->
+ if (!allNames.contains(entry.key)) allNames.add(entry.key)
+ }
+ }
+
+ allNames.sort {it}
+
+ for (i in 0..<repses.size()) {
+ allNames.each { name ->
+ if(!repses2[i].containsKey(name)) repses2[i].putAt(name, 0)
+ }
+ }
+
+ def tableData = []
+
+ for (i in 0..allNames.size()) {
+ tableData[i] = []
+ tableData[i][0] = allNames[i]
+
+ for (j in 1..repses2.size()) {
+ tableData[i][j] = repses2[j-1][allNames[i]]
+ }
+ }
+
+ return tableData
+ }
+
+}
View
41 grails-app/views/show/represent.gsp
@@ -16,28 +16,55 @@
<%-- <div style="float: left" class="list">--%>
- <g:each in="${reps}" status="j" var="rep">
+
+<%-- <g:each in="${reps}" status="j" var="rep">--%>
+<%-- <table>--%>
+<%-- --%>
+<%-- <thead>--%>
+<%-- <tr>--%>
+<%-- <th>${type}</th> --%>
+<%-- <th>${sites[j]}</th>--%>
+<%-- </tr> --%>
+<%-- </thead>--%>
+<%-- --%>
+<%-- --%>
+<%-- <tbody>--%>
+<%-- --%>
+<%-- <g:each in="${rep.entrySet()}" status="i" var="entry">--%>
+<%-- <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">--%>
+<%-- <td><a href="http://www.ncbi.nlm.nih.gov/taxonomy?term=${entry.key}">${entry.key}</a></td> --%>
+<%-- <td>${entry.value}</td>--%>
+<%-- </tr>--%>
+<%-- </g:each> --%>
+<%-- </tbody>--%>
+<%-- </table>--%>
+<%-- </g:each>--%>
+
+
<table>
<thead>
<tr>
- <th>${type}</th>
- <th>${sites[j]}</th>
+ <th>${type}</th>
+ <g:each in="${sites}" var="site">
+ <th>${site}</th>
+ </g:each>
</tr>
</thead>
<tbody>
- <g:each in="${rep.entrySet()}" status="i" var="entry">
+ <g:each in="${tableData}" status="i" var="siteData">
<tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
- <td><a href="http://www.ncbi.nlm.nih.gov/taxonomy?term=${entry.key}">${entry.key}</a></td>
- <td>${entry.value}</td>
+ <g:each in="${siteData}" var="entry">
+ <td>${entry}</td>
+ </g:each>
</tr>
</g:each>
</tbody>
</table>
- </g:each>
+
<%-- </div>--%>

0 comments on commit 7801b85

Please sign in to comment.