~ubuntu-branches/ubuntu/trusty/bioperl/trusty-proposed

« back to all changes in this revision

Viewing changes to scripts/Bio-DB-GFF/genbank2gff3.PLS

  • Committer: Bazaar Package Importer
  • Author(s): Charles Plessy
  • Date: 2011-06-17 13:51:18 UTC
  • mfrom: (1.2.5 upstream)
  • Revision ID: james.westby@ubuntu.com-20110617135118-xgpxhaanue57cwqs
Tags: 1.6.901-1
* New upstream release.
* Point debian/watch to search.cpan.org.
* Build using dh and overrides:
  - Use Debhelper 8 (debian/rules, debian/control).
  - Simplified debian/rules.
* Split into libbio-perl-perl, as discussed with the Debian Perl team.
  (debian/control, debian/bioperl.install, debian libbio-perl-perl.install)
* debian/control:
  - Incremented Standards-Version to reflect conformance with Policy 3.9.2.
    No other changes needed.
  - Vcs-Browser URL made redirectable to viewvc.
  - Removed useless ‘svn’ in the Vcs-Svn URL.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/perl -w
 
1
#!/usr/bin/perl
2
2
 
3
 
#$Id: genbank2gff3.PLS 15505 2009-02-05 18:22:58Z cjfields $;
 
3
#$Id$;
4
4
 
5
5
=pod
6
6
 
27
27
  | perl gmod_bulk_load_gff3.pl -dbname mychado -organism fromdata
28
28
 
29
29
    Options:
30
 
        --dir     -d  path to a list of genbank flatfiles
31
 
        --outdir  -o  location to write GFF files (can be 'stdout' or '-' for pipe)
32
 
        --zip     -z  compress GFF3 output files with gzip
33
 
        --summary -s  print a summary of the features in each contig
34
 
        --filter  -x  genbank feature type(s) to ignore
35
 
        --split   -y  split output to seperate GFF and fasta files for
36
 
                      each genbank record
37
 
        --nolump  -n  seperate file for each reference sequence
38
 
                      (default is to lump all records together into one 
 
30
        --noinfer  -r  don't infer exon/mRNA subfeatures
 
31
        --conf     -i  path to the curation configuration file that contains user preferences
 
32
                       for Genbank entries (must be YAML format)
 
33
                       (if --manual is passed without --ini, user will be prompted to 
 
34
                        create the file if any manual input is saved)
 
35
        --sofile  -l  path to to the so.obo file to use for feature type mapping
 
36
                       (--sofile live will download the latest online revision)
 
37
        --manual   -m  when trying to guess the proper SO term, if more than
 
38
                       one option matches the primary tag, the converter will 
 
39
                       wait for user input to choose the correct one
 
40
                       (only works with --sofile)
 
41
        --dir      -d  path to a list of genbank flatfiles
 
42
        --outdir   -o  location to write GFF files (can be 'stdout' or '-' for pipe)
 
43
        --zip      -z  compress GFF3 output files with gzip
 
44
        --summary  -s  print a summary of the features in each contig
 
45
        --filter   -x  genbank feature type(s) to ignore
 
46
        --split    -y  split output to separate GFF and fasta files for
 
47
                       each genbank record
 
48
        --nolump   -n  separate file for each reference sequence
 
49
                       (default is to lump all records together into one 
39
50
                       output file for each input file)
40
 
        --ethresh -e  error threshold for unflattener
41
 
                      set this high (>2) to ignore all unflattener errors
42
 
        --[no]CDS -c  Keep CDS-exons, or convert to alternate gene-RNA-protein-exon 
43
 
                      model. --CDS is default. Use --CDS to keep default GFF gene model, 
44
 
                      use --noCDS to convert to g-r-p-e.
45
 
        --format  -f  Input format (SeqIO types): GenBank, Swiss or Uniprot, EMBL work
46
 
                      (GenBank is default)
47
 
        --GFF_VERSION 3 is default, 2 and 2.5 and other Bio::Tools::GFF versions available
48
 
        --quiet       dont talk about what is being processed 
49
 
        --typesource  SO sequence type for source (e.g. chromosome; region; contig)
50
 
        --help    -h  display this message
 
51
        --ethresh  -e  error threshold for unflattener
 
52
                       set this high (>2) to ignore all unflattener errors
 
53
        --[no]CDS  -c  Keep CDS-exons, or convert to alternate gene-RNA-protein-exon 
 
54
                       model. --CDS is default. Use --CDS to keep default GFF gene model, 
 
55
                       use --noCDS to convert to g-r-p-e.
 
56
        --format   -f  Input format (SeqIO types): GenBank, Swiss or Uniprot, EMBL work
 
57
                       (GenBank is default)
 
58
        --GFF_VERSION  3 is default, 2 and 2.5 and other Bio::Tools::GFF versions available
 
59
        --quiet        don't talk about what is being processed 
 
60
        --typesource   SO sequence type for source (e.g. chromosome; region; contig)
 
61
        --help     -h  display this message
51
62
 
52
63
 
53
64
=head1 DESCRIPTION
59
70
The input files are assumed to be gzipped GenBank flatfiles for refseq
60
71
contigs.  The files may contain multiple GenBank records.  Either a
61
72
single file or an entire directory can be processed.  By default, the
62
 
DNA sequence is embedded in the GFF but it can be saved into seperate
 
73
DNA sequence is embedded in the GFF but it can be saved into separate
63
74
fasta file with the --split(-y) option.
64
75
 
65
76
If an input file contains multiple records, the default behaviour is
66
77
to dump all GFF and sequence to a file of the same name (with .gff
67
 
appended).  Using the 'nolump' option will create a seperate file for
68
 
each genbank record.  Using the 'split' option will create seperate
 
78
appended).  Using the 'nolump' option will create a separate file for
 
79
each genbank record.  Using the 'split' option will create separate
69
80
GFF and Fasta files for each genbank record.
70
81
 
71
82
 
204
215
use Bio::Location::Simple;
205
216
use Bio::Tools::GFF;
206
217
use Getopt::Long;
 
218
use List::Util qw(first);
 
219
use Bio::OntologyIO;
 
220
use YAML qw(Dump LoadFile DumpFile);
 
221
use File::Basename; 
207
222
 
208
223
use vars qw/$split @filter $zip $outdir $help $ethresh
 
224
            $ONTOLOGY %FEATURES %DESCENDANTS @RETURN $MANUAL @GFF_LINE_FEAT
 
225
            $CONF $YAML $TYPE_MAP $SYN_MAP $noinfer $SO_FILE 
209
226
            $file @files $dir $summary $nolump 
210
227
            $source_type %proteinfa %exonpar $didheader $verbose $DEBUG $GFF_VERSION 
211
228
            $gene_id $rna_id $tnum $ncrna_id $rnum %method %id %seen/;
212
229
 
 
230
use constant SO_URL => 
 
231
    'http://song.cvs.sourceforge.net/viewvc/*checkout*/song/ontology/so.obo';
 
232
use constant ALPHABET => [qw(a b c d e f g h i j k l m n o p q r s t u v w x y z)];
 
233
use constant ALPHABET_TO_NUMBER => {
 
234
    a => 0, b => 1, c => 2, d => 3, e => 4, f => 5, g => 6, h => 7, i => 8, 
 
235
    j => 9, k => 10, l => 11, m => 12, n => 13, o => 14, p => 15, q => 16,
 
236
    r => 17, s => 18, t => 19, u => 20, v => 21, w => 22, x => 23, y => 24,
 
237
    z => 25,
 
238
    };
 
239
use constant ALPHABET_DIVISOR => 26;
213
240
use constant GM_NEW_TOPLEVEL => 2;
214
241
use constant GM_NEW_PART => 1;
215
242
use constant GM_DUP_PART => 0;
216
243
use constant GM_NOT_PART => -1;
217
244
 
 
245
# Options cycle in multiples of 2 because of left side/right side pairing. 
 
246
# You can make this number odd, but displayed matches will still round up
 
247
use constant OPTION_CYCLE => 6; 
 
248
 
218
249
$GFF_VERSION = 3; # allow v2 ...
219
250
$verbose = 1; # right default? -nov to turn off
220
251
 
245
276
            'z|zip'     => \$zip, 
246
277
            'h|help'    => \$help,
247
278
            's|summary' => \$summary,
 
279
            'r|noinfer' => \$noinfer,
 
280
            'i|conf=s' => \$CONF,
 
281
            'sofile=s'  => \$SO_FILE,
 
282
            'm|manual' => \$MANUAL,
248
283
            'o|outdir|output:s'=> \$outdir,
249
284
            'x|filter:s'=> \@filter,
250
285
            'y|split'   => \$split,
282
317
# dgg
283
318
$source_type ||= "region"; # should really parse from FT.source contents below
284
319
 
285
 
my $FTSOmap = $tm->FT_SO_map();
 
320
#my $FTSOmap = $tm->FT_SO_map();
 
