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