source formatting
[jalview.git] / src / jalview / io / FeaturesFile.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8)
3  * Copyright (C) 2012 J Procter, AM Waterhouse, LM Lui, J Engelhardt, G Barton, M Clamp, S Searle
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 of the License, or (at your option) any later version.
10  *  
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.io;
19
20 import java.io.*;
21 import java.util.*;
22
23 import javax.xml.parsers.ParserConfigurationException;
24
25 import org.xml.sax.SAXException;
26
27 import fr.orsay.lri.varna.exceptions.ExceptionFileFormatOrSyntax;
28 import fr.orsay.lri.varna.exceptions.ExceptionLoadingFailed;
29 import fr.orsay.lri.varna.exceptions.ExceptionPermissionDenied;
30 import fr.orsay.lri.varna.exceptions.ExceptionUnmatchedClosingParentheses;
31
32 import jalview.analysis.SequenceIdMatcher;
33 import jalview.datamodel.*;
34 import jalview.schemes.*;
35 import jalview.util.Format;
36
37 /**
38  * Parse and create Jalview Features files Detects GFF format features files and
39  * parses. Does not implement standard print() - call specific printFeatures or
40  * printGFF. Uses AlignmentI.findSequence(String id) to find the sequence object
41  * for the features annotation - this normally works on an exact match.
42  * 
43  * @author AMW
44  * @version $Revision$
45  */
46 public class FeaturesFile extends AlignFile
47 {
48   /**
49    * work around for GFF interpretation bug where source string becomes
50    * description rather than a group
51    */
52   private boolean doGffSource = true;
53
54   /**
55    * Creates a new FeaturesFile object.
56    */
57   public FeaturesFile()
58   {
59   }
60
61   /**
62    * Creates a new FeaturesFile object.
63    * 
64    * @param inFile
65    *          DOCUMENT ME!
66    * @param type
67    *          DOCUMENT ME!
68    * 
69    * @throws IOException
70    *           DOCUMENT ME!
71    * @throws SAXException
72    * @throws ParserConfigurationException
73    * @throws ExceptionFileFormatOrSyntax
74    * @throws ExceptionLoadingFailed
75    * @throws ExceptionPermissionDenied
76    * @throws InterruptedException
77    * @throws ExceptionUnmatchedClosingParentheses
78    */
79   public FeaturesFile(String inFile, String type) throws IOException,
80           ExceptionFileFormatOrSyntax, ParserConfigurationException,
81           SAXException, ExceptionPermissionDenied, ExceptionLoadingFailed,
82           InterruptedException, ExceptionUnmatchedClosingParentheses
83   {
84     super(inFile, type);
85   }
86
87   public FeaturesFile(FileParse source) throws IOException,
88           ExceptionFileFormatOrSyntax, ParserConfigurationException,
89           SAXException, ExceptionPermissionDenied, ExceptionLoadingFailed,
90           InterruptedException, ExceptionUnmatchedClosingParentheses
91   {
92     super(source);
93   }
94
95   /**
96    * Parse GFF or sequence features file using case-independent matching,
97    * discarding URLs
98    * 
99    * @param align
100    *          - alignment/dataset containing sequences that are to be annotated
101    * @param colours
102    *          - hashtable to store feature colour definitions
103    * @param removeHTML
104    *          - process html strings into plain text
105    * @return true if features were added
106    */
107   public boolean parse(AlignmentI align, Hashtable colours,
108           boolean removeHTML)
109   {
110     return parse(align, colours, null, removeHTML, false);
111   }
112
113   /**
114    * Parse GFF or sequence features file optionally using case-independent
115    * matching, discarding URLs
116    * 
117    * @param align
118    *          - alignment/dataset containing sequences that are to be annotated
119    * @param colours
120    *          - hashtable to store feature colour definitions
121    * @param removeHTML
122    *          - process html strings into plain text
123    * @param relaxedIdmatching
124    *          - when true, ID matches to compound sequence IDs are allowed
125    * @return true if features were added
126    */
127   public boolean parse(AlignmentI align, Map colours, boolean removeHTML,
128           boolean relaxedIdMatching)
129   {
130     return parse(align, colours, null, removeHTML, relaxedIdMatching);
131   }
132
133   /**
134    * Parse GFF or sequence features file optionally using case-independent
135    * matching
136    * 
137    * @param align
138    *          - alignment/dataset containing sequences that are to be annotated
139    * @param colours
140    *          - hashtable to store feature colour definitions
141    * @param featureLink
142    *          - hashtable to store associated URLs
143    * @param removeHTML
144    *          - process html strings into plain text
145    * @return true if features were added
146    */
147   public boolean parse(AlignmentI align, Map colours, Map featureLink,
148           boolean removeHTML)
149   {
150     return parse(align, colours, featureLink, removeHTML, false);
151   }
152
153   /**
154    * /** Parse GFF or sequence features file
155    * 
156    * @param align
157    *          - alignment/dataset containing sequences that are to be annotated
158    * @param colours
159    *          - hashtable to store feature colour definitions
160    * @param featureLink
161    *          - hashtable to store associated URLs
162    * @param removeHTML
163    *          - process html strings into plain text
164    * @param relaxedIdmatching
165    *          - when true, ID matches to compound sequence IDs are allowed
166    * @return true if features were added
167    */
168   public boolean parse(AlignmentI align, Map colours, Map featureLink,
169           boolean removeHTML, boolean relaxedIdmatching)
170   {
171
172     String line = null;
173     try
174     {
175       SequenceI seq = null;
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);
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);
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(line);
632       System.out.println("Error parsing feature file: " + ex + "\n" + line);
633       ex.printStackTrace(System.err);
634       resetMatcher();
635       return false;
636     }
637
638     return true;
639   }
640
641   private AlignmentI lastmatchedAl = null;
642
643   private SequenceIdMatcher matcher = null;
644
645   /**
646    * clear any temporary handles used to speed up ID matching
647    */
648   private void resetMatcher()
649   {
650     lastmatchedAl = null;
651     matcher = null;
652   }
653
654   private SequenceI findName(AlignmentI align, String seqId,
655           boolean relaxedIdMatching)
656   {
657     SequenceI match = null;
658     if (relaxedIdMatching)
659     {
660       if (lastmatchedAl != align)
661       {
662         matcher = new SequenceIdMatcher(
663                 (lastmatchedAl = align).getSequencesArray());
664       }
665       match = matcher.findIdMatch(seqId);
666     }
667     else
668     {
669       match = align.findName(seqId, true);
670     }
671     return match;
672   }
673
674   public void parseDescriptionHTML(SequenceFeature sf, boolean removeHTML)
675   {
676     if (sf.getDescription() == null)
677     {
678       return;
679     }
680     jalview.util.ParseHtmlBodyAndLinks parsed = new jalview.util.ParseHtmlBodyAndLinks(
681             sf.getDescription(), removeHTML, newline);
682
683     sf.description = (removeHTML) ? parsed.getNonHtmlContent()
684             : sf.description;
685     for (String link : parsed.getLinks())
686     {
687       sf.addLink(link);
688     }
689
690   }
691
692   /**
693    * generate a features file for seqs includes non-pos features by default.
694    * 
695    * @param seqs
696    *          source of sequence features
697    * @param visible
698    *          hash of feature types and colours
699    * @return features file contents
700    */
701   public String printJalviewFormat(SequenceI[] seqs, Hashtable visible)
702   {
703     return printJalviewFormat(seqs, visible, true, true);
704   }
705
706   /**
707    * generate a features file for seqs with colours from visible (if any)
708    * 
709    * @param seqs
710    *          source of features
711    * @param visible
712    *          hash of Colours for each feature type
713    * @param visOnly
714    *          when true only feature types in 'visible' will be output
715    * @param nonpos
716    *          indicates if non-positional features should be output (regardless
717    *          of group or type)
718    * @return features file contents
719    */
720   public String printJalviewFormat(SequenceI[] seqs, Hashtable visible,
721           boolean visOnly, boolean nonpos)
722   {
723     StringBuffer out = new StringBuffer();
724     SequenceFeature[] next;
725     boolean featuresGen = false;
726     if (visOnly && !nonpos && (visible == null || visible.size() < 1))
727     {
728       // no point continuing.
729       return "No Features Visible";
730     }
731
732     if (visible != null && visOnly)
733     {
734       // write feature colours only if we're given them and we are generating
735       // viewed features
736       // TODO: decide if feature links should also be written here ?
737       Enumeration en = visible.keys();
738       String type, color;
739       while (en.hasMoreElements())
740       {
741         type = en.nextElement().toString();
742
743         if (visible.get(type) instanceof GraduatedColor)
744         {
745           GraduatedColor gc = (GraduatedColor) visible.get(type);
746           color = (gc.isColourByLabel() ? "label|" : "")
747                   + Format.getHexString(gc.getMinColor()) + "|"
748                   + Format.getHexString(gc.getMaxColor())
749                   + (gc.isAutoScale() ? "|" : "|abso|") + gc.getMin() + "|"
750                   + gc.getMax() + "|";
751           if (gc.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
752           {
753             if (gc.getThreshType() == AnnotationColourGradient.BELOW_THRESHOLD)
754             {
755               color += "below";
756             }
757             else
758             {
759               if (gc.getThreshType() != AnnotationColourGradient.ABOVE_THRESHOLD)
760               {
761                 System.err.println("WARNING: Unsupported threshold type ("
762                         + gc.getThreshType() + ") : Assuming 'above'");
763               }
764               color += "above";
765             }
766             // add the value
767             color += "|" + gc.getThresh();
768           }
769           else
770           {
771             color += "none";
772           }
773         }
774         else if (visible.get(type) instanceof java.awt.Color)
775         {
776           color = Format.getHexString((java.awt.Color) visible.get(type));
777         }
778         else
779         {
780           // legacy support for integer objects containing colour triplet values
781           color = Format.getHexString(new java.awt.Color(Integer
782                   .parseInt(visible.get(type).toString())));
783         }
784         out.append(type);
785         out.append("\t");
786         out.append(color);
787         out.append(newline);
788       }
789     }
790     // Work out which groups are both present and visible
791     Vector groups = new Vector();
792     int groupIndex = 0;
793     boolean isnonpos = false;
794
795     for (int i = 0; i < seqs.length; i++)
796     {
797       next = seqs[i].getSequenceFeatures();
798       if (next != null)
799       {
800         for (int j = 0; j < next.length; j++)
801         {
802           isnonpos = next[j].begin == 0 && next[j].end == 0;
803           if ((!nonpos && isnonpos)
804                   || (!isnonpos && visOnly && !visible
805                           .containsKey(next[j].type)))
806           {
807             continue;
808           }
809
810           if (next[j].featureGroup != null
811                   && !groups.contains(next[j].featureGroup))
812           {
813             groups.addElement(next[j].featureGroup);
814           }
815         }
816       }
817     }
818
819     String group = null;
820     do
821     {
822
823       if (groups.size() > 0 && groupIndex < groups.size())
824       {
825         group = groups.elementAt(groupIndex).toString();
826         out.append(newline);
827         out.append("STARTGROUP\t");
828         out.append(group);
829         out.append(newline);
830       }
831       else
832       {
833         group = null;
834       }
835
836       for (int i = 0; i < seqs.length; i++)
837       {
838         next = seqs[i].getSequenceFeatures();
839         if (next != null)
840         {
841           for (int j = 0; j < next.length; j++)
842           {
843             isnonpos = next[j].begin == 0 && next[j].end == 0;
844             if ((!nonpos && isnonpos)
845                     || (!isnonpos && visOnly && !visible
846                             .containsKey(next[j].type)))
847             {
848               // skip if feature is nonpos and we ignore them or if we only
849               // output visible and it isn't non-pos and it's not visible
850               continue;
851             }
852
853             if (group != null
854                     && (next[j].featureGroup == null || !next[j].featureGroup
855                             .equals(group)))
856             {
857               continue;
858             }
859
860             if (group == null && next[j].featureGroup != null)
861             {
862               continue;
863             }
864             // we have features to output
865             featuresGen = true;
866             if (next[j].description == null
867                     || next[j].description.equals(""))
868             {
869               out.append(next[j].type + "\t");
870             }
871             else
872             {
873               if (next[j].links != null
874                       && next[j].getDescription().indexOf("<html>") == -1)
875               {
876                 out.append("<html>");
877               }
878
879               out.append(next[j].description + " ");
880               if (next[j].links != null)
881               {
882                 for (int l = 0; l < next[j].links.size(); l++)
883                 {
884                   String label = next[j].links.elementAt(l).toString();
885                   String href = label.substring(label.indexOf("|") + 1);
886                   label = label.substring(0, label.indexOf("|"));
887
888                   if (next[j].description.indexOf(href) == -1)
889                   {
890                     out.append("<a href=\"" + href + "\">" + label + "</a>");
891                   }
892                 }
893
894                 if (next[j].getDescription().indexOf("</html>") == -1)
895                 {
896                   out.append("</html>");
897                 }
898               }
899
900               out.append("\t");
901             }
902             out.append(seqs[i].getName());
903             out.append("\t-1\t");
904             out.append(next[j].begin);
905             out.append("\t");
906             out.append(next[j].end);
907             out.append("\t");
908             out.append(next[j].type);
909             if (next[j].score != Float.NaN)
910             {
911               out.append("\t");
912               out.append(next[j].score);
913             }
914             out.append(newline);
915           }
916         }
917       }
918
919       if (group != null)
920       {
921         out.append("ENDGROUP\t");
922         out.append(group);
923         out.append(newline);
924         groupIndex++;
925       }
926       else
927       {
928         break;
929       }
930
931     } while (groupIndex < groups.size() + 1);
932
933     if (!featuresGen)
934     {
935       return "No Features Visible";
936     }
937
938     return out.toString();
939   }
940
941   /**
942    * generate a gff file for sequence features includes non-pos features by
943    * default.
944    * 
945    * @param seqs
946    * @param visible
947    * @return
948    */
949   public String printGFFFormat(SequenceI[] seqs, Hashtable visible)
950   {
951     return printGFFFormat(seqs, visible, true, true);
952   }
953
954   public String printGFFFormat(SequenceI[] seqs, Hashtable visible,
955           boolean visOnly, boolean nonpos)
956   {
957     StringBuffer out = new StringBuffer();
958     SequenceFeature[] next;
959     String source;
960     boolean isnonpos;
961     for (int i = 0; i < seqs.length; i++)
962     {
963       if (seqs[i].getSequenceFeatures() != null)
964       {
965         next = seqs[i].getSequenceFeatures();
966         for (int j = 0; j < next.length; j++)
967         {
968           isnonpos = next[j].begin == 0 && next[j].end == 0;
969           if ((!nonpos && isnonpos)
970                   || (!isnonpos && visOnly && !visible
971                           .containsKey(next[j].type)))
972           {
973             continue;
974           }
975
976           source = next[j].featureGroup;
977           if (source == null)
978           {
979             source = next[j].getDescription();
980           }
981
982           out.append(seqs[i].getName());
983           out.append("\t");
984           out.append(source);
985           out.append("\t");
986           out.append(next[j].type);
987           out.append("\t");
988           out.append(next[j].begin);
989           out.append("\t");
990           out.append(next[j].end);
991           out.append("\t");
992           out.append(next[j].score);
993           out.append("\t");
994
995           if (next[j].getValue("STRAND") != null)
996           {
997             out.append(next[j].getValue("STRAND"));
998             out.append("\t");
999           }
1000           else
1001           {
1002             out.append(".\t");
1003           }
1004
1005           if (next[j].getValue("FRAME") != null)
1006           {
1007             out.append(next[j].getValue("FRAME"));
1008           }
1009           else
1010           {
1011             out.append(".");
1012           }
1013           // TODO: verify/check GFF - should there be a /t here before attribute
1014           // output ?
1015
1016           if (next[j].getValue("ATTRIBUTES") != null)
1017           {
1018             out.append(next[j].getValue("ATTRIBUTES"));
1019           }
1020
1021           out.append(newline);
1022
1023         }
1024       }
1025     }
1026
1027     return out.toString();
1028   }
1029
1030   /**
1031    * this is only for the benefit of object polymorphism - method does nothing.
1032    */
1033   public void parse()
1034   {
1035     // IGNORED
1036   }
1037
1038   /**
1039    * this is only for the benefit of object polymorphism - method does nothing.
1040    * 
1041    * @return error message
1042    */
1043   public String print()
1044   {
1045     return "USE printGFFFormat() or printJalviewFormat()";
1046   }
1047
1048 }