JAL-653 allow dummy sequences to be created if a sequence ID is not found in the...
[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                 while (st.hasMoreTokens())
519                 {
520                   attributes.append("\t" + st.nextElement());
521                 }
522                 // TODO validate and split GFF2 attributes field ? parse out
523                 // ([A-Za-z][A-Za-z0-9_]*) <value> ; and add as
524                 // sf.setValue(attrib, val);
525                 sf.setValue("ATTRIBUTES", attributes.toString());
526               }
527
528               seq.addSequenceFeature(sf);
529               while ((seq = align.findName(seq, seqId, true)) != null)
530               {
531                 seq.addSequenceFeature(new SequenceFeature(sf));
532               }
533               break;
534             }
535           }
536
537           if (GFFFile && seq == null)
538           {
539             desc = token;
540           }
541           else
542           {
543             desc = st.nextToken();
544           }
545           if (!st.hasMoreTokens())
546           {
547             System.err
548                     .println("DEBUG: Run out of tokens when trying to identify the destination for the feature.. giving up.");
549             // in all probability, this isn't a file we understand, so bail
550             // quietly.
551             return false;
552           }
553
554           token = st.nextToken();
555
556           if (!token.equals("ID_NOT_SPECIFIED"))
557           {
558             seq = findName(align, seqId = token, relaxedIdmatching, null);
559             st.nextToken();
560           }
561           else
562           {
563             seqId = null;
564             try
565             {
566               index = Integer.parseInt(st.nextToken());
567               seq = align.getSequenceAt(index);
568             } catch (NumberFormatException ex)
569             {
570               seq = null;
571             }
572           }
573
574           if (seq == null)
575           {
576             System.out.println("Sequence not found: " + line);
577             break;
578           }
579
580           start = Integer.parseInt(st.nextToken());
581           end = Integer.parseInt(st.nextToken());
582
583           type = st.nextToken();
584
585           if (!colours.containsKey(type))
586           {
587             // Probably the old style groups file
588             UserColourScheme ucs = new UserColourScheme(type);
589             colours.put(type, ucs.findColour('A'));
590           }
591           sf = new SequenceFeature(type, desc, "", start, end, featureGroup);
592           if (st.hasMoreTokens())
593           {
594             try
595             {
596               score = new Float(st.nextToken()).floatValue();
597               // update colourgradient bounds if allowed to
598             } catch (NumberFormatException ex)
599             {
600               score = 0;
601             }
602             sf.setScore(score);
603           }
604           if (groupLink != null && removeHTML)
605           {
606             sf.addLink(groupLink);
607             sf.description += "%LINK%";
608           }
609           if (typeLink.containsKey(type) && removeHTML)
610           {
611             sf.addLink(typeLink.get(type).toString());
612             sf.description += "%LINK%";
613           }
614
615           parseDescriptionHTML(sf, removeHTML);
616
617           seq.addSequenceFeature(sf);
618
619           while (seqId != null
620                   && (seq = align.findName(seq, seqId, false)) != null)
621           {
622             seq.addSequenceFeature(new SequenceFeature(sf));
623           }
624           // If we got here, its not a GFFFile
625           GFFFile = false;
626         }
627       }
628       resetMatcher();
629     } catch (Exception ex)
630     {
631       System.out.println("Error parsing feature file: " + ex + "\n" + line);
632       ex.printStackTrace(System.err);
633       resetMatcher();
634       return false;
635     }
636
637     return true;
638   }
639
640   private AlignmentI lastmatchedAl = null;
641
642   private SequenceIdMatcher matcher = null;
643
644   /**
645    * clear any temporary handles used to speed up ID matching
646    */
647   private void resetMatcher()
648   {
649     lastmatchedAl = null;
650     matcher = null;
651   }
652
653   private SequenceI findName(AlignmentI align, String seqId,
654           boolean relaxedIdMatching, List<SequenceI> newseqs)
655   {
656     SequenceI match = null;
657     if (relaxedIdMatching)
658     {
659       if (lastmatchedAl != align)
660       {
661         matcher = new SequenceIdMatcher(
662                 (lastmatchedAl = align).getSequencesArray());
663         if (newseqs != null)
664         {
665           matcher.addAll(newseqs);
666         }
667       }
668       match = matcher.findIdMatch(seqId);
669     }
670     else
671     {
672       match = align.findName(seqId, true);
673       
674     }
675     if (match==null && newseqs!=null)
676     {
677       match = new SequenceDummy(seqId);
678       if (relaxedIdMatching)
679       {
680         matcher.addAll(Arrays.asList(new SequenceI[]
681         { match }));
682       }
683     }
684     return match;
685   }
686   public void parseDescriptionHTML(SequenceFeature sf, boolean removeHTML)
687   {
688     if (sf.getDescription() == null)
689     {
690       return;
691     }
692     jalview.util.ParseHtmlBodyAndLinks parsed = new jalview.util.ParseHtmlBodyAndLinks(
693             sf.getDescription(), removeHTML, newline);
694
695     sf.description = (removeHTML) ? parsed.getNonHtmlContent()
696             : sf.description;
697     for (String link : parsed.getLinks())
698     {
699       sf.addLink(link);
700     }
701
702   }
703
704   /**
705    * generate a features file for seqs includes non-pos features by default.
706    * 
707    * @param seqs
708    *          source of sequence features
709    * @param visible
710    *          hash of feature types and colours
711    * @return features file contents
712    */
713   public String printJalviewFormat(SequenceI[] seqs, Map<String,Object> visible)
714   {
715     return printJalviewFormat(seqs, visible, true, true);
716   }
717
718   /**
719    * generate a features file for seqs with colours from visible (if any)
720    * 
721    * @param seqs
722    *          source of features
723    * @param visible
724    *          hash of Colours for each feature type
725    * @param visOnly
726    *          when true only feature types in 'visible' will be output
727    * @param nonpos
728    *          indicates if non-positional features should be output (regardless
729    *          of group or type)
730    * @return features file contents
731    */
732   public String printJalviewFormat(SequenceI[] seqs, Map visible,
733           boolean visOnly, boolean nonpos)
734   {
735     StringBuffer out = new StringBuffer();
736     SequenceFeature[] next;
737     boolean featuresGen = false;
738     if (visOnly && !nonpos && (visible == null || visible.size() < 1))
739     {
740       // no point continuing.
741       return "No Features Visible";
742     }
743
744     if (visible != null && visOnly)
745     {
746       // write feature colours only if we're given them and we are generating
747       // viewed features
748       // TODO: decide if feature links should also be written here ?
749       Iterator en = visible.keySet().iterator();
750       String type, color;
751       while (en.hasNext())
752       {
753         type = en.next().toString();
754
755         if (visible.get(type) instanceof GraduatedColor)
756         {
757           GraduatedColor gc = (GraduatedColor) visible.get(type);
758           color = (gc.isColourByLabel() ? "label|" : "")
759                   + Format.getHexString(gc.getMinColor()) + "|"
760                   + Format.getHexString(gc.getMaxColor())
761                   + (gc.isAutoScale() ? "|" : "|abso|") + gc.getMin() + "|"
762                   + gc.getMax() + "|";
763           if (gc.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
764           {
765             if (gc.getThreshType() == AnnotationColourGradient.BELOW_THRESHOLD)
766             {
767               color += "below";
768             }
769             else
770             {
771               if (gc.getThreshType() != AnnotationColourGradient.ABOVE_THRESHOLD)
772               {
773                 System.err.println("WARNING: Unsupported threshold type ("
774                         + gc.getThreshType() + ") : Assuming 'above'");
775               }
776               color += "above";
777             }
778             // add the value
779             color += "|" + gc.getThresh();
780           }
781           else
782           {
783             color += "none";
784           }
785         }
786         else if (visible.get(type) instanceof java.awt.Color)
787         {
788           color = Format.getHexString((java.awt.Color) visible.get(type));
789         }
790         else
791         {
792           // legacy support for integer objects containing colour triplet values
793           color = Format.getHexString(new java.awt.Color(Integer
794                   .parseInt(visible.get(type).toString())));
795         }
796         out.append(type);
797         out.append("\t");
798         out.append(color);
799         out.append(newline);
800       }
801     }
802     // Work out which groups are both present and visible
803     Vector groups = new Vector();
804     int groupIndex = 0;
805     boolean isnonpos = false;
806
807     for (int i = 0; i < seqs.length; i++)
808     {
809       next = seqs[i].getSequenceFeatures();
810       if (next != null)
811       {
812         for (int j = 0; j < next.length; j++)
813         {
814           isnonpos = next[j].begin == 0 && next[j].end == 0;
815           if ((!nonpos && isnonpos)
816                   || (!isnonpos && visOnly && !visible
817                           .containsKey(next[j].type)))
818           {
819             continue;
820           }
821
822           if (next[j].featureGroup != null
823                   && !groups.contains(next[j].featureGroup))
824           {
825             groups.addElement(next[j].featureGroup);
826           }
827         }
828       }
829     }
830
831     String group = null;
832     do
833     {
834
835       if (groups.size() > 0 && groupIndex < groups.size())
836       {
837         group = groups.elementAt(groupIndex).toString();
838         out.append(newline);
839         out.append("STARTGROUP\t");
840         out.append(group);
841         out.append(newline);
842       }
843       else
844       {
845         group = null;
846       }
847
848       for (int i = 0; i < seqs.length; i++)
849       {
850         next = seqs[i].getSequenceFeatures();
851         if (next != null)
852         {
853           for (int j = 0; j < next.length; j++)
854           {
855             isnonpos = next[j].begin == 0 && next[j].end == 0;
856             if ((!nonpos && isnonpos)
857                     || (!isnonpos && visOnly && !visible
858                             .containsKey(next[j].type)))
859             {
860               // skip if feature is nonpos and we ignore them or if we only
861               // output visible and it isn't non-pos and it's not visible
862               continue;
863             }
864
865             if (group != null
866                     && (next[j].featureGroup == null || !next[j].featureGroup
867                             .equals(group)))
868             {
869               continue;
870             }
871
872             if (group == null && next[j].featureGroup != null)
873             {
874               continue;
875             }
876             // we have features to output
877             featuresGen = true;
878             if (next[j].description == null
879                     || next[j].description.equals(""))
880             {
881               out.append(next[j].type + "\t");
882             }
883             else
884             {
885               if (next[j].links != null
886                       && next[j].getDescription().indexOf("<html>") == -1)
887               {
888                 out.append("<html>");
889               }
890
891               out.append(next[j].description + " ");
892               if (next[j].links != null)
893               {
894                 for (int l = 0; l < next[j].links.size(); l++)
895                 {
896                   String label = next[j].links.elementAt(l).toString();
897                   String href = label.substring(label.indexOf("|") + 1);
898                   label = label.substring(0, label.indexOf("|"));
899
900                   if (next[j].description.indexOf(href) == -1)
901                   {
902                     out.append("<a href=\"" + href + "\">" + label + "</a>");
903                   }
904                 }
905
906                 if (next[j].getDescription().indexOf("</html>") == -1)
907                 {
908                   out.append("</html>");
909                 }
910               }
911
912               out.append("\t");
913             }
914             out.append(seqs[i].getName());
915             out.append("\t-1\t");
916             out.append(next[j].begin);
917             out.append("\t");
918             out.append(next[j].end);
919             out.append("\t");
920             out.append(next[j].type);
921             if (next[j].score != Float.NaN)
922             {
923               out.append("\t");
924               out.append(next[j].score);
925             }
926             out.append(newline);
927           }
928         }
929       }
930
931       if (group != null)
932       {
933         out.append("ENDGROUP\t");
934         out.append(group);
935         out.append(newline);
936         groupIndex++;
937       }
938       else
939       {
940         break;
941       }
942
943     } while (groupIndex < groups.size() + 1);
944
945     if (!featuresGen)
946     {
947       return "No Features Visible";
948     }
949
950     return out.toString();
951   }
952
953   /**
954    * generate a gff file for sequence features includes non-pos features by
955    * default.
956    * 
957    * @param seqs
958    * @param visible
959    * @return
960    */
961   public String printGFFFormat(SequenceI[] seqs, Map<String,Object> visible)
962   {
963     return printGFFFormat(seqs, visible, true, true);
964   }
965
966   public String printGFFFormat(SequenceI[] seqs, Map<String,Object> visible,
967           boolean visOnly, boolean nonpos)
968   {
969     StringBuffer out = new StringBuffer();
970     SequenceFeature[] next;
971     String source;
972     boolean isnonpos;
973     for (int i = 0; i < seqs.length; i++)
974     {
975       if (seqs[i].getSequenceFeatures() != null)
976       {
977         next = seqs[i].getSequenceFeatures();
978         for (int j = 0; j < next.length; j++)
979         {
980           isnonpos = next[j].begin == 0 && next[j].end == 0;
981           if ((!nonpos && isnonpos)
982                   || (!isnonpos && visOnly && !visible
983                           .containsKey(next[j].type)))
984           {
985             continue;
986           }
987
988           source = next[j].featureGroup;
989           if (source == null)
990           {
991             source = next[j].getDescription();
992           }
993
994           out.append(seqs[i].getName());
995           out.append("\t");
996           out.append(source);
997           out.append("\t");
998           out.append(next[j].type);
999           out.append("\t");
1000           out.append(next[j].begin);
1001           out.append("\t");
1002           out.append(next[j].end);
1003           out.append("\t");
1004           out.append(next[j].score);
1005           out.append("\t");
1006
1007           if (next[j].getValue("STRAND") != null)
1008           {
1009             out.append(next[j].getValue("STRAND"));
1010             out.append("\t");
1011           }
1012           else
1013           {
1014             out.append(".\t");
1015           }
1016
1017           if (next[j].getValue("FRAME") != null)
1018           {
1019             out.append(next[j].getValue("FRAME"));
1020           }
1021           else
1022           {
1023             out.append(".");
1024           }
1025           // TODO: verify/check GFF - should there be a /t here before attribute
1026           // output ?
1027
1028           if (next[j].getValue("ATTRIBUTES") != null)
1029           {
1030             out.append(next[j].getValue("ATTRIBUTES"));
1031           }
1032
1033           out.append(newline);
1034
1035         }
1036       }
1037     }
1038
1039     return out.toString();
1040   }
1041
1042   /**
1043    * this is only for the benefit of object polymorphism - method does nothing.
1044    */
1045   public void parse()
1046   {
1047     // IGNORED
1048   }
1049
1050   /**
1051    * this is only for the benefit of object polymorphism - method does nothing.
1052    * 
1053    * @return error message
1054    */
1055   public String print()
1056   {
1057     return "USE printGFFFormat() or printJalviewFormat()";
1058   }
1059
1060 }