Class: Bio::Graphics::Panel::Track::Feature

Inherits:
Object
  • Object
show all
Defined in:
lib/bio/graphics/feature.rb

Overview

The Bio::Graphics::Track::Feature class describes features to be placed on the graph. See Bio::Graphics documentation for explanation of interplay between different classes.

The position of the Feature is a Bio::Locations object to make it possible to transparently work with simple and spliced features.

Defined Under Namespace

Classes: PixelRange

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(track, name, location = Bio::Locations.new('1..' + track.panel.length.to_s), link = nil) ⇒ Feature

!!Not to be used directly. Use Bio::Graphics::Panel::Track.add_feature instead!! A feature can not exist except within the confines of a Bio::Graphics::Panel::Track object.

– This is necessary because the feature needs to know the colour and glyph, both of which are defined within the panel. ++


Arguments:

  • panel (required)

    Bio::Graphics::Panel::Track object that this

    feature belongs to

  • name (required)

    Name of the feature

  • location

    Bio::Locations object. Default = whole panel, forward strand

  • link

    URL for clickable images

Returns

Bio::Graphics::Track::Feature object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/bio/graphics/feature.rb', line 38

def initialize(track, name, location = Bio::Locations.new('1..' + track.panel.length.to_s), link = nil)
  @track = track
  @name = name
  @location = location
  @start = location.collect{|l| l.from}.min.to_i
  @stop = location.collect{|l| l.to}.max.to_i
  @strand = location[0].strand.to_i
  @link = link
  @pixel_range_collection = Array.new
  @chopped_at_start = false
  @chopped_at_stop = false
  @hidden_subfeatures_at_start = false
  @hidden_subfeatures_at_stop = false

  # Get all pixel ranges for the subfeatures
  location.each do |l|
    #   xxxxxx  [          ]
    if l.to < track.panel.display_start
      @hidden_subfeatures_at_start = true
      next
    #           [          ]   xxxxx
    elsif l.from > track.panel.display_stop
      @hidden_subfeatures_at_stop = true
      next
    #      xxxx[xxx       ]
    elsif l.from < track.panel.display_start and l.to > track.panel.display_start
      start_pixel = 1
      stop_pixel = ( l.to - track.panel.display_start ).to_f / track.panel.rescale_factor
      @chopped_at_start = true
    #          [      xxxx]xxxx
    elsif l.from < track.panel.display_stop and l.to > track.panel.display_stop
      start_pixel = ( l.from - track.panel.display_start ).to_f / track.panel.rescale_factor
      stop_pixel = track.panel.width
      @chopped_at_stop = true
    #      xxxx[xxxxxxxxxx]xxxx
    elsif l.from < track.panel.display_start and l.to > track.panel.display_stop
      start_pixel = 1
      stop_pixel = track.panel.width
      @chopped_at_start = true
      @chopped_at_stop = true
    #          [   xxxxx  ]
    else
      start_pixel = ( l.from - track.panel.display_start ).to_f / track.panel.rescale_factor
      stop_pixel = ( l.to - track.panel.display_start ).to_f / track.panel.rescale_factor
    end
    
    @pixel_range_collection.push(PixelRange.new(start_pixel, stop_pixel))
    
  end
end

Instance Attribute Details

#chopped_at_startObject

Is the first subfeature incomplete?



116
117
118
# File 'lib/bio/graphics/feature.rb', line 116

def chopped_at_start
  @chopped_at_start
end

#chopped_at_stopObject

Is the last subfeature incomplete?



119
120
121
# File 'lib/bio/graphics/feature.rb', line 119

def chopped_at_stop
  @chopped_at_stop
end

#hidden_subfeatures_at_startObject

Are there subfeatures out of view at the left side of the picture?



122
123
124
# File 'lib/bio/graphics/feature.rb', line 122

def hidden_subfeatures_at_start
  @hidden_subfeatures_at_start
end

#hidden_subfeatures_at_stopObject

Are there subfeatures out of view at the right side of the picture?



125
126
127
# File 'lib/bio/graphics/feature.rb', line 125

def hidden_subfeatures_at_stop
  @hidden_subfeatures_at_stop
end

The URL to be followed when the glyph for this feature is clicked



108
109
110
# File 'lib/bio/graphics/feature.rb', line 108

def link
  @link
end

#locationObject

The location of the feature (which is a Bio::Locations object)



96
97
98
# File 'lib/bio/graphics/feature.rb', line 96

def location
  @location
end

#nameObject

The name of the feature



93
94
95
# File 'lib/bio/graphics/feature.rb', line 93

def name
  @name
end

#pixel_range_collectionObject

The array keeping the pixel ranges for the sub-features. Unspliced features will just have one element, while spliced features will have more than one.



113
114
115
# File 'lib/bio/graphics/feature.rb', line 113

def pixel_range_collection
  @pixel_range_collection