321
my $FTSOmap;
 
322
my $FTSOsynonyms;
 
323
 
 
324
if (defined($SO_FILE) && $SO_FILE eq 'live') {
 
325
    print "\nDownloading the latest SO file from ".SO_URL."\n\n";
 
326
    use LWP::UserAgent;
 
327
    my $ua = LWP::UserAgent->new(timeout => 30);
 
328
    my $request = HTTP::Request->new(GET => SO_URL);
 
329
    my $response = $ua->request($request);
 
330
 
 
331
 
 
332
    if ($response->status_line =~ /200/) {
 
333
        use File::Temp qw/ tempfile /;
 
334
        my ($fh, $fn) = tempfile();
 
335
        print $fh $response->content;
 
336
        $SO_FILE = $fn;
 
337
    } else {
 
338
        print "Couldn't download SO file online...skipping validation.\n" 
 
339
            . "HTTP Status was " . $response->status_line . "\n" 
 
340
            and undef $SO_FILE
 
341
    }
 
342
}
 
343
 
 
344
if ($SO_FILE) {
 
345
 
 
346
 
 
347
    my (%terms, %syn);
 
348
 
 
349
    my $parser = Bio::OntologyIO->new( -format => "obo", -file => $SO_FILE );
 
350
    $ONTOLOGY = $parser->next_ontology();
 
351
 
 
352
    for ($ONTOLOGY->get_all_terms) { 
 
353
        my $feat = $_;
 
354
 
 
355
        $terms{$feat->name} = $feat->name;
 
356
        #$terms{$feat->name} = $feat;
 
357
 
 
358
        my @syn = $_->each_synonym;
 
359
 
 
360
        push @{$syn{$_}}, $feat->name for @syn;
 
361
        #push @{$syn{$_}}, $feat for @syn;
 
362
    }
 
363
 
 
364
    $FTSOmap = \%terms;
 
365
    $FTSOsynonyms = \%syn;
 
366
 
 
367
    my %hardTerms = %{ $tm->FT_SO_map() };
 
368
    map { $FTSOmap->{$_} ||= $hardTerms{$_} } keys %hardTerms;
 
369
 
 
370
} else { 
 
371
    my %terms = %{ $tm->FT_SO_map() };
 
372
    while (my ($k,$v) = each %terms) {
 
373
        $FTSOmap->{$k} = ref($v) ? shift @$v : $v;
 
374
    }
 
375
}
 
376
 
 
377
$TYPE_MAP = $FTSOmap;
 
378
$SYN_MAP = $FTSOsynonyms;
 
379
 
 
380
 
286
381
# #convert $FTSOmap undefined to valid SO : moved to TypeMapper->map_types( -undefined => "region")
287
382
 
288
383
# stringify filter list if applicable
363
458
    my $in = Bio::SeqIO->new(-fh => \*FH, -format => $FORMAT, -debug=>$DEBUG);
364
459
    my $gffio = Bio::Tools::GFF->new( -noparse => 1, -gff_version => $GFF_VERSION );
365
460
 
366
 
    while ( my $seq = $in->next_seq ) {
 
461
    while ( my $seq = $in->next_seq() ) {
367
462
        my $seq_name = $seq->accession_number;
368
463
        my $end = $seq->length;
369
464
        my @to_print;
435
530
            #?? should gene_name from /locus_tag,/gene,/product,/transposon=xxx
436
531
            # be converted to or added as  Name=xxx (if not ID= or as well)
437
532
            ## problematic: convert_to_name ($feature); # drops /locus_tag,/gene, tags
 
533
            convert_to_name($feature); 
438
534
            
439
535
            ## dgg: extended to protein|polypeptide
440
536
            ## this test ($feature->has_tag('gene') ||) is not good: repeat_regions over genes
457
553
                  
458
554
                } elsif ($action == GM_NOT_PART) {
459
555
                  add_generic_id( $feature, $gene_name, "nocount");
460
 
                  my $gff= $gffio->gff_string($feature);
461
 
                  print $out "$gff\n" if $gff;
 
556
                  my $gff = $gffio->gff_string($feature);
 
557
                  push @GFF_LINE_FEAT, $feature;
 
558
                  #print $out "$gff\n" if $gff;
462
559
 
463
560
                } elsif ($action > 0) {
464
561
                 # hold off print because exon etc. may get 2nd, 3rd parents
465
562
                  @to_print= print_held($out, $gffio, \@to_print) if ($action == GM_NEW_TOPLEVEL);
466
563
                  push(@to_print, $feature);
467
564
                }
 
565
 
468
566
            }
469
567
            
470
568
            # otherwise handle as generic feats with IDHandler labels 
471
569
            else {
472
570
                add_generic_id( $feature, $gene_name, "");
473
571
                my $gff= $gffio->gff_string($feature);
474
 
                print $out "$gff\n" if $gff;
 
572
                push @GFF_LINE_FEAT, $feature;
 
573
                #print $out "$gff\n" if $gff;
475
574
            }
476
575
        }
477
576
 
478
 
        # dont like doing this after others; do after each new gene id?
 
577
        # don't like doing this after others; do after each new gene id?
479
578
        @to_print= print_held($out, $gffio, \@to_print);
480
 
          
 
579
 
 
580
        gff_validate(@GFF_LINE_FEAT);
 
581
 
 
582
        for my $feature (@GFF_LINE_FEAT) {
 
583
            my $gff= $gffio->gff_string($feature);
 
584
            print $out "$gff\n" if $gff;
 
585
        }
 
586
 
481
587
        # deal with the corresponding DNA
482
588
        my ($fa_out,$fa_outfile);
483
589
        my $dna = $seq->seq;
549
655
    }
550
656
    
551
657
     ## FIXME for piped output w/ split FA files ...
552
 
    close($lumpfa_fh);
 
658
    close($lumpfa_fh) if $lumpfa_fh;
553
659
    if (!$split && $outfa && $lump_fh) {     
554
660
      print $lump_fh "##FASTA\n"; # GFF3 spec
555
661
      open $lumpfa_fh, $outfa or warn "reading FA $outfa: $!";
590
696
  @$to_print = sort sort_by_feattype  @$to_print; # put exons after mRNA, otherwise chado loader chokes
591
697
  while ( my $feature = shift @$to_print) {
592
698
    my $gff= $gffio->gff_string($feature); # $gff =~ s/\'/./g; # dang bug in encode
593
 
    print $out "$gff\n";
 
699
    push @GFF_LINE_FEAT, $feature;
 
700
    #print $out "$gff\n";
594
701
    }
595
702
  return (); # @to_print
596
703
}
830
937
#           $f->remove_tag("translation");
831
938
#           $f->add_tag_value("translation","length.".length($aa)); # hack for odd chado gbl problem
832
939
#         }
833
 
        
 
940
    } elsif ( /region/ ) {       
 
941
        $f->primary_tag('gene_component_region');
 
942
        $f->add_tag_value( Parent => $gene_id ); 
834
943
    } else {
835
944
        return GM_NOT_PART unless $gene_id;
836
945
        $f->add_tag_value( Parent => $gene_id );  
910
1019
    
911
1020
    eval {
912
1021
        $unflattener->unflatten_seq( -seq => $seq, 
 
1022
                                     -noinfer => $noinfer,
913
1023
                                     -use_magic => 1 );
914
1024
    };
915
1025
    
924
1034
 
925
1035
    # map feature types to the sequence ontology
926
1036
    ## $tm->map_types_to_SO( -seq => $seq );
927
 
    $tm->map_types( -seq => $seq, -type_map => $FTSOmap, -undefined => "region" ); #dgg
928
 
    
929
 
    1;
 
1037
    #$tm->map_types( -seq => $seq, -type_map => $FTSOmap, -undefined => "region" ); #dgg
 
1038
 
 
1039
    map_types( 
 
1040
            $tm, 
 
1041
            -seq => $seq, 
 
1042
            -type_map  => $FTSOmap, 
 
1043
            -syn_map  => $FTSOsynonyms, 
 
1044
            -undefined => "region" 
 
1045
    ); #nml
 
1046
 
930
1047
}
931
1048
 
