JAL-653 JAL-1968 FeaturesFile now handles Jalview or GFF2 or GFF3
[jalview.git] / src / jalview / io / FeaturesFile.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 package jalview.io;
22
23 import jalview.analysis.SequenceIdMatcher;
24 import jalview.api.AlignViewportI;
25 import jalview.datamodel.AlignedCodonFrame;
26 import jalview.datamodel.Alignment;
27 import jalview.datamodel.AlignmentI;
28 import jalview.datamodel.SequenceDummy;
29 import jalview.datamodel.SequenceFeature;
30 import jalview.datamodel.SequenceI;
31 import jalview.schemes.AnnotationColourGradient;
32 import jalview.schemes.GraduatedColor;
33 import jalview.schemes.UserColourScheme;
34 import jalview.util.Format;
35 import jalview.util.MapList;
36 import jalview.util.ParseHtmlBodyAndLinks;
37 import jalview.util.StringUtils;
38
39 import java.awt.Color;
40 import java.io.IOException;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.HashMap;
44 import java.util.Iterator;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Map.Entry;
48 import java.util.StringTokenizer;
49
50 /**
51  * Parses and writes features files, which may be in Jalview, GFF2 or GFF3
52  * format. These are tab-delimited formats but with differences in the use of
53  * columns.
54  * 
55  * A Jalview feature file may define feature colours and then declare that the
56  * remainder of the file is in GFF format with the line 'GFF'.
57  * 
58  * GFF3 files may include alignment mappings for features, which Jalview will
59  * attempt to model, and may include sequence data following a ##FASTA line.
60  * 
61  * 
62  * @author AMW
63  * @author jbprocter
64  * @author gmcarstairs
65  */
66 public class FeaturesFile extends AlignFile
67 {
68   protected static final String STRAND = "STRAND";
69
70   protected static final String FRAME = "FRAME";
71
72   protected static final String ATTRIBUTES = "ATTRIBUTES";
73
74   protected static final String TAB = "\t";
75
76   protected static final String GFF_VERSION = "##gff-version";
77
78   private AlignmentI lastmatchedAl = null;
79
80   private SequenceIdMatcher matcher = null;
81
82   protected AlignmentI dataset;
83
84   protected int gffVersion;
85
86   /**
87    * Creates a new FeaturesFile object.
88    */
89   public FeaturesFile()
90   {
91   }
92
93   /**
94    * Constructor which does not parse the file immediately
95    * 
96    * @param inFile
97    * @param type
98    * @throws IOException
99    */
100   public FeaturesFile(String inFile, String type) throws IOException
101   {
102     super(false, inFile, type);
103   }
104
105   /**
106    * @param source
107    * @throws IOException
108    */
109   public FeaturesFile(FileParse source) throws IOException
110   {
111     super(source);
112   }
113
114   /**
115    * Constructor that optionally parses the file immediately
116    * 
117    * @param parseImmediately
118    * @param inFile
119    * @param type
120    * @throws IOException
121    */
122   public FeaturesFile(boolean parseImmediately, String inFile, String type)
123           throws IOException
124   {
125     super(parseImmediately, inFile, type);
126   }
127
128   /**
129    * Parse GFF or sequence features file using case-independent matching,
130    * discarding URLs
131    * 
132    * @param align
133    *          - alignment/dataset containing sequences that are to be annotated
134    * @param colours
135    *          - hashtable to store feature colour definitions
136    * @param removeHTML
137    *          - process html strings into plain text
138    * @return true if features were added
139    */
140   public boolean parse(AlignmentI align, Map<String, Object> colours,
141           boolean removeHTML)
142   {
143     return parse(align, colours, removeHTML, false);
144   }
145
146   /**
147    * Extends the default addProperties by also adding peptide-to-cDNA mappings
148    * (if any) derived while parsing a GFF file
149    */
150   @Override
151   public void addProperties(AlignmentI al)
152   {
153     super.addProperties(al);
154     if (dataset != null && dataset.getCodonFrames() != null)
155     {
156       AlignmentI ds = (al.getDataset() == null) ? al : al.getDataset();
157       for (AlignedCodonFrame codons : dataset.getCodonFrames())
158       {
159         ds.addCodonFrame(codons);
160       }
161     }
162   }
163
164   /**
165    * Parse GFF or Jalview format sequence features file
166    * 
167    * @param align
168    *          - alignment/dataset containing sequences that are to be annotated
169    * @param colours
170    *          - hashtable to store feature colour definitions
171    * @param removeHTML
172    *          - process html strings into plain text
173    * @param relaxedIdmatching
174    *          - when true, ID matches to compound sequence IDs are allowed
175    * @return true if features were added
176    */
177   public boolean parse(AlignmentI align, Map<String, Object> colours,
178           boolean removeHTML, boolean relaxedIdmatching)
179   {
180     Map<String, String> gffProps = new HashMap<String, String>();
181     /*
182      * keep track of any sequences we try to create from the data
183      */
184     List<SequenceI> newseqs = new ArrayList<SequenceI>();
185
186     String line = null;
187     try
188     {
189       StringTokenizer st;
190       String featureGroup = null;
191
192       while ((line = nextLine()) != null)
193       {
194         // skip comments/process pragmas
195         if (line.length() == 0 || line.startsWith("#"))
196         {
197           if (line.toLowerCase().startsWith("##"))
198           {
199             processGffPragma(line, gffProps, align, newseqs);
200           }
201           continue;
202         }
203
204         st = new StringTokenizer(line, TAB);
205         if (st.countTokens() == 1)
206         {
207           if (line.trim().equalsIgnoreCase("GFF"))
208           {
209             /*
210              * Jalview features file with appendded GFF
211              * assume GFF2 (though it may declare gff-version 3)
212              */
213             gffVersion = 2;
214             continue;
215           }
216         }
217
218         if (st.countTokens() > 1 && st.countTokens() < 4)
219         {
220           /*
221            * if 2 or 3 tokens, we anticipate either 'startgroup', 'endgroup' or
222            * a feature type colour specification; not GFF format
223            */
224           String ft = st.nextToken();
225           if (ft.equalsIgnoreCase("startgroup"))
226           {
227             featureGroup = st.nextToken();
228           }
229           else if (ft.equalsIgnoreCase("endgroup"))
230           {
231             // We should check whether this is the current group,
232             // but at present theres no way of showing more than 1 group
233             st.nextToken();
234             featureGroup = null;
235           }
236           else
237           {
238             parseFeatureColour(line, ft, st, colours);
239           }
240           continue;
241         }
242
243         /*
244          * if not a comment, GFF pragma, startgroup, endgroup or feature
245          * colour specification, that just leaves a feature details line
246          * in either Jalview or GFF format
247          */
248         if (gffVersion == 0)
249         {
250           parseJalviewFeature(line, st, align, colours, removeHTML,
251                   relaxedIdmatching, featureGroup);
252         }
253         else
254         {
255           parseGffFeature(st, align, relaxedIdmatching, newseqs);
256         }
257       }
258       resetMatcher();
259     } catch (Exception ex)
260     {
261       // should report somewhere useful for UI if necessary
262       warningMessage = ((warningMessage == null) ? "" : warningMessage)
263               + "Parsing error at\n" + line;
264       System.out.println("Error parsing feature file: " + ex + "\n" + line);
265       ex.printStackTrace(System.err);
266       resetMatcher();
267       return false;
268     }
269
270     return true;
271   }
272
273   /**
274    * Try to parse a Jalview format feature specification. Returns true if
275    * successful or false if not.
276    * 
277    * @param line
278    * @param st
279    * @param alignment
280    * @param featureColours
281    * @param removeHTML
282    * @param relaxedIdmatching
283    * @param featureGroup
284    */
285   protected boolean parseJalviewFeature(String line, StringTokenizer st,
286           AlignmentI alignment, Map<String, Object> featureColours,
287           boolean removeHTML, boolean relaxedIdmatching, String featureGroup)
288   {
289     /*
290      * Jalview: description seqid  seqIndex start end type [score]
291      */
292     String desc = st.nextToken();
293     String seqId = st.nextToken();
294     SequenceI seq = findName(alignment, seqId, relaxedIdmatching, null);
295     if (!st.hasMoreTokens())
296     {
297       System.err
298               .println("DEBUG: Run out of tokens when trying to identify the destination for the feature.. giving up.");
299       // in all probability, this isn't a file we understand, so bail
300       // quietly.
301       return false;
302     }
303
304     if (!seqId.equals("ID_NOT_SPECIFIED"))
305     {
306       seq = findName(alignment, seqId, relaxedIdmatching, null);
307       st.nextToken();
308     }
309     else
310     {
311       seqId = null;
312       seq = null;
313       try
314       {
315         int idx = Integer.parseInt(st.nextToken());
316         seq = alignment.getSequenceAt(idx);
317       } catch (NumberFormatException ex)
318       {
319         // continue
320       }
321     }
322
323     if (seq == null)
324     {
325       System.out.println("Sequence not found: " + line);
326       return false;
327     }
328
329     int startPos = Integer.parseInt(st.nextToken());
330     int endPos = Integer.parseInt(st.nextToken());
331
332     String ft = st.nextToken();
333
334     if (!featureColours.containsKey(ft))
335     {
336       /* 
337        * Perhaps an old style groups file with no colours -
338        * synthesize a colour from the feature type
339        */
340       UserColourScheme ucs = new UserColourScheme(ft);
341       featureColours.put(ft, ucs.findColour('A'));
342     }
343     SequenceFeature sf = new SequenceFeature(ft, desc, "",
344             startPos, endPos, featureGroup);
345     if (st.hasMoreTokens())
346     {
347       float score = 0f;
348       try
349       {
350         score = new Float(st.nextToken()).floatValue();
351         // update colourgradient bounds if allowed to
352       } catch (NumberFormatException ex)
353       {
354         // leave as 0
355       }
356       sf.setScore(score);
357     }
358
359     parseDescriptionHTML(sf, removeHTML);
360
361     seq.addSequenceFeature(sf);
362
363     while (seqId != null
364             && (seq = alignment.findName(seq, seqId, false)) != null)
365     {
366       seq.addSequenceFeature(new SequenceFeature(sf));
367     }
368     return true;
369   }
370
371   /**
372    * Process a feature type colour specification
373    * 
374    * @param line
375    *          the current input line (for error messages only)
376    * @param featureType
377    *          the first token on the line
378    * @param st
379    *          holds remaining tokens on the line
380    * @param colours
381    *          map to which to add derived colour specification
382    */
383   protected void parseFeatureColour(String line, String featureType,
384           StringTokenizer st, Map<String, Object> colours)
385   {
386     Object colour = null;
387     String colscheme = st.nextToken();
388     if (colscheme.indexOf("|") > -1
389             || colscheme.trim().equalsIgnoreCase("label"))
390     {
391       colour = parseGraduatedColourScheme(line, colscheme);
392     }
393     else
394     {
395       UserColourScheme ucs = new UserColourScheme(colscheme);
396       colour = ucs.findColour('A');
397     }
398     if (colour != null)
399     {
400       colours.put(featureType, colour);
401     }
402   }
403
404   /**
405    * Parse a Jalview graduated colour descriptor
406    * 
407    * @param line
408    * @param colourDescriptor
409    * @return
410    */
411   protected GraduatedColor parseGraduatedColourScheme(String line,
412           String colourDescriptor)
413   {
414     // Parse '|' separated graduated colourscheme fields:
415     // [label|][mincolour|maxcolour|[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue]
416     // can either provide 'label' only, first is optional, next two
417     // colors are required (but may be
418     // left blank), next is optional, nxt two min/max are required.
419     // first is either 'label'
420     // first/second and third are both hexadecimal or word equivalent
421     // colour.
422     // next two are values parsed as floats.
423     // fifth is either 'above','below', or 'none'.
424     // sixth is a float value and only required when fifth is either
425     // 'above' or 'below'.
426     StringTokenizer gcol = new StringTokenizer(colourDescriptor, "|", true);
427     // set defaults
428     float min = Float.MIN_VALUE, max = Float.MAX_VALUE;
429     boolean labelCol = false;
430     // Parse spec line
431     String mincol = gcol.nextToken();
432     if (mincol == "|")
433     {
434       System.err
435               .println("Expected either 'label' or a colour specification in the line: "
436                       + line);
437       return null;
438     }
439     String maxcol = null;
440     if (mincol.toLowerCase().indexOf("label") == 0)
441     {
442       labelCol = true;
443       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); // skip '|'
444       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
445     }
446     String abso = null, minval, maxval;
447     if (mincol != null)
448     {
449       // at least four more tokens
450       if (mincol.equals("|"))
451       {
452         mincol = "";
453       }
454       else
455       {
456         gcol.nextToken(); // skip next '|'
457       }
458       // continue parsing rest of line
459       maxcol = gcol.nextToken();
460       if (maxcol.equals("|"))
461       {
462         maxcol = "";
463       }
464       else
465       {
466         gcol.nextToken(); // skip next '|'
467       }
468       abso = gcol.nextToken();
469       gcol.nextToken(); // skip next '|'
470       if (abso.toLowerCase().indexOf("abso") != 0)
471       {
472         minval = abso;
473         abso = null;
474       }
475       else
476       {
477         minval = gcol.nextToken();
478         gcol.nextToken(); // skip next '|'
479       }
480       maxval = gcol.nextToken();
481       if (gcol.hasMoreTokens())
482       {
483         gcol.nextToken(); // skip next '|'
484       }
485       try
486       {
487         if (minval.length() > 0)
488         {
489           min = Float.valueOf(minval);
490         }
491       } catch (Exception e)
492       {
493         System.err
494                 .println("Couldn't parse the minimum value for graduated colour for type ("
495                         + colourDescriptor
496                         + ") - did you misspell 'auto' for the optional automatic colour switch ?");
497         e.printStackTrace();
498       }
499       try
500       {
501         if (maxval.length() > 0)
502         {
503           max = Float.valueOf(maxval);
504         }
505       } catch (Exception e)
506       {
507         System.err
508                 .println("Couldn't parse the maximum value for graduated colour for type ("
509                         + colourDescriptor + ")");
510         e.printStackTrace();
511       }
512     }
513     else
514     {
515       // add in some dummy min/max colours for the label-only
516       // colourscheme.
517       mincol = "FFFFFF";
518       maxcol = "000000";
519     }
520
521     GraduatedColor colour = null;
522     try
523     {
524       colour = new GraduatedColor(
525               new UserColourScheme(mincol).findColour('A'),
526               new UserColourScheme(maxcol).findColour('A'), min, max);
527     } catch (Exception e)
528     {
529       System.err.println("Couldn't parse the graduated colour scheme ("
530               + colourDescriptor + ")");
531       e.printStackTrace();
532     }
533     if (colour != null)
534     {
535       colour.setColourByLabel(labelCol);
536       colour.setAutoScaled(abso == null);
537       // add in any additional parameters
538       String ttype = null, tval = null;
539       if (gcol.hasMoreTokens())
540       {
541         // threshold type and possibly a threshold value
542         ttype = gcol.nextToken();
543         if (ttype.toLowerCase().startsWith("below"))
544         {
545           colour.setThreshType(AnnotationColourGradient.BELOW_THRESHOLD);
546         }
547         else if (ttype.toLowerCase().startsWith("above"))
548         {
549           colour.setThreshType(AnnotationColourGradient.ABOVE_THRESHOLD);
550         }
551         else
552         {
553           colour.setThreshType(AnnotationColourGradient.NO_THRESHOLD);
554           if (!ttype.toLowerCase().startsWith("no"))
555           {
556             System.err.println("Ignoring unrecognised threshold type : "
557                     + ttype);
558           }
559         }
560       }
561       if (colour.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
562       {
563         try
564         {
565           gcol.nextToken();
566           tval = gcol.nextToken();
567           colour.setThresh(new Float(tval).floatValue());
568         } catch (Exception e)
569         {
570           System.err.println("Couldn't parse threshold value as a float: ("
571                   + tval + ")");
572           e.printStackTrace();
573         }
574       }
575       // parse the thresh-is-min token ?
576       if (gcol.hasMoreTokens())
577       {
578         System.err
579                 .println("Ignoring additional tokens in parameters in graduated colour specification\n");
580         while (gcol.hasMoreTokens())
581         {
582           System.err.println("|" + gcol.nextToken());
583         }
584         System.err.println("\n");
585       }
586     }
587     return colour;
588   }
589
590   /**
591    * clear any temporary handles used to speed up ID matching
592    */
593   protected void resetMatcher()
594   {
595     lastmatchedAl = null;
596     matcher = null;
597   }
598
599   /**
600    * Returns a sequence matching the given id, as follows
601    * <ul>
602    * <li>matching is on exact sequence name, or on a token within the sequence
603    * name, or a dbxref, if relaxed matching is selected</li>
604    * <li>first tries to find a match in the alignment sequences</li>
605    * <li>else tries to find a match in the new sequences already generated
606    * parsing the features file</li>
607    * <li>else creates a new placeholder sequence, adds it to the new sequences
608    * list, and returns it</li>
609    * </ul>
610    * 
611    * @param align
612    * @param seqId
613    * @param relaxedIdMatching
614    * @param newseqs
615    * @return
616    */
617   protected SequenceI findName(AlignmentI align, String seqId,
618           boolean relaxedIdMatching, List<SequenceI> newseqs)
619   {
620     SequenceI match = null;
621     if (relaxedIdMatching)
622     {
623       if (lastmatchedAl != align)
624       {
625         lastmatchedAl = align;
626         matcher = new SequenceIdMatcher(align.getSequencesArray());
627         if (newseqs != null)
628         {
629           matcher.addAll(newseqs);
630         }
631       }
632       match = matcher.findIdMatch(seqId);
633     }
634     else
635     {
636       match = align.findName(seqId, true);
637       if (match == null && newseqs != null)
638       {
639         for (SequenceI m : newseqs)
640         {
641           if (seqId.equals(m.getName()))
642           {
643             return m;
644           }
645         }
646       }
647
648     }
649     if (match == null && newseqs != null)
650     {
651       match = new SequenceDummy(seqId);
652       if (relaxedIdMatching)
653       {
654         matcher.addAll(Arrays.asList(new SequenceI[] { match }));
655       }
656       // add dummy sequence to the newseqs list
657       newseqs.add(match);
658     }
659     return match;
660   }
661
662   public void parseDescriptionHTML(SequenceFeature sf, boolean removeHTML)
663   {
664     if (sf.getDescription() == null)
665     {
666       return;
667     }
668     ParseHtmlBodyAndLinks parsed = new ParseHtmlBodyAndLinks(
669             sf.getDescription(), removeHTML, newline);
670
671     sf.description = (removeHTML) ? parsed.getNonHtmlContent()
672             : sf.description;
673     for (String link : parsed.getLinks())
674     {
675       sf.addLink(link);
676     }
677
678   }
679
680   /**
681    * generate a features file for seqs includes non-pos features by default.
682    * 
683    * @param sequences
684    *          source of sequence features
685    * @param visible
686    *          hash of feature types and colours
687    * @return features file contents
688    */
689   public String printJalviewFormat(SequenceI[] sequences,
690           Map<String, Object> visible)
691   {
692     return printJalviewFormat(sequences, visible, true, true);
693   }
694
695   /**
696    * generate a features file for seqs with colours from visible (if any)
697    * 
698    * @param sequences
699    *          source of features
700    * @param visible
701    *          hash of Colours for each feature type
702    * @param visOnly
703    *          when true only feature types in 'visible' will be output
704    * @param nonpos
705    *          indicates if non-positional features should be output (regardless
706    *          of group or type)
707    * @return features file contents
708    */
709   public String printJalviewFormat(SequenceI[] sequences,
710           Map<String, Object> visible, boolean visOnly, boolean nonpos)
711   {
712     StringBuilder out = new StringBuilder(256);
713     boolean featuresGen = false;
714     if (visOnly && !nonpos && (visible == null || visible.size() < 1))
715     {
716       // no point continuing.
717       return "No Features Visible";
718     }
719
720     if (visible != null && visOnly)
721     {
722       // write feature colours only if we're given them and we are generating
723       // viewed features
724       // TODO: decide if feature links should also be written here ?
725       Iterator<String> en = visible.keySet().iterator();
726       String featureType, color;
727       while (en.hasNext())
728       {
729         featureType = en.next().toString();
730
731         if (visible.get(featureType) instanceof GraduatedColor)
732         {
733           GraduatedColor gc = (GraduatedColor) visible.get(featureType);
734           color = (gc.isColourByLabel() ? "label|" : "")
735                   + Format.getHexString(gc.getMinColor()) + "|"
736                   + Format.getHexString(gc.getMaxColor())
737                   + (gc.isAutoScale() ? "|" : "|abso|") + gc.getMin() + "|"
738                   + gc.getMax() + "|";
739           if (gc.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
740           {
741             if (gc.getThreshType() == AnnotationColourGradient.BELOW_THRESHOLD)
742             {
743               color += "below";
744             }
745             else
746             {
747               if (gc.getThreshType() != AnnotationColourGradient.ABOVE_THRESHOLD)
748               {
749                 System.err.println("WARNING: Unsupported threshold type ("
750                         + gc.getThreshType() + ") : Assuming 'above'");
751               }
752               color += "above";
753             }
754             // add the value
755             color += "|" + gc.getThresh();
756           }
757           else
758           {
759             color += "none";
760           }
761         }
762         else if (visible.get(featureType) instanceof Color)
763         {
764           color = Format.getHexString((Color) visible.get(featureType));
765         }
766         else
767         {
768           // legacy support for integer objects containing colour triplet values
769           color = Format.getHexString(new Color(Integer.parseInt(visible
770                   .get(featureType).toString())));
771         }
772         out.append(featureType);
773         out.append(TAB);
774         out.append(color);
775         out.append(newline);
776       }
777     }
778     // Work out which groups are both present and visible
779     List<String> groups = new ArrayList<String>();
780     int groupIndex = 0;
781     boolean isnonpos = false;
782
783     SequenceFeature[] features;
784     for (int i = 0; i < sequences.length; i++)
785     {
786       features = sequences[i].getSequenceFeatures();
787       if (features != null)
788       {
789         for (int j = 0; j < features.length; j++)
790         {
791           isnonpos = features[j].begin == 0 && features[j].end == 0;
792           if ((!nonpos && isnonpos)
793                   || (!isnonpos && visOnly && !visible
794                           .containsKey(features[j].type)))
795           {
796             continue;
797           }
798
799           if (features[j].featureGroup != null
800                   && !groups.contains(features[j].featureGroup))
801           {
802             groups.add(features[j].featureGroup);
803           }
804         }
805       }
806     }
807
808     String group = null;
809     do
810     {
811       if (groups.size() > 0 && groupIndex < groups.size())
812       {
813         group = groups.get(groupIndex);
814         out.append(newline);
815         out.append("STARTGROUP").append(TAB);
816         out.append(group);
817         out.append(newline);
818       }
819       else
820       {
821         group = null;
822       }
823
824       for (int i = 0; i < sequences.length; i++)
825       {
826         features = sequences[i].getSequenceFeatures();
827         if (features != null)
828         {
829           for (int j = 0; j < features.length; j++)
830           {
831             isnonpos = features[j].begin == 0 && features[j].end == 0;
832             if ((!nonpos && isnonpos)
833                     || (!isnonpos && visOnly && !visible
834                             .containsKey(features[j].type)))
835             {
836               // skip if feature is nonpos and we ignore them or if we only
837               // output visible and it isn't non-pos and it's not visible
838               continue;
839             }
840
841             if (group != null
842                     && (features[j].featureGroup == null || !features[j].featureGroup
843                             .equals(group)))
844             {
845               continue;
846             }
847
848             if (group == null && features[j].featureGroup != null)
849             {
850               continue;
851             }
852             // we have features to output
853             featuresGen = true;
854             if (features[j].description == null
855                     || features[j].description.equals(""))
856             {
857               out.append(features[j].type).append(TAB);
858             }
859             else
860             {
861               if (features[j].links != null
862                       && features[j].getDescription().indexOf("<html>") == -1)
863               {
864                 out.append("<html>");
865               }
866
867               out.append(features[j].description + " ");
868               if (features[j].links != null)
869               {
870                 for (int l = 0; l < features[j].links.size(); l++)
871                 {
872                   String label = features[j].links.elementAt(l).toString();
873                   String href = label.substring(label.indexOf("|") + 1);
874                   label = label.substring(0, label.indexOf("|"));
875
876                   if (features[j].description.indexOf(href) == -1)
877                   {
878                     out.append("<a href=\"" + href + "\">" + label + "</a>");
879                   }
880                 }
881
882                 if (features[j].getDescription().indexOf("</html>") == -1)
883                 {
884                   out.append("</html>");
885                 }
886               }
887
888               out.append(TAB);
889             }
890             out.append(sequences[i].getName());
891             out.append("\t-1\t");
892             out.append(features[j].begin);
893             out.append(TAB);
894             out.append(features[j].end);
895             out.append(TAB);
896             out.append(features[j].type);
897             if (!Float.isNaN(features[j].score))
898             {
899               out.append(TAB);
900               out.append(features[j].score);
901             }
902             out.append(newline);
903           }
904         }
905       }
906
907       if (group != null)
908       {
909         out.append("ENDGROUP").append(TAB);
910         out.append(group);
911         out.append(newline);
912         groupIndex++;
913       }
914       else
915       {
916         break;
917       }
918
919     } while (groupIndex < groups.size() + 1);
920
921     if (!featuresGen)
922     {
923       return "No Features Visible";
924     }
925
926     return out.toString();
927   }
928
929   /**
930    * Parse method that is called when a GFF file is dragged to the desktop
931    */
932   @Override
933   public void parse()
934   {
935     AlignViewportI av = getViewport();
936     if (av != null)
937     {
938       if (av.getAlignment() != null)
939       {
940         dataset = av.getAlignment().getDataset();
941       }
942       if (dataset == null)
943       {
944         // working in the applet context ?
945         dataset = av.getAlignment();
946       }
947     }
948     else
949     {
950       dataset = new Alignment(new SequenceI[] {});
951     }
952
953     boolean parseResult = parse(dataset, null, false, true);
954     if (!parseResult)
955     {
956       // pass error up somehow
957     }
958     if (av != null)
959     {
960       // update viewport with the dataset data ?
961     }
962     else
963     {
964       setSeqs(dataset.getSequencesArray());
965     }
966   }
967
968   /**
969    * Implementation of unused abstract method
970    * 
971    * @return error message
972    */
973   @Override
974   public String print()
975   {
976     return "Use printGffFormat() or printJalviewFormat()";
977   }
978
979   /**
980    * Returns features output in GFF2 format, including hidden and non-positional
981    * features
982    * 
983    * @param sequences
984    *          the sequences whose features are to be output
985    * @param visible
986    *          a map whose keys are the type names of visible features
987    * @return
988    */
989   public String printGffFormat(SequenceI[] sequences, Map<String, Object> visible)
990   {
991     return printGffFormat(sequences, visible, true, true);
992   }
993
994   /**
995    * Returns features output in GFF2 format
996    * 
997    * @param sequences
998    *          the sequences whose features are to be output
999    * @param visible
1000    *          a map whose keys are the type names of visible features
1001    * @param outputVisibleOnly
1002    * @param includeNonPositionalFeatures
1003    * @return
1004    */
1005   public String printGffFormat(SequenceI[] sequences, Map<String, Object> visible, boolean outputVisibleOnly,
1006           boolean includeNonPositionalFeatures)
1007   {
1008     StringBuilder out = new StringBuilder(256);
1009     out.append(String.format("%s %d\n", GFF_VERSION, gffVersion));
1010     String source;
1011     boolean isnonpos;
1012     for (SequenceI seq : sequences)
1013     {
1014       SequenceFeature[] features = seq.getSequenceFeatures();
1015       if (features != null)
1016       {
1017         for (SequenceFeature sf : features)
1018         {
1019           isnonpos = sf.begin == 0 && sf.end == 0;
1020           if (!includeNonPositionalFeatures && isnonpos)
1021           {
1022             /*
1023              * ignore non-positional features if not wanted
1024              */
1025             continue;
1026           }
1027           // TODO why the test !isnonpos here?
1028           // what about not visible non-positional features?
1029           if (!isnonpos && outputVisibleOnly
1030                   && !visible.containsKey(sf.type))
1031           {
1032             /*
1033              * ignore not visible features if not wanted
1034              */
1035             continue;
1036           }
1037   
1038           source = sf.featureGroup;
1039           if (source == null)
1040           {
1041             source = sf.getDescription();
1042           }
1043   
1044           out.append(seq.getName());
1045           out.append(TAB);
1046           out.append(source);
1047           out.append(TAB);
1048           out.append(sf.type);
1049           out.append(TAB);
1050           out.append(sf.begin);
1051           out.append(TAB);
1052           out.append(sf.end);
1053           out.append(TAB);
1054           out.append(sf.score);
1055           out.append(TAB);
1056   
1057           out.append(sf.getValue(STRAND, "."));
1058           out.append(TAB);
1059   
1060           out.append(sf.getValue(FRAME, "."));
1061   
1062           // miscellaneous key-values (GFF column 9)
1063           String attributes = (String) sf.getValue(ATTRIBUTES);
1064           if (attributes != null)
1065           {
1066             out.append(TAB).append(attributes);
1067           }
1068   
1069           out.append(newline);
1070         }
1071       }
1072     }
1073   
1074     return out.toString();
1075   }
1076
1077   /**
1078    * Helper method to make a mapping given a set of attributes for a GFF feature
1079    * 
1080    * @param set
1081    * @param attr
1082    * @param strand
1083    *          either 1 (forward) or -1 (reverse)
1084    * @return
1085    * @throws InvalidGFF3FieldException
1086    */
1087   protected MapList constructCodonMappingFromAlign(
1088           Map<String, List<String>> set, String attr,
1089           int strand) throws InvalidGFF3FieldException
1090   {
1091     if (strand == 0)
1092     {
1093       throw new InvalidGFF3FieldException(attr, set,
1094               "Invalid strand for a codon mapping (cannot be 0)");
1095     }
1096     List<Integer> fromrange = new ArrayList<Integer>();
1097     List<Integer> torange = new ArrayList<Integer>();
1098     int lastppos = 0, lastpframe = 0;
1099     for (String range : set.get(attr))
1100     {
1101       List<Integer> ints = new ArrayList<Integer>();
1102       StringTokenizer st = new StringTokenizer(range, " ");
1103       while (st.hasMoreTokens())
1104       {
1105         String num = st.nextToken();
1106         try
1107         {
1108           ints.add(new Integer(num));
1109         } catch (NumberFormatException nfe)
1110         {
1111           throw new InvalidGFF3FieldException(attr, set,
1112                   "Invalid number in field " + num);
1113         }
1114       }
1115       /* 
1116        * Align positionInRef positionInQuery LengthInRef
1117        * contig_1146 exonerate:p2g:local similarity 8534 11269 3652 - .
1118        *     alignment_id 0 ; Query DDB_G0269124 Align 11270 143 120
1119        * means:
1120        *     120 bases align at pos 143 in protein to 11270 on dna (-ve strand)
1121        * and so on for additional ' ; Align x y z' groups
1122        */
1123       if (ints.size() != 3)
1124       {
1125         throw new InvalidGFF3FieldException(attr, set,
1126                 "Invalid number of fields for this attribute ("
1127                         + ints.size() + ")");
1128       }
1129       fromrange.add(ints.get(0));
1130       fromrange.add(ints.get(0) + strand * ints.get(2));
1131       // how are intron/exon boundaries that do not align in codons
1132       // represented
1133       if (ints.get(1).intValue() == lastppos && lastpframe > 0)
1134       {
1135         // extend existing to map
1136         lastppos += ints.get(2) / 3;
1137         lastpframe = ints.get(2) % 3;
1138         torange.set(torange.size() - 1, new Integer(lastppos));
1139       }
1140       else
1141       {
1142         // new to map range
1143         torange.add(ints.get(1));
1144         lastppos = ints.get(1) + ints.get(2) / 3;
1145         lastpframe = ints.get(2) % 3;
1146         torange.add(new Integer(lastppos));
1147       }
1148     }
1149     // from and to ranges must end up being a series of start/end intervals
1150     if (fromrange.size() % 2 == 1)
1151     {
1152       throw new InvalidGFF3FieldException(attr, set,
1153               "Couldn't parse the DNA alignment range correctly");
1154     }
1155     if (torange.size() % 2 == 1)
1156     {
1157       throw new InvalidGFF3FieldException(attr, set,
1158               "Couldn't parse the protein alignment range correctly");
1159     }
1160     // finally, build the map
1161     int[] frommap = new int[fromrange.size()], tomap = new int[torange
1162             .size()];
1163     int p = 0;
1164     for (Integer ip : fromrange)
1165     {
1166       frommap[p++] = ip.intValue();
1167     }
1168     p = 0;
1169     for (Integer ip : torange)
1170     {
1171       tomap[p++] = ip.intValue();
1172     }
1173   
1174     return new MapList(frommap, tomap, 3, 1);
1175   }
1176
1177   private List<SequenceI> findNames(AlignmentI align, List<SequenceI> newseqs, boolean relaxedIdMatching,
1178           List<String> list)
1179   {
1180     List<SequenceI> found = new ArrayList<SequenceI>();
1181     for (String seqId : list)
1182     {
1183       SequenceI seq = findName(align, seqId, relaxedIdMatching, newseqs);
1184       if (seq != null)
1185       {
1186         found.add(seq);
1187       }
1188     }
1189     return found;
1190   }
1191
1192   /**
1193    * Parse a GFF format feature. This may include creating a 'dummy' sequence
1194    * for the feature or its mapped sequence
1195    * 
1196    * @param st
1197    * @param alignment
1198    * @param relaxedIdmatching
1199    * @param newseqs
1200    * @return
1201    */
1202   protected SequenceI parseGffFeature(StringTokenizer st, AlignmentI alignment, boolean relaxedIdmatching,
1203           List<SequenceI> newseqs)
1204   {
1205     SequenceI seq;
1206     /*
1207      * GFF: seqid source type start end score strand phase [attributes]
1208      */
1209     String seqId = st.nextToken();
1210   
1211     /*
1212      * locate referenced sequence in alignment _or_ 
1213      * as a forward reference (SequenceDummy)
1214      */
1215     seq = findName(alignment, seqId, relaxedIdmatching, newseqs);
1216   
1217     String desc = st.nextToken();
1218     String group = null;
1219     if (desc.indexOf(' ') == -1)
1220     {
1221       // could also be a source term rather than description line
1222       group = desc;
1223     }
1224     String ft = st.nextToken();
1225     int startPos = StringUtils.parseInt(st.nextToken());
1226     int endPos = StringUtils.parseInt(st.nextToken());
1227     // TODO: decide if non positional feature assertion for input data
1228     // where end==0 is generally valid
1229     if (endPos == 0)
1230     {
1231       // treat as non-positional feature, regardless.
1232       startPos = 0;
1233     }
1234     float score = 0f;
1235     try
1236     {
1237       score = new Float(st.nextToken()).floatValue();
1238     } catch (NumberFormatException ex)
1239     {
1240       // leave at 0
1241     }
1242   
1243     SequenceFeature sf = new SequenceFeature(ft, desc, startPos,
1244             endPos, score, group);
1245     if (st.hasMoreTokens())
1246     {
1247       sf.setValue(STRAND, st.nextToken());
1248     }
1249     if (st.hasMoreTokens())
1250     {
1251       sf.setValue(FRAME, st.nextToken());
1252     }
1253   
1254     if (st.hasMoreTokens())
1255     {
1256       String attributes = st.nextToken();
1257       sf.setValue(ATTRIBUTES, attributes);
1258   
1259       /*
1260        * parse semi-structured attributes in column 9 and add them to the 
1261        * sequence feature's 'otherData' table; use Note as a best proxy for 
1262        * description
1263        */
1264       Map<String, List<String>> nameValues = StringUtils.parseNameValuePairs(attributes, ";",
1265               new char[] { ' ', '=' });
1266       for (Entry<String, List<String>> attr : nameValues.entrySet())
1267       {
1268         String values = StringUtils.listToDelimitedString(attr.getValue(),
1269                 "; ");
1270         sf.setValue(attr.getKey(), values);
1271         if ("Note".equals(attr.getKey()))
1272         {
1273           sf.setDescription(values);
1274         }
1275       }
1276     }
1277   
1278     if (processOrAddSeqFeature(alignment, newseqs, seq, sf,
1279             relaxedIdmatching))
1280     {
1281       // check whether we should add the sequence feature to any other
1282       // sequences in the alignment with the same or similar
1283       while ((seq = alignment.findName(seq, seqId, true)) != null)
1284       {
1285         seq.addSequenceFeature(new SequenceFeature(sf));
1286       }
1287     }
1288     return seq;
1289   }
1290
1291   /**
1292    * After encountering ##fasta in a GFF3 file, process the remainder of the
1293    * file as FAST sequence data. Any placeholder sequences created during
1294    * feature parsing are updated with the actual sequences.
1295    * 
1296    * @param align
1297    * @param newseqs
1298    * @throws IOException
1299    */
1300   protected void processAsFasta(AlignmentI align, List<SequenceI> newseqs)
1301           throws IOException
1302   {
1303     try
1304     {
1305       mark();
1306     } catch (IOException q)
1307     {
1308     }
1309     FastaFile parser = new FastaFile(this);
1310     List<SequenceI> includedseqs = parser.getSeqs();
1311     SequenceIdMatcher smatcher = new SequenceIdMatcher(newseqs);
1312     // iterate over includedseqs, and replacing matching ones with newseqs
1313     // sequences. Generic iterator not used here because we modify includedseqs
1314     // as we go
1315     for (int p = 0, pSize = includedseqs.size(); p < pSize; p++)
1316     {
1317       // search for any dummy seqs that this sequence can be used to update
1318       SequenceI dummyseq = smatcher.findIdMatch(includedseqs.get(p));
1319       if (dummyseq != null)
1320       {
1321         // dummyseq was created so it could be annotated and referred to in
1322         // alignments/codon mappings
1323   
1324         SequenceI mseq = includedseqs.get(p);
1325         // mseq is the 'template' imported from the FASTA file which we'll use
1326         // to coomplete dummyseq
1327         if (dummyseq instanceof SequenceDummy)
1328         {
1329           // probably have the pattern wrong
1330           // idea is that a flyweight proxy for a sequence ID can be created for
1331           // 1. stable reference creation
1332           // 2. addition of annotation
1333           // 3. future replacement by a real sequence
1334           // current pattern is to create SequenceDummy objects - a convenience
1335           // constructor for a Sequence.
1336           // problem is that when promoted to a real sequence, all references
1337           // need
1338           // to be updated somehow.
1339           ((SequenceDummy) dummyseq).become(mseq);
1340           includedseqs.set(p, dummyseq); // template is no longer needed
1341         }
1342       }
1343     }
1344     // finally add sequences to the dataset
1345     for (SequenceI seq : includedseqs)
1346     {
1347       align.addSequence(seq);
1348     }
1349   }
1350
1351   /**
1352    * Process a ## directive
1353    * 
1354    * @param line
1355    * @param gffProps
1356    * @param align
1357    * @param newseqs
1358    * @throws IOException
1359    */
1360   protected void processGffPragma(String line, Map<String, String> gffProps, AlignmentI align,
1361           List<SequenceI> newseqs) throws IOException
1362   {
1363     line = line.trim();
1364     if ("###".equals(line))
1365     {
1366       // close off any open 'forward references'
1367       return;
1368     }
1369   
1370     String[] tokens = line.substring(2).split(" ");
1371     String pragma = tokens[0];
1372     String value = tokens.length == 1 ? null : tokens[1];
1373   
1374     if ("gff-version".equalsIgnoreCase(pragma))
1375     {
1376       if (value != null)
1377       {
1378         try
1379         {
1380           // value may be e.g. "3.1.2"
1381           gffVersion = Integer.parseInt(value.split("\\.")[0]);
1382         } catch (NumberFormatException e)
1383         {
1384           // ignore
1385         }
1386       }
1387     }
1388     else if ("feature-ontology".equalsIgnoreCase(pragma))
1389     {
1390       // should resolve against the specified feature ontology URI
1391     }
1392     else if ("attribute-ontology".equalsIgnoreCase(pragma))
1393     {
1394       // URI of attribute ontology - not currently used in GFF3
1395     }
1396     else if ("source-ontology".equalsIgnoreCase(pragma))
1397     {
1398       // URI of source ontology - not currently used in GFF3
1399     }
1400     else if ("species-build".equalsIgnoreCase(pragma))
1401     {
1402       // save URI of specific NCBI taxon version of annotations
1403       gffProps.put("species-build", value);
1404     }
1405     else if ("fasta".equalsIgnoreCase(pragma))
1406     {
1407       // process the rest of the file as a fasta file and replace any dummy
1408       // sequence IDs
1409       processAsFasta(align, newseqs);
1410     }
1411     else
1412     {
1413       System.err.println("Ignoring unknown pragma: " + line);
1414     }
1415   }
1416
1417   /**
1418    * Processes the 'Query' and 'Align' properties associated with a GFF
1419    * similarity feature; these properties define the mapping of the annotated
1420    * feature to another from which it has transferred annotation
1421    * 
1422    * @param set
1423    * @param seq
1424    * @param sf
1425    * @return
1426    */
1427   public void processGffSimilarity(Map<String, List<String>> set, SequenceI seq,
1428           SequenceFeature sf, AlignmentI align, List<SequenceI> newseqs, boolean relaxedIdMatching)
1429           throws InvalidGFF3FieldException
1430   {
1431     int strand = sf.getStrand();
1432     // exonerate cdna/protein map
1433     // look for fields
1434     List<SequenceI> querySeq = findNames(align, newseqs, relaxedIdMatching,
1435             set.get("Query"));
1436     if (querySeq == null || querySeq.size() != 1)
1437     {
1438       throw new InvalidGFF3FieldException("Query", set,
1439               "Expecting exactly one sequence in Query field (got "
1440                       + set.get("Query") + ")");
1441     }
1442     if (set.containsKey("Align"))
1443     {
1444       // process the align maps and create cdna/protein maps
1445       // ideally, the query sequences are in the alignment, but maybe not...
1446   
1447       AlignedCodonFrame alco = new AlignedCodonFrame();
1448       MapList codonmapping = constructCodonMappingFromAlign(set, "Align",
1449               strand);
1450   
1451       // add codon mapping, and hope!
1452       alco.addMap(seq, querySeq.get(0), codonmapping);
1453       align.addCodonFrame(alco);
1454     }
1455   
1456   }
1457
1458   /**
1459    * take a sequence feature and examine its attributes to decide how it should
1460    * be added to a sequence
1461    * 
1462    * @param seq
1463    *          - the destination sequence constructed or discovered in the
1464    *          current context
1465    * @param sf
1466    *          - the base feature with ATTRIBUTES property containing any
1467    *          additional attributes
1468    * @param gFFFile
1469    *          - true if we are processing a GFF annotation file
1470    * @return true if sf was actually added to the sequence, false if it was
1471    *         processed in another way
1472    */
1473   public boolean processOrAddSeqFeature(AlignmentI align, List<SequenceI> newseqs,
1474           SequenceI seq, SequenceFeature sf, boolean relaxedIdMatching)
1475   {
1476     String attr = (String) sf.getValue(ATTRIBUTES);
1477     boolean addFeature = true;
1478     if (attr != null)
1479     {
1480       for (String attset : attr.split(TAB))
1481       {
1482         Map<String, List<String>> set = StringUtils.parseNameValuePairs(
1483                 attset, ";", new char[] { ' ', '-' });
1484   
1485         if ("similarity".equals(sf.getType()))
1486         {
1487           try
1488           {
1489             processGffSimilarity(set, seq, sf, align, newseqs,
1490                     relaxedIdMatching);
1491             addFeature = false;
1492           } catch (InvalidGFF3FieldException ivfe)
1493           {
1494             System.err.println(ivfe);
1495           }
1496         }
1497       }
1498     }
1499     if (addFeature)
1500     {
1501       seq.addSequenceFeature(sf);
1502     }
1503     return addFeature;
1504   }
1505
1506 }
1507
1508 class InvalidGFF3FieldException extends Exception
1509 {
1510   String field, value;
1511
1512   public InvalidGFF3FieldException(String field,
1513           Map<String, List<String>> set, String message)
1514   {
1515     super(message + " (Field was " + field + " and value was "
1516             + set.get(field).toString());
1517     this.field = field;
1518     this.value = set.get(field).toString();
1519   }
1520 }