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