932
1049
sub filter {
969
1086
sub gene_name {
970
1087
    my $g = shift;
971
1088
    my $gene_id = ''; # zero it;
972
 
    
973
 
    if ($g->has_tag('gene')) {
 
1089
 
 
1090
    if ($g->has_tag('locus_tag')) {
 
1091
        ($gene_id) = $g->get_tag_values('locus_tag');
 
1092
    }
 
1093
 
 
1094
    elsif ($g->has_tag('gene')) {
974
1095
        ($gene_id) = $g->get_tag_values('gene'); 
975
1096
    }
976
 
    elsif ($g->has_tag('locus_tag')) {
977
 
        ($gene_id) = $g->get_tag_values('locus_tag');
978
 
    }
979
1097
    elsif ($g->has_tag('ID')) { # for non-Genbank > Entrezgene
980
1098
        ($gene_id) = $g->get_tag_values('ID');
981
1099
    }
1029
1147
        ## $g->remove_tag('transposon');
1030
1148
        $g->add_tag_value('Name', $gene_id);
1031
1149
    }
 
1150
    elsif ($g->has_tag('ID')) { 
 
1151
        my ($name)= $g->get_tag_values('ID');
 
1152
        $g->add_tag_value('Name', $name);
 
1153
    }    
1032
1154
    return $gene_id;
1033
1155
}
1034
1156
 
1055
1177
 
1056
1178
}
1057
1179
 
 
1180
sub map_types {
 
1181
 
 
1182
    my ($self, @args) = @_;
 
1183
 
 
1184
    my($sf, $seq, $type_map, $syn_map, $undefmap) =
 
1185
        $self->_rearrange([qw(FEATURE
 
1186
                    SEQ
 
1187
                    TYPE_MAP
 
1188
                    SYN_MAP
 
1189
                    UNDEFINED
 
1190
                    )],
 
1191
                @args);
 
1192
 
 
1193
    if (!$sf && !$seq) {
 
1194
        $self->throw("you need to pass in either -feature or -seq");
 
1195
    }
 
1196
 
 
1197
    my @sfs = ($sf);
 
1198
    if ($seq) {
 
1199
        $seq->isa("Bio::SeqI") || $self->throw("$seq NOT A SeqI");
 
1200
        @sfs = $seq->get_all_SeqFeatures;
 
1201
    }
 
1202
    $type_map = $type_map || $self->typemap; # dgg: was type_map;
 
1203
    foreach my $feat (@sfs) {
 
1204
 
 
1205
        $feat->isa("Bio::SeqFeatureI") || $self->throw("$feat NOT A SeqFeatureI");
 
1206
        $feat->isa("Bio::FeatureHolderI") || $self->throw("$feat NOT A FeatureHolderI");
 
1207
 
 
1208
        my $primary_tag = $feat->primary_tag;
 
1209
 
 
1210
        #if ($primary_tag =~ /^pseudo(.*)$/) {
 
1211
            #$primary_tag = $1;
 
1212
            #$feat->primary_tag($primary_tag);
 
1213
        #}
 
1214
 
 
1215
        my $mtype = $type_map->{$primary_tag};
 
1216
        if ($mtype) {
 
1217
            if (ref($mtype)) {
 
1218
                if (ref($mtype) eq 'ARRAY') {
 
1219
                    my $soID;
 
1220
                    ($mtype, $soID) = @$mtype;
 
1221
 
 
1222
                    if ($soID && ref($ONTOLOGY)) {
 
1223
                        my ($term) = $ONTOLOGY->find_terms(-identifier => $soID);
 
1224
                        $mtype = $term->name if $term;
 
1225
                    } 
 
1226
# if SO ID is undefined AND we have an ontology to search, we want to delete 
 
1227
# the feature type hash entry in order to force a fuzzy search
 
1228
                    elsif (! defined $soID && ref($ONTOLOGY)) {
 
1229
                        undef $mtype;
 
1230
                        delete $type_map->{$primary_tag};
 
1231
                    } 
 
1232
                    elsif ($undefmap && $mtype eq 'undefined') { # dgg
 
1233
                        $mtype= $undefmap;
 
1234
                    }
 
1235
 
 
1236
                    $type_map->{$primary_tag} = $mtype if $mtype;
 
1237
                }
 
1238
                elsif (ref($mtype) eq 'CODE') {
 
1239
                    $mtype = $mtype->($feat);
 
1240
                }
 
1241
                else {
 
1242
                    $self->throw('must be scalar or CODE ref');
 
1243
                }
 
1244
            }
 
1245
            elsif ($undefmap && $mtype eq 'undefined') { # dgg
 
1246
                $mtype= $undefmap;
 
1247
            }
 
1248
            $feat->primary_tag($mtype);
 
1249
        }
 
1250
 
 
1251
        if ($CONF) {
 
1252
            conf_read(); 
 
1253
            my %perfect_matches;
 
1254
            while (my ($p_tag,$rules) = each %$YAML) {
 
1255
                RULE:
 
1256
                for my $rule (@$rules) {
 
1257
                    for my $tags (@$rule) {
 
1258
                        while (my ($tag,$values) = each %$tags) {
 
1259
                            for my $value (@$values) {
 
1260
                                if ($feat->has_tag($tag)) {
 
1261
                                    for ($feat->get_tag_values($tag)) {
 
1262
                                        next RULE unless $_ =~ /\Q$value\E/;
 
1263
                                    }
 
1264
                                } elsif ($tag eq 'primary_tag') {
 
1265
                                    next RULE unless $value eq
 
1266
                                        $feat->primary_tag; 
 
1267
                                } elsif ($tag eq 'location') {
 
1268
                                    next RULE unless $value eq
 
1269
                                        $feat->start.'..'.$feat->end;
 
1270
                                } else { next RULE }
 
1271
                            }
 
1272
                        }
 
1273
                    }
 
1274
                    $perfect_matches{$p_tag}++;
 
1275
                }
 
1276
            }
 
1277
            if (scalar(keys %perfect_matches) == 1) {
 
1278
                $mtype = $_ for keys %perfect_matches;
 
1279
            } elsif (scalar(keys %perfect_matches) > 1) {
 
1280
                warn "There are conflicting rules in the config file for the" .
 
1281
                     " following types: ";
 
1282
                warn "\t$_\n" for keys %perfect_matches;
 
1283
                warn "Until conflict resolution is built into the converter," .
 
1284
                     " you will have to manually edit the config file to remove the" .
 
1285
                     " conflict. Sorry :(. Skipping user preference for this entry";
 
1286
                sleep(2);
 
1287
            }
 
1288
        } 
 
1289
 
 
1290
        if ( ! $mtype  && $syn_map) {
 
1291
            if ($feat->has_tag('note')) {
 
1292
 
 
1293
                my @all_matches;
 
1294
 
 
1295
                my @note = $feat->each_tag_value('note');
 
1296
 
 
1297
                for my $k (keys %$syn_map) {
 
1298
 
 
1299
                    if ($k =~ /"(.+)"/) {
 
1300
 
 
1301
                        my $syn = $1;
 
1302
 
 
1303
                        for my $note (@note) {
 
1304
 
 
1305
                            # look through the notes to see if the description
 
1306
                            # is an exact match for synonyms
 
1307
                            if ( $syn eq $note ) { 
 
1308
 
 
1309
                                my @map = @{$syn_map->{$k}};
 
1310
 
 
1311
                                
 
1312
                                my $best_guess = $map[0];
 
1313
 
 
1314
                                unshift @{$all_matches[-1]}, [$best_guess];
 
1315
 
 
1316
                                $mtype = $MANUAL
 
1317
                                    ? manual_curation($feat, $best_guess, \@all_matches)
 
1318
                                    : $best_guess;
 
1319
 
 
1320
                                print '#' x 78 . "\nGuessing the proper SO term for GenBank"
 
1321
                                    . " entry:\n\n" . GenBank_entry($feat) . "\nis:\t$mtype\n" 
 
1322
                                    . '#' x 78 . "\n\n";
 
1323
 
 
1324
                            } else {
 
1325
                                # check both primary tag and and note against
 
1326
                                # SO synonyms for best matching description
 
1327
 
 
1328
                                SO_fuzzy_match( $k, $primary_tag, $note, $syn, \@all_matches); 
 
1329
                            }
 
1330
 
 
1331
                        }
 
1332
                    } 
 
1333
                }
 
1334
 
 
1335
                #unless ($mtype) {
 
1336
                for my $note (@note) {
 
1337
                    for my $name (values %$type_map) {
 
1338
                    # check primary tag against SO names for best matching
 
1339
                    # descriptions //NML also need to check against
 
1340
                    # definition && camel case split terms
 
1341
 
 
1342
                        SO_fuzzy_match($name, $primary_tag, $note, $name, \@all_matches);
 
1343
                    }
 
1344
                }
 
1345
                #}
 
1346
 
 
1347
                if (scalar(@all_matches) && !$mtype) {
 
1348
 
 
1349
                    my $top_matches = first { defined $_ } @{$all_matches[-1]}; 
 
1350
 
 
1351
                    my $best_guess = $top_matches->[0];
 
1352
 
 
1353
 
 
1354
 
 
1355
            # if guess has quotes, it is a synonym term. we need to 
 
1356
            # look up the corresponding name term
 
1357
            # otherwise, guess is a name, so we can use it directly
 
1358
                    if ($best_guess =~ /"(.+)"/) {
 
1359
 
 
1360
                        $best_guess = $syn_map->{$best_guess}->[0];
 
1361
 
 
1362
                    } 
 
1363
 
 
1364
                    @RETURN = @all_matches;
 
1365
                    $mtype = $MANUAL
 
1366
                        ? manual_curation($feat, $best_guess, \@all_matches)
 
1367
                        : $best_guess;
 
1368
 
 
1369
                    print '#' x 78 . "\nGuessing the proper SO term for GenBank"
 
1370
                        . " entry:\n\n" . GenBank_entry($feat) . "\nis:\t$mtype\n" 
 
1371
                        . '#' x 78 . "\n\n";
 
1372
 
 
1373
                }
 
1374
            }
 
1375
            $mtype ||= $undefmap;
 
1376
            $feat->primary_tag($mtype);
 
1377
        } 
 
1378
    }
 
1379
 
 
1380
 
 
1381
}
 
