JAL-3210 Merge branch 'develop' into trialMerge
[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.api.FeatureColourI;
24 import jalview.datamodel.DBRefEntry;
25 import jalview.datamodel.DBRefSource;
26 import jalview.datamodel.GeneLociI;
27 import jalview.datamodel.SequenceFeature;
28 import jalview.datamodel.SequenceI;
29 import jalview.util.MessageManager;
30 import jalview.util.StringUtils;
31 import jalview.util.UrlLink;
32 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
33
34 import java.util.Arrays;
35 import java.util.Collection;
36 import java.util.Comparator;
37 import java.util.LinkedHashMap;
38 import java.util.List;
39 import java.util.Map;
40
41 /**
42  * generate HTML reports for a sequence
43  * 
44  * @author jimp
45  */
46 public class SequenceAnnotationReport
47 {
48   private static final String COMMA = ",";
49
50   private static final String ELLIPSIS = "...";
51
52   private static final int MAX_REFS_PER_SOURCE = 4;
53
54   private static final int MAX_SOURCES = 40;
55
56  // public static final String[][] PRIMARY_SOURCES  moved to DBRefSource.java
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 instanceof GeneLociI)
71       {
72         return -1;
73       }
74       if (ref2 instanceof GeneLociI)
75       {
76         return 1;
77       }
78       String s1 = ref1.getSource();
79       String s2 = ref2.getSource();
80       boolean s1Primary = DBRefSource.isPrimarySource(s1);
81       boolean s2Primary = DBRefSource.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 : (s2 == null ? 1 : s1
91               .compareToIgnoreCase(s2));
92       if (comp == 0)
93       {
94         String a1 = ref1.getAccessionId();
95         String a2 = ref2.getAccessionId();
96         comp = a1 == null ? -1 : (a2 == null ? 1 : a1
97                 .compareToIgnoreCase(a2));
98       }
99       return comp;
100     }
101
102 //    private boolean isPrimarySource(String source)
103 //    {
104 //      for (String[] primary : DBRefSource.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 linkURL)
119   {
120     this.linkImageURL = linkURL;
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, FeatureRendererModel fr)
133   {
134     if (features != null)
135     {
136       for (SequenceFeature feature : features)
137       {
138         appendFeature(sb, rpos, fr, 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           FeatureRendererModel fr, 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       return;
166     }
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
194       if (showScore(feature, fr))
195       {
196         sb.append(" Score=").append(String.valueOf(feature.getScore()));
197       }
198       String status = (String) feature.getValue("status");
199       if (status != null && status.length() > 0)
200       {
201         sb.append("; (").append(status).append(")");
202       }
203
204       /*
205        * add attribute value if coloured by attribute
206        */
207       if (fr != null)
208       {
209         FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
210         if (fc != null && fc.isColourByAttribute())
211         {
212           String[] attName = fc.getAttributeName();
213           String attVal = feature.getValueAsString(attName);
214           if (attVal != null)
215           {
216             sb.append("; ").append(String.join(":", attName)).append("=")
217                     .append(attVal);
218           }
219         }
220       }
221     }
222   }
223
224   /**
225    * Answers true if score should be shown, else false. Score is shown if it is
226    * not NaN, and the feature type has a non-trivial min-max score range
227    */
228   boolean showScore(SequenceFeature feature, FeatureRendererModel fr)
229   {
230     if (Float.isNaN(feature.getScore()))
231     {
232       return false;
233     }
234     if (fr == null)
235     {
236       return true;
237     }
238     float[][] minMax = fr.getMinMax().get(feature.getType());
239
240     /*
241      * minMax[0] is the [min, max] score range for positional features
242      */
243     if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1])
244     {
245       return false;
246     }
247     return true;
248   }
249
250   /**
251    * Format and appends any hyperlinks for the sequence feature to the string
252    * buffer
253    * 
254    * @param sb
255    * @param feature
256    */
257   void appendLinks(final StringBuffer sb, SequenceFeature feature)
258   {
259     if (feature.links != null)
260     {
261       if (linkImageURL != null)
262       {
263         sb.append(" <img src=\"" + linkImageURL + "\">");
264       }
265       else
266       {
267         for (String urlstring : feature.links)
268         {
269           try
270           {
271             for (List<String> urllink : createLinksFrom(null, urlstring))
272             {
273               sb.append("<br/> <a href=\""
274                       + urllink.get(3)
275                       + "\" target=\""
276                       + urllink.get(0)
277                       + "\">"
278                       + (urllink.get(0).toLowerCase()
279                               .equals(urllink.get(1).toLowerCase()) ? urllink
280                               .get(0) : (urllink.get(0) + ":" + urllink
281                               .get(1))) + "</a></br>");
282             }
283           } catch (Exception x)
284           {
285             System.err.println("problem when creating links from "
286                     + urlstring);
287             x.printStackTrace();
288           }
289         }
290       }
291
292     }
293   }
294
295   /**
296    * 
297    * @param seq
298    * @param link
299    * @return Collection< List<String> > { List<String> { link target, link
300    *         label, dynamic component inserted (if any), url }}
301    */
302   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
303   {
304     Map<String, List<String>> urlSets = new LinkedHashMap<>();
305     UrlLink urlLink = new UrlLink(link);
306     if (!urlLink.isValid())
307     {
308       System.err.println(urlLink.getInvalidMessage());
309       return null;
310     }
311
312     urlLink.createLinksFromSeq(seq, urlSets);
313
314     return urlSets.values();
315   }
316
317   public void createSequenceAnnotationReport(final StringBuilder tip,
318           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
319           FeatureRendererModel fr)
320   {
321     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
322             fr, false);
323   }
324
325   /**
326    * Builds an html formatted report of sequence details and appends it to the
327    * provided buffer.
328    * 
329    * @param sb
330    *          buffer to append report to
331    * @param sequence
332    *          the sequence the report is for
333    * @param showDbRefs
334    *          whether to include database references for the sequence
335    * @param showNpFeats
336    *          whether to include non-positional sequence features
337    * @param fr
338    * @param summary
339    * @return
340    */
341   int createSequenceAnnotationReport(final StringBuilder sb,
342           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
343           FeatureRendererModel fr, boolean summary)
344   {
345     String tmp;
346     sb.append("<i>");
347
348     int maxWidth = 0;
349     if (sequence.getDescription() != null)
350     {
351       tmp = sequence.getDescription();
352       sb.append("<br>").append(tmp);
353       maxWidth = Math.max(maxWidth, tmp.length());
354     }
355     SequenceI ds = sequence;
356     while (ds.getDatasetSequence() != null)
357     {
358       ds = ds.getDatasetSequence();
359     }
360
361     if (showDbRefs)
362     {
363       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
364     }
365
366     /*
367      * add non-positional features if wanted
368      */
369     if (showNpFeats)
370     {
371       for (SequenceFeature sf : sequence.getFeatures()
372               .getNonPositionalFeatures())
373       {
374         int sz = -sb.length();
375         appendFeature(sb, 0, fr, sf);
376         sz += sb.length();
377         maxWidth = Math.max(maxWidth, sz);
378       }
379     }
380     sb.append("</i>");
381     return maxWidth;
382   }
383
384   /**
385    * A helper method that appends any DBRefs, returning the maximum line length
386    * added
387    * 
388    * @param sb
389    * @param ds
390    * @param summary
391    * @return
392    */
393   protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
394           boolean summary)
395   {
396     List<DBRefEntry> dbrefs = ds.getDBRefs();
397     if (dbrefs == null)
398     {
399       return 0;
400     }
401
402     // note this sorts the refs held on the sequence!
403     dbrefs.sort(comparator);
404     boolean ellipsis = false;
405     String source = null;
406     String lastSource = null;
407     int countForSource = 0;
408     int sourceCount = 0;
409     boolean moreSources = false;
410     int maxLineLength = 0;
411     int lineLength = 0;
412
413     for (DBRefEntry ref : dbrefs)
414     {
415       source = ref.getSource();
416       if (source == null)
417       {
418         // shouldn't happen
419         continue;
420       }
421       boolean sourceChanged = !source.equals(lastSource);
422       if (sourceChanged)
423       {
424         lineLength = 0;
425         countForSource = 0;
426         sourceCount++;
427       }
428       if (sourceCount > MAX_SOURCES && summary)
429       {
430         ellipsis = true;
431         moreSources = true;
432         break;
433       }
434       lastSource = source;
435       countForSource++;
436       if (countForSource == 1 || !summary)
437       {
438         sb.append("<br>");
439       }
440       if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
441       {
442         String accessionId = ref.getAccessionId();
443         lineLength += accessionId.length() + 1;
444         if (countForSource > 1 && summary)
445         {
446           sb.append(", ").append(accessionId);
447           lineLength++;
448         }
449         else
450         {
451           sb.append(source).append(" ").append(accessionId);
452           lineLength += source.length();
453         }
454         maxLineLength = Math.max(maxLineLength, lineLength);
455       }
456       if (countForSource == MAX_REFS_PER_SOURCE && summary)
457       {
458         sb.append(COMMA).append(ELLIPSIS);
459         ellipsis = true;
460       }
461     }
462     if (moreSources)
463     {
464       sb.append("<br>").append(source).append(COMMA).append(ELLIPSIS);
465     }
466     if (ellipsis)
467     {
468       sb.append("<br>(");
469       sb.append(MessageManager.getString("label.output_seq_details"));
470       sb.append(")");
471     }
472
473     return maxLineLength;
474   }
475
476   public void createTooltipAnnotationReport(final StringBuilder tip,
477           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
478           FeatureRendererModel fr)
479   {
480     int maxWidth = createSequenceAnnotationReport(tip, sequence,
481             showDbRefs, showNpFeats, fr, true);
482
483     if (maxWidth > 60)
484     {
485       // ? not sure this serves any useful purpose
486       // tip.insert(0, "<table width=350 border=0><tr><td>");
487       // tip.append("</td></tr></table>");
488     }
489   }
490 }