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