1382
 
 
1383
sub SO_fuzzy_match {
 
1384
 
 
1385
    my $candidate = shift;
 
1386
    my $primary_tag = shift;
 
1387
    my $note = shift;
 
1388
    my $SO_terms = shift;
 
1389
    my $best_matches_ref = shift;
 
1390
    my $modifier = shift; 
 
1391
 
 
1392
    $modifier ||= '';
 
1393
 
 
1394
        my @feat_terms;
 
1395
 
 
1396
    for ( split(" |_", $primary_tag) ) {
 
1397
        #my @camelCase = /(?:[A-Z]|[a-z])(?:[A-Z]+|[a-z]*)(?=$|[A-Z])/g;
 
1398
        my @camelCase = /(?:[A-Z]|[a-z])(?:[A-Z]+|[a-z]*)(?=$|[A-Z]|[;:.,])/g;
 
1399
        push @feat_terms, @camelCase;
 
1400
    }
 
1401
 
 
1402
    for ( split(" |_", $note) ) {
 
1403
        #my @camelCase = /(?:[A-Z]|[a-z])(?:[A-Z]+|[a-z]*)(?=$|[A-Z])/g;
 
1404
        #my @camelCase = /(?:[A-Z]|[a-z])(?:[A-Z]+|[a-z]*)(?=$|[A-Z]|[;:.,])/g;
 
1405
        (my $word = $_) =~ s/[;:.,]//g;
 
1406
        push @feat_terms, $word;
 
1407
    }
 
1408
 
 
1409
 
 
1410
    my @SO_terms = split(" |_", $SO_terms);
 
1411
 
 
1412
# fuzzy match works on a simple point system. When 2 words match,
 
1413
# the $plus counter adds one. When they don't, the $minus counter adds
 
1414
# one. This is used to sort similar matches together. Better matches
 
1415
# are found at the end of the array, near the top.
 
1416
 
 
1417
    # NML: can we improve best match by using synonym tags
 
1418
    # EXACT,RELATED,NARROW,BROAD?
 
1419
 
 
1420
    my ($plus, $minus) = (0, 0); 
 
1421
    my %feat_terms;
 
1422
    my %SO_terms;
 
1423
 
 
1424
    #unique terms
 
1425
    map {$feat_terms{$_} = 1} @feat_terms;
 
1426
    map {$SO_terms{$_} = 1} @SO_terms;
 
1427
 
 
1428
    for my $st (keys %SO_terms) {
 
1429
        for my $ft (keys %feat_terms) {
 
1430
 
 
1431
            ($st =~ m/$modifier\Q$ft\E/) ? $plus++ : $minus++;
 
1432
 
 
1433
        }
 
1434
    }
 
1435
 
 
1436
    push @{$$best_matches_ref[$plus][$minus]}, $candidate if $plus;
 
1437
 
 
1438
}
 
1439
 
 
1440
sub manual_curation {
 
1441
 
 
1442
    my ($feat, $default_opt,  $all_matches) = @_; 
 
1443
 
 
1444
    my @all_matches = @$all_matches;
 
1445
 
 
1446
    # convert all SO synonyms into names and filter
 
1447
    # all matches into unique term list because
 
1448
    # synonyms can map to multiple duplicate names
 
1449
 
 
1450
    my (@unique_SO_terms, %seen);
 
1451
    for (reverse @all_matches) {
 
1452
        for (@$_) {
 
1453
            for (@$_) {
 
1454
                #my @names;
 
1455
                if ($_ =~ /"(.+)"/) {
 
1456
                    for (@{$SYN_MAP->{$_}}) {
 
1457
                        push @unique_SO_terms, $_ unless $seen{$_};
 
1458
                        $seen{$_}++;
 
1459
                    }
 
1460
                } else { 
 
1461
                    push @unique_SO_terms, $_ unless $seen{$_};
 
1462
                    $seen{$_}++;
 
1463
                }
 
1464
            }
 
1465
        }
 
1466
    }
 
1467
 
 
1468
    my $s = scalar(@unique_SO_terms);
 
1469
 
 
1470
    my $choice = 0;
 
1471
 
 
1472
    my $more = 
 
1473
        "[a]uto   : automatic input (selects best guess for remaining entries)\r" .
 
1474
        "[f]ind   : search for other SO terms matching your query (e.g. f gene)\r" . 
 
1475
        "[i]nput  : add a specific term\r" .
 
1476
        "[r]eset  : reset to the beginning of matches\r" .
 
1477
        "[s]kip   : skip this entry (selects best guess for this entry)\r"
 
1478
    ;
 
1479
 
 
1480
    $more .= 
 
1481
        "[n]ext   : view the next ".OPTION_CYCLE." terms\r" .
 
1482
        "[p]rev   : view the previous ".OPTION_CYCLE." terms" if ($s > OPTION_CYCLE);
 
1483
 
 
1484
    my $msg = #"\n\n" . '-' x 156 . "\n"
 
1485
         "The converter found $s possible matches for the following GenBank entry: ";
 
1486
 
 
1487
    my $directions = 
 
1488
        "Type a number to select the SO term that best matches"
 
1489
        . " the genbank entry, or use any of the following options:\r" . '_' x 76 . "\r$more"; 
 
1490
 
 
1491
 
 
1492
    # lookup filtered list to pull out definitions
 
1493
    my @options = map { 
 
1494
        my $term = $_;
 
1495
        my %term;
 
1496
        for (['name', 'name'], ['def', 'definition'], ['synonym',
 
1497
                'each_synonym']) {
 
1498
            my ($label, $method) = @$_;
 
1499
            $term{$label} = \@{[$term->$method]};
 
1500
        }
 
1501
        [++$choice, $_->name, ($_->definition || 'none'), \%term, $_->each_synonym ];
 
1502
    }  map { $ONTOLOGY->find_terms(-name => $_) } @unique_SO_terms;
 
1503
 
 
1504
 
 
1505
    my $option = options_cycle(0, OPTION_CYCLE, $msg, $feat, $directions,
 
1506
            $default_opt, @options);
 
1507
 
 
1508
    if ($option eq 'skip') { return $default_opt 
 
1509
    } elsif ($option eq 'auto') {
 
1510
        $MANUAL = 0;
 
1511
        return $default_opt;
 
1512
    } else { return $option }
 
1513
 
 
1514
}
 
