JAL-2792 html table for Feature Details report
[jalview.git] / src / jalview / io / SequenceAnnotationReport.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.datamodel.DBRefEntry;
24 import jalview.datamodel.DBRefSource;
25 import jalview.datamodel.SequenceFeature;
26 import jalview.datamodel.SequenceI;
27 import jalview.io.gff.GffConstants;
28 import jalview.util.MessageManager;
29 import jalview.util.StringUtils;
30 import jalview.util.UrlLink;
31
32 import java.util.Arrays;
33 import java.util.Collection;
34 import java.util.Comparator;
35 import java.util.LinkedHashMap;
36 import java.util.List;
37 import java.util.Map;
38
39 /**
40  * generate HTML reports for a sequence
41  * 
42  * @author jimp
43  */
44 public class SequenceAnnotationReport
45 {
46   private static final String COMMA = ",";
47
48   private static final String ELLIPSIS = "...";
49
50   private static final int MAX_REFS_PER_SOURCE = 4;
51
52   private static final int MAX_SOURCES = 40;
53
54   private static final String[][] PRIMARY_SOURCES = new String[][] {
55       DBRefSource.CODINGDBS, DBRefSource.DNACODINGDBS,
56       DBRefSource.PROTEINDBS };
57
58   final String linkImageURL;
59
60   /*
61    * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
62    * with 'Primary' sources placed before others, and 'chromosome' first of all
63    */
64   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
65   {
66
67     @Override
68     public int compare(DBRefEntry ref1, DBRefEntry ref2)
69     {
70       if (ref1.isChromosome())
71       {
72         return -1;
73       }
74       if (ref2.isChromosome())
75       {
76         return 1;
77       }
78       String s1 = ref1.getSource();
79       String s2 = ref2.getSource();
80       boolean s1Primary = isPrimarySource(s1);
81       boolean s2Primary = isPrimarySource(s2);
82       if (s1Primary && !s2Primary)
83       {
84         return -1;
85       }
86       if (!s1Primary && s2Primary)
87       {
88         return 1;
89       }
90       int comp = s1 == null ? -1
91               : (s2 == null ? 1 : s1.compareToIgnoreCase(s2));
92       if (comp == 0)
93       {
94         String a1 = ref1.getAccessionId();
95         String a2 = ref2.getAccessionId();
96         comp = a1 == null ? -1
97                 : (a2 == null ? 1 : a1.compareToIgnoreCase(a2));
98       }
99       return comp;
100     }
101
102     private boolean isPrimarySource(String source)
103     {
104       for (String[] primary : PRIMARY_SOURCES)
105       {
106         for (String s : primary)
107         {
108           if (source.equals(s))
109           {
110             return true;
111           }
112         }
113       }
114       return false;
115     }
116   };
117
118   public SequenceAnnotationReport(String linkImageURL)
119   {
120     this.linkImageURL = linkImageURL;
121   }
122
123   /**
124    * Append text for the list of features to the tooltip
125    * 
126    * @param sb
127    * @param rpos
128    * @param features
129    * @param minmax
130    */
131   public void appendFeatures(final StringBuilder sb, int rpos,
132           List<SequenceFeature> features, Map<String, float[][]> minmax)
133   {
134     if (features != null)
135     {
136       for (SequenceFeature feature : features)
137       {
138         appendFeature(sb, rpos, minmax, feature);
139       }
140     }
141   }
142
143   /**
144    * Appends the feature at rpos to the given buffer
145    * 
146    * @param sb
147    * @param rpos
148    * @param minmax
149    * @param feature
150    */
151   void appendFeature(final StringBuilder sb, int rpos,
152           Map<String, float[][]> minmax, SequenceFeature feature)
153   {
154     if (feature.isContactFeature())
155     {
156       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
157       {
158         if (sb.length() > 6)
159         {
160           sb.append("<br>");
161         }
162         sb.append(feature.getType()).append(" ").append(feature.getBegin())
163                 .append(":").append(feature.getEnd());
164       }
165     }
166     else
167     {
168       if (sb.length() > 6)
169       {
170         sb.append("<br>");
171       }
172       // TODO: remove this hack to display link only features
173       boolean linkOnly = feature.getValue("linkonly") != null;
174       if (!linkOnly)
175       {
176         sb.append(feature.getType()).append(" ");
177         if (rpos != 0)
178         {
179           // we are marking a positional feature
180           sb.append(feature.begin);
181         }
182         if (feature.begin != feature.end)
183         {
184           sb.append(" ").append(feature.end);
185         }
186
187         String description = feature.getDescription();
188         if (description != null && !description.equals(feature.getType()))
189         {
190           description = StringUtils.stripHtmlTags(description);
191           sb.append("; ").append(description);
192         }
193         // check score should be shown
194         if (!Float.isNaN(feature.getScore()))
195         {
196           float[][] rng = (minmax == null) ? null
197                   : minmax.get(feature.getType());
198           if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
199           {
200             sb.append(" Score=").append(String.valueOf(feature.getScore()));
201           }
202         }
203         String status = (String) feature.getValue("status");
204         if (status != null && status.length() > 0)
205         {
206           sb.append("; (").append(status).append(")");
207         }
208         String clinSig = (String) feature
209                 .getValue(GffConstants.CLINICAL_SIGNIFICANCE);
210         if (clinSig != null)
211         {
212           sb.append("; ").append(clinSig);
213         }
214       }
215     }
216   }
217
218   /**
219    * Format and appends any hyperlinks for the sequence feature to the string
220    * buffer
221    * 
222    * @param sb
223    * @param feature
224    */
225   void appendLinks(final StringBuffer sb, SequenceFeature feature)
226   {
227     if (feature.links != null)
228     {
229       if (linkImageURL != null)
230       {
231         sb.append(" <img src=\"" + linkImageURL + "\">");
232       }
233       else
234       {
235         for (String urlstring : feature.links)
236         {
237           try
238           {
239             for (List<String> urllink : createLinksFrom(null, urlstring))
240             {
241               sb.append("<br/> <a href=\"" + urllink.get(3) + "\" target=\""
242                       + urllink.get(0) + "\">"
243                       + (urllink.get(0).toLowerCase()
244                               .equals(urllink.get(1).toLowerCase())
245                                       ? urllink.get(0)
246                                       : (urllink.get(0) + ":"
247                                               + urllink.get(1)))
248                       + "</a></br>");
249             }
250           } catch (Exception x)
251           {
252             System.err.println(
253                     "problem when creating links from " + urlstring);
254             x.printStackTrace();
255           }
256         }
257       }
258
259     }
260   }
261
262   /**
263    * 
264    * @param seq
265    * @param link
266    * @return Collection< List<String> > { List<String> { link target, link
267    *         label, dynamic component inserted (if any), url }}
268    */
269   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
270   {
271     Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
272     UrlLink urlLink = new UrlLink(link);
273     if (!urlLink.isValid())
274     {
275       System.err.println(urlLink.getInvalidMessage());
276       return null;
277     }
278
279     urlLink.createLinksFromSeq(seq, urlSets);
280
281     return urlSets.values();
282   }
283
284   public void createSequenceAnnotationReport(final StringBuilder tip,
285           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
286           Map<String, float[][]> minmax)
287   {
288     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
289             minmax, false);
290   }
291
292   /**
293    * Builds an html formatted report of sequence details and appends it to the
294    * provided buffer.
295    * 
296    * @param sb
297    *          buffer to append report to
298    * @param sequence
299    *          the sequence the report is for
300    * @param showDbRefs
301    *          whether to include database references for the sequence
302    * @param showNpFeats
303    *          whether to include non-positional sequence features
304    * @param minmax
305    * @param summary
306    * @return
307    */
308   int createSequenceAnnotationReport(final StringBuilder sb,
309           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
310           Map<String, float[][]> minmax, boolean summary)
311   {
312     String tmp;
313     sb.append("<i>");
314
315     int maxWidth = 0;
316     if (sequence.getDescription() != null)
317     {
318       tmp = sequence.getDescription();
319       sb.append("<br>").append(tmp);
320       maxWidth = Math.max(maxWidth, tmp.length());
321     }
322     SequenceI ds = sequence;
323     while (ds.getDatasetSequence() != null)
324     {
325       ds = ds.getDatasetSequence();
326     }
327     
328     if (showDbRefs)
329     {
330       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
331     }
332
333     /*
334      * add non-positional features if wanted
335      */
336     if (showNpFeats)
337     {
338       for (SequenceFeature sf : sequence.getFeatures()
339               .getNonPositionalFeatures())
340       {
341         int sz = -sb.length();
342         appendFeature(sb, 0, minmax, sf);
343         sz += sb.length();
344         maxWidth = Math.max(maxWidth, sz);
345       }
346     }
347     sb.append("</i>");
348     return maxWidth;
349   }
350
351   /**
352    * A helper method that appends any DBRefs, returning the maximum line length
353    * added
354    * 
355    * @param sb
356    * @param ds
357    * @param summary
358    * @return
359    */
360   protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
361           boolean summary)
362   {
363     DBRefEntry[] dbrefs = ds.getDBRefs();
364     if (dbrefs == null)
365     {
366       return 0;
367     }
368
369     // note this sorts the refs held on the sequence!
370     Arrays.sort(dbrefs, comparator);
371     boolean ellipsis = false;
372     String source = null;
373     String lastSource = null;
374     int countForSource = 0;
375     int sourceCount = 0;
376     boolean moreSources = false;
377     int maxLineLength = 0;
378     int lineLength = 0;
379
380     for (DBRefEntry ref : dbrefs)
381     {
382       source = ref.getSource();
383       if (source == null)
384       {
385         // shouldn't happen
386         continue;
387       }
388       boolean sourceChanged = !source.equals(lastSource);
389       if (sourceChanged)
390       {
391         lineLength = 0;
392         countForSource = 0;
393         sourceCount++;
394       }
395       if (sourceCount > MAX_SOURCES && summary)
396       {
397         ellipsis = true;
398         moreSources = true;
399         break;
400       }
401       lastSource = source;
402       countForSource++;
403       if (countForSource == 1 || !summary)
404       {
405         sb.append("<br>");
406       }
407       if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
408       {
409         String accessionId = ref.getAccessionId();
410         lineLength += accessionId.length() + 1;
411         if (countForSource > 1 && summary)
412         {
413           sb.append(", ").append(accessionId);
414           lineLength++;
415         }
416         else
417         {
418           sb.append(source).append(" ").append(accessionId);
419           lineLength += source.length();
420         }
421         maxLineLength = Math.max(maxLineLength, lineLength);
422       }
423       if (countForSource == MAX_REFS_PER_SOURCE && summary)
424       {
425         sb.append(COMMA).append(ELLIPSIS);
426         ellipsis = true;
427       }
428     }
429     if (moreSources)
430     {
431       sb.append("<br>").append(source)
432               .append(COMMA).append(ELLIPSIS);
433     }
434     if (ellipsis)
435     {
436       sb.append("<br>(");
437       sb.append(MessageManager.getString("label.output_seq_details"));
438       sb.append(")");
439     }
440
441     return maxLineLength;
442   }
443
444   public void createTooltipAnnotationReport(final StringBuilder tip,
445           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
446           Map<String, float[][]> minmax)
447   {
448     int maxWidth = createSequenceAnnotationReport(tip, sequence, showDbRefs,
449             showNpFeats, minmax, true);
450
451     if (maxWidth > 60)
452     {
453       // ? not sure this serves any useful purpose
454       // tip.insert(0, "<table width=350 border=0><tr><td>");
455       // tip.append("</td></tr></table>");
456     }
457   }
458 }