end

#startObject

The start position of the feature (in bp)



99
100
101
# File 'lib/bio/graphics/feature.rb', line 99

def start
  @start
end

#stopObject

The stop position of the feature (in bp)



102
103
104
# File 'lib/bio/graphics/feature.rb', line 102

def stop
  @stop
end

#strandObject

The strand of the feature



105
106
107
# File 'lib/bio/graphics/feature.rb', line 105

def strand
  @strand
end

#trackObject

The track that this feature belongs to



90
91
92
# File 'lib/bio/graphics/feature.rb', line 90

def track
  @track
end

Instance Method Details

#arrow(track, direction, x, y, size) ⇒ Object

Method to draw the arrows of directed glyphs. Not to be used directly, but called by Feature#draw.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/bio/graphics/feature.rb', line 129

def arrow(track,direction,x,y,size)
  case direction
  when :right
    track.move_to(x,y)
    track.rel_line_to(size,size)
    track.rel_line_to(-size,size)
    track.close_path.fill
  when :left
    track.move_to(x,y)
    track.rel_line_to(-size,size)
    track.rel_line_to(size,size)
    track.close_path.fill
  when :north
    track.move_to(x-size,y+size)
    track.rel_line_to(size,-size)
    track.rel_line_to(size,size)
    track.close_path.fill
  when :south
    track.move_to(x-size,y-size)
    track.rel_line_to(size,size)
    track.rel_line_to(size,-size)
    track.close_path.fill
  end
end

#connector(track, from, to, top, color) ⇒ Object

Method to draw the connections (introns) of spliced glyphs. Not to be used directly, but called by Feature#draw.



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/bio/graphics/feature.rb', line 156

def connector(track,from,to,top, color)
  line_width = track.line_width
  track.set_source_rgb([0,0,0])
  track.set_line_width(0.5)
  middle = from + ((to - from)/2)
  track.move_to(from, top+2)
  track.line_to(middle, top+7)
  track.line_to(to, top+2)
  track.stroke
  track.set_line_width(line_width)
  track.set_source_rgb(color)
end

#draw(track_drawing) ⇒ Object

Adds the feature to the track cairo context. This method should not be used directly by the user, but is called by Bio::Graphics::Panel::Track.draw


Arguments:

  • trackdrawing (required)

    the track cairo object

  • row (required)

    row within the track that this feature has been bumped to

Returns

FIXME: I don’t know



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/bio/graphics/feature.rb', line 178

