sequenceserver 0.8.9 → 1.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sequenceserver might be problematic. Click here for more details.

Files changed (213) hide show
  1. checksums.yaml +4 -4
  2. data/{README.txt → README.md} +2 -0
  3. data/bin/sequenceserver +255 -55
  4. data/config.ru +2 -4
  5. data/lib/sequenceserver.rb +293 -447
  6. data/lib/sequenceserver/blast.rb +464 -64
  7. data/lib/sequenceserver/database.rb +185 -19
  8. data/lib/sequenceserver/links.rb +114 -0
  9. data/lib/sequenceserver/logger.rb +27 -0
  10. data/lib/sequenceserver/sequence.rb +141 -0
  11. data/public/css/bootstrap.min.css +8 -413
  12. data/public/css/custom.css +363 -122
  13. data/public/css/font-awesome.min.css +4 -0
  14. data/public/fonts/FontAwesome.otf +0 -0
  15. data/public/fonts/fontawesome-webfont.eot +0 -0
  16. data/public/fonts/fontawesome-webfont.svg +565 -0
  17. data/public/fonts/fontawesome-webfont.ttf +0 -0
  18. data/public/fonts/fontawesome-webfont.woff +0 -0
  19. data/public/fonts/fontawesome-webfont.woff2 +0 -0
  20. data/public/js/bootstrap.min.js +11 -0
  21. data/public/js/d3.v3.min.js +5 -0
  22. data/public/js/html5shiv.min.js +4 -0
  23. data/public/js/jquery.scrollspy.js +74 -0
  24. data/public/js/jquery.t.js +353 -0
  25. data/public/js/sequence.js +2419 -0
  26. data/public/js/sequenceserver.blast.js +29 -30
  27. data/public/js/sequenceserver.js +544 -120
  28. data/public/js/underscore.min.js +6 -0
  29. data/public/js/webshims/polyfiller.js +1 -0
  30. data/public/js/webshims/shims/FlashCanvas/canvas2png.js +1 -0
  31. data/public/js/webshims/shims/FlashCanvas/flashcanvas.js +1 -0
  32. data/public/js/webshims/shims/FlashCanvas/flashcanvas.swf +0 -0
  33. data/public/js/webshims/shims/FlashCanvasPro/canvas2png.js +1 -0
  34. data/public/js/webshims/shims/FlashCanvasPro/flash10canvas.swf +0 -0
  35. data/public/js/webshims/shims/FlashCanvasPro/flash9canvas.swf +0 -0
  36. data/public/js/webshims/shims/FlashCanvasPro/flashcanvas.js +1 -0
  37. data/public/js/webshims/shims/canvas-blob.js +1 -0
  38. data/public/js/webshims/shims/color-picker.js +2 -0
  39. data/public/js/webshims/shims/combos/1.js +6 -0
  40. data/public/js/webshims/shims/combos/10.js +2 -0
  41. data/public/js/webshims/shims/combos/11.js +2 -0
  42. data/public/js/webshims/shims/combos/12.js +6 -0
  43. data/public/js/webshims/shims/combos/13.js +1 -0
  44. data/public/js/webshims/shims/combos/14.js +1 -0
  45. data/public/js/webshims/shims/combos/15.js +2 -0
  46. data/public/js/webshims/shims/combos/16.js +7 -0
  47. data/public/js/webshims/shims/combos/17.js +2 -0
  48. data/public/js/webshims/shims/combos/18.js +3 -0
  49. data/public/js/webshims/shims/combos/2.js +7 -0
  50. data/public/js/webshims/shims/combos/21.js +2 -0
  51. data/public/js/webshims/shims/combos/22.js +1 -0
  52. data/public/js/webshims/shims/combos/23.js +6 -0
  53. data/public/js/webshims/shims/combos/25.js +2 -0
  54. data/public/js/webshims/shims/combos/27.js +1 -0
  55. data/public/js/webshims/shims/combos/28.js +1 -0
  56. data/public/js/webshims/shims/combos/29.js +1 -0
  57. data/public/js/webshims/shims/combos/3.js +1 -0
  58. data/public/js/webshims/shims/combos/30.js +2 -0
  59. data/public/js/webshims/shims/combos/31.js +1 -0
  60. data/public/js/webshims/shims/combos/33.js +1 -0
  61. data/public/js/webshims/shims/combos/34.js +1 -0
  62. data/public/js/webshims/shims/combos/4.js +1 -0
  63. data/public/js/webshims/shims/combos/5.js +2 -0
  64. data/public/js/webshims/shims/combos/6.js +2 -0
  65. data/public/js/webshims/shims/combos/7.js +7 -0
  66. data/public/js/webshims/shims/combos/8.js +7 -0
  67. data/public/js/webshims/shims/combos/9.js +2 -0
  68. data/public/js/webshims/shims/combos/97.js +1 -0
  69. data/public/js/webshims/shims/combos/98.js +1 -0
  70. data/public/js/webshims/shims/combos/99.js +1 -0
  71. data/public/js/webshims/shims/details.js +1 -0
  72. data/public/js/webshims/shims/dom-extend.js +1 -0
  73. data/public/js/webshims/shims/es5.js +1 -0
  74. data/public/js/webshims/shims/es6.js +1 -0
  75. data/public/js/webshims/shims/excanvas.js +1 -0
  76. data/public/js/webshims/shims/filereader-xhr.js +1 -0
  77. data/public/js/webshims/shims/form-combat.js +1 -0
  78. data/public/js/webshims/shims/form-core.js +1 -0
  79. data/public/js/webshims/shims/form-datalist-lazy.js +1 -0
  80. data/public/js/webshims/shims/form-datalist.js +1 -0
  81. data/public/js/webshims/shims/form-fixrangechange.js +1 -0
  82. data/public/js/webshims/shims/form-inputmode.js +1 -0
  83. data/public/js/webshims/shims/form-message.js +1 -0
  84. data/public/js/webshims/shims/form-native-extend.js +1 -0
  85. data/public/js/webshims/shims/form-number-date-api.js +1 -0
  86. data/public/js/webshims/shims/form-number-date-ui.js +1 -0
  87. data/public/js/webshims/shims/form-shim-extend.js +1 -0
  88. data/public/js/webshims/shims/form-shim-extend2.js +1 -0
  89. data/public/js/webshims/shims/form-validation.js +1 -0
  90. data/public/js/webshims/shims/form-validators.js +1 -0
  91. data/public/js/webshims/shims/forms-picker.js +1 -0
  92. data/public/js/webshims/shims/geolocation.js +1 -0
  93. data/public/js/webshims/shims/i18n/formcfg-ar.js +1 -0
  94. data/public/js/webshims/shims/i18n/formcfg-ch-CN.js +1 -0
  95. data/public/js/webshims/shims/i18n/formcfg-cs.js +1 -0
  96. data/public/js/webshims/shims/i18n/formcfg-de.js +1 -0
  97. data/public/js/webshims/shims/i18n/formcfg-el.js +1 -0
  98. data/public/js/webshims/shims/i18n/formcfg-en.js +1 -0
  99. data/public/js/webshims/shims/i18n/formcfg-es.js +1 -0
  100. data/public/js/webshims/shims/i18n/formcfg-fa.js +1 -0
  101. data/public/js/webshims/shims/i18n/formcfg-fr.js +1 -0
  102. data/public/js/webshims/shims/i18n/formcfg-he.js +1 -0
  103. data/public/js/webshims/shims/i18n/formcfg-hi.js +1 -0
  104. data/public/js/webshims/shims/i18n/formcfg-hu.js +1 -0
  105. data/public/js/webshims/shims/i18n/formcfg-it.js +1 -0
  106. data/public/js/webshims/shims/i18n/formcfg-ja.js +1 -0
  107. data/public/js/webshims/shims/i18n/formcfg-lt.js +1 -0
  108. data/public/js/webshims/shims/i18n/formcfg-nl.js +1 -0
  109. data/public/js/webshims/shims/i18n/formcfg-pl.js +1 -0
  110. data/public/js/webshims/shims/i18n/formcfg-pt-BR.js +1 -0
  111. data/public/js/webshims/shims/i18n/formcfg-pt-PT.js +1 -0
  112. data/public/js/webshims/shims/i18n/formcfg-pt.js +1 -0
  113. data/public/js/webshims/shims/i18n/formcfg-ru.js +1 -0
  114. data/public/js/webshims/shims/i18n/formcfg-sv.js +1 -0
  115. data/public/js/webshims/shims/i18n/formcfg-zh-CN.js +1 -0
  116. data/public/js/webshims/shims/i18n/formcfg-zh-TW.js +1 -0
  117. data/public/js/webshims/shims/jme/alternate-media.js +1 -0
  118. data/public/js/webshims/shims/jme/base.js +1 -0
  119. data/public/js/webshims/shims/jme/controls.css +1 -0
  120. data/public/js/webshims/shims/jme/jme.eot +0 -0
  121. data/public/js/webshims/shims/jme/jme.svg +36 -0
  122. data/public/js/webshims/shims/jme/jme.ttf +0 -0
  123. data/public/js/webshims/shims/jme/jme.woff +0 -0
  124. data/public/js/webshims/shims/jme/mediacontrols-lazy.js +1 -0
  125. data/public/js/webshims/shims/jme/mediacontrols.js +1 -0
  126. data/public/js/webshims/shims/jme/playlist.js +1 -0
  127. data/public/js/webshims/shims/jpicker/images/AlphaBar.png +0 -0
  128. data/public/js/webshims/shims/jpicker/images/Bars.png +0 -0
  129. data/public/js/webshims/shims/jpicker/images/Maps.png +0 -0
  130. data/public/js/webshims/shims/jpicker/images/NoColor.png +0 -0
  131. data/public/js/webshims/shims/jpicker/images/bar-opacity.png +0 -0
  132. data/public/js/webshims/shims/jpicker/images/map-opacity.png +0 -0
  133. data/public/js/webshims/shims/jpicker/images/mappoint.gif +0 -0
  134. data/public/js/webshims/shims/jpicker/images/picker.gif +0 -0
  135. data/public/js/webshims/shims/jpicker/images/preview-opacity.png +0 -0
  136. data/public/js/webshims/shims/jpicker/images/rangearrows.gif +0 -0
  137. data/public/js/webshims/shims/jpicker/jpicker.css +1 -0
  138. data/public/js/webshims/shims/matchMedia.js +3 -0
  139. data/public/js/webshims/shims/mediacapture-picker.js +1 -0
  140. data/public/js/webshims/shims/mediacapture.js +1 -0
  141. data/public/js/webshims/shims/mediaelement-core.js +1 -0
  142. data/public/js/webshims/shims/mediaelement-debug.js +1 -0
  143. data/public/js/webshims/shims/mediaelement-jaris.js +1 -0
  144. data/public/js/webshims/shims/mediaelement-native-fix.js +1 -0
  145. data/public/js/webshims/shims/mediaelement-yt.js +1 -0
  146. data/public/js/webshims/shims/moxie/flash/Moxie.cdn.swf +0 -0
  147. data/public/js/webshims/shims/moxie/flash/Moxie.min.swf +0 -0
  148. data/public/js/webshims/shims/moxie/js/moxie-html4.js +3 -0
  149. data/public/js/webshims/shims/moxie/js/moxie-swf.js +2 -0
  150. data/public/js/webshims/shims/picture.js +1 -0
  151. data/public/js/webshims/shims/plugins/jquery.ui.position.js +11 -0
  152. data/public/js/webshims/shims/range-ui.js +1 -0
  153. data/public/js/webshims/shims/sizzle.js +11 -0
  154. data/public/js/webshims/shims/sticky.js +1 -0
  155. data/public/js/webshims/shims/styles/color-picker.png +0 -0
  156. data/public/js/webshims/shims/styles/forms-ext.css +1 -0
  157. data/public/js/webshims/shims/styles/forms-picker.css +1 -0
  158. data/public/js/webshims/shims/styles/progress.gif +0 -0
  159. data/public/js/webshims/shims/styles/progress.png +0 -0
  160. data/public/js/webshims/shims/styles/shim-ext.css +1 -0
  161. data/public/js/webshims/shims/styles/shim.css +1 -0
  162. data/public/js/webshims/shims/styles/transparent.png +0 -0
  163. data/public/js/webshims/shims/styles/widget.eot +0 -0
  164. data/public/js/webshims/shims/styles/widget.svg +12 -0
  165. data/public/js/webshims/shims/styles/widget.ttf +0 -0
  166. data/public/js/webshims/shims/styles/widget.woff +0 -0
  167. data/public/js/webshims/shims/swf/JarisFLVPlayer.swf +0 -0
  168. data/public/js/webshims/shims/swfmini-embed.js +1 -0
  169. data/public/js/webshims/shims/swfmini.js +6 -0
  170. data/public/js/webshims/shims/track-ui.js +1 -0
  171. data/public/js/webshims/shims/track.js +1 -0
  172. data/public/js/webshims/shims/url.js +1 -0
  173. data/public/js/webshims/shims/usermedia-core.js +1 -0
  174. data/public/js/webshims/shims/usermedia-shim.js +1 -0
  175. data/sequenceserver.gemspec +16 -13
  176. data/views/400.erb +28 -0
  177. data/views/500.erb +35 -19
  178. data/views/_options.erb +6 -15
  179. data/views/result.erb +218 -0
  180. data/views/search.erb +354 -151
  181. metadata +254 -62
  182. data/example.config.yml +0 -39
  183. data/lib/sequenceserver/customisation.rb +0 -60
  184. data/lib/sequenceserver/database_formatter.rb +0 -190
  185. data/lib/sequenceserver/helpers.rb +0 -136
  186. data/lib/sequenceserver/sequencehelpers.rb +0 -93
  187. data/lib/sequenceserver/sinatralikeloggerformatter.rb +0 -12
  188. data/lib/sequenceserver/version.rb +0 -9
  189. data/public/css/beige.css.css +0 -254
  190. data/public/css/bootstrap.dropdown.css +0 -29
  191. data/public/css/bootstrap.icons.css +0 -155
  192. data/public/css/bootstrap.modal.css +0 -28
  193. data/public/js/bootstrap.dropdown.js +0 -92
  194. data/public/js/bootstrap.modal.js +0 -7
  195. data/public/js/bootstrap.transition.js +0 -7
  196. data/public/js/jquery-scrollspy.js +0 -98
  197. data/public/js/jquery.activity.js +0 -10
  198. data/public/js/jquery.enablePlaceholder.min.js +0 -10
  199. data/public/js/store.min.js +0 -2
  200. data/public/sequence.html +0 -28
  201. data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta +0 -5486
  202. data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nhr +0 -0
  203. data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nin +0 -0
  204. data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nsq +0 -0
  205. data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta +0 -6449
  206. data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.phr +0 -0
  207. data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.pin +0 -0
  208. data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.psq +0 -0
  209. data/tests/run +0 -26
  210. data/tests/test_sequencehelpers.rb +0 -77
  211. data/tests/test_sequenceserver_blast.rb +0 -60
  212. data/tests/test_ui.rb +0 -104
  213. data/tests/ui.specs.todo +0 -10