1515
 
 
1516
sub options_cycle {
 
1517
 
 
1518
    my ($start, $stop, $msg, $feat, $directions, $best_guess, @opt) = @_;
 
1519
 
 
1520
    #NML: really should only call GenBank_entry once. Will need to change
 
1521
    #method to return array & shift off header
 
1522
    my $entry = GenBank_entry($feat, "\r");
 
1523
 
 
1524
    my $total = scalar(@opt);
 
1525
 
 
1526
    ($start,$stop) = (0, OPTION_CYCLE) 
 
1527
        if ( ($start < 0) && ($stop > 0) );
 
1528
 
 
1529
    ($start,$stop) = (0, OPTION_CYCLE) 
 
1530
        if ( ( ($stop - $start) < OPTION_CYCLE ) && $stop < $total);
 
1531
 
 
1532
    ($start,$stop) = ($total - OPTION_CYCLE, $total) if $start < 0;
 
1533
    ($start,$stop) = (0, OPTION_CYCLE) if $start >= $total;
 
1534
 
 
1535
    $stop = $total if $stop > $total;
 
1536
 
 
1537
    my $dir_copy = $directions;
 
1538
    my $msg_copy = $msg;
 
1539
    my $format = "format STDOUT = \n" .
 
1540
        '-' x 156 . "\n" . 
 
1541
        '^' . '<' x 77 .  '| Available Commands:' . "\n" .
 
1542
        '$msg_copy' . "\n" .
 
1543
        '-' x 156 . "\n" . 
 
1544
        ' ' x 78 . "|\n" .
 
1545
        '^' . '<' x 77 . '| ^' . '<' x 75 . '~' . "\n" .
 
1546
        '$entry' . ' ' x 74 . '$dir_copy,' . "\n" .
 
1547
        (' ' x 20 . '^' . '<' x 57 . '| ^' . '<' x 75 . '~' . "\n" .
 
1548
        ' ' x 20 . '$entry,' . ' ' x 53 . '$dir_copy,' . "\n") x 1000  . ".\n";
 
1549
 
 
1550
    {
 
1551
        # eval throws redefined warning that breaks formatting. 
 
1552
        # Turning off warnings just for the eval to fix this.
 
1553
        local $^W = 0;
 
1554
        eval $format;
 
1555
    }
 
1556
 
 
1557
    write;
 
1558
 
 
1559
    print '-' x 156 . "\n" .
 
1560
        'Showing results ' . ( $stop ? ( $start + 1 ) : $start ) . 
 
1561
        " - $stop of possible SO term matches: (best guess is \"$best_guess\")" .
 
1562
        "\n" . '-' x 156 . "\n"; 
 
1563
 
 
1564
    for  (my $i = $start; $i < $stop; $i+=2) {
 
1565
 
 
1566
        my ($left, $right) = @opt[$i,$i+1];
 
1567
 
 
1568
        my ($nL, $nmL, $descL, $termL, @synL) = @$left;
 
1569
 
 
1570
        #odd numbered lists can cause fatal undefined errors, so check
 
1571
        #to make sure we have data
 
1572
        
 
1573
        my ($nR, $nmR, $descR, $termR, @synR) = ref($right) ? @$right : (undef, undef, undef);
 
1574
 
 
1575
 
 
1576
        my $format = "format STDOUT = \n";
 
1577
 
 
1578
        $format .=
 
1579
            ' ' x 78 . "|\n" .
 
1580
 
 
1581
            '@>>: name: ^' . '<' x 64 . '~' . ' |' .
 
1582
                ( ref($right) ? ('@>>: name: ^' . '<' x 64 . '~' ) : '' ) .  "\n" .
 
1583
            '$nL,' . ' ' x 7 . '$nmL,' .
 
1584
                ( ref($right) ? (' ' x 63 . '$nR,' .  ' ' x 7 .  "\$nmR,") : '' ) . "\n" .
 
1585
 
 
1586
            ' ' x 11 . '^' . '<' x 61 . '...~' . ' |' . 
 
1587
                (ref($right) ? ('           ^' . '<' x 61 .  '...~') : '') . "\n" .
 
1588
            ' ' x 11 . '$nmL,' . 
 
1589
                (ref($right) ? (' ' x 74 . '$nmR,') : '') . "\n" .
 
1590
            #' ' x 78 . '|' . "\n" .
 
1591
 
 
1592
 
 
1593
            '     def:  ^' . '<' x 65 . ' |' . 
 
1594
                (ref($right) ? ('     def:  ^' . '<' x 64 . '~') : '') . "\n" .
 
1595
            ' ' x 11 . '$descL,' . 
 
1596
                (ref($right) ? (' ' x 72 . '$descR,') : '') . "\n" .
 
1597
 
 
1598
 
 
1599
            ('           ^' . '<' x 65 . ' |' . 
 
1600
                (ref($right) ? ('           ^' . '<' x 64 . '~') : '') . "\n" .
 
1601
            ' ' x 11 . '$descL,' . 
 
1602
                (ref($right) ? (' ' x 72 . '$descR,') : '') . "\n") x 5 .
 
1603
 
 
1604
 
 
1605
            '           ^' . '<' x 61 . '...~ |' . 
 
1606
                (ref($right) ? ('           ^' . '<' x 61 . '...~') : '') . "\n" .
 
1607
            ' ' x 11 . '$descL,' . 
 
1608
                (ref($right) ? (' ' x 72 . '$descR,') : '') . "\n" .
 
1609
 
 
1610
            ".\n";
 
1611
 
 
1612
        {
 
1613
            # eval throws redefined warning that breaks formatting. 
 
1614
            # Turning off warnings just for the eval to fix this.
 
1615
            local $^W = 0;
 
1616
            eval $format;
 
1617
        }
 
1618
        write;
 
1619
 
 
1620
    }   
 
1621
    print '-' x 156 . "\nenter a command:";
 
1622
 
 
1623
    while (<STDIN>) {
 
1624
 
 
1625
        (my $input = $_) =~ s/\s+$//;
 
1626
 
 
1627
        if ($input =~ /^\d+$/) {
 
1628
            if ( $input && defined $opt[$input-1] ) {
 
1629
                return $opt[$input-1]->[1]
 
1630
            } else {
 
1631
                print "\nThat number is not an option. Please enter a valid number.\n:";
 
1632
            }
 
1633
        } elsif ($input =~ /^n/i | $input =~ /next/i ) {
 
1634
            return options_cycle($start + OPTION_CYCLE, $stop + OPTION_CYCLE, $msg, 
 
1635
                    $feat, $directions, $best_guess, @opt)
 
1636
        } elsif ($input =~ /^p/i | $input =~ /prev/i ) {
 
1637
            return options_cycle($start - OPTION_CYCLE, $stop - OPTION_CYCLE, $msg,
 
1638
                    $feat, $directions, $best_guess, @opt)
 
1639
        } elsif ( $input =~ /^s/i || $input =~ /skip/i ) { return 'skip' 
 
1640
        } elsif ( $input =~ /^a/i || $input =~ /auto/i ) { return 'auto' 
 
1641
        } elsif ( $input =~ /^r/i || $input =~ /reset/i ) { 
 
1642
            return manual_curation($feat, $best_guess, \@RETURN );
 
1643
        } elsif ( $input =~ /^f/i || $input =~ /find/i ) {
 
1644
 
 
1645
            my ($query, @query_results);
 
1646
 
 
1647
            if ($input =~ /(?:^f|find)\s+?(.*?)$/) { $query = $1;
 
1648
            } else {
 
1649
 
 
1650
                #do a SO search
 
1651
                print "Type your search query\n:";
 
1652
                while (<STDIN>) { chomp($query = $_); last }
 
1653
            }
 
1654
 
 
1655
            for (keys(%$TYPE_MAP), keys(%$SYN_MAP)) {
 
1656
                SO_fuzzy_match($_, $query, '', $_, \@query_results, '(?i)');
 
1657
            }
 
1658
 
 
1659
            return manual_curation($feat, $best_guess, \@query_results);
 
1660
 
 
1661
        } elsif ( $input =~ /^i/i || $input =~ /input/i ) {
 
1662
 
 
1663
            #NML fast input for later
 
1664
            #my $query;
 
1665
            #if ($input =~ /(?:^i|input)\s+?(.*?)$/) { $query = $1 };
 
1666
 
 
1667
            #manual input
 
1668
            print "Type the term you want to use\n:";
 
1669
            while (<STDIN>) {
 
1670
                chomp(my $input = $_);
 
1671
 
 
1672
                if (! $TYPE_MAP->{$input}) {
 
1673
 
 
1674
                    print "\"$input\" doesn't appear to be a valid SO term. Are ".
 
1675
                        "you sure you want to use it? (y or n)\n:";
 
1676
 
 
1677
                    while (<STDIN>) {
 
1678
 
 
1679
                        chomp(my $choice = $_);
 
1680
 
 
1681
                        if ($choice eq 'y') {
 
1682
                            print 
 
1683
                                "\nWould you like to save your preference for " .
 
1684
                                "future use (so you don't have to redo manual " .
 
1685
                                "curation for this feature everytime you run " . 
 
1686
                                "the converter)? (y or n)\n";
 
1687
 
 
1688
                            #NML: all these while loops are a mess. Really should condense it.
 
1689
                            while (<STDIN>) {
 
1690
 
 
1691
                                chomp(my $choice = $_);
 
1692
 
 
1693
                                if ($choice eq 'y') {
 
1694
                                    curation_save($feat, $input);
 
1695
                                    return $input;
 
1696
                                } elsif ($choice eq 'n') {
 
1697
                                    return $input
 
1698
                                } else {
 
1699
                                    print "\nDidn't recognize that command. Please " . 
 
1700
                                        "type y or n.\n:" 
 
1701
                                }
 
1702
                            }
 
1703
 
 
1704
                                
 
1705
                        } elsif ($choice eq 'n') {
 
1706
                            return options_cycle($start, $stop, $msg, $feat,
 
1707
                                    $directions, $best_guess, @opt)
 
1708
                        } else {
 
1709
                            print "\nDidn't recognize that command. Please " . 
 
1710
                                "type y or n.\n:" 
 
1711
                        }
 
1712
                    }
 
1713
 
 
1714
                } else { 
 
1715
                    print 
 
1716
                        "\nWould you like to save your preference for " .
 
1717
                        "future use (so you don't have to redo manual " .
 
1718
                        "curation for this feature everytime you run  " . 
 
1719
                        "the converter)? (y or n)\n";
 
1720
 
 
1721
                    #NML: all these while loops are a mess. Really should condense it.
 
1722
                    while (<STDIN>) {
 
1723
 
 
1724
                        chomp(my $choice = $_);
 
1725
 
 
1726
                        if ($choice eq 'y') {
 
1727
                            curation_save($feat, $input);
 
1728
                            return $input;
 
1729
                        } elsif ($choice eq 'n') {
 
1730
                            return $input
 
1731
                        } else {
 
1732
                            print "\nDidn't recognize that command. Please " . 
 
1733
                                "type y or n.\n:" 
 
1734
                        }
 
1735
                    }
 
1736
 
 
1737
                } 
 
1738
 
 
1739
            }
 
1740
        } else { 
 
1741
            print "\nDidn't recognize that command. Please re-enter your choice.\n:" 
 
1742
        }
 
1743
    }
 
1744
 
 
1745
}
 
