61f28a9361a454e8ec0ad916111f47bdfb4f39df
[jalview.git] / src / jalview / io / StockholmFile.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 /*
22  * This extension was written by Benjamin Schuster-Boeckler at sanger.ac.uk
23  */
24 package jalview.io;
25
26 import jalview.analysis.Rna;
27 import jalview.datamodel.AlignmentAnnotation;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.Annotation;
30 import jalview.datamodel.DBRefEntry;
31 import jalview.datamodel.DBRefSource;
32 import jalview.datamodel.Mapping;
33 import jalview.datamodel.Sequence;
34 import jalview.datamodel.SequenceFeature;
35 import jalview.datamodel.SequenceI;
36 import jalview.schemes.ResidueProperties;
37 import jalview.util.Comparison;
38 import jalview.util.Format;
39 import jalview.util.MessageManager;
40
41 import java.io.BufferedReader;
42 import java.io.FileReader;
43 import java.io.IOException;
44 import java.util.ArrayList;
45 import java.util.Enumeration;
46 import java.util.HashMap;
47 import java.util.Hashtable;
48 import java.util.LinkedHashMap;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Map.Entry;
52 import java.util.Vector;
53
54 import com.stevesoft.pat.Regex;
55
56 import fr.orsay.lri.varna.exceptions.ExceptionUnmatchedClosingParentheses;
57 import fr.orsay.lri.varna.factories.RNAFactory;
58 import fr.orsay.lri.varna.models.rna.RNA;
59
60 // import org.apache.log4j.*;
61
62 /**
63  * This class is supposed to parse a Stockholm format file into Jalview There
64  * are TODOs in this class: we do not know what the database source and version
65  * is for the file when parsing the #GS= AC tag which associates accessions with
66  * sequences. Database references are also not parsed correctly: a separate
67  * reference string parser must be added to parse the database reference form
68  * into Jalview's local representation.
69  * 
70  * @author bsb at sanger.ac.uk
71  * @author Natasha Shersnev (Dundee, UK) (Stockholm file writer)
72  * @author Lauren Lui (UCSC, USA) (RNA secondary structure annotation import as
73  *         stockholm)
74  * @author Anne Menard (Paris, FR) (VARNA parsing of Stockholm file data)
75  * @version 0.3 + jalview mods
76  * 
77  */
78 public class StockholmFile extends AlignFile
79 {
80   private static final String ANNOTATION = "annotation";
81
82   private static final char UNDERSCORE = '_';
83
84   // private static final Regex OPEN_PAREN = new Regex("(<|\\[)", "(");
85   // private static final Regex CLOSE_PAREN = new Regex("(>|\\])", ")");
86
87   public static final Regex DETECT_BRACKETS = new Regex(
88           "(<|>|\\[|\\]|\\(|\\)|\\{|\\})");
89
90   /*
91    * lookup table of Stockholm 'feature' (annotation) types
92    * see http://sonnhammer.sbc.su.se/Stockholm.html
93    */
94   private static Map<String, String> featureTypes = null;
95
96   // WUSS extended symbols. Avoid ambiguity with protein SS annotations by using NOT_RNASS first.
97   public static final String RNASS_BRACKETS = "<>[](){}AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz";
98
99   // use the following regex to decide an annotations (whole) line is NOT an RNA
100   // SS (it contains only E,H,e,h and other non-brace/non-alpha chars)
101   private static final Regex NOT_RNASS = new Regex(
102           "^[^<>[\\](){}A-DF-Za-df-z]*$");
103
104   StringBuffer out; // output buffer
105
106   static
107   {
108     featureTypes = new HashMap<>();
109     featureTypes.put("SS", "Secondary Structure");
110     featureTypes.put("SA", "Surface Accessibility");
111     featureTypes.put("TM", "transmembrane");
112     featureTypes.put("PP", "Posterior Probability");
113     featureTypes.put("LI", "ligand binding");
114     featureTypes.put("AS", "active site");
115     featureTypes.put("IN", "intron");
116     featureTypes.put("IR", "interacting residue");
117     featureTypes.put("AC", "accession");
118     featureTypes.put("OS", "organism");
119     featureTypes.put("CL", "class");
120     featureTypes.put("DE", "description");
121     featureTypes.put("DR", "reference");
122     featureTypes.put("LO", "look");
123     featureTypes.put("RF", "Reference Positions");
124   }
125
126   private AlignmentI al;
127
128   public StockholmFile()
129   {
130   }
131
132   /**
133    * Creates a new StockholmFile object for output
134    */
135   public StockholmFile(AlignmentI al)
136   {
137     this.al = al;
138   }
139
140   public StockholmFile(String inFile, DataSourceType type)
141           throws IOException
142   {
143     super(inFile, type);
144   }
145
146   public StockholmFile(FileParse source) throws IOException
147   {
148     super(source);
149   }
150
151   /**
152    * Answers the readable description for a (case-sensitive) annotation type
153    * code, for example "Reference Positions" for "RF". Returns the type code if
154    * no description is found.
155    * 
156    * @param id
157    * @return
158    */
159   public static String typeToDescription(String id)
160   {
161     if (featureTypes.containsKey(id))
162     {
163       return featureTypes.get(id);
164     }
165     System.err.println(
166             "Warning : Unknown Stockholm annotation type code " + id);
167     return id;
168   }
169
170   /**
171    * Answers the annotation type code for a (non-case-sensitive) readable
172    * description, for example "RF" for "Reference Positions" (or null if not
173    * found)
174    * 
175    * @param description
176    * @return
177    */
178   public static String descriptionToType(String description)
179   {
180     for (Entry<String, String> entry : featureTypes.entrySet())
181     {
182       if (entry.getValue().equalsIgnoreCase(description))
183       {
184         return entry.getKey();
185       }
186     }
187     System.err.println(
188             "Warning : Unknown Stockholm annotation type: " + description);
189     return null;
190   }
191
192   @Override
193   public void initData()
194   {
195     super.initData();
196   }
197
198   /**
199    * Parse a file in Stockholm format into Jalview's data model using VARNA
200    * 
201    * @throws IOException
202    *           If there is an error with the input file
203    */
204   public void parse_with_VARNA(java.io.File inFile) throws IOException
205   {
206     FileReader fr = null;
207     fr = new FileReader(inFile);
208
209     BufferedReader r = new BufferedReader(fr);
210     List<RNA> result = null;
211     try
212     {
213       result = RNAFactory.loadSecStrStockholm(r);
214     } catch (ExceptionUnmatchedClosingParentheses umcp)
215     {
216       errormessage = "Unmatched parentheses in annotation. Aborting ("
217               + umcp.getMessage() + ")";
218       throw new IOException(umcp);
219     }
220     // DEBUG System.out.println("this is the secondary scructure:"
221     // +result.size());
222     SequenceI[] seqs = new SequenceI[result.size()];
223     String id = null;
224     for (int i = 0; i < result.size(); i++)
225     {
226       // DEBUG System.err.println("Processing i'th sequence in Stockholm file")
227       RNA current = result.get(i);
228
229       String seq = current.getSeq();
230       String rna = current.getStructDBN(true);
231       // DEBUG System.out.println(seq);
232       // DEBUG System.err.println(rna);
233       int begin = 0;
234       int end = seq.length() - 1;
235       id = safeName(getDataName());
236       seqs[i] = new Sequence(id, seq, begin, end);
237       String[] annot = new String[rna.length()];
238       Annotation[] ann = new Annotation[rna.length()];
239       for (int j = 0; j < rna.length(); j++)
240       {
241         annot[j] = rna.substring(j, j + 1);
242
243       }
244
245       for (int k = 0; k < rna.length(); k++)
246       {
247         ann[k] = new Annotation(annot[k], "",
248                 Rna.getRNASecStrucState(annot[k]).charAt(0), 0f);
249
250       }
251       AlignmentAnnotation align = new AlignmentAnnotation("Sec. str.",
252               current.getID(), ann);
253
254       seqs[i].addAlignmentAnnotation(align);
255       seqs[i].setRNA(result.get(i));
256       this.annotations.addElement(align);
257     }
258     this.setSeqs(seqs);
259
260   }
261
262   /**
263    * Parse a file in Stockholm format into Jalview's data model. The file has to
264    * be passed at construction time
265    * 
266    * @throws IOException
267    *           If there is an error with the input file
268    */
269   @Override
270   public void parse() throws IOException
271   {
272     StringBuffer treeString = new StringBuffer();
273     String treeName = null;
274     // --------------- Variable Definitions -------------------
275     String line;
276     String version;
277     // String id;
278     Hashtable seqAnn = new Hashtable(); // Sequence related annotations
279     LinkedHashMap<String, String> seqs = new LinkedHashMap<>();
280     Regex p, r, rend, s, x;
281     // Temporary line for processing RNA annotation
282     // String RNAannot = "";
283
284     // ------------------ Parsing File ----------------------
285     // First, we have to check that this file has STOCKHOLM format, i.e. the
286     // first line must match
287
288     r = new Regex("# STOCKHOLM ([\\d\\.]+)");
289     if (!r.search(nextLine()))
290     {
291       throw new IOException(MessageManager
292               .getString("exception.stockholm_invalid_format"));
293     }
294     else
295     {
296       version = r.stringMatched(1);
297
298       // logger.debug("Stockholm version: " + version);
299     }
300
301     // We define some Regexes here that will be used regularly later
302     rend = new Regex("^\\s*\\/\\/"); // Find the end of an alignment
303     p = new Regex("(\\S+)\\/(\\d+)\\-(\\d+)"); // split sequence id in
304     // id/from/to
305     s = new Regex("(\\S+)\\s+(\\S*)\\s+(.*)"); // Parses annotation subtype
306     r = new Regex("#=(G[FSRC]?)\\s+(.*)"); // Finds any annotation line
307     x = new Regex("(\\S+)\\s+(\\S+)"); // split id from sequence
308
309     // Convert all bracket types to parentheses (necessary for passing to VARNA)
310     Regex openparen = new Regex("(<|\\[)", "(");
311     Regex closeparen = new Regex("(>|\\])", ")");
312
313     // Detect if file is RNA by looking for bracket types
314     // Regex detectbrackets = new Regex("(<|>|\\[|\\]|\\(|\\))");
315
316     rend.optimize();
317     p.optimize();
318     s.optimize();
319     r.optimize();
320     x.optimize();
321     openparen.optimize();
322     closeparen.optimize();
323
324     while ((line = nextLine()) != null)
325     {
326       if (line.length() == 0)
327       {
328         continue;
329       }
330       if (rend.search(line))
331       {
332         // End of the alignment, pass stuff back
333         this.noSeqs = seqs.size();
334
335         String seqdb, dbsource = null;
336         Regex pf = new Regex("PF[0-9]{5}(.*)"); // Finds AC for Pfam
337         Regex rf = new Regex("RF[0-9]{5}(.*)"); // Finds AC for Rfam
338         if (getAlignmentProperty("AC") != null)
339         {
340           String dbType = getAlignmentProperty("AC").toString();
341           if (pf.search(dbType))
342           {
343             // PFAM Alignment - so references are typically from Uniprot
344             dbsource = "PFAM";
345           }
346           else if (rf.search(dbType))
347           {
348             dbsource = "RFAM";
349           }
350         }
351         // logger.debug("Number of sequences: " + this.noSeqs);
352         for (Map.Entry<String, String> skey : seqs.entrySet())
353         {
354           // logger.debug("Processing sequence " + acc);
355           String acc = skey.getKey();
356           String seq = skey.getValue();
357           if (maxLength < seq.length())
358           {
359             maxLength = seq.length();
360           }
361           int start = 1;
362           int end = -1;
363           String sid = acc;
364           /*
365            * Retrieve hash of annotations for this accession Associate
366            * Annotation with accession
367            */
368           Hashtable accAnnotations = null;
369
370           if (seqAnn != null && seqAnn.containsKey(acc))
371           {
372             accAnnotations = (Hashtable) seqAnn.remove(acc);
373             // TODO: add structures to sequence
374           }
375
376           // Split accession in id and from/to
377           if (p.search(acc))
378           {
379             sid = p.stringMatched(1);
380             start = Integer.parseInt(p.stringMatched(2));
381             end = Integer.parseInt(p.stringMatched(3));
382           }
383           // logger.debug(sid + ", " + start + ", " + end);
384
385           Sequence seqO = new Sequence(sid, seq, start, end);
386           // Add Description (if any)
387           if (accAnnotations != null && accAnnotations.containsKey("DE"))
388           {
389             String desc = (String) accAnnotations.get("DE");
390             seqO.setDescription((desc == null) ? "" : desc);
391           }
392           // Add DB References (if any)
393           if (accAnnotations != null && accAnnotations.containsKey("DR"))
394           {
395             String dbr = (String) accAnnotations.get("DR");
396             if (dbr != null && dbr.indexOf(";") > -1)
397             {
398               String src = dbr.substring(0, dbr.indexOf(";"));
399               String acn = dbr.substring(dbr.indexOf(";") + 1);
400               jalview.util.DBRefUtils.parseToDbRef(seqO, src, "0", acn);
401             }
402           }
403
404           if (accAnnotations != null && accAnnotations.containsKey("AC"))
405           {
406             if (dbsource != null)
407             {
408               String dbr = (String) accAnnotations.get("AC");
409               if (dbr != null)
410               {
411                 // we could get very clever here - but for now - just try to
412                 // guess accession type from source of alignment plus structure
413                 // of accession
414                 guessDatabaseFor(seqO, dbr, dbsource);
415
416               }
417             }
418             // else - do what ? add the data anyway and prompt the user to
419             // specify what references these are ?
420           }
421
422           Hashtable features = null;
423           // We need to adjust the positions of all features to account for gaps
424           try
425           {
426             features = (Hashtable) accAnnotations.remove("features");
427           } catch (java.lang.NullPointerException e)
428           {
429             // loggerwarn("Getting Features for " + acc + ": " +
430             // e.getMessage());
431             // continue;
432           }
433           // if we have features
434           if (features != null)
435           {
436             int posmap[] = seqO.findPositionMap();
437             Enumeration i = features.keys();
438             while (i.hasMoreElements())
439             {
440               // TODO: parse out secondary structure annotation as annotation
441               // row
442               // TODO: parse out scores as annotation row
443               // TODO: map coding region to core jalview feature types
444               String type = i.nextElement().toString();
445               Hashtable content = (Hashtable) features.remove(type);
446
447               // add alignment annotation for this feature
448               String key = descriptionToType(type);
449
450               /*
451                * have we added annotation rows for this type ?
452                */
453               boolean annotsAdded = false;
454               if (key != null)
455               {
456                 if (accAnnotations != null
457                         && accAnnotations.containsKey(key))
458                 {
459                   Vector vv = (Vector) accAnnotations.get(key);
460                   for (int ii = 0; ii < vv.size(); ii++)
461                   {
462                     annotsAdded = true;
463                     AlignmentAnnotation an = (AlignmentAnnotation) vv
464                             .elementAt(ii);
465                     seqO.addAlignmentAnnotation(an);
466                     annotations.add(an);
467                   }
468                 }
469               }
470
471               Enumeration j = content.keys();
472               while (j.hasMoreElements())
473               {
474                 String desc = j.nextElement().toString();
475                 if (ANNOTATION.equals(desc) && annotsAdded)
476                 {
477                   // don't add features if we already added an annotation row
478                   continue;
479                 }
480                 String ns = content.get(desc).toString();
481                 char[] byChar = ns.toCharArray();
482                 for (int k = 0; k < byChar.length; k++)
483                 {
484                   char c = byChar[k];
485                   if (!(c == ' ' || c == '_' || c == '-' || c == '.')) // PFAM
486                   // uses
487                   // '.'
488                   // for
489                   // feature
490                   // background
491                   {
492                     int new_pos = posmap[k]; // look up nearest seqeunce
493                     // position to this column
494                     SequenceFeature feat = new SequenceFeature(type, desc,
495                             new_pos, new_pos, null);
496
497                     seqO.addSequenceFeature(feat);
498                   }
499                 }
500               }
501
502             }
503
504           }
505           // garbage collect
506
507           // logger.debug("Adding seq " + acc + " from " + start + " to " + end
508           // + ": " + seq);
509           this.seqs.addElement(seqO);
510         }
511         return; // finished parsing this segment of source
512       }
513       else if (!r.search(line))
514       {
515         // System.err.println("Found sequence line: " + line);
516
517         // Split sequence in sequence and accession parts
518         if (!x.search(line))
519         {
520           // logger.error("Could not parse sequence line: " + line);
521           throw new IOException(MessageManager.formatMessage(
522                   "exception.couldnt_parse_sequence_line", new String[]
523                   { line }));
524         }
525         String ns = seqs.get(x.stringMatched(1));
526         if (ns == null)
527         {
528           ns = "";
529         }
530         ns += x.stringMatched(2);
531
532         seqs.put(x.stringMatched(1), ns);
533       }
534       else
535       {
536         String annType = r.stringMatched(1);
537         String annContent = r.stringMatched(2);
538
539         // System.err.println("type:" + annType + " content: " + annContent);
540
541         if (annType.equals("GF"))
542         {
543           /*
544            * Generic per-File annotation, free text Magic features: #=GF NH
545            * <tree in New Hampshire eXtended format> #=GF TN <Unique identifier
546            * for the next tree> Pfam descriptions: 7. DESCRIPTION OF FIELDS
547            * 
548            * Compulsory fields: ------------------
549            * 
550            * AC Accession number: Accession number in form PFxxxxx.version or
551            * PBxxxxxx. ID Identification: One word name for family. DE
552            * Definition: Short description of family. AU Author: Authors of the
553            * entry. SE Source of seed: The source suggesting the seed members
554            * belong to one family. GA Gathering method: Search threshold to
555            * build the full alignment. TC Trusted Cutoff: Lowest sequence score
556            * and domain score of match in the full alignment. NC Noise Cutoff:
557            * Highest sequence score and domain score of match not in full
558            * alignment. TP Type: Type of family -- presently Family, Domain,
559            * Motif or Repeat. SQ Sequence: Number of sequences in alignment. AM
560            * Alignment Method The order ls and fs hits are aligned to the model
561            * to build the full align. // End of alignment.
562            * 
563            * Optional fields: ----------------
564            * 
565            * DC Database Comment: Comment about database reference. DR Database
566            * Reference: Reference to external database. RC Reference Comment:
567            * Comment about literature reference. RN Reference Number: Reference
568            * Number. RM Reference Medline: Eight digit medline UI number. RT
569            * Reference Title: Reference Title. RA Reference Author: Reference
570            * Author RL Reference Location: Journal location. PI Previous
571            * identifier: Record of all previous ID lines. KW Keywords: Keywords.
572            * CC Comment: Comments. NE Pfam accession: Indicates a nested domain.
573            * NL Location: Location of nested domains - sequence ID, start and
574            * end of insert.
575            * 
576            * Obsolete fields: ----------- AL Alignment method of seed: The
577            * method used to align the seed members.
578            */
579           // Let's save the annotations, maybe we'll be able to do something
580           // with them later...
581           Regex an = new Regex("(\\w+)\\s*(.*)");
582           if (an.search(annContent))
583           {
584             if (an.stringMatched(1).equals("NH"))
585             {
586               treeString.append(an.stringMatched(2));
587             }
588             else if (an.stringMatched(1).equals("TN"))
589             {
590               if (treeString.length() > 0)
591               {
592                 if (treeName == null)
593                 {
594                   treeName = "Tree " + (getTreeCount() + 1);
595                 }
596                 addNewickTree(treeName, treeString.toString());
597               }
598               treeName = an.stringMatched(2);
599               treeString = new StringBuffer();
600             }
601             setAlignmentProperty(an.stringMatched(1), an.stringMatched(2));
602           }
603         }
604         else if (annType.equals("GS"))
605         {
606           // Generic per-Sequence annotation, free text
607           /*
608            * Pfam uses these features: Feature Description ---------------------
609            * ----------- AC <accession> ACcession number DE <freetext>
610            * DEscription DR <db>; <accession>; Database Reference OS <organism>
611            * OrganiSm (species) OC <clade> Organism Classification (clade, etc.)
612            * LO <look> Look (Color, etc.)
613            */
614           if (s.search(annContent))
615           {
616             String acc = s.stringMatched(1);
617             String type = s.stringMatched(2);
618             String content = s.stringMatched(3);
619             // TODO: store DR in a vector.
620             // TODO: store AC according to generic file db annotation.
621             Hashtable ann;
622             if (seqAnn.containsKey(acc))
623             {
624               ann = (Hashtable) seqAnn.get(acc);
625             }
626             else
627             {
628               ann = new Hashtable();
629             }
630             ann.put(type, content);
631             seqAnn.put(acc, ann);
632           }
633           else
634           {
635             // throw new IOException("Error parsing " + line);
636             System.err.println(">> missing annotation: " + line);
637           }
638         }
639         else if (annType.equals("GC"))
640         {
641           // Generic per-Column annotation, exactly 1 char per column
642           // always need a label.
643           if (x.search(annContent))
644           {
645             // parse out and create alignment annotation directly.
646             parseAnnotationRow(annotations, x.stringMatched(1),
647                     x.stringMatched(2));
648           }
649         }
650         else if (annType.equals("GR"))
651         {
652           // Generic per-Sequence AND per-Column markup, exactly 1 char per
653           // column
654           /*
655            * Feature Description Markup letters ------- -----------
656            * -------------- SS Secondary Structure [HGIEBTSCX] SA Surface
657            * Accessibility [0-9X] (0=0%-10%; ...; 9=90%-100%) TM TransMembrane
658            * [Mio] PP Posterior Probability [0-9*] (0=0.00-0.05; 1=0.05-0.15;
659            * *=0.95-1.00) LI LIgand binding [*] AS Active Site [*] IN INtron (in
660            * or after) [0-2]
661            */
662           if (s.search(annContent))
663           {
664             String acc = s.stringMatched(1);
665             String type = s.stringMatched(2);
666             String oseq = s.stringMatched(3);
667             /*
668              * copy of annotation field that may be processed into whitespace chunks
669              */
670             String seq = new String(oseq);
671
672             Hashtable ann;
673             // Get an object with all the annotations for this sequence
674             if (seqAnn.containsKey(acc))
675             {
676               // logger.debug("Found annotations for " + acc);
677               ann = (Hashtable) seqAnn.get(acc);
678             }
679             else
680             {
681               // logger.debug("Creating new annotations holder for " + acc);
682               ann = new Hashtable();
683               seqAnn.put(acc, ann);
684             }
685
686             // // start of block for appending annotation lines for wrapped
687             // stokchholm file
688             // TODO test structure, call parseAnnotationRow with vector from
689             // hashtable for specific sequence
690
691             Hashtable features;
692             // Get an object with all the content for an annotation
693             if (ann.containsKey("features"))
694             {
695               // logger.debug("Found features for " + acc);
696               features = (Hashtable) ann.get("features");
697             }
698             else
699             {
700               // logger.debug("Creating new features holder for " + acc);
701               features = new Hashtable();
702               ann.put("features", features);
703             }
704
705             Hashtable content;
706             if (features.containsKey(StockholmFile.typeToDescription(type)))
707             {
708               // logger.debug("Found content for " + this.id2type(type));
709               content = (Hashtable) features
710                       .get(StockholmFile.typeToDescription(type));
711             }
712             else
713             {
714               // logger.debug("Creating new content holder for " +
715               // this.id2type(type));
716               content = new Hashtable();
717               features.put(StockholmFile.typeToDescription(type), content);
718             }
719             String ns = (String) content.get(ANNOTATION);
720
721             if (ns == null)
722             {
723               ns = "";
724             }
725             // finally, append the annotation line
726             ns += seq;
727             content.put(ANNOTATION, ns);
728             // // end of wrapped annotation block.
729             // // Now a new row is created with the current set of data
730
731             Hashtable strucAnn;
732             if (seqAnn.containsKey(acc))
733             {
734               strucAnn = (Hashtable) seqAnn.get(acc);
735             }
736             else
737             {
738               strucAnn = new Hashtable();
739             }
740
741             Vector<AlignmentAnnotation> newStruc = new Vector<>();
742             parseAnnotationRow(newStruc, type, ns);
743             for (AlignmentAnnotation alan : newStruc)
744             {
745               alan.visible = false;
746             }
747             // new annotation overwrites any existing annotation...
748
749             strucAnn.put(type, newStruc);
750             seqAnn.put(acc, strucAnn);
751           }
752           // }
753           else
754           {
755             System.err.println(
756                     "Warning - couldn't parse sequence annotation row line:\n"
757                             + line);
758             // throw new IOException("Error parsing " + line);
759           }
760         }
761         else
762         {
763           throw new IOException(MessageManager.formatMessage(
764                   "exception.unknown_annotation_detected", new String[]
765                   { annType, annContent }));
766         }
767       }
768     }
769     if (treeString.length() > 0)
770     {
771       if (treeName == null)
772       {
773         treeName = "Tree " + (1 + getTreeCount());
774       }
775       addNewickTree(treeName, treeString.toString());
776     }
777   }
778
779   /**
780    * Demangle an accession string and guess the originating sequence database
781    * for a given sequence
782    * 
783    * @param seqO
784    *          sequence to be annotated
785    * @param dbr
786    *          Accession string for sequence
787    * @param dbsource
788    *          source database for alignment (PFAM or RFAM)
789    */
790   private void guessDatabaseFor(Sequence seqO, String dbr, String dbsource)
791   {
792     DBRefEntry dbrf = null;
793     List<DBRefEntry> dbrs = new ArrayList<>();
794     String seqdb = "Unknown", sdbac = "" + dbr;
795     int st = -1, en = -1, p;
796     if ((st = sdbac.indexOf("/")) > -1)
797     {
798       String num, range = sdbac.substring(st + 1);
799       sdbac = sdbac.substring(0, st);
800       if ((p = range.indexOf("-")) > -1)
801       {
802         p++;
803         if (p < range.length())
804         {
805           num = range.substring(p).trim();
806           try
807           {
808             en = Integer.parseInt(num);
809           } catch (NumberFormatException x)
810           {
811             // could warn here that index is invalid
812             en = -1;
813           }
814         }
815       }
816       else
817       {
818         p = range.length();
819       }
820       num = range.substring(0, p).trim();
821       try
822       {
823         st = Integer.parseInt(num);
824       } catch (NumberFormatException x)
825       {
826         // could warn here that index is invalid
827         st = -1;
828       }
829     }
830     if (dbsource.equals("PFAM"))
831     {
832       seqdb = "UNIPROT";
833       if (sdbac.indexOf(".") > -1)
834       {
835         // strip of last subdomain
836         sdbac = sdbac.substring(0, sdbac.indexOf("."));
837         dbrf = jalview.util.DBRefUtils.parseToDbRef(seqO, seqdb, dbsource,
838                 sdbac);
839         if (dbrf != null)
840         {
841           dbrs.add(dbrf);
842         }
843       }
844       dbrf = jalview.util.DBRefUtils.parseToDbRef(seqO, dbsource, dbsource,
845               dbr);
846       if (dbr != null)
847       {
848         dbrs.add(dbrf);
849       }
850     }
851     else
852     {
853       seqdb = "EMBL"; // total guess - could be ENA, or something else these
854                       // days
855       if (sdbac.indexOf(".") > -1)
856       {
857         // strip off last subdomain
858         sdbac = sdbac.substring(0, sdbac.indexOf("."));
859         dbrf = jalview.util.DBRefUtils.parseToDbRef(seqO, seqdb, dbsource,
860                 sdbac);
861         if (dbrf != null)
862         {
863           dbrs.add(dbrf);
864         }
865       }
866
867       dbrf = jalview.util.DBRefUtils.parseToDbRef(seqO, dbsource, dbsource,
868               dbr);
869       if (dbrf != null)
870       {
871         dbrs.add(dbrf);
872       }
873     }
874     if (st != -1 && en != -1)
875     {
876       for (DBRefEntry d : dbrs)
877       {
878         jalview.util.MapList mp = new jalview.util.MapList(
879                 new int[]
880                 { seqO.getStart(), seqO.getEnd() }, new int[] { st, en }, 1,
881                 1);
882         jalview.datamodel.Mapping mping = new Mapping(mp);
883         d.setMap(mping);
884       }
885     }
886   }
887
888   protected static AlignmentAnnotation parseAnnotationRow(
889           Vector<AlignmentAnnotation> annotation, String label,
890           String annots)
891   {
892     // String convert1 = OPEN_PAREN.replaceAll(annots);
893     // String convert2 = CLOSE_PAREN.replaceAll(convert1);
894     // annots = convert2;
895
896     String type = label;
897     if (label.contains("_cons"))
898     {
899       type = (label.indexOf("_cons") == label.length() - 5)
900               ? label.substring(0, label.length() - 5)
901               : label;
902     }
903     boolean ss = false, posterior = false;
904     type = id2type(type);
905
906     boolean isrnass = false;
907
908     if (type.equalsIgnoreCase("secondary structure"))
909     {
910       ss = true;
911       isrnass = !NOT_RNASS.search(annots); // sorry about the double negative
912                                            // here (it's easier for dealing with
913                                            // other non-alpha-non-brace chars)
914     }
915     if (type.equalsIgnoreCase("posterior probability"))
916     {
917       posterior = true;
918     }
919     // decide on secondary structure or not.
920     Annotation[] els = new Annotation[annots.length()];
921     for (int i = 0; i < annots.length(); i++)
922     {
923       String pos = annots.substring(i, i + 1);
924       if (UNDERSCORE == pos.charAt(0))
925       {
926         pos = " ";
927       }
928       Annotation ann;
929       ann = new Annotation(pos, "", ' ', 0f); // 0f is 'valid' null - will not
930       // be written out
931       if (ss)
932       {
933         // if (" .-_".indexOf(pos) == -1)
934         {
935           if (isrnass && RNASS_BRACKETS.indexOf(pos) >= 0)
936           {
937             ann.secondaryStructure = Rna.getRNASecStrucState(pos).charAt(0);
938             ann.displayCharacter = "" + pos.charAt(0);
939           }
940           else
941           {
942             ann.secondaryStructure = ResidueProperties.getDssp3state(pos)
943                     .charAt(0);
944
945             if (ann.secondaryStructure == pos.charAt(0))
946             {
947               ann.displayCharacter = ""; // null; // " ";
948             }
949             else
950             {
951               ann.displayCharacter = " " + ann.displayCharacter;
952             }
953           }
954         }
955
956       }
957       if (posterior && !ann.isWhitespace()
958               && !Comparison.isGap(pos.charAt(0)))
959       {
960         float val = 0;
961         // symbol encodes values - 0..*==0..10
962         if (pos.charAt(0) == '*')
963         {
964           val = 10;
965         }
966         else
967         {
968           val = pos.charAt(0) - '0';
969           if (val > 9)
970           {
971             val = 10;
972           }
973         }
974         ann.value = val;
975       }
976
977       els[i] = ann;
978     }
979     AlignmentAnnotation annot = null;
980     Enumeration<AlignmentAnnotation> e = annotation.elements();
981     while (e.hasMoreElements())
982     {
983       annot = e.nextElement();
984       if (annot.label.equals(type))
985       {
986         break;
987       }
988       annot = null;
989     }
990     if (annot == null)
991     {
992       annot = new AlignmentAnnotation(type, type, els);
993       annotation.addElement(annot);
994     }
995     else
996     {
997       Annotation[] anns = new Annotation[annot.annotations.length
998               + els.length];
999       System.arraycopy(annot.annotations, 0, anns, 0,
1000               annot.annotations.length);
1001       System.arraycopy(els, 0, anns, annot.annotations.length, els.length);
1002       annot.annotations = anns;
1003       // System.out.println("else: ");
1004     }
1005     return annot;
1006   }
1007
1008   @Override
1009   public String print(final SequenceI[] sequences, boolean jvSuffix)
1010   {
1011     StringBuilder out = new StringBuilder();
1012     out.append("# STOCKHOLM 1.0");
1013     out.append(newline);
1014
1015     int maxIdWidth = 0;
1016     for (SequenceI seq : sequences)
1017     {
1018       if (seq != null)
1019       {
1020         String formattedId = printId(seq, jvSuffix);
1021         maxIdWidth = Math.max(maxIdWidth, formattedId.length());
1022       }
1023     }
1024     maxIdWidth += 9;
1025
1026     /*
1027      * generic alignment properties
1028      */
1029     Hashtable props = al.getProperties();
1030     if (props != null)
1031     {
1032       for (Object key : props.keySet())
1033       {
1034         out.append(String.format("#=GF %s %s", key.toString(),
1035                 props.get(key).toString()));
1036         out.append(newline);
1037       }
1038     }
1039
1040     /*
1041      * output database accessions as #=GS (per sequence annotation)
1042      * PFAM or RFAM are output as AC <accession number>
1043      * others are output as DR <dbname> ; <accession>
1044      */
1045     Format formatter = new Format("%-" + (maxIdWidth - 2) + "s");
1046     for (SequenceI seq : sequences)
1047     {
1048       if (seq != null)
1049       {
1050         DBRefEntry[] dbRefs = seq.getDBRefs();
1051         if (dbRefs != null)
1052         {
1053           String idField = formatter
1054                   .form("#=GS " + printId(seq, jvSuffix) + " ");
1055           for (DBRefEntry dbRef : dbRefs)
1056           {
1057             out.append(idField);
1058             printDbRef(out, dbRef);
1059           }
1060         }
1061       }
1062     }
1063
1064     /*
1065      * output annotations
1066      */
1067     for (SequenceI seq : sequences)
1068     {
1069       if (seq != null)
1070       {
1071         AlignmentAnnotation[] alAnot = seq.getAnnotation();
1072         if (alAnot != null)
1073         {
1074           for (int j = 0; j < alAnot.length; j++)
1075           {
1076             AlignmentAnnotation ann = alAnot[j];
1077             String key = descriptionToType(ann.label);
1078             boolean isrna = ann.isValidStruc();
1079             if (isrna)
1080             {
1081               /*
1082                * output as secondary structure if there is 
1083                * RNA secondary structure on the annotation
1084                */
1085               key = "SS";
1086             }
1087             if (key == null)
1088             {
1089               continue;
1090             }
1091
1092             out.append(new Format("%-" + maxIdWidth + "s").form(
1093                     "#=GR " + printId(seq, jvSuffix) + " " + key + " "));
1094             Annotation[] anns = ann.annotations;
1095             StringBuilder seqString = new StringBuilder();
1096             for (int k = 0; k < anns.length; k++)
1097             {
1098               seqString
1099                       .append(getAnnotationCharacter(key, k, anns[k], seq));
1100             }
1101             out.append(seqString.toString());
1102             out.append(newline);
1103           }
1104         }
1105
1106         out.append(new Format("%-" + maxIdWidth + "s")
1107                 .form(printId(seq, jvSuffix) + " "));
1108         out.append(seq.getSequenceAsString());
1109         out.append(newline);
1110       }
1111     }
1112
1113     /*
1114      * output alignment annotation (but not auto-calculated or sequence-related)
1115      */
1116     if (al.getAlignmentAnnotation() != null)
1117     {
1118       for (int ia = 0; ia < al.getAlignmentAnnotation().length; ia++)
1119       {
1120         AlignmentAnnotation aa = al.getAlignmentAnnotation()[ia];
1121         if (aa.autoCalculated || !aa.visible || aa.sequenceRef != null)
1122         {
1123           continue;
1124         }
1125         String label = aa.label;
1126         String key = "";
1127         if (aa.label.equals("seq"))
1128         {
1129           label = "seq_cons";
1130         }
1131         else
1132         {
1133           key = descriptionToType(aa.label);
1134           if ("RF".equals(key))
1135           {
1136             label = key;
1137           }
1138           else if (key != null)
1139           {
1140             label = key + "_cons";
1141           }
1142         }
1143         label = label.replace(" ", "_");
1144
1145         out.append(
1146                 new Format("%-" + maxIdWidth + "s")
1147                         .form("#=GC " + label + " "));
1148         StringBuilder sb = new StringBuilder(aa.annotations.length);
1149         for (int j = 0; j < aa.annotations.length; j++)
1150         {
1151           sb.append(
1152                   getAnnotationCharacter(key, j, aa.annotations[j], null));
1153         }
1154         out.append(sb.toString());
1155         out.append(newline);
1156       }
1157     }
1158
1159     out.append("//");
1160     out.append(newline);
1161
1162     return out.toString();
1163   }
1164
1165   /**
1166    * A helper method that appends a formatted dbref to the output buffer
1167    * 
1168    * @param out
1169    * @param dbRef
1170    */
1171   protected void printDbRef(StringBuilder out, DBRefEntry dbRef)
1172   {
1173     String db = dbRef.getSource();
1174     String acc = dbRef.getAccessionId();
1175     if (DBRefSource.PFAM.equalsIgnoreCase(db)
1176             || DBRefSource.RFAM.equalsIgnoreCase(db))
1177     {
1178       out.append(" AC " + acc);
1179     }
1180     else
1181     {
1182       out.append(" DR " + db + " ; " + acc);
1183     }
1184     out.append(newline);
1185   }
1186
1187   /**
1188    * Returns an annotation character to add to the output row
1189    * 
1190    * @param seq
1191    * @param key
1192    * @param k
1193    * @param ann
1194    * @param sequenceI
1195    */
1196   static char getAnnotationCharacter(String key, int k, Annotation annot,
1197           SequenceI sequenceI)
1198   {
1199     char seq = ' ';
1200     String ch = (annot == null)
1201             ? ((sequenceI == null) ? "-"
1202                     : Character.toString(sequenceI.getCharAt(k)))
1203             : (annot.displayCharacter == null
1204                     ? String.valueOf(annot.secondaryStructure)
1205                     : annot.displayCharacter);
1206     if (ch == null)
1207     {
1208       ch = " ";
1209     }
1210     if (key != null && key.equals("SS"))
1211     {
1212       char ssannotchar = ' ';
1213       boolean charset = false;
1214       if (annot == null)
1215       {
1216           // TODO: TVA: Check against develop for correct behaviour!
1217         // Stockholm format requires underscore, not space
1218         return UNDERSCORE;
1219       }
1220       else
1221       {
1222         // valid secondary structure AND no alternative label (e.g. ' B')
1223         if (annot.secondaryStructure > ' ' && ch.length() < 2)
1224         {
1225           ssannotchar = annot.secondaryStructure;
1226           charset = true;
1227         }
1228       }
1229       if (charset)
1230       {
1231         return (ssannotchar == ' ' && isrna) ? '.' : ssannotchar;
1232       }
1233     }
1234
1235     if (ch.length() == 0)
1236     {
1237       seq = '.';
1238     }
1239     else if (ch.length() == 1)
1240     {
1241       seq = ch.charAt(0);
1242     }
1243     else if (ch.length() > 1)
1244     {
1245       seq = ch.charAt(1);
1246     }
1247
1248     return (seq == ' ' && key != null && key.equals("SS") && isrna) ? '.'
1249             : seq;
1250   }
1251
1252   /**
1253    * make a friendly ID string.
1254    * 
1255    * @param dataName
1256    * @return truncated dataName to after last '/'
1257    */
1258   private String safeName(String dataName)
1259   {
1260     int b = 0;
1261     while ((b = dataName.indexOf("/")) > -1 && b < dataName.length())
1262     {
1263       dataName = dataName.substring(b + 1).trim();
1264
1265     }
1266     int e = (dataName.length() - dataName.indexOf(".")) + 1;
1267     dataName = dataName.substring(1, e).trim();
1268     return dataName;
1269   }
1270 }