@@ -1,92 +1,492 @@
1
+ require 'forwardable'
1
2
  require 'tempfile'
3
+ require 'ox'
4
+
5
+ require 'sequenceserver/links'
2
6
 
3
7
  module SequenceServer
4
- # Simple BLAST+ wrapper.
5
- class Blast
8
+ # Simple wrapper around BLAST+ search algorithms.
9
+ #
10
+ # `BLAST::ArgumentError` and `BLAST::RuntimeError` signal errors encountered
11
+ # when attempting a BLAST search.
12
+ module BLAST
13
+ # To signal error in query sequence or options.
14
+ #
15
+ # ArgumentError is raised when BLAST+'s exit status is 1; see [1].
16
+ class ArgumentError < ArgumentError
17
+ end
6
18
 
7
- ERROR_LINE = /\(CArgException.*\)\s(.*)/
19
+ # To signal internal errors.
20
+ #
21
+ # RuntimeError is raised when BLAST+'s exits status is one of 2, 3, 4, or
22
+ # 255; see [1]. These are rare, infrastructure errors, used internally,
23
+ # and of concern only to the admins/developers.
24
+ class RuntimeError < RuntimeError
25
+ def initialize(status, message)
26
+ @status = status
27
+ @message = message
28
+ end
29
+
30
+ attr_reader :status, :message
8
31
 
