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