1746
 
 
1747
sub GenBank_entry {
 
1748
    my ($f, $delimiter, $num) = @_;
 
1749
 
 
1750
    $delimiter ||= "\n";
 
1751
 
 
1752
 
 
1753
    my $entry  = 
 
1754
 
 
1755
        ($num ? ' [1] ' : ' ' x 5) . $f->primary_tag 
 
1756
        . ($num 
 
1757
            ? ' ' x (12 - length $f->primary_tag ) . ' [2] '
 
1758
            : ' ' x (15 - length $f->primary_tag) 
 
1759
          )
 
1760
        . $f->start.'..'.$f->end
 
1761
 
 
1762
        . "$delimiter";
 
1763
 
 
1764
    if ($num) {
 
1765
        words_tag($f, \$entry);
 
1766
    } else {
 
1767
        for my $tag ($f->all_tags) {
 
1768
            for my $val ( $f->each_tag_value($tag) ) {
 
1769
                $entry .= ' ' x 20;
 
1770
                #$entry .= "/$tag=\"$val\"$delimiter";
 
1771
                $entry .= $val eq '_no_value'
 
1772
                    ? "/$tag$delimiter"
 
1773
                    : "/$tag=\"$val\"$delimiter";
 
1774
            }
 
1775
        }
 
1776
 
 
1777
    }
 
1778
 
 
1779
    return $entry;
 
1780
}
 
1781
 
 
1782
 
 
1783
sub gff_validate {
 
1784
    warn "Validating GFF file\n" if $DEBUG;
 
1785
    my @feat = @_;
 
1786
 
 
1787
    my (%parent2child, %all_ids, %descendants, %reserved);
 
1788
 
 
1789
    for my $f (@feat) {
 
1790
        for my $aTags (['Parent', \%parent2child], ['ID', \%all_ids]) {
 
1791
            map { push @{$$aTags[1]->{$_}}, $f } $f->get_tag_values($$aTags[0])
 
1792
                if $f->has_tag($$aTags[0]); 
 
1793
        }
 
1794
    }
 
1795
 
 
1796
    if ($SO_FILE) {
 
1797
        while (my ($parentID, $aChildren) = each %parent2child) {
 
1798
            parent_validate($parentID, $aChildren, \%all_ids, \%descendants, \%reserved);
 
1799
        }
 
1800
    }
 
1801
 
 
1802
    id_validate(\%all_ids, \%reserved); 
 
1803
}
 
1804
 
 
1805
sub parent_validate {
 
1806
    my ($parentID, $aChildren, $hAll, $hDescendants, $hReserved) = @_;
 
1807
 
 
1808
    my $aParents = $hAll->{$parentID};
 
1809
 
 
1810
    map { 
 
1811
        my $child = $_;
 
1812
        $child->add_tag_value( validation_error => 
 
1813
        "feature tried to add Parent tag, but no Parent found with ID $parentID"
 
1814
        );
 
1815
        my %parents;
 
1816
        map { $parents{$_} = 1 } $child->get_tag_values('Parent');
 
1817
        delete $parents{$parentID};
 
1818
        my @parents = keys %parents;
 
1819
 
 
1820
        $child->remove_tag('Parent');
 
1821
 
 
1822
        unless ($child->has_tag('ID')) {
 
1823
            my $id = gene_name($child);
 
1824
            $child->add_tag_value('ID', $id);
 
1825
            push @{$hAll->{$id}}, $child
 
1826
        }
 
1827
 
 
1828
        $child->add_tag_value('Parent', @parents) if @parents;
 
1829
 
 
1830
    } @$aChildren and return unless scalar(@$aParents);
 
1831
 
 
1832
    my $par = join(',', map { $_->primary_tag } @$aParents);
 
1833
    warn scalar(@$aParents)." POSSIBLE PARENT(S): $par" if $DEBUG;
 
1834
 
 
1835
    #NML manual curation needs to happen here
 
1836
 
 
1837
 
 
1838
    my %parentsToRemove;
 
1839
 
 
1840
    CHILD:
 
1841
    for my $child (@$aChildren) {
 
1842
        my $childType  = $child->primary_tag;
 
1843
 
 
1844
        warn "WORKING ON $childType at ".$child->start.' to '.$child->end 
 
1845
            if $DEBUG;
 
1846
 
 
1847
        for (my $i = 0; $i < scalar(@$aParents); $i++) {
 
1848
            my $parent = $aParents->[$i];
 
1849
            my $parentType = $parent->primary_tag;
 
1850
 
 
1851
            warn "CHECKING $childType against $parentType" if $DEBUG;
 
1852
 
 
1853
            #cache descendants so we don't have to do repeat searches
 
1854
            unless ($hDescendants->{$parentType}) {
 
1855
 
 
1856
                for my $term ($ONTOLOGY->find_terms(
 
1857
                        -name => $parentType
 
1858
                    ) ) {
 
1859
                    
 
1860
                    map {
 
1861
                        $hDescendants->{$parentType}{$_->name}++
 
1862
                    } $ONTOLOGY->get_descendant_terms($term);
 
1863
 
 
1864
                }
 
1865
 
 
1866
                # NML: hopefully temporary fix.
 
1867
                # SO doesn't consider exon/CDS to be a child of mRNA
 
1868
                # even though common knowledge dictates that they are
 
1869
                # This cheat fixes the false positives for now
 
1870
                if ($parentType eq 'mRNA') {
 
1871
                    $hDescendants->{$parentType}{'exon'} = 1;
 
1872
                    $hDescendants->{$parentType}{'CDS'} = 1;
 
1873
                }
 
1874
 
 
1875
            }
 
1876
 
 
1877
            warn "\tCAN $childType at " . $child->start . ' to ' . $child->end .
 
1878
                " be a child of $parentType?" if $DEBUG;
 
1879
            if ($hDescendants->{$parentType}{$childType}) {
 
1880
                warn "\tYES, $childType can be a child of $parentType" if $DEBUG;
 
1881
 
 
1882
                #NML need to deal with multiple children matched to multiple different
 
1883
                #parents. This model only assumes the first parent id that matches a child will
 
1884
                #be the reserved feature. 
 
1885
 
 
1886
                $hReserved->{$parentID}{$parent}{'parent'} = $parent;
 
1887
                push @{$hReserved->{$parentID}{$parent}{'children'}}, $child;
 
1888
 
 
1889
                #mark parent for later removal from all IDs 
 
1890
                #so we don't accidentally change any parents
 
1891
 
 
1892
                $parentsToRemove{$i}++;
 
1893
 
 
1894
                next CHILD;
 
1895
            } 
 
1896
        }
 
1897
 
 
1898
 
 
1899
        
 
1900
#NML shouldn't have to check this; somehow child can lose Parent
 
1901
#it's happening W3110
 
1902
#need to track this down
 
1903
        if ( $child->has_tag('Parent') ) {
 
1904
 
 
1905
            warn "\tNO, @{[$child->primary_tag]} cannot be a child of $parentID"
 
1906
                if $DEBUG;
 
1907
 
 
1908
            my %parents;
 
1909
 
 
1910
            map { $parents{$_} = 1 } $child->get_tag_values('Parent');
 
1911
 
 
1912
            delete $parents{$parentID};
 
1913
            my @parents = keys %parents;
 
1914
 
 
1915
            warn 'VALIDATION ERROR '.$child->primary_tag." at ".$child->start .
 
1916
                ' to ' . $child->end . " cannot be a child of ID $parentID"
 
1917
                if $DEBUG;
 
1918
 
 
1919
            $child->add_tag_value( validation_error => 
 
1920
                    "feature cannot be a child of $parentID");
 
1921
 
 
1922
            $child->remove_tag('Parent');
 
1923
 
 
1924
            unless ($child->has_tag('ID')) {
 
1925
                my $id = gene_name($child);
 
1926
                $child->add_tag_value('ID', $id);
 
1927
                push @{$hAll->{$id}}, $child
 
1928
            }
 
1929
 
 
1930
            $child->add_tag_value('Parent', @parents) if @parents;
 
1931
        }
 
1932
 
 
1933
    }
 
1934
    
 
1935
    #delete $aParents->[$_] for keys %parentsToRemove;
 
1936
    splice(@$aParents, $_, 1) for keys %parentsToRemove;
 
1937
}
 