9
- # command string to be executed
10
- attr_reader :command
32
+ def to_s
33
+ "#{status}, #{message}"
34
+ end
35
+ end
11
36
 
12
- # result of executing command
13
- attr_reader :result
37
+ # Capture results per query of a BLAST search.
38
+ # @member [String] number
39
+ # @member [String] def
40
+ # @member [Fixnum] len
41
+ # @member [Array(Hit)] hits
42
+ Query = Struct.new(:number, :def, :len, :hits) do
43
+ def initialize(*args)
44
+ args[0] = args[0].to_i
45
+ args[1] = "Query_#{args[0]}" if args[1] == 'No definition line'
46
+ args[2] = args[2].to_i
47
+ @id, *rest = args[1].split
48
+ @title = rest.join(' ')
49
+ super
50
+ end
14
51
 
15
- # errors as [status, message]
16
- attr_reader :error
52
+ def sort_hits_by_evalue!
53
+ @hits = hits.sort_by(&:evalue)
54
+ end
17
55
 
18
- # Initialize a new blast search.
19
- # ---
20
- # Arguments:
21
- # * method (String) - blast executable (shell executable, or absolute path)
22
- # * query (String) - query string
23
- # * databases (String) - database name as returned by 'blastdbcmd -list'
24
- # * options (String) - other options
25
- #
26
- # ---
27
- # Examples:
28
- #
29
- # b = Blast.new("blastn", 'ATGTCCGCGAATCGATTGAACGTGCTGGTGACCCTGATGCTCGCCGTCGCGCTTCTTGTG', "S.cdna.fasta", "-html -num_threads 4")
30
- #
31
- # b.run! => true
32
- # b.result => "blast output"
33
- def initialize(method, query, databases, options = nil)
34
- @method = method
35
- @databases = databases
36
-
37
- # create a tempfile for the given query
38
- @qfile = Tempfile.new('sequenceserver_query')
39
- @qfile.puts(query)
40
- @qfile.close
41
-
42
- # Add -outfmt 11 to list of options so that it outputs a blast archive
43
- @options = options.to_s
44
- @options += " -html"
56
+ attr_reader :id, :title
57
+
58
+ alias_method :length, :len
45
59
  end
