JAL-1551 formatting
[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.datamodel.AlignedCodonFrame;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.SequenceDummy;
27 import jalview.datamodel.SequenceFeature;
28 import jalview.datamodel.SequenceI;
29 import jalview.schemes.AnnotationColourGradient;
30 import jalview.schemes.GraduatedColor;
31 import jalview.schemes.UserColourScheme;
32 import jalview.util.Format;
33 import jalview.util.MapList;
34
35 import java.io.IOException;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.HashMap;
39 import java.util.Hashtable;
40 import java.util.Iterator;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.StringTokenizer;
44 import java.util.Vector;
45
46 /**
47  * Parse and create Jalview Features files Detects GFF format features files and
48  * parses. Does not implement standard print() - call specific printFeatures or
49  * printGFF. Uses AlignmentI.findSequence(String id) to find the sequence object
50  * for the features annotation - this normally works on an exact match.
51  * 
52  * @author AMW
53  * @version $Revision$
54  */
55 public class FeaturesFile extends AlignFile
56 {
57   /**
58    * work around for GFF interpretation bug where source string becomes
59    * description rather than a group
60    */
61   private boolean doGffSource = true;
62
63   /**
64    * Creates a new FeaturesFile object.
65    */
66   public FeaturesFile()
67   {
68   }
69
70   /**
71    * Creates a new FeaturesFile object.
72    * 
73    * @param inFile
74    *          DOCUMENT ME!
75    * @param type
76    *          DOCUMENT ME!
77    * 
78    * @throws IOException
79    *           DOCUMENT ME!
80    */
81   public FeaturesFile(String inFile, String type) throws IOException
82   {
83     super(inFile, type);
84   }
85
86   public FeaturesFile(FileParse source) throws IOException
87   {
88     super(source);
89   }
90
91   /**
92    * Parse GFF or sequence features file using case-independent matching,
93    * discarding URLs
94    * 
95    * @param align
96    *          - alignment/dataset containing sequences that are to be annotated
97    * @param colours
98    *          - hashtable to store feature colour definitions
99    * @param removeHTML
100    *          - process html strings into plain text
101    * @return true if features were added
102    */
103   public boolean parse(AlignmentI align, Hashtable colours,
104           boolean removeHTML)
105   {
106     return parse(align, colours, null, removeHTML, false);
107   }
108
109   /**
110    * Parse GFF or sequence features file optionally using case-independent
111    * matching, discarding URLs
112    * 
113    * @param align
114    *          - alignment/dataset containing sequences that are to be annotated
115    * @param colours
116    *          - hashtable to store feature colour definitions
117    * @param removeHTML
118    *          - process html strings into plain text
119    * @param relaxedIdmatching
120    *          - when true, ID matches to compound sequence IDs are allowed
121    * @return true if features were added
122    */
123   public boolean parse(AlignmentI align, Map colours, boolean removeHTML,
124           boolean relaxedIdMatching)
125   {
126     return parse(align, colours, null, removeHTML, relaxedIdMatching);
127   }
128
129   /**
130    * Parse GFF or sequence features file optionally using case-independent
131    * matching
132    * 
133    * @param align
134    *          - alignment/dataset containing sequences that are to be annotated
135    * @param colours
136    *          - hashtable to store feature colour definitions
137    * @param featureLink
138    *          - hashtable to store associated URLs
139    * @param removeHTML
140    *          - process html strings into plain text
141    * @return true if features were added
142    */
143   public boolean parse(AlignmentI align, Map colours, Map featureLink,
144           boolean removeHTML)
145   {
146     return parse(align, colours, featureLink, removeHTML, false);
147   }
148
149   /**
150    * Parse GFF or sequence features file
151    * 
152    * @param align
153    *          - alignment/dataset containing sequences that are to be annotated
154    * @param colours
155    *          - hashtable to store feature colour definitions
156    * @param featureLink
157    *          - hashtable to store associated URLs
158    * @param removeHTML
159    *          - process html strings into plain text
160    * @param relaxedIdmatching
161    *          - when true, ID matches to compound sequence IDs are allowed
162    * @return true if features were added
163    */
164   public boolean parse(AlignmentI align, Map colours, Map featureLink,
165           boolean removeHTML, boolean relaxedIdmatching)
166   {
167
168     String line = null;
169     try
170     {
171       SequenceI seq = null;
172       String type, desc, token = null;
173
174       int index, start, end;
175       float score;
176       StringTokenizer st;
177       SequenceFeature sf;
178       String featureGroup = null, groupLink = null;
179       Map typeLink = new Hashtable();
180       /**
181        * when true, assume GFF style features rather than Jalview style.
182        */
183       boolean GFFFile = true;
184       while ((line = nextLine()) != null)
185       {
186         if (line.startsWith("#"))
187         {
188           continue;
189         }
190
191         st = new StringTokenizer(line, "\t");
192         if (st.countTokens() == 1)
193         {
194           if (line.trim().equalsIgnoreCase("GFF"))
195           {
196             // Start parsing file as if it might be GFF again.
197             GFFFile = true;
198             continue;
199           }
200         }
201         if (st.countTokens() > 1 && st.countTokens() < 4)
202         {
203           GFFFile = false;
204           type = st.nextToken();
205           if (type.equalsIgnoreCase("startgroup"))
206           {
207             featureGroup = st.nextToken();
208             if (st.hasMoreElements())
209             {
210               groupLink = st.nextToken();
211               featureLink.put(featureGroup, groupLink);
212             }
213           }
214           else if (type.equalsIgnoreCase("endgroup"))
215           {
216             // We should check whether this is the current group,
217             // but at present theres no way of showing more than 1 group
218             st.nextToken();
219             featureGroup = null;
220             groupLink = null;
221           }
222           else
223           {
224             Object colour = null;
225             String colscheme = st.nextToken();
226             if (colscheme.indexOf("|") > -1
227                     || colscheme.trim().equalsIgnoreCase("label"))
228             {
229               // Parse '|' separated graduated colourscheme fields:
230               // [label|][mincolour|maxcolour|[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue]
231               // can either provide 'label' only, first is optional, next two
232               // colors are required (but may be
233               // left blank), next is optional, nxt two min/max are required.
234               // first is either 'label'
235               // first/second and third are both hexadecimal or word equivalent
236               // colour.
237               // next two are values parsed as floats.
238               // fifth is either 'above','below', or 'none'.
239               // sixth is a float value and only required when fifth is either
240               // 'above' or 'below'.
241               StringTokenizer gcol = new StringTokenizer(colscheme, "|",
242                       true);
243               // set defaults
244               int threshtype = AnnotationColourGradient.NO_THRESHOLD;
245               float min = Float.MIN_VALUE, max = Float.MAX_VALUE, threshval = Float.NaN;
246               boolean labelCol = false;
247               // Parse spec line
248               String mincol = gcol.nextToken();
249               if (mincol == "|")
250               {
251                 System.err
252                         .println("Expected either 'label' or a colour specification in the line: "
253                                 + line);
254                 continue;
255               }
256               String maxcol = null;
257               if (mincol.toLowerCase().indexOf("label") == 0)
258               {
259                 labelCol = true;
260                 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); // skip
261                                                                            // '|'
262                 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
263               }
264               String abso = null, minval, maxval;
265               if (mincol != null)
266               {
267                 // at least four more tokens
268                 if (mincol.equals("|"))
269                 {
270                   mincol = "";
271                 }
272                 else
273                 {
274                   gcol.nextToken(); // skip next '|'
275                 }
276                 // continue parsing rest of line
277                 maxcol = gcol.nextToken();
278                 if (maxcol.equals("|"))
279                 {
280                   maxcol = "";
281                 }
282                 else
283                 {
284                   gcol.nextToken(); // skip next '|'
285                 }
286                 abso = gcol.nextToken();
287                 gcol.nextToken(); // skip next '|'
288                 if (abso.toLowerCase().indexOf("abso") != 0)
289                 {
290                   minval = abso;
291                   abso = null;
292                 }
293                 else
294                 {
295                   minval = gcol.nextToken();
296                   gcol.nextToken(); // skip next '|'
297                 }
298                 maxval = gcol.nextToken();
299                 if (gcol.hasMoreTokens())
300                 {
301                   gcol.nextToken(); // skip next '|'
302                 }
303                 try
304                 {
305                   if (minval.length() > 0)
306                   {
307                     min = new Float(minval).floatValue();
308                   }
309                 } catch (Exception e)
310                 {
311                   System.err
312                           .println("Couldn't parse the minimum value for graduated colour for type ("
313                                   + colscheme
314                                   + ") - did you misspell 'auto' for the optional automatic colour switch ?");
315                   e.printStackTrace();
316                 }
317                 try
318                 {
319                   if (maxval.length() > 0)
320                   {
321                     max = new Float(maxval).floatValue();
322                   }
323                 } catch (Exception e)
324                 {
325                   System.err
326                           .println("Couldn't parse the maximum value for graduated colour for type ("
327                                   + colscheme + ")");
328                   e.printStackTrace();
329                 }
330               }
331               else
332               {
333                 // add in some dummy min/max colours for the label-only
334                 // colourscheme.
335                 mincol = "FFFFFF";
336                 maxcol = "000000";
337               }
338               try
339               {
340                 colour = new jalview.schemes.GraduatedColor(
341                         new UserColourScheme(mincol).findColour('A'),
342                         new UserColourScheme(maxcol).findColour('A'), min,
343                         max);
344               } catch (Exception e)
345               {
346                 System.err
347                         .println("Couldn't parse the graduated colour scheme ("
348                                 + colscheme + ")");
349                 e.printStackTrace();
350               }
351               if (colour != null)
352               {
353                 ((jalview.schemes.GraduatedColor) colour)
354                         .setColourByLabel(labelCol);
355                 ((jalview.schemes.GraduatedColor) colour)
356                         .setAutoScaled(abso == null);
357                 // add in any additional parameters
358                 String ttype = null, tval = null;
359                 if (gcol.hasMoreTokens())
360                 {
361                   // threshold type and possibly a threshold value
362                   ttype = gcol.nextToken();
363                   if (ttype.toLowerCase().startsWith("below"))
364                   {
365                     ((jalview.schemes.GraduatedColor) colour)
366                             .setThreshType(AnnotationColourGradient.BELOW_THRESHOLD);
367                   }
368                   else if (ttype.toLowerCase().startsWith("above"))
369                   {
370                     ((jalview.schemes.GraduatedColor) colour)
371                             .setThreshType(AnnotationColourGradient.ABOVE_THRESHOLD);
372                   }
373                   else
374                   {
375                     ((jalview.schemes.GraduatedColor) colour)
376                             .setThreshType(AnnotationColourGradient.NO_THRESHOLD);
377                     if (!ttype.toLowerCase().startsWith("no"))
378                     {
379                       System.err
380                               .println("Ignoring unrecognised threshold type : "
381                                       + ttype);
382                     }
383                   }
384                 }
385                 if (((GraduatedColor) colour).getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
386                 {
387                   try
388                   {
389                     gcol.nextToken();
390                     tval = gcol.nextToken();
391                     ((jalview.schemes.GraduatedColor) colour)
392                             .setThresh(new Float(tval).floatValue());
393                   } catch (Exception e)
394                   {
395                     System.err
396                             .println("Couldn't parse threshold value as a float: ("
397                                     + tval + ")");
398                     e.printStackTrace();
399                   }
400                 }
401                 // parse the thresh-is-min token ?
402                 if (gcol.hasMoreTokens())
403                 {
404                   System.err
405                           .println("Ignoring additional tokens in parameters in graduated colour specification\n");
406                   while (gcol.hasMoreTokens())
407                   {
408                     System.err.println("|" + gcol.nextToken());
409                   }
410                   System.err.println("\n");
411                 }
412               }
413             }
414             else
415             {
416               UserColourScheme ucs = new UserColourScheme(colscheme);
417               colour = ucs.findColour('A');
418             }
419             if (colour != null)
420             {
421               colours.put(type, colour);
422             }
423             if (st.hasMoreElements())
424             {
425               String link = st.nextToken();
426               typeLink.put(type, link);
427               if (featureLink == null)
428               {
429                 featureLink = new Hashtable();
430               }
431               featureLink.put(type, link);
432             }
433           }
434           continue;
435         }
436         String seqId = "";
437         while (st.hasMoreElements())
438         {
439
440           if (GFFFile)
441           {
442             // Still possible this is an old Jalview file,
443             // which does not have type colours at the beginning
444             seqId = token = st.nextToken();
445             seq = findName(align, seqId, relaxedIdmatching);
446             if (seq != null)
447             {
448               desc = st.nextToken();
449               String group = null;
450               if (doGffSource && desc.indexOf(' ') == -1)
451               {
452                 // could also be a source term rather than description line
453                 group = new String(desc);
454               }
455               type = st.nextToken();
456               try
457               {
458                 String stt = st.nextToken();
459                 if (stt.length() == 0 || stt.equals("-"))
460                 {
461                   start = 0;
462                 }
463                 else
464                 {
465                   start = Integer.parseInt(stt);
466                 }
467               } catch (NumberFormatException ex)
468               {
469                 start = 0;
470               }
471               try
472               {
473                 String stt = st.nextToken();
474                 if (stt.length() == 0 || stt.equals("-"))
475                 {
476                   end = 0;
477                 }
478                 else
479                 {
480                   end = Integer.parseInt(stt);
481                 }
482               } catch (NumberFormatException ex)
483               {
484                 end = 0;
485               }
486               // TODO: decide if non positional feature assertion for input data
487               // where end==0 is generally valid
488               if (end == 0)
489               {
490                 // treat as non-positional feature, regardless.
491                 start = 0;
492               }
493               try
494               {
495                 score = new Float(st.nextToken()).floatValue();
496               } catch (NumberFormatException ex)
497               {
498                 score = 0;
499               }
500
501               sf = new SequenceFeature(type, desc, start, end, score, group);
502
503               try
504               {
505                 sf.setValue("STRAND", st.nextToken());
506                 sf.setValue("FRAME", st.nextToken());
507               } catch (Exception ex)
508               {
509               }
510
511               if (st.hasMoreTokens())
512               {
513                 StringBuffer attributes = new StringBuffer();
514                 while (st.hasMoreTokens())
515                 {
516                   attributes.append("\t" + st.nextElement());
517                 }
518                 // TODO validate and split GFF2 attributes field ? parse out
519                 // ([A-Za-z][A-Za-z0-9_]*) <value> ; and add as
520                 // sf.setValue(attrib, val);
521                 sf.setValue("ATTRIBUTES", attributes.toString());
522               }
523
524               seq.addSequenceFeature(sf);
525               while ((seq = align.findName(seq, seqId, true)) != null)
526               {
527                 seq.addSequenceFeature(new SequenceFeature(sf));
528               }
529               break;
530             }
531           }
532
533           if (GFFFile && seq == null)
534           {
535             desc = token;
536           }
537           else
538           {
539             desc = st.nextToken();
540           }
541           if (!st.hasMoreTokens())
542           {
543             System.err
544                     .println("DEBUG: Run out of tokens when trying to identify the destination for the feature.. giving up.");
545             // in all probability, this isn't a file we understand, so bail
546             // quietly.
547             return false;
548           }
549
550           token = st.nextToken();
551
552           if (!token.equals("ID_NOT_SPECIFIED"))
553           {
554             seq = findName(align, seqId = token, relaxedIdmatching);
555             st.nextToken();
556           }
557           else
558           {
559             seqId = null;
560             try
561             {
562               index = Integer.parseInt(st.nextToken());
563               seq = align.getSequenceAt(index);
564             } catch (NumberFormatException ex)
565             {
566               seq = null;
567             }
568           }
569
570           if (seq == null)
571           {
572             System.out.println("Sequence not found: " + line);
573             break;
574           }
575
576           start = Integer.parseInt(st.nextToken());
577           end = Integer.parseInt(st.nextToken());
578
579           type = st.nextToken();
580
581           if (!colours.containsKey(type))
582           {
583             // Probably the old style groups file
584             UserColourScheme ucs = new UserColourScheme(type);
585             colours.put(type, ucs.findColour('A'));
586           }
587           sf = new SequenceFeature(type, desc, "", start, end, featureGroup);
588           if (st.hasMoreTokens())
589           {
590             try
591             {
592               score = new Float(st.nextToken()).floatValue();
593               // update colourgradient bounds if allowed to
594             } catch (NumberFormatException ex)
595             {
596               score = 0;
597             }
598             sf.setScore(score);
599           }
600           if (groupLink != null && removeHTML)
601           {
602             sf.addLink(groupLink);
603             sf.description += "%LINK%";
604           }
605           if (typeLink.containsKey(type) && removeHTML)
606           {
607             sf.addLink(typeLink.get(type).toString());
608             sf.description += "%LINK%";
609           }
610
611           parseDescriptionHTML(sf, removeHTML);
612
613           seq.addSequenceFeature(sf);
614
615           while (seqId != null
616                   && (seq = align.findName(seq, seqId, false)) != null)
617           {
618             seq.addSequenceFeature(new SequenceFeature(sf));
619           }
620           // If we got here, its not a GFFFile
621           GFFFile = false;
622         }
623       }
624       resetMatcher();
625     } catch (Exception ex)
626     {
627       System.out.println("Error parsing feature file: " + ex + "\n" + line);
628       ex.printStackTrace(System.err);
629       resetMatcher();
630       return false;
631     }
632
633     return true;
634   }
635
636   private AlignmentI lastmatchedAl = null;
637
638   private SequenceIdMatcher matcher = null;
639
640   /**
641    * clear any temporary handles used to speed up ID matching
642    */
643   private void resetMatcher()
644   {
645     lastmatchedAl = null;
646     matcher = null;
647   }
648
649   private SequenceI findName(AlignmentI align, String seqId,
650           boolean relaxedIdMatching)
651   {
652     SequenceI match = null;
653     if (relaxedIdMatching)
654     {
655       if (lastmatchedAl != align)
656       {
657         matcher = new SequenceIdMatcher(
658                 (lastmatchedAl = align).getSequencesArray());
659       }
660       match = matcher.findIdMatch(seqId);
661     }
662     else
663     {
664       match = align.findName(seqId, true);
665     }
666     return match;
667   }
668
669   public void parseDescriptionHTML(SequenceFeature sf, boolean removeHTML)
670   {
671     if (sf.getDescription() == null)
672     {
673       return;
674     }
675     jalview.util.ParseHtmlBodyAndLinks parsed = new jalview.util.ParseHtmlBodyAndLinks(
676             sf.getDescription(), removeHTML, newline);
677
678     sf.description = (removeHTML) ? parsed.getNonHtmlContent()
679             : sf.description;
680     for (String link : parsed.getLinks())
681     {
682       sf.addLink(link);
683     }
684
685   }
686
687   /**
688    * generate a features file for seqs includes non-pos features by default.
689    * 
690    * @param seqs
691    *          source of sequence features
692    * @param visible
693    *          hash of feature types and colours
694    * @return features file contents
695    */
696   public String printJalviewFormat(SequenceI[] seqs, Map<String,Object> visible)
697   {
698     return printJalviewFormat(seqs, visible, true, true);
699   }
700
701   /**
702    * generate a features file for seqs with colours from visible (if any)
703    * 
704    * @param seqs
705    *          source of features
706    * @param visible
707    *          hash of Colours for each feature type
708    * @param visOnly
709    *          when true only feature types in 'visible' will be output
710    * @param nonpos
711    *          indicates if non-positional features should be output (regardless
712    *          of group or type)
713    * @return features file contents
714    */
715   public String printJalviewFormat(SequenceI[] seqs, Map visible,
716           boolean visOnly, boolean nonpos)
717   {
718     StringBuffer out = new StringBuffer();
719     SequenceFeature[] next;
720     boolean featuresGen = false;
721     if (visOnly && !nonpos && (visible == null || visible.size() < 1))
722     {
723       // no point continuing.
724       return "No Features Visible";
725     }
726
727     if (visible != null && visOnly)
728     {
729       // write feature colours only if we're given them and we are generating
730       // viewed features
731       // TODO: decide if feature links should also be written here ?
732       Iterator en = visible.keySet().iterator();
733       String type, color;
734       while (en.hasNext())
735       {
736         type = en.next().toString();
737
738         if (visible.get(type) instanceof GraduatedColor)
739         {
740           GraduatedColor gc = (GraduatedColor) visible.get(type);
741           color = (gc.isColourByLabel() ? "label|" : "")
742                   + Format.getHexString(gc.getMinColor()) + "|"
743                   + Format.getHexString(gc.getMaxColor())
744                   + (gc.isAutoScale() ? "|" : "|abso|") + gc.getMin() + "|"
745                   + gc.getMax() + "|";
746           if (gc.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
747           {
748             if (gc.getThreshType() == AnnotationColourGradient.BELOW_THRESHOLD)
749             {
750               color += "below";
751             }
752             else
753             {
754               if (gc.getThreshType() != AnnotationColourGradient.ABOVE_THRESHOLD)
755               {
756                 System.err.println("WARNING: Unsupported threshold type ("
757                         + gc.getThreshType() + ") : Assuming 'above'");
758               }
759               color += "above";
760             }
761             // add the value
762             color += "|" + gc.getThresh();
763           }
764           else
765           {
766             color += "none";
767           }
768         }
769         else if (visible.get(type) instanceof java.awt.Color)
770         {
771           color = Format.getHexString((java.awt.Color) visible.get(type));
772         }
773         else
774         {
775           // legacy support for integer objects containing colour triplet values
776           color = Format.getHexString(new java.awt.Color(Integer
777                   .parseInt(visible.get(type).toString())));
778         }
779         out.append(type);
780         out.append("\t");
781         out.append(color);
782         out.append(newline);
783       }
784     }
785     // Work out which groups are both present and visible
786     Vector groups = new Vector();
787     int groupIndex = 0;
788     boolean isnonpos = false;
789
790     for (int i = 0; i < seqs.length; i++)
791     {
792       next = seqs[i].getSequenceFeatures();
793       if (next != null)
794       {
795         for (int j = 0; j < next.length; j++)
796         {
797           isnonpos = next[j].begin == 0 && next[j].end == 0;
798           if ((!nonpos && isnonpos)
799                   || (!isnonpos && visOnly && !visible
800                           .containsKey(next[j].type)))
801           {
802             continue;
803           }
804
805           if (next[j].featureGroup != null
806                   && !groups.contains(next[j].featureGroup))
807           {
808             groups.addElement(next[j].featureGroup);
809           }
810         }
811       }
812     }
813
814     String group = null;
815     do
816     {
817
818       if (groups.size() > 0 && groupIndex < groups.size())
819       {
820         group = groups.elementAt(groupIndex).toString();
821         out.append(newline);
822         out.append("STARTGROUP\t");
823         out.append(group);
824         out.append(newline);
825       }
826       else
827       {
828         group = null;
829       }
830
831       for (int i = 0; i < seqs.length; i++)
832       {
833         next = seqs[i].getSequenceFeatures();
834         if (next != null)
835         {
836           for (int j = 0; j < next.length; j++)
837           {
838             isnonpos = next[j].begin == 0 && next[j].end == 0;
839             if ((!nonpos && isnonpos)
840                     || (!isnonpos && visOnly && !visible
841                             .containsKey(next[j].type)))
842             {
843               // skip if feature is nonpos and we ignore them or if we only
844               // output visible and it isn't non-pos and it's not visible
845               continue;
846             }
847
848             if (group != null
849                     && (next[j].featureGroup == null || !next[j].featureGroup
850                             .equals(group)))
851             {
852               continue;
853             }
854
855             if (group == null && next[j].featureGroup != null)
856             {
857               continue;
858             }
859             // we have features to output
860             featuresGen = true;
861             if (next[j].description == null
862                     || next[j].description.equals(""))
863             {
864               out.append(next[j].type + "\t");
865             }
866             else
867             {
868               if (next[j].links != null
869                       && next[j].getDescription().indexOf("<html>") == -1)
870               {
871                 out.append("<html>");
872               }
873
874               out.append(next[j].description + " ");
875               if (next[j].links != null)
876               {
877                 for (int l = 0; l < next[j].links.size(); l++)
878                 {
879                   String label = next[j].links.elementAt(l).toString();
880                   String href = label.substring(label.indexOf("|") + 1);
881                   label = label.substring(0, label.indexOf("|"));
882
883                   if (next[j].description.indexOf(href) == -1)
884                   {
885                     out.append("<a href=\"" + href + "\">" + label + "</a>");
886                   }
887                 }
888
889                 if (next[j].getDescription().indexOf("</html>") == -1)
890                 {
891                   out.append("</html>");
892                 }
893               }
894
895               out.append("\t");
896             }
897             out.append(seqs[i].getName());
898             out.append("\t-1\t");
899             out.append(next[j].begin);
900             out.append("\t");
901             out.append(next[j].end);
902             out.append("\t");
903             out.append(next[j].type);
904             if (next[j].score != Float.NaN)
905             {
906               out.append("\t");
907               out.append(next[j].score);
908             }
909             out.append(newline);
910           }
911         }
912       }
913
914       if (group != null)
915       {
916         out.append("ENDGROUP\t");
917         out.append(group);
918         out.append(newline);
919         groupIndex++;
920       }
921       else
922       {
923         break;
924       }
925
926     } while (groupIndex < groups.size() + 1);
927
928     if (!featuresGen)
929     {
930       return "No Features Visible";
931     }
932
933     return out.toString();
934   }
935
936   /**
937    * generate a gff file for sequence features includes non-pos features by
938    * default.
939    * 
940    * @param seqs
941    * @param visible
942    * @return
943    */
944   public String printGFFFormat(SequenceI[] seqs, Map<String,Object> visible)
945   {
946     return printGFFFormat(seqs, visible, true, true);
947   }
948
949   public String printGFFFormat(SequenceI[] seqs, Map<String,Object> visible,
950           boolean visOnly, boolean nonpos)
951   {
952     StringBuffer out = new StringBuffer();
953     SequenceFeature[] next;
954     String source;
955     boolean isnonpos;
956     for (int i = 0; i < seqs.length; i++)
957     {
958       if (seqs[i].getSequenceFeatures() != null)
959       {
960         next = seqs[i].getSequenceFeatures();
961         for (int j = 0; j < next.length; j++)
962         {
963           isnonpos = next[j].begin == 0 && next[j].end == 0;
964           if ((!nonpos && isnonpos)
965                   || (!isnonpos && visOnly && !visible
966                           .containsKey(next[j].type)))
967           {
968             continue;
969           }
970
971           source = next[j].featureGroup;
972           if (source == null)
973           {
974             source = next[j].getDescription();
975           }
976
977           out.append(seqs[i].getName());
978           out.append("\t");
979           out.append(source);
980           out.append("\t");
981           out.append(next[j].type);
982           out.append("\t");
983           out.append(next[j].begin);
984           out.append("\t");
985           out.append(next[j].end);
986           out.append("\t");
987           out.append(next[j].score);
988           out.append("\t");
989
990           if (next[j].getValue("STRAND") != null)
991           {
992             out.append(next[j].getValue("STRAND"));
993             out.append("\t");
994           }
995           else
996           {
997             out.append(".\t");
998           }
999
1000           if (next[j].getValue("FRAME") != null)
1001           {
1002             out.append(next[j].getValue("FRAME"));
1003           }
1004           else
1005           {
1006             out.append(".");
1007           }
1008           // TODO: verify/check GFF - should there be a /t here before attribute
1009           // output ?
1010
1011           if (next[j].getValue("ATTRIBUTES") != null)
1012           {
1013             out.append(next[j].getValue("ATTRIBUTES"));
1014           }
1015
1016           out.append(newline);
1017
1018         }
1019       }
1020     }
1021
1022     return out.toString();
1023   }
1024
1025   /**
1026    * this is only for the benefit of object polymorphism - method does nothing.
1027    */
1028   public void parse()
1029   {
1030     // IGNORED
1031   }
1032
1033   /**
1034    * this is only for the benefit of object polymorphism - method does nothing.
1035    * 
1036    * @return error message
1037    */
1038   public String print()
1039   {
1040     return "USE printGFFFormat() or printJalviewFormat()";
1041   }
1042
1043 }