1938
 
 
1939
sub id_validate {
 
1940
    my ($hAll, $hReserved) = @_;
 
1941
 
 
1942
 
 
1943
    for my $id (keys %$hAll) {
 
1944
 
 
1945
        #since 1 feature can have this id, 
 
1946
        #let's just shift it off and uniquify
 
1947
        #the rest (unless it's reserved)
 
1948
 
 
1949
        shift @{$hAll->{$id}} unless $hReserved->{$id};
 
1950
        for my $feat (@{$hAll->{$id}}) {
 
1951
            id_uniquify(0, $id, $feat, $hAll);
 
1952
        }
 
1953
    }
 
1954
 
 
1955
    for my $parentID (keys %$hReserved) {
 
1956
 
 
1957
        my @keys = keys %{$hReserved->{$parentID}};
 
1958
 
 
1959
        shift @keys;
 
1960
 
 
1961
        for my $k (@keys) {
 
1962
            my $parent = $hReserved->{$parentID}{$k}{'parent'};
 
1963
            my $aChildren= $hReserved->{$parentID}{$k}{'children'};
 
1964
 
 
1965
            my $value = id_uniquify(0, $parentID, $parent, $hAll);
 
1966
            for my $child (@$aChildren) {
 
1967
 
 
1968
                my %parents;
 
1969
                map { $parents{$_}++ } $child->get_tag_values('Parent');
 
1970
                $child->remove_tag('Parent');
 
1971
                delete $parents{$parentID};
 
1972
                $parents{$value}++;
 
1973
                my @parents = keys %parents;
 
1974
                $child->add_tag_value('Parent', @parents);
 
1975
            }
 
1976
 
 
1977
        }
 
1978
    }
 
1979
}
 
1980
 
 
1981
sub id_uniquify {
 
1982
    my ($count, $value, $feat, $hAll) = @_;
 
1983
 
 
1984
    warn "UNIQUIFYING $value" if $DEBUG;
 
1985
 
 
1986
    if (! $count) {
 
1987
        $feat->add_tag_value(Alias => $value);
 
1988
        $value .= ('.' . $feat->primary_tag) 
 
1989
    } elsif ($count == 1) {
 
1990
        $value .= ".$count" 
 
1991
    } else { 
 
1992
        chop $value;
 
1993
        $value .= $count 
 
1994
    }
 
1995
    $count++;
 
1996
 
 
1997
    warn "ENDED UP WITH $value" if $DEBUG;
 
1998
    if ( $hAll->{$value} ) { 
 
1999
        warn "$value IS ALREADY TAKEN" if $DEBUG;
 
2000
        id_uniquify($count, $value, $feat, $hAll);
 
2001
    } else { 
 
2002
        #warn "something's breaking ".$feat->primary_tag.' at '.$feat->start.' to '.$feat->end;
 
2003
        $feat->remove_tag('ID');
 
2004
        $feat->add_tag_value('ID', $value);
 
2005
        push @{$hAll->{$value}}, $value;
 
2006
    }
 
2007
 
 
2008
    $value;
 
2009
}
 
2010
 
 
2011
sub conf_read {
 
2012
 
 
2013
    print "\nCannot read $CONF. Change file permissions and retry, " .
 
2014
        "or enter another file\n" and conf_locate() unless -r $CONF;
 
2015
 
 
2016
    print "\nCannot write $CONF. Change file permissions and retry, " .
 
2017
        "or enter another file\n" and conf_locate() unless -w $CONF;
 
2018
 
 
2019
    $YAML = LoadFile($CONF);
 
2020
 
 
2021
}
 
2022
 
 
2023
sub conf_create {
 
2024
 
 
2025
    my ($path, $input) = @_;
 
2026
 
 
2027
    print "Cannot write to $path. Change directory permissions and retry " .
 
2028
        "or enter another save path\n" and conf_locate() unless -w $path;
 
2029
 
 
2030
    $CONF = $input;
 
2031
 
 
2032
    open(FH, '>', $CONF);
 
2033
    close(FH);
 
2034
    conf_read();
 
2035
 
 
2036
 
 
2037
}
 
2038
 
 
2039
sub conf_write { DumpFile($CONF, $YAML) }
 
2040
 
 
2041
sub conf_locate {
 
2042
 
 
2043
    print "\nEnter the location of a previously saved config, or a new " .
 
2044
        "path and file name to create a new config (this step is " .
 
2045
        "necessary to save any preferences)";
 
2046
 
 
2047
    print "\n\nenter a command:";
 
2048
 
 
2049
    while (<STDIN>) {
 
2050
        chomp(my $input = $_);
 
2051
        my ($fn, $path, $suffix) = fileparse($input, qr/\.[^.]*/);
 
2052
 
 
2053
        if (-e $input && (! -d $input)) {
 
2054
 
 
2055
            print "\nReading $input...\n";
 
2056
            $CONF = $input;
 
2057
 
 
2058
            conf_read(); 
 
2059
            last;
 
2060
 
 
2061
        } elsif (! -d $input && $fn.$suffix) {
 
2062
 
 
2063
            print "Creating $input...\n";
 
2064
            conf_create($path, $input);
 
2065
            last;
 
2066
 
 
2067
        } elsif (-e $input && -d $input) {
 
2068
            print "You only entered a directory. " .
 
2069
                "Please enter BOTH a directory and filename\n";
 
2070
        } else { 
 
2071
            print "$input does not appear to be a valid path. Please enter a " .
 
2072
                "valid directory and filename\n";
 
2073
        }
 
2074
        print "\nenter a command:";
 
2075
    }
 
2076
}
 
2077
 
 
2078
sub curation_save {
 
2079
 
 
2080
    my ($feat, $input) = @_;
 
2081
 
 
2082
    #my $error = "Enter the location of a previously saved config, or a new " .
 
2083
        #"path and file name to create a new config (this step is " .
 
2084
        #"necessary to save any preferences)\n";
 
2085
 
 
2086
    if (!$CONF) {
 
2087
        print "\n\n"; 
 
2088
        conf_locate();
 
2089
    } elsif (! -e $CONF) {
 
2090
        print "\n\nThe config file you have chosen doesn't exist.\n";
 
2091
        conf_locate();
 
2092
    } else { conf_read() }
 
2093
 
 
2094
    my $entry = GenBank_entry($feat, "\r", 1);
 
2095
 
 
2096
    my $msg = "Term entered: $input";
 
2097
    my $directions = "Please select any/all tags that provide evidence for the term you
 
2098
have entered. You may enter multiple tags by separating them by commas/dashes
 
2099
(e.g 1,3,5-7). For tags with more than one word value (i.e 'note'), you have
 
2100
the option of either selecting the entire note as evidence, or specific
 
2101
keywords. If a tag has multiple keywords, they will be tagged alphabetically
 
2102
for selection. To select a specific keyword in a tag field, you must enter the
 
2103
tag number followed by the keyword letter (e.g 3a). Multiple keywords may be
 
2104
selected by entering each letter separated by commas/dashes (e.g 3b,f,4a-c). The more tags you select, the more specific the GenBank entry will have
 
2105
to be to match your curation. To match the GenBank entry exactly as it
 
2106
appears, type every number (start-end), or just type 'all'. Remember, once the converter saves your
 
2107
preference, you will no longer be prompted to choose a feature type for any
 
2108
matching entries until you edit the curation.ini file.";
 
2109
    my $msg_copy = $msg;
 
2110
    my $dir_copy = $directions;
 
2111
 
 
2112
    my $format = "format STDOUT = \n" .
 
2113
        '-' x 156 . "\n" . 
 
2114
        '^' . '<' x 77 .  '| Directions:' . "\n" .
 
2115
        '$msg_copy' . "\n" .
 
2116
        '-' x 156 . "\n" . 
 
2117
        ' ' x 78 . "|\n" .
 
2118
        '^' . '<' x 77 . '| ^' . '<' x 75 . '~' . "\n" .
 
2119
        '$entry' . ' ' x 74 . '$dir_copy,' . "\n" .
 
2120
        (' ' x 15 . '^' . '<' x 62 . '| ^' . '<' x 75 . '~' . "\n" .
 
2121
        ' ' x 15 . '$entry,' . ' ' x 58 . '$dir_copy,' . "\n") x 20  . ".\n";
 
2122
 
 
2123
    {
 
2124
        # eval throws redefined warning that breaks formatting. 
 
2125
        # Turning off warnings just for the eval to fix this.
 
2126
        local $^W = 0;
 
2127
        eval $format;
 
2128
    }
 
2129
 
 
2130
    write;
 
2131
    print '-' x 156 . "\nenter a command:";
 
2132
 
 
2133
    my @tags = words_tag($feat); 
 
2134
 
 
2135
    my $final = {};
 
2136
    my $choices;
 
2137
    while (<STDIN>) {
 
2138
 
 
2139
        chomp(my $choice = $_);
 
2140
 
 
2141
        if (scalar(keys %$final) && $choice =~ /^y/i) { last 
 
2142
 
 
2143
        } elsif (scalar(keys %$final) && $choice =~ /^n/i) { curation_save($feat, $input) 
 
2144
 
 
2145
        } elsif (scalar(keys %$final)) { print "\nInvalid selection. Please try again\n";
 
2146
 
 
2147
        } elsif ($choice eq 'all') {
 
2148
 
 
2149
            $choice = '';
 
2150
            for (my $i=1; $i < scalar(@tags); $i++) {
 
2151
                $choice .= "$i,";
 
2152
            }
 
2153
            chop $choice;
 
2154
        } 
 
2155
#       print "CHOICE [$choice]";
 
2156
 
 
2157
        my @selections;
 
2158
        for (split(/(?<=\w)[^[:alnum:]\-]+(?=\d)/, $choice)) {
 
2159
            if ($_ =~ /(\d+)(?:\D*)-(\d+)(.*)/) { 
 
2160
 
 
2161
                for ($1..$2) { push @selections, $_ }
 
2162
 
 
2163
                my $dangling_alphas = $3;
 
2164
                alpha_expand($dangling_alphas, \@selections);
 
2165
 
 
2166
            } else { 
 
2167
                alpha_expand($_, \@selections);
 
2168
            }
 
2169
        }
 
2170
 
 
2171
        foreach my $numbers (@selections) {
 
2172
 
 
2173
            my @c = split(/(?=[\w])/, $numbers);
 
2174
            s/\W+//g foreach @c;
 
2175
            my $num;
 
2176
            
 
2177
            {
 
2178
                $^W = 0;
 
2179
                $num = 0 + shift @c;
 
2180
            }
 
2181
 
 
2182
            my $tag = $tags[$num];
 
2183
            if (ref $tag && scalar(@c)) {
 
2184
                my $no_value;
 
2185
                foreach (@c) {
 
2186
                    if (defined $tag->{$_}) {
 
2187
                        $choices .= "${num}[$_] ";
 
2188
                        my ($t,$v) = @{$tag->{$_}};
 
2189
                        push @{${$final->{$input}}[0]{$t}}, $v;
 
2190
 
 
2191
                    } else { $no_value++ }
 
2192
                }
 
2193
 
 
2194
                if ($no_value) { 
 
2195
                    _selection_add($tag,$final,$input,\$choices,$num);
 
2196
                    #my ($t,$v) = @{$tag->{'all'}};
 
2197
                    #unless (defined ${$final->{$input}}[0]{$t}) {
 
2198
                        #$choices .= "$num, ";
 
2199
                        #push @{${$final->{$input}}[0]{$t}}, $v
 
2200
                    #}
 
2201
                }
 
2202
 
 
2203
                $choices = substr($choices, 0, -2);
 
2204
                $choices .= ', ';
 
2205
 
 
2206
            } elsif (ref $tag) { 
 
2207
                _selection_add($tag,$final,$input,\$choices,$num);
 
2208
                #my ($t,$v) = @{$tag->{'all'}};
 
2209
                #unless (defined ${$final->{$input}}[0]{$t}) {
 
2210
                    #$choices .= "$num, ";
 
2211
                    #push @{${$final->{$input}}[0]{$t}}, $v
 
2212
                #}
 
2213
            } 
 
2214
        }
 
2215
        $choices = substr($choices, 0, -2) if $choices;
 
2216
        if ($final) {
 
2217
            print "\nYou have chosen the following tags:\n$choices\n";
 
2218
            print "This will be written to the config file as:\n";
 
2219
            print Dump $final;
 
2220
            print "\nIs this correct? (y or n)\n";
 
2221
        } else { print "\nInvalid selection. Please try again\n" }
 
2222
    }
 
2223
    push @{$YAML->{$input}}, $final->{$input};
 
2224
    conf_write();
 
2225
}
 