46
60
 
47
- # Run blast everytime it is called. Returns the success
48
- # status - true, or false.
49
- def run!
50
- @result, @error, status = execute(command)
61
+ # Hit Object to store all the hits per Query.
62
+ # @member [Fixnum] number
63
+ # @member [String] id
64
+ # @member [String] def
65
+ # @member [String] accession
66
+ # @member [Fixnum] len
67
+ # @member [HSP] hsp
68
+ Hit = Struct.new(:number, :id, :title, :accession, :len, :hsps) do
69
+ def initialize(*args)
70
+ args[0] = args[0].to_i
71
+ args[2] = '' if args[2] == 'No definition line'
72
+ args[4] = args[4].to_i
73
+ super
74
+ end
51
75
 
52
- status == 0 and return @success = true
76
+ alias_method :length, :len
53
77
 
54
- if status == 1
55
- message = @error.each{|l| l.match(ERROR_LINE) and break Regexp.last_match[1]}
56
- message = message || @error
57
- @error = [400, message]
58
- else
59
- @error = [500, @error]
78
+ # Hit evalue is the minimum evalue of all HSP(s).
79
+ def evalue
80
+ hsps.map(&:evalue).min
60
81
  end
61
82
 
62
- false
83
+ # Hit score is the sum of bit scores of all HSP(s).
84
+ def score
85
+ hsps.map(&:bit_score).reduce(:+)
86
+ end
63
87
  end