def draw(track_drawing)
  # We have to check if we want to change the glyph type from directed to
  #    undirected
  # There are 2 cases where we don't want to draw arrows on
  # features:
  # (a) when the picture is really zoomed out, features are
  #     so small that the arrow itself is too big
  # (b) if a directed feature on the fw strand extends beyond
  #     the end of the picture, the arrow is out of view. This
  #     is the same as considering the feature as undirected.
  #     The same obviously goes for features on the reverse
  #     strand that extend beyond the left side of the image.
  #
  # (a) Zoomed out
  replace_directed_with_undirected = false
  if (self.stop - self.start).to_f/self.track.panel.rescale_factor.to_f < 2
    replace_directed_with_undirected = true
  end
  # (b) Extending beyond borders picture
  if ( self.chopped_at_stop and self.strand = 1 ) or ( self.chopped_at_start and self.strand = -1 )
    replace_directed_with_undirected = true
  end

  local_feature_glyph = nil
  if self.track.glyph == :directed_generic and replace_directed_with_undirected
    local_feature_glyph = :generic
  elsif self.track.glyph == :directed_spliced and replace_directed_with_undirected
    local_feature_glyph = :spliced
  else
    local_feature_glyph = self.track.glyph
  end

  # And draw the thing.
  row = self.find_row
  top_pixel_of_feature = FEATURE_V_DISTANCE + (FEATURE_HEIGHT+FEATURE_V_DISTANCE)*row
  bottom_pixel_of_feature = top_pixel_of_feature + FEATURE_HEIGHT

  case local_feature_glyph
    # triangles are typical for features which have a 1 bp position (start == stop)
    when :triangle
      raise "Start and stop are not the same (necessary if you want triangle glyphs)" if self.start != self.stop
 
      # Need to get this for the imagemap
      left_pixel_of_feature = self.pixel_range_collection[0].start_pixel - FEATURE_ARROW_LENGTH
      right_pixel_of_feature = self.pixel_range_collection[0].stop_pixel + FEATURE_ARROW_LENGTH
      arrow(track_drawing,:north,left_pixel_of_feature + FEATURE_ARROW_LENGTH, top_pixel_of_feature, FEATURE_ARROW_LENGTH)
      track_drawing.close_path.stroke
    when :line
      left_pixel_of_feature = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[0].start_pixel
      right_pixel_of_feature = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[-1].stop_pixel
      track_drawing.move_to(left_pixel_of_feature,top_pixel_of_feature+FEATURE_ARROW_LENGTH)               
      track_drawing.line_to(right_pixel_of_feature,top_pixel_of_feature+FEATURE_ARROW_LENGTH)
      track_drawing.stroke

      track_drawing.set_source_rgb([0,0,0])
      arrow(track_drawing,:right,left_pixel_of_feature,top_pixel_of_feature,FEATURE_ARROW_LENGTH)
      track_drawing.close_path.stroke              
      arrow(track_drawing,:left,right_pixel_of_feature,top_pixel_of_feature,FEATURE_ARROW_LENGTH)
      track_drawing.close_path.stroke

      track_drawing.set_source_rgb(self.track.colour)
  when :directed_generic
      # Need to get this for the imagemap
      left_pixel_of_feature = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[0].start_pixel
      right_pixel_of_feature = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[-1].stop_pixel
      if self.strand == -1 # Reverse strand
        track_drawing.rectangle(left_pixel_of_feature+FEATURE_ARROW_LENGTH, top_pixel_of_feature, right_pixel_of_feature - left_pixel_of_feature - FEATURE_ARROW_LENGTH, FEATURE_HEIGHT).fill
        arrow(track_drawing,:left,left_pixel_of_feature+FEATURE_ARROW_LENGTH,top_pixel_of_feature,FEATURE_ARROW_LENGTH)
        track_drawing.close_path.fill
      else #default is forward strand
        track_drawing.rectangle(left_pixel_of_feature, top_pixel_of_feature, right_pixel_of_feature - left_pixel_of_feature - FEATURE_ARROW_LENGTH, FEATURE_HEIGHT).fill
        arrow(track_drawing,:right,right_pixel_of_feature-FEATURE_ARROW_LENGTH,top_pixel_of_feature,FEATURE_ARROW_LENGTH)
        track_drawing.close_path.fill
      end
    when :spliced
      gap_starts = Array.new
      gap_stops = Array.new

      # Need to get this for the imagemap
      left_pixel_of_feature = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[0].start_pixel
      right_pixel_of_feature = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[-1].stop_pixel

      # First draw the parts
      self.pixel_range_collection.sort_by{|pr| pr.start_pixel}.each do |pr|
        track_drawing.rectangle(pr.start_pixel, top_pixel_of_feature, (pr.stop_pixel - pr.start_pixel), FEATURE_HEIGHT).fill
        gap_starts.push(pr.stop_pixel)
        gap_stops.push(pr.start_pixel)
      end

      # And then draw the connections in the gaps
      # Start with removing the very first start and the very last stop.
      gap_starts.sort!.pop
      gap_stops.sort!.shift

      gap_starts.length.times do |gap_number|
        connector(track_drawing,gap_starts[gap_number].to_f,gap_stops[gap_number].to_f,top_pixel_of_feature,track.colour)
      end

      if self.hidden_subfeatures_at_stop
        from = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[-1].stop_pixel
        to = self.track.panel.width
        track_drawing.move_to(from, top_pixel_of_feature+5)
        track_drawing.line_to(to, top_pixel_of_feature+5)
        track_drawing.stroke
      end

      if self.hidden_subfeatures_at_start
        from = 1
        to = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[0].start_pixel
        track_drawing.move_to(from, top_pixel_of_feature+5)
        track_drawing.line_to(to, top_pixel_of_feature+5)
        track_drawing.stroke
      end

    when :directed_spliced
      gap_starts = Array.new
      gap_stops = Array.new
      # First draw the parts
      locations = self.location.sort_by{|l| l.from}

      # Need to get this for the imagemap
      left_pixel_of_feature = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[0].start_pixel
      right_pixel_of_feature = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[-1].stop_pixel

      #   Start with the one with the arrow
      pixel_ranges = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}
      range_with_arrow = nil
      if self.strand == -1 # reverse strand => box with arrow is first one
        range_with_arrow = pixel_ranges.shift
        track_drawing.rectangle((range_with_arrow.start_pixel)+FEATURE_ARROW_LENGTH, top_pixel_of_feature, range_with_arrow.stop_pixel - range_with_arrow.start_pixel - FEATURE_ARROW_LENGTH, FEATURE_HEIGHT).fill
        arrow(track_drawing,:left,range_with_arrow.start_pixel+FEATURE_ARROW_LENGTH, top_pixel_of_feature,FEATURE_ARROW_LENGTH)
        track_drawing.close_path.fill
      else # forward strand => box with arrow is last one
        range_with_arrow = pixel_ranges.pop
        track_drawing.rectangle(range_with_arrow.start_pixel, top_pixel_of_feature, range_with_arrow.stop_pixel - range_with_arrow.start_pixel - FEATURE_ARROW_LENGTH, FEATURE_HEIGHT).fill
        arrow(track_drawing,:right,range_with_arrow.stop_pixel-FEATURE_ARROW_LENGTH, top_pixel_of_feature,FEATURE_ARROW_LENGTH)
        track_drawing.close_path.fill
      end
      gap_starts.push(range_with_arrow.stop_pixel)
      gap_stops.push(range_with_arrow.start_pixel)

      #   And then add the others
      pixel_ranges.each do |range|
        track_drawing.rectangle(range.start_pixel, top_pixel_of_feature, range.stop_pixel - range.start_pixel, FEATURE_HEIGHT).fill
        gap_starts.push(range.stop_pixel)
        gap_stops.push(range.start_pixel)
      end

      # And then draw the connections in the gaps
      # Start with removing the very first start and the very last stop.
      gap_starts.sort!.pop
      gap_stops.sort!.shift

      gap_starts.length.times do |gap_number|
        connector(track_drawing,gap_starts[gap_number].to_f,gap_stops[gap_number].to_f,top_pixel_of_feature,track.colour)
      end

      if self.hidden_subfeatures_at_stop
        from = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[-1].stop_pixel
        to = self.track.panel.width
        track_drawing.move_to(from, top_pixel_of_feature+FEATURE_ARROW_LENGTH)
        track_drawing.line_to(to, top_pixel_of_feature+FEATURE_ARROW_LENGTH)
        track_drawing.stroke
      end

      if self.hidden_subfeatures_at_start
        from = 1
        to = self.pixel_range_collection.sort_by{|pr| pr.start_pixel}[0].start_pixel
        track_drawing.move_to(from, top_pixel_of_feature+FEATURE_ARROW_LENGTH)
        track_drawing.line_to(to, top_pixel_of_feature+FEATURE_ARROW_LENGTH)
        track_drawing.stroke
      end

    else #treat as 'generic'
      left_pixel_of_feature, right_pixel_of_feature = self.pixel_range_collection[0].start_pixel, self.pixel_range_collection[0].stop_pixel
      track_drawing.rectangle(left_pixel_of_feature, top_pixel_of_feature, (right_pixel_of_feature - left_pixel_of_feature), FEATURE_HEIGHT).fill
  end

  # Add the label for the feature
  if self.track.show_label
    pango_layout = track_drawing.create_pango_layout
    pango_layout.text = self.name
    fdesc = Pango::FontDescription.new('Sans Serif')
    fdesc.set_size(8 * Pango::SCALE)
    pango_layout.font_description = fdesc
  
    text_range = self.start.floor..(self.start.floor + pango_layout.pixel_size[0]*self.track.panel.rescale_factor)
    if self.track.grid[row+1].nil?
      self.track.grid[row+1] = Array.new
    end
    self.track.grid[row].push(text_range)
    self.track.grid[row+1].push(text_range)
    track_drawing.move_to(left_pixel_of_feature, top_pixel_of_feature + TRACK_HEADER_HEIGHT)
    track_drawing.set_source_rgb(0,0,0)
    track_drawing.show_pango_layout(pango_layout)
    track_drawing.set_source_rgb(self.track.colour)
  end
  
  
  # And add the region to the image map
  if self.track.panel.clickable
    # Comment: we have to add the vertical_offset and TRACK_HEADER_HEIGHT!
    self.track.panel.image_map.elements.push(ImageMap::ImageMapElement.new(left_pixel_of_feature,
                                                                           top_pixel_of_feature + self.track.vertical_offset + TRACK_HEADER_HEIGHT,
                                                                           right_pixel_of_feature,
                                                                           bottom_pixel_of_feature + self.track.vertical_offset + TRACK_HEADER_HEIGHT,
                                                                           self.link
                                                                          ))
  end
end

#find_rowObject

Calculates the row within the track where this feature should be drawn. This method should not be used directly by the user, but is called by Bio::Graphics::Panel::Track::Feature.draw


Arguments

none

Returns

row number



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/bio/graphics/feature.rb', line 396

def find_row
  row_found = false
  
  # We've got to find out what row to draw the feature on. If two 
  # features overlap, one of them has to be 'bumped' down. So we'll
  # first try to draw a new feature at the top of the track. If
  # it however would overlap with another one, we'll bump it down
  # to the next row.
  feature_range = (self.start.floor..self.stop.ceil)
  row = 1
  row_available = true
  until row_found
    if ! self.track.grid[row].nil?
      self.track.grid[row].each do |covered|
        if feature_range.include?(covered.first) or covered.include?(feature_range.first)
          row_available = false
        end
      end
    end
  
    if ! row_available
      row += 1
      row_available = true
    else # We've found the place where to draw the feature.
      if self.track.grid[row].nil?
        self.track.grid[row] = Array.new
      end
      self.track.grid[row].push(feature_range)
      row_found = true
    end
  end
  return row
end