2226
 
 
2227
#  words_tag() splits each tag value string into multiple words so that the 
 
2228
#  user can select the parts he/she wants to use for curation
 
2229
#  it can tag 702 (a - zz) separate words; this should be enough
 
2230
 
 
2231
sub words_tag {
 
2232
 
 
2233
    my ($feat, $entry) = @_;
 
2234
 
 
2235
    my @tags;
 
2236
 
 
2237
    @tags[1,2] = ({'all' => ['primary_tag', $feat->primary_tag]}, {'all' => ['location', $feat->start.'..'.$feat->end]});
 
2238
    my $i = 3;
 
2239
    foreach my $tag ($feat->all_tags) {
 
2240
        foreach my $value ($feat->each_tag_value($tag)) {
 
2241
 
 
2242
            my ($string, $tagged_string);
 
2243
 
 
2244
            my @words = split(/(?=\w+?)/, $value);
 
2245
 
 
2246
            my $pos = 0;
 
2247
 
 
2248
 
 
2249
            foreach my $word (@words) {
 
2250
 
 
2251
                (my $sanitized_word = $word) =~ s/\W+?//g;
 
2252
                $string .= $word;
 
2253
 
 
2254
                my $lead = int($pos/ALPHABET_DIVISOR);
 
2255
                my $lag = $pos % ALPHABET_DIVISOR;
 
2256
 
 
2257
                my $a =  $lead ? ${(ALPHABET)}[$lead-1] : '';
 
2258
                $a .= $lag ? ${(ALPHABET)}[$lag] : 'a';
 
2259
 
 
2260
                $tagged_string .= " ($a) $word";
 
2261
 
 
2262
                $tags[$i]{$a} = [$tag, $sanitized_word];
 
2263
                $pos++;
 
2264
            }
 
2265
 
 
2266
            $value = $tagged_string if scalar(@words) > 1;
 
2267
 
 
2268
            $$entry .= "[$i] /$tag=\"$value\"\r";
 
2269
 
 
2270
            $tags[$i]{'all'} = [$tag, $string];
 
2271
        }
 
2272
        $i++;
 
2273
    }
 
2274
 
 
2275
    return @tags;
 
2276
    
 
2277
}
 
2278
 
 
2279
sub alpha_expand {
 
2280
 
 
2281
    my ($dangling_alphas, $selections) = @_;
 
2282
 
 
2283
    if (defined($dangling_alphas) && $dangling_alphas =~ /(\d)*([[:alpha:]]+)-([[:alpha:]]+)/) {
 
2284
 
 
2285
        my $digit = $1;
 
2286
        push @$selections, $digit if $digit;
 
2287
 
 
2288
        my $start = $2;
 
2289
        my $stop = $3;
 
2290
 
 
2291
        my @starts = split('', $start);
 
2292
        my @stops = split('', $stop);
 
2293
 
 
2294
        my ($final_start, $final_stop);
 
2295
 
 
2296
        for ([\$final_start, \@starts], [\$final_stop, \@stops]) {
 
2297
 
 
2298
            my ($final, $splits) = @$_;
 
2299
 
 
2300
            my $int = ${(ALPHABET_TO_NUMBER)}{$$splits[0]};
 
2301
            my $rem;
 
2302
 
 
2303
 
 
2304
            if ($$splits[1]) {
 
2305
                $rem = ${(ALPHABET_TO_NUMBER)}{$$splits[1]};
 
2306
                $int++
 
2307
            } else {
 
2308
                $rem = $int;
 
2309
                $int = 0;
 
2310
            }
 
2311
 
 
2312
 
 
2313
            $$final = $int * ALPHABET_DIVISOR;
 
2314
            $$final += $rem;
 
2315
 
 
2316
        }
 
2317
 
 
2318
        my $last_number = pop @$selections;
 
2319
        for my $pos ($final_start..$final_stop) {
 
2320
            my $lead = int($pos/ALPHABET_DIVISOR);
 
2321
            my $lag = $pos % ALPHABET_DIVISOR;
 
2322
            my $alpha =  $lead ? ${(ALPHABET)}[$lead-1] : '';
 
2323
            $alpha .= $lag ? ${(ALPHABET)}[$lag] : 'a';
 
2324
            push @$selections, $last_number.$alpha;     
 
2325
        }
 
2326
 
 
2327
    } elsif (defined($dangling_alphas)) { 
 
2328
        if ($dangling_alphas =~ /^\d/) {
 
2329
            push @$selections, $dangling_alphas;
 
2330
        } elsif ($dangling_alphas =~ /^\D/) {
 
2331
            #print "$dangling_alphas ".Dumper @$selections;
 
2332
            my $last_number = pop @$selections;
 
2333
            $last_number ||= '';
 
2334
            push @$selections, $last_number.$dangling_alphas;
 
2335
            #$$selections[-1] .= $dangling_alphas;
 
2336
        }
 
2337
    }
 
2338
 
 
2339
}
 
2340
 
 
2341
sub _selection_add {
 
2342
 
 
2343
    my ($tag, $final, $input, $choices, $num) = @_;
 
2344
    my ($t,$v) = @{$tag->{'all'}};
 
2345
    unless (defined ${$final->{$input}}[0]{$t}) {
 
2346
        $$choices .= "$num, ";
 
2347
        push @{${$final->{$input}}[0]{$t}}, $v
 
2348
    }
 
2349
 
 
2350
}