64
88
 
65
- # The command that will be executed.
66
- def command
67
- @command ||= "#@method -db '#@databases' -query '#{@qfile.path}' #@options"
89
+ # Structure to hold the HSP information about each hit. For more
90
+ # information, check the link contained in the references section at the
91
+ # end of the file.
92
+ HSP = Struct.new(:number, :bit_score, :score, :evalue, :qstart, :qend,
93
+ :sstart, :send, :qframe, :sframe, :identity, :positives,
94
+ :gaps, :len, :qseq, :sseq, :midline) do
95
+
96
+ INTEGER_ARGS = [0, 2].concat((4..13).to_a)
97
+ FLOAT_ARGS = [1, 3]
98
+
99
+ def initialize(*args)
100
+ INTEGER_ARGS.each do |i|
101
+ args[i] = args[i].to_i
102
+ end
103
+
104
+ FLOAT_ARGS.each do |i|
105
+ args[i] = args[i].to_f
106
+ end
107
+
108
+ super
109
+ end
110
+
111
+ alias_method :length, :len
112
+
68
113
  end
69
114
 
70
- # Return success status.
71
- def success?
72
- @success
115
+ # Captures BLAST results from BLAST+'s XML output.
116
+ class Report
117
+
118
+ include Links
119
+
120
+ # Expects a File object and Database objects used to BLAST against.
121
+ #
122
+ # Parses the XML file into an intermediate representation (ir) and
123
+ # constructs an object model from that.
124
+ #
125
+ # NOTE:
126
+ # Databases param is optional for test suite.
127
+ def initialize(rfile, databases = nil)
128
+ ir = node_to_array Ox.parse(rfile.read).root
129
+
130
+ @program = ir[0]
131
+ @program_version = ir[1]
132
+ @querydb = Array databases
133
+ @parameters = {
134
+ :matrix => ir[7][0],
135
+ :evalue => ir[7][1],
136
+ :gapopen => ir[7][2],
137
+ :gapextend => ir[7][3],
138
+ :filters => ir[7][4]
139
+ }
140
+
141
+ ir[8].each_with_index do |n, i|
142
+ @stats ||= n[5][0]
143
+ @queries ||= []
144
+ @queries.push(Query.new(n[0], n[2], n[3], []))
145
+
146
+ # Ensure a hit object is received. No hits, returns a newline. Note
147
+ # that checking to "\n" doesn't work since n[4] = ["\n"]
148
+ if n[4] == ["\n"]
149
+ @queries[i][:hits] = []
150
+ else
151
+ n[4].each_with_index do |hits, j|
152
+ @queries[i][:hits].push(Hit.new(hits[0], hits[1], hits[2],
153
+ hits[3], hits[4], []))
154
+ hits[5].each do |hsp|
155
+ @queries[i][:hits][j][:hsps].push(HSP.new(*hsp))
156
+ end
157
+ end
158
+ @queries[i].sort_hits_by_evalue!
159
+ end
160
+ end
161
+ end
162
+
163
+ attr_reader :program, :program_version
164
+
165
+ # :nodoc:
166
+ # params are defaults provided by BLAST or user input to tweak the
167
+ # result. stats are computed metrics provided by BLAST.
168
+ #
169
+ # BLAST+ doesn't list all input params (like word_size) in the XML
170
+ # output. Only matrix, evalue, gapopen, gapextend, and filters.
171
+ attr_reader :params, :stats
172
+
173
+ attr_reader :querydb
174
+
175
+ attr_reader :queries
176
+
177
+ # Helper methods for pretty printing results
178
+
179
+ # FIXME: HTML!!
180
+ def pretty_evalue(hsp)
181
+ hsp.evalue.to_s.sub(/(\d*\.\d*)e?([+-]\d*)?/) do
182
+ s = '%.3f' % Regexp.last_match[1]
183
+ s << " &times; 10<sup>#{Regexp.last_match[2]}</sup>" if Regexp.last_match[2]
184
+ s
185
+ end
186
+ end
187
+
188
+ def identity_fraction(hsp)
189
+ "#{hsp.identity}/#{hsp.length}"
190
+ end
191
+
192
+ def positives_fraction(hsp)
193
+ "#{hsp.positives}/#{hsp.length}"
194
+ end
195
+
196
+ def gaps_fraction(hsp)
197
+ "#{hsp.gaps}/#{hsp.length}"
198
+ end
199
+
200
+ def identity_percentage(hsp)
201
+ "#{'%.2f' % (hsp.identity * 100.0 / hsp.length)}"
202
+ end
203
+
204
+ def positives_percentage(hsp)
205
+ "#{'%.2f' % (hsp.positives * 100.0 / hsp.length)}"
206
+ end
207
+
208
+ def gaps_percentage(hsp)
209
+ "#{'%.2f' % (hsp.gaps * 100.0 / hsp.length)}"
210
+ end
211
+
212
+ # FIXME: Test me.
213
+ def pp_hsp(hsp)
214
+ # In many of the BLAST algorithms, translated queries are performed
215
+ # which has to be taken care while determining end co ordinates of
216
+ # query and subject sequences. Since each amino acid is encoded using
217
+ # three nucl. referred to as codons, necessary value is multiplied
218
+ # to determine the coordinates.
219
+
220
+ # blastn and blastp search the nucleotide and protein databases using
221
+ # nucleotide and protein queries respectively.
222
+ qframe_unit = 1
223
+ sframe_unit = 1
224
+ # tblastn searches translated nucleotide database using a protein query
225
+ if @program == 'tblastn'
226
+ sframe_unit = 3
227
+ # blastx searches protein database using a translated nucleotide query,
228
+ elsif @program == 'blastx'
229
+ qframe_unit = 3
230
+ # tblastx searches translated nucleotide database using a translated
231
+ # nucleotide query.
232
+ elsif @program == 'tblastx'
233
+ qframe_unit = 3
234
+ sframe_unit = 3
235
+ end
236
+
237
+ qframe_sign = hsp.qframe >= 0 ? 1 : -1
238
+ sframe_sign = hsp.sframe >= 0 ? 1 : -1
239
+
240
+ chars = 60
241
+ lines = (hsp.length / chars.to_f).ceil
242
+ width = [hsp.qend, hsp.send, hsp.qstart,
243
+ hsp.sstart].map(&:to_s).map(&:length).max
244
+
245
+ # blastn results are inconsistent with the other methods as it
246
+ # automatically reverse the start and end coordinates (based on
247
+ # frame), while for others it has to be inferred.
248
+ if @program != 'blastn'
249
+ nqseq = hsp.qframe >= 0 ? hsp.qstart : hsp.qend
250
+ nsseq = hsp.sframe >= 0 ? hsp.sstart : hsp.send
251
+ else
252
+ nqseq = hsp.qstart
253
+ nsseq = hsp.sstart
254
+ end
255
+
256
+ s = ''
257
+ (1..lines).each do |i|
258
+ lqstart = nqseq
259
+ lqseq = hsp.qseq[chars * (i - 1), chars]
260
+ nqseq += (lqseq.length - lqseq.count('-')) * qframe_unit * qframe_sign
261
+ lqend = nqseq - qframe_sign
262
+ s << "Query %#{width}d #{lqseq} #{lqend}\n" % lqstart
263
+
264
+ lmseq = hsp.midline[chars * (i - 1), chars]
265
+ s << "#{' ' * (width + 8)} #{lmseq}\n"
266
+
267
+ lsstart = nsseq
268
+ lsseq = hsp.sseq[chars * (i - 1), chars]
269
+ nsseq += (lsseq.length - lsseq.count('-')) * sframe_unit * sframe_sign
270
+ lsend = nsseq - sframe_sign
271
+ s << "Subject %#{width}d #{lsseq} #{lsend}\n" % lsstart
272
+
273
+ s << "\n" unless i == lines
274
+ end
275
+ s
276
+ end
277
+
278
+ # FIXME: Document me.
279
+ def filter_hsp_stats(hsp)
280
+ hsp_stats = {
281
+ 'Score' => "#{'%.2f' % hsp[:bit_score]} (#{hsp[:score]})",
282
+ 'E value' => "#{pretty_evalue hsp}",
283
+ 'Identities' => "#{identity_fraction hsp} " \
284
+ "(#{identity_percentage hsp}%)",
285
+ 'Gaps' => "#{gaps_fraction hsp} (#{gaps_percentage hsp}%)"
286
+ }
287
+
288
+ if @program == 'blastp'
289
+ hsp_stats['Positives'] = "#{positives_fraction hsp}" \
290
+ "(#{positives_percentage hsp}%)"
291
+ elsif @program == 'blastx'
292
+ hsp_stats['Query frame'] = "#{hsp[:qframe]}"
293
+ elsif @program == 'tblastn'
294
+ hsp_stats['Hit frame'] = "#{hsp[:sframe]}"
295
+ elsif @program == 'tblastx'
296
+ hsp_stats['Positives'] = "#{positives_fraction hsp}" \
297
+ "(#{positives_percentage hsp}%)"
298
+ hsp_stats['Frame'] = "#{hsp[:qframe]}/#{hsp[:sframe]}"
299
+ elsif @program == 'blastn'
300
+ hsp_stats['Strand'] = "#{hsp[:qframe] > 0 ? '+' : '-'}/" \
301
+ "#{hsp[:sframe] > 0 ? '+' : '-'}"
302
+ end
303
+
304
+ hsp_stats
305
+ end
306
+
307
+ def link_per_hit(sequence_id)
308
+ links = Links.instance_methods.map {|m| send(m, sequence_id)}
309
+
310
+ # Sort links based on :order key (ascending)
311
+ links.compact!.sort_by! {|link| link[:order]}
312
+ end
313
+
314
+ # Returns an array of database objects which contain the queried
315
+ # sequence id.
316
+ # NOTE: This function may return more than one database object for
317
+ # a single sequence id.
318
+ #
319
+ # e.g., which_blastdb('SI_2.2.23') => [<Database: ...>, ...]
320
+ def which_blastdb(sequence_id)
321
+ querydb.select {|db| db.include? sequence_id}
322
+ end
323
+
324
+ private
325
+
326
+ PARSEABLE_AS_ARRAY = %w(Parameters BlastOutput_param Iteration_stat
327
+ Statistics Iteration_hits BlastOutput_iterations
328
+ Iteration Hit Hit_hsps Hsp)
329
+
330
+ def node_to_array(element)
331
+ element.nodes.map {|n| node_to_value n}
332
+ end
333
+
334
+ def node_to_value(node)
335
+ # Ensure that the recursion doesn't fails when String value is received.
336
+ return node if node.is_a?(String)
337
+
338
+ if PARSEABLE_AS_ARRAY.include? node.name
339
+ value = node_to_array(node)
340
+ else
341
+ value = first_text(node)
342
+ end
343
+ value
344
+ end
345
+
346
+ def first_text(node)
347
+ node.nodes.find {|n| n.is_a? String}
348
+ end
73
349
  end
