File Coverage

blib/lib/Bio/Gonzales/Feat/IO/GFF3.pm
Criterion Covered Total %
statement 123 179 68.7
branch 44 76 57.8
condition 7 29 24.1
subroutine 20 24 83.3
pod 3 6 50.0
total 197 314 62.7


line stmt bran cond sub pod time code
1             package Bio::Gonzales::Feat::IO::GFF3;
2              
3 2     2   1345 use Mouse;
  2         5  
  2         12  
4              
5 2     2   793 use warnings;
  2         4  
  2         87  
6 2     2   13 use strict;
  2         4  
  2         63  
7              
8 2     2   42 use 5.010;
  2         6  
9 2     2   20 use Carp;
  2         4  
  2         154  
10              
11 2     2   620 use List::MoreUtils qw/zip/;
  2         13437  
  2         22  
12 2     2   2769 use Bio::Gonzales::Feat;
  2         7  
  2         78  
13 2     2   14 use Data::Dumper;
  2         4  
  2         118  
14 2     2   14 use Carp;
  2         4  
  2         118  
15 2     2   13 use Scalar::Util qw/blessed/;
  2         4  
  2         80  
16 2     2   12 use Bio::Gonzales::Seq::Util qw/strand_convert/;
  2         4  
  2         5102  
17              
18             extends 'Bio::Gonzales::Feat::IO::Base';
19              
20             our $VERSION = '0.083'; # VERSION
21              
22             our $FASTA_RE = qr/^\>/;
23             our $SEQ_REGION_RE = qr/^\#\#sequence-region\s+(\S+)\s+(\S+)\s+(\S+)\s*/;
24             our $ATTR_UNESCAPE_RE = qr/%([0-9A-Fa-f]{2})/;
25              
26             our @GFF_COLUMN_NAMES = qw/
27             seq_id
28             source
29             type
30             start
31             end
32             score
33             strand
34             phase
35             attributes
36             /;
37              
38             has segments => ( is => 'rw', default => sub { [] } );
39             has _wrote_sth_before => ( is => 'rw' );
40             has pragmas => ( is => 'rw', default => sub { {} } );
41             has preprocess => ( is => 'rw' );
42             has comments => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } );
43             has no_header => ( is => 'rw' );
44             has escape_whitespace => ( is => 'rw' );
45             has quiet => ( is => 'rw' );
46              
47             sub BUILD {
48 2     2 1 7 my ($self) = @_;
49              
50 2 100       16 $self->_parse_header if ( $self->mode eq '<' );
51             }
52              
53             sub _parse_header {
54 1     1   3 my ($self) = @_;
55              
56 1         12 my $fhi = $self->_fhi;
57              
58 1         4 my $l;
59 1         4 while ( defined( $l = $fhi->() ) ) {
60 4 50 33     27 next if ( !$l || $l =~ /^\s*$/ );
61              
62 4 100       46 if ( $l =~ /^##gff-version\s+(\d+)/ ) {
    100          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
63 1 50       6 confess "I need gff version 3" if ( $1 != 3 );
64             # if no tag given, assume 3 by default
65             } elsif ( $l =~ /$SEQ_REGION_RE/ ) {
66 1         14 my ( $seqid, $start, $end ) = ( $1, $2, $3 );
67 1         4 push @{ $self->segments }, { id => $seqid, start => $start, end => $end };
  1         10  
68             } elsif ( $l =~ /^\#\#(feature-ontology)\s+(.*)$/ ) {
69 0   0     0 $self->pragmas->{$1} //= [];
70 0         0 push @{ $self->pragmas->{$1} }, $2;
  0         0  
71             } elsif ( $l =~ /^\#\#(attribute-ontology)\s+(.*)$/ ) {
72 0   0     0 $self->pragmas->{$1} //= [];
73 0         0 push @{ $self->pragmas->{$1} }, $2;
  0         0  
74             } elsif ( $l =~ /^\#\#(source-ontology)\s+(.*)$/ ) {
75 0   0     0 $self->pragmas->{$1} //= [];
76 0         0 push @{ $self->pragmas->{$1} }, $2;
  0         0  
77             } elsif ( $l =~ /^\#\#(species)\s+(.*)$/ ) {
78 0   0     0 $self->pragmas->{$1} //= [];
79 0         0 push @{ $self->pragmas->{$1} }, $2;
  0         0  
80             } elsif ( $l =~ /^\#\#(genome-build)\s+(.*)$/ ) {
81 0   0     0 $self->pragmas->{$1} //= [];
82 0         0 push @{ $self->pragmas->{$1} }, $2;
  0         0  
83             } elsif ( $l =~ /^(\#\#\#)/ ) {
84             } elsif ( $l =~ /^(\#\#FASTA)/ ) {
85             } elsif ( $l =~ /$FASTA_RE/ ) {
86 0         0 $self->_parse_seq($l);
87             }
88              
89             #looks like the header is over!
90 4 100       18 last unless $l =~ /^\#/;
91             }
92              
93 1         3 push @{ $self->_cached_records }, $l;
  1         6  
94              
95 1         4 return;
96             }
97              
98             sub _write_header {
99 1     1   3 my ($self) = @_;
100              
101 1 50 0     6 $self->_wrote_sth_before(1) && return if ( $self->no_header );
102 1         4 my $fh = $self->fh;
103 1         7 say $fh '##gff-version 3';
104              
105 1         3 while ( my ( $p, $entries ) = each %{ $self->pragmas } ) {
  1         7  
106 0         0 for my $e (@$entries) {
107 0         0 say $fh '##' . $p . " " . $e;
108             }
109             }
110              
111 1         3 for my $c ( @{ $self->comments } ) {
  1         6  
112 0         0 say $fh '#' . $c;
113             }
114              
115 1         2 for my $s ( @{ $self->segments } ) {
  1         5  
116 0         0 say $fh join ' ', '##sequence-region', @{$s}{qw/id start end/};
  0         0  
117             }
118              
119 1         4 $self->_wrote_sth_before(1);
120             }
121              
122             sub _parse_seq {
123 3     3   9 my ( $self, $faheader ) = @_;
124              
125 3         8 my $fhi = $self->_fhi;
126              
127             # defined check not necessary
128 3         19 while ( my $l = $fhi->() ) {
129 1265 50       2455 if ( $l =~ /^\#/ ) {
130 0         0 last;
131             }
132 1265 100       4049 if ( $l =~ /$FASTA_RE/ ) {
133 2         5 push @{ $self->_cached_records }, $l;
  2         7  
134 2         6 last;
135             }
136             }
137 3         8 return;
138             }
139              
140             sub next_feat {
141 58     58 1 130 my ($self) = @_;
142              
143 58         167 my $fhi = $self->_fhi;
144              
145 58         89 my $l;
146 58         152 while ( defined( $l = $fhi->() ) ) {
147 77 100 100     794 if ( $l =~ /$SEQ_REGION_RE/ ) {
    50 66        
    100          
    100          
148 1         7 my ( $seqid, $start, $end ) = ( $1, $2, $3 );
149 1         2 push @{ $self->segments }, { id => $seqid, start => $start, end => $end };
  1         51  
150             } elsif ( $l =~ /^\#\#\#/ ) {
151 0         0 next;
152             } elsif ( $l =~ /^\#/ || $l =~ /^\s*$/ || $l =~ m{^//} ) {
153 16         44 next;
154             } elsif ( $l =~ /$FASTA_RE/ ) {
155 3         11 $self->_parse_seq($l);
156 3         8 next;
157             } else {
158 57         115 last;
159             }
160             }
161 58 100       114 return unless $l;
162              
163 57         123 my $feat = $self->_parse_gff_line($l);
164              
165 57         194 return $feat;
166             }
167              
168             sub next_feat_raw {
169 0     0 0 0 my ($self) = @_;
170              
171 0         0 my $fhi = $self->_fhi;
172              
173 0         0 my $l;
174 0         0 while ( defined( $l = $fhi->() ) ) {
175 0 0 0     0 if ( $l =~ /$SEQ_REGION_RE/ ) {
    0 0        
    0          
    0          
176 0         0 my ( $seqid, $start, $end ) = ( $1, $2, $3 );
177 0         0 push @{ $self->segments }, { id => $seqid, start => $start, end => $end };
  0         0  
178             } elsif ( $l =~ /^\#\#\#/ ) {
179 0         0 next;
180             } elsif ( $l =~ /^\#/ || $l =~ /^\s*$/ || $l =~ m{^//} ) {
181 0         0 next;
182             } elsif ( $l =~ /$FASTA_RE/ ) {
183 0         0 $self->_parse_seq($l);
184 0         0 next;
185             } else {
186 0         0 last;
187             }
188             }
189 0 0       0 return unless $l;
190              
191 0         0 return $l;
192             }
193              
194             sub write_feat {
195 141     141 1 317 my ( $self, @feats ) = @_;
196 141         292 my $fh = $self->fh;
197              
198 141 50       377 $self->_write_header
199             unless ( $self->_wrote_sth_before );
200              
201 141         259 for my $f (@feats) {
202 141 50       422 confess "feature is no a Bio::Gonzales::Feat: " . Dumper($f)
203             unless ( blessed $f eq 'Bio::Gonzales::Feat' );
204 141         476 print $fh $f->to_gff3( $self->escape_whitespace );
205             }
206              
207 141         969 return $self;
208             }
209              
210             sub write_comment {
211 3     3 0 7 my ( $self, $text ) = @_;
212              
213 3         8 my $fh = $self->fh;
214              
215 3 100       13 $self->_write_header
216             unless ( $self->_wrote_sth_before );
217              
218 3         9 print $fh '#' . $text . "\n";
219              
220 3         11 return $self;
221             }
222              
223       0     sub _from_gff3_string {
224             }
225              
226             sub _split_attributes {
227 57     57   97 my ( $self, $attributes ) = @_;
228              
229 57         85 my %attrs;
230 57         241 my @groups = split( /\s*;\s*/, $attributes );
231              
232 57         120 for my $group (@groups) {
233 114         271 my ( $tag, $value ) = split /=/, $group;
234              
235 114         310 $tag =~ s/$ATTR_UNESCAPE_RE/chr(hex($1))/eg;
  0         0  
236 114 50       210 if ( defined($value) ) {
237 114         235 my @values = map { s/$ATTR_UNESCAPE_RE/chr(hex($1))/eg; $_ } split /,/, $value;
  117         295  
  1         8  
  117         274  
238 114   50     566 $attrs{$tag} //= [];
239 114         156 push @{ $attrs{$tag} }, @values;
  114         314  
240             } else {
241 0 0       0 carp "WARNING: Problems to extract attribute, found: $tag" unless ( $self->quiet );
242             }
243             }
244 57         149 return \%attrs;
245             }
246              
247             sub _parse_gff_line {
248 57     57   105 my ( $self, $l ) = @_;
249              
250 57         228 my @d = split( /\t/, $l );
251              
252 57 50       156 confess "error in gff:\n$l\n"
253             unless ( @d == 9 );
254              
255 57 50       173 if ( ref $self->preprocess eq 'CODE' ) {
256 0         0 @d = $self->preprocess->(@d);
257             }
258              
259 57         152 $d[8] = $self->_split_attributes( $d[8] );
260              
261 57 100       140 if ( $d[6] eq '-' ) { $d[6] = -1; }
  9 100       15  
262 38         69 elsif ( $d[6] eq '+' ) { $d[6] = 1; }
263 10         16 else { $d[6] = 0; }
264              
265 57         73 my %feat;
266 57         133 for ( my $i = 0; $i < @GFF_COLUMN_NAMES; $i++ ) {
267 513 100       1391 $feat{ $GFF_COLUMN_NAMES[$i] } = $d[$i] unless ( $d[$i] eq '.' );
268             }
269              
270 57         731 return Bio::Gonzales::Feat->new( \%feat );
271             }
272              
273             sub write_collected_feats {
274 0     0 0   my ( $self, $sub ) = @_;
275 0           my $fh = $self->fh;
276              
277 0           $self->_connect_feats;
278 0           my $parents = $self->_find_parent_feats;
279 0           my $escape_whitespace = $self->escape_whitespace;
280              
281             my $gsub = sub {
282 0     0     my ( $f, $id ) = @_;
283 0 0         $sub->( $f, $id ) if ($sub);
284 0           $f->sort_subfeats;
285 0           print $fh $f->to_gff3($escape_whitespace);
286 0           return;
287 0           };
288              
289 0           for my $p (@$parents) {
290 0           $gsub->($p);
291 0           $p->recurse_subfeats($gsub);
292             }
293 0           return;
294              
295             }
296              
297             1;
298              
299             __END__
300              
301             =head1 NAME
302              
303             Bio::Gonzales::Feat::IO::GFF3 - read and write gff files
304              
305             =head1 SYNOPSIS
306              
307             use Bio::Gonzales::Feat::IO::GFF3;
308              
309             my $output = Bio::Gonzales::Feat::IO::GFF3->new( file => 'a.gff', mode => '>', escape_whitespace => 1 );
310             my $gffin = Bio::Gonzales::Feat::IO::GFF3->new( file => 'a.gff' );
311              
312             # gzipped files can be read directly.
313             my $gffin = Bio::Gonzales::Feat::IO::GFF3->new( file => 'a.gff.gz' );
314              
315             my $gffin = Bio::Gonzales::Feat::IO::GFF3->new('a.gff');
316              
317             while ( my $feat = $gffin->next_feat ) {
318             # $feat is a Bio::Gonzales::Feat
319             next if ( $feat->type ne 'mRNA' );
320              
321             say STDERR $feat->id . " - " . $feat->parent_id;
322             }
323              
324             $gffin->close;
325              
326              
327             =head1 DESCRIPTION
328              
329             =head1 OPTIONS
330              
331             =over 4
332              
333             =item B<< mode => $mode >>
334              
335             Bio::Gonzales::Feat::IO::GFF3 supports 3 different modes,
336              
337             Bio::Gonzales::Feat::IO::GFF3->new(mode => '>', ...); #output
338             Bio::Gonzales::Feat::IO::GFF3->new(mode => '<', ...); #input, DEFAULT
339             Bio::Gonzales::Feat::IO::GFF3->new(mode => '>>', ...); #append
340              
341             all modes also work with gzipped files (ending on '.gz').
342              
343             =item B<< fh => $fh >>
344              
345             Bio::Gonzales::Feat::IO::GFF3 uses $fh to read or write data.
346              
347             open my $fh, '<', 'file.gff3' or confess "Can't open filehandle: $!";
348             my $gff = Bio::Gonzales::Feat::IO::GFF3->new(fh => $fh, ...);
349              
350             # ... do something ...
351              
352             $gff->close;
353             close $fh;
354              
355             =item B<< file => $file >>
356              
357             read from or write to the file C<$file>
358              
359             =item B<< escape_whitespace => 1 >>
360              
361             Usually, only whitespaces in the C<Target> attribute are escaped. If this
362             feature is turned on, whitespace in all attribute values will be escaped.
363              
364             =item B<< record_filter => sub { ... } >>
365              
366             Before reading in the GFF3 information, filter the raw line content according
367             to the supplied function. This functionality is handy for big gff3 files where
368             only a part of the output should be parsed.
369              
370             Example:
371              
372             my $sub = sub {
373             my $line = shift;
374              
375             return $line =~ /\tmRNA\t/;
376             };
377             my $gff = Bio::Gonzales::Feat::IO::GFF3->new( file => '...', mode => '<', record_filter => $sub );
378              
379             # ... only lines with the type 'mRNA' will be parsed ...
380              
381             $gff->close;
382              
383              
384             =back
385              
386             =head1 METHODS
387              
388             =over 4
389              
390             =item B<< $gffio->write_feat($feat) >>
391              
392             Write the feature to the output. Do not forget to call C<$gffio->close> at the
393             end of the processing, otherwise you probably end up writing only half of the
394             features.
395              
396             =item B<< my $feat = $gffio->next_feat() >>
397              
398             Retrieve the next feature, if in reading mode.
399              
400             =item B<< $gffio->segments >>
401              
402             =item B<< $gffio->pragmas >>
403              
404             =item B<< $gffio->preprocess(\&process) >>
405              
406             Change the gff input before the feature object gets instantiated. Arguments of the C<&process> sub are the nine columns of the gff file split into an array.
407              
408             Example sub:
409             sub process {
410             my @cols = @_;
411              
412             $cols[1] = "createdbyme";
413             return @cols;
414             }
415              
416             =item B<< $gffio->comments >>
417              
418             =back
419              
420             =head1 SEE ALSO
421              
422             =head1 AUTHOR
423              
424             jw bargsten, C<< <joachim.bargsten at wur.nl> >>
425              
426             =cut