74
350
 
75
- private
351
+ ERROR_LINE = /\(CArgException.*\)\s(.*)/
352
+
353
+ ALGORITHMS = %w(blastn blastp blastx tblastn tblastx)
354
+
355
+ extend self
356
+
357
+ extend Forwardable
358
+
359
+ def_delegators SequenceServer, :config, :logger
76
360
 
77
- # Execute a command and return its stdout, stderr, and exit status.
78
- def execute(command)
79
- rfile = Tempfile.new('sequenceserver_result')
80
- efile = Tempfile.new('sequenceserver_error')
81
- [rfile, efile].each {|file| file.close}
361
+ def run(params)
362
+ pre_process params
363
+ validate_blast_params params
82
364
 
365
+ # Compile parameters for BLAST search into a shell executable command.
366
+ #
367
+ # BLAST method to use.
368
+ method = params[:method]
369
+ #
370
+ # BLAST+ expects query sequence as a file.
371
+ qfile = Tempfile.new('sequenceserver_query')
372
+ qfile.puts(params[:sequence])
373
+ qfile.close
374
+ #
375
+ # Retrieve database objects from database id.
376
+ databases = Database[params[:databases]]
377
+ #
378
+ # Concatenate other blast options.
379
+ options = params[:advanced].to_s.strip + defaults
380
+ #
381
+ # blastn implies blastn, not megablast; but let's not interfere if a user
382
+ # specifies `task` herself.
383
+ if method == 'blastn' and not options =~ /task/
384
+ options << ' -task blastn'
385
+ end
386
+
387
+ # Run BLAST search.
388
+ #
389
+ # Command to execute.
390
+ command = "#{method} -db '#{databases.map(&:name).join(' ')}'" \
391
+ " -query '#{qfile.path}' #{options}"
392
+ #
393
+ # Debugging log.
394
+ logger.debug("Executing: #{command}")
395
+ #
396
+ # Temporary files to capture stdout and stderr.
397
+ rfile = Tempfile.new('sequenceserver_blast_result')
398
+ efile = Tempfile.new('sequenceserver_blast_error')
399
+ [rfile, efile].each(&:close)
400
+ #
401
+ # Execute.
83
402
  system("#{command} > #{rfile.path} 2> #{efile.path}")
403
+
404
+ # Capture error.
84
405
  status = $?.exitstatus
406
+ case status
407
+ when 1 # error in query sequence or options; see [1]
408
+ efile.open
409
+
410
+ # Most of the time BLAST+ generates a verbose error message with
411
+ # details we don't require. So we parse out the relevant lines.
412
+ error = efile.each_line do |l|
413
+ break Regexp.last_match[1] if l.match(ERROR_LINE)
414
+ end
415
+
416
+ # But sometimes BLAST+ returns the exact/relevant error message.
417
+ # Trying to parse such messages returns nil, and we use the error
418
+ # message from BLAST+ as it is.
419
+ error = efile.rewind && efile.read unless error.is_a? String
420
+
421
+ efile.close
422
+ raise ArgumentError, error
423
+ when 2, 3, 4, 255 # see [1]
424
+ efile.open
425
+ error = efile.read
426
+ efile.close
427
+ raise RuntimeError.new(status, error)
428
+ end
429
+
430
+ # Report the results, ensures that file is closed after execution.
431
+ File.open(rfile.path) {|f| Report.new(f, databases)}
432
+ end
433
+
434
+ def pre_process(params)
435
+ unless params[:sequence].nil?
436
+ params[:sequence].strip!
437
+ end
438
+ end
439
+
440
+ def validate_blast_params(params)
441
+ validate_blast_method params[:method]
442
+ validate_blast_sequences params[:sequence]
443
+ validate_blast_databases params[:databases]
444
+ validate_blast_options params[:advanced]
445
+ end
85
446
 
86
- return File.readlines(rfile.path), File.readlines(efile.path), status
87
- ensure
88
- rfile.unlink
89
- efile.unlink
447
+ def defaults
448
+ " -outfmt 5 -num_threads #{config[:num_threads]}"
449
+ end
450
+
451
+ def validate_blast_method(method)
452
+ return true if ALGORITHMS.include? method
453
+ raise ArgumentError, "BLAST algorithm should be one of:" \
454
+ " #{ALGORITHMS.join(', ')}."
455
+ end
456
+
457
+ def validate_blast_sequences(sequences)
458
+ return true if sequences.is_a? String and not sequences.empty?
459
+ raise ArgumentError, 'Sequences should be a non-empty string.'
460
+ end
461
+
462
+ def validate_blast_databases(database_ids)
463
+ ids = Database.ids
464
+ return true if database_ids.is_a?(Array) && !database_ids.empty? &&
465
+ (ids & database_ids).length == database_ids.length
466
+ raise ArgumentError, "Database id should be one of:" \
467
+ " #{ids.join("\n")}."
468
+ end
469
+
470
+ # Advanced options are specified by the user. Here they are checked for
471
+ # interference with SequenceServer operations.
472
+ # raise ArgumentError if an error has occurred, else return without value
473
+ def validate_blast_options(options)
474
+ return true if !options || (options.is_a?(String) && options.strip.empty?)
475
+
476
+ unless options =~ /\A[a-z0-9\-_\. ']*\Z/i
477
+ raise ArgumentError, 'Invalid characters detected in options.'
478
+ end
479
+
480
+ disallowed_options = %w(-out -html -outfmt -db -query)
481
+ disallowed_options.each do |o|
482
+ if options =~ /#{o}/i
483
+ raise ArgumentError, "Option \"#{o}\" is prohibited."
484
+ end
485
+ end
90
486
  end
91
487
  end
92
488
  end
489
+
490
+ # References
491
+ # ----------
492
+ # [1]: http://www.ncbi.nlm.nih.gov/books/NBK1763/