AnnotationStack.java
001 /*
002  *  Copyright (c) 1998-2009, The University of Sheffield and Ontotext.
003  *
004  *  This file is part of GATE (see http://gate.ac.uk/), and is free
005  *  software, licenced under the GNU Library General Public License,
006  *  Version 2, June 1991 (in the distribution as file licence.html,
007  *  and also available at http://gate.ac.uk/gate/licence.html).
008  *
009  *  Thomas Heitz - 7 July 2009
010  *
011  *  $Id$
012  */
013 
014 package gate.gui.docview;
015 
016 import gate.FeatureMap;
017 import gate.Node;
018 import gate.annotation.NodeImpl;
019 import gate.util.Strings;
020 
021 import java.awt.Color;
022 import java.awt.GridBagConstraints;
023 import java.awt.GridBagLayout;
024 import java.awt.Insets;
025 import java.util.ArrayList;
026 import java.util.Collections;
027 import java.util.Comparator;
028 import java.util.HashMap;
029 import java.util.HashSet;
030 import java.util.List;
031 import java.util.Set;
032 import java.util.TreeSet;
033 
034 import javax.swing.BorderFactory;
035 import javax.swing.JButton;
036 import javax.swing.JLabel;
037 import javax.swing.JPanel;
038 import javax.swing.UIManager;
039 import javax.swing.border.CompoundBorder;
040 import javax.swing.border.EmptyBorder;
041 import javax.swing.border.EtchedBorder;
042 import javax.swing.event.MouseInputAdapter;
043 
044 import org.apache.commons.lang.StringEscapeUtils;
045 
046 /**
047  * Stack of annotations in a JPanel.
048  <br><br>
049  * To use, respect this order:<br><code>
050  * AnnotationStack stackPanel = new AnnotationStack(...);<br>
051  * stackPanel.set...(...);<br>
052  * stackPanel.clearAllRows();<br>
053  * stackPanel.addRow(...);<br>
054  * stackPanel.addAnnotation(...);<br>
055  * stackPanel.drawStack();</code>
056  */
057 @SuppressWarnings("serial")
058 public class AnnotationStack extends JPanel {
059 
060   public AnnotationStack() {
061     super();
062     init();
063   }
064 
065   /**
066    @param maxTextLength maximum number of characters for the text,
067    * if too long an ellipsis is added in the middle
068    @param maxFeatureValueLength maximum number of characters
069    *  for a feature value
070    */
071   public AnnotationStack(int maxTextLength, int maxFeatureValueLength) {
072     super();
073     this.maxTextLength = maxTextLength;
074     this.maxFeatureValueLength = maxFeatureValueLength;
075     init();
076   }
077 
078   void init() {
079     setLayout(new GridBagLayout());
080     setOpaque(true);
081     setBackground(Color.WHITE);
082     stackRows = new ArrayList<StackRow>();
083     textMouseListener = new StackMouseListener();
084     headerMouseListener = new StackMouseListener();
085     annotationMouseListener = new StackMouseListener();
086   }
087 
088   /**
089    * Add a row to the annotation stack.
090    *
091    @param set set name for the annotation, may be null
092    @param type annotation type
093    @param feature feature name, may be null
094    @param lastColumnButton button at the end of the column, may be null
095    @param shortcut replace the header of the row, may be null
096    @param crop how to crop the text for the annotation if too long, one of
097    *   {@link #CROP_START}{@link #CROP_MIDDLE} or {@link #CROP_END}
098    */
099   public void addRow(String set, String type, String feature,
100                      JButton lastColumnButton, String shortcut, int crop) {
101     stackRows.add(
102       new StackRow(set, type, feature, lastColumnButton, shortcut, crop));
103   }
104 
105   /**
106    * Add an annotation to the current stack row.
107    *
108    @param startOffset document offset where starts the annotation
109    @param endOffset document offset where ends the annotation
110    @param type annotation type
111    @param features annotation features map
112    */
113   public void addAnnotation(int startOffset, int endOffset,
114                             String type, FeatureMap features) {
115     stackRows.get(stackRows.size()-1).addAnnotation(
116       StackAnnotation.createAnnotation(startOffset, endOffset, type, features));
117   }
118 
119   /**
120    * Add an annotation to the current stack row.
121    *
122    @param annotation annotation to add to the current stack row
123    */
124   public void addAnnotation(gate.Annotation annotation) {
125     stackRows.get(stackRows.size()-1).addAnnotation(
126       StackAnnotation.createAnnotation(annotation));
127   }
128 
129   /**
130    * Clear all rows in the stack. To be called before adding the first row.
131    */
132   public void clearAllRows() {
133     stackRows.clear();
134   }
135 
136   /**
137    * Draw the annotation stack in a JPanel with a GridBagLayout.
138    */
139   public void drawStack() {
140 
141     // clear the panel
142     removeAll();
143 
144     boolean textTooLong = text.length() > maxTextLength;
145     int upperBound = text.length() (maxTextLength/2);
146 
147     GridBagConstraints gbc = new GridBagConstraints();
148     gbc.gridx = 0;
149     gbc.gridy = 0;
150     gbc.fill = GridBagConstraints.BOTH;
151 
152     /**********************
153      * First row of text *
154      *********************/
155 
156     gbc.gridwidth = 1;
157     gbc.insets = new java.awt.Insets(10101010);
158     JLabel labelTitle = new JLabel("Context");
159     labelTitle.setOpaque(true);
160     labelTitle.setBackground(Color.WHITE);
161     labelTitle.setBorder(new CompoundBorder(
162       new EtchedBorder(EtchedBorder.LOWERED,
163         new Color(250250250)new Color(250250250).darker()),
164       new EmptyBorder(new Insets(0202))));
165     labelTitle.setToolTipText("Expression and its context.");
166     add(labelTitle, gbc);
167     gbc.insets = new java.awt.Insets(100100);
168 
169     int expressionStart = contextBeforeSize;
170     int expressionEnd = text.length() - contextAfterSize;
171 
172     // for each character
173     for (int charNum = 0; charNum < text.length(); charNum++) {
174 
175       gbc.gridx = charNum + 1;
176       if (textTooLong) {
177         if (charNum == maxTextLength/2) {
178           // add ellipsis dots in case of a too long text displayed
179           add(new JLabel("..."), gbc);
180           // skip the middle part of the text if too long
181           charNum = upperBound + 1;
182           continue;
183         else if (charNum > upperBound) {
184           gbc.gridx -= upperBound - (maxTextLength/21;
185         }
186       }
187 
188       // set the text and color of the feature value
189       JLabel label = new JLabel(text.substring(charNum, charNum+1));
190       if (charNum >= expressionStart && charNum < expressionEnd) {
191         // this part is matched by the pattern, color it
192         label.setBackground(new Color(240201184));
193       else {
194         // this part is the context, no color
195         label.setBackground(Color.WHITE);
196       }
197       label.setOpaque(true);
198 
199       // get the word from which belongs the current character charNum
200       int start = text.lastIndexOf(" ", charNum);
201       int end = text.indexOf(" ", charNum);
202       String word = text.substring(
203         (start == -1: start,
204         (end == -1? text.length() : end);
205       // add a mouse listener that modify the query
206       label.addMouseListener(textMouseListener.createListener(word));
207       add(label, gbc);
208     }
209 
210       /************************************
211        * Subsequent rows with annotations *
212        ************************************/
213 
214     // for each row to display
215     for (StackRow stackRow : stackRows) {
216       String type = stackRow.getType();
217       String feature = stackRow.getFeature();
218       if (feature == null) { feature = ""}
219       String shortcut = stackRow.getShortcut();
220       if (shortcut == null) { shortcut = ""}
221 
222       gbc.gridy++;
223       gbc.gridx = 0;
224       gbc.gridwidth = 1;
225       gbc.insets = new Insets(0030);
226 
227       // add the header of the row
228       JLabel annotationTypeAndFeature = new JLabel();
229       String typeAndFeature = type + (feature.equals("""" "."+ feature;
230       annotationTypeAndFeature.setText(!shortcut.equals(""?
231         shortcut : stackRow.getSet() != null ?
232           stackRow.getSet() "#" + typeAndFeature : typeAndFeature);
233       annotationTypeAndFeature.setOpaque(true);
234       annotationTypeAndFeature.setBackground(Color.WHITE);
235       annotationTypeAndFeature.setBorder(new CompoundBorder(
236         new EtchedBorder(EtchedBorder.LOWERED,
237           new Color(250250250)new Color(250250250).darker()),
238         new EmptyBorder(new Insets(0202))));
239       if (feature.equals("")) {
240         annotationTypeAndFeature.addMouseListener(
241           headerMouseListener.createListener(type));
242       else {
243         annotationTypeAndFeature.addMouseListener(
244           headerMouseListener.createListener(type, feature));
245       }
246       gbc.insets = new java.awt.Insets(010310);
247       add(annotationTypeAndFeature, gbc);
248       gbc.insets = new java.awt.Insets(0030);
249 
250       // add all annotations for this row
251       HashMap<Integer,TreeSet<Integer>> gridSet =
252         new HashMap<Integer,TreeSet<Integer>>();
253       int gridyMax = gbc.gridy;
254       for(StackAnnotation ann : stackRow.getAnnotations()) {
255         gbc.gridx = ann.getStartNode().getOffset().intValue()
256                   - expressionStartOffset + contextBeforeSize + 1;
257         gbc.gridwidth = ann.getEndNode().getOffset().intValue()
258                       - ann.getStartNode().getOffset().intValue();
259         if (gbc.gridx == 0) {
260           // column 0 is already the row header
261           gbc.gridwidth -= 1;
262           gbc.gridx = 1;
263         else if (gbc.gridx < 0) {
264           // annotation starts before displayed text
265           gbc.gridwidth += gbc.gridx - 1;
266           gbc.gridx = 1;
267         }
268         if (gbc.gridx + gbc.gridwidth > text.length()) {
269           // annotation ends after displayed text
270           gbc.gridwidth = text.length() - gbc.gridx + 1;
271         }
272         if(textTooLong) {
273           if(gbc.gridx > (upperBound + 1)) {
274             // x starts after the hidden middle part
275             gbc.gridx -= upperBound - (maxTextLength / 21;
276           }
277           else if(gbc.gridx > (maxTextLength / 2)) {
278             // x starts in the hidden middle part
279             if(gbc.gridx + gbc.gridwidth <= (upperBound + 3)) {
280               // x ends in the hidden middle part
281               continue// skip the middle part of the text
282             }
283             else {
284               // x ends after the hidden middle part
285               gbc.gridwidth -= upperBound - gbc.gridx + 2;
286               gbc.gridx = (maxTextLength / 22;
287             }
288           }
289           else {
290             // x starts before the hidden middle part
291             if(gbc.gridx + gbc.gridwidth < (maxTextLength / 2)) {
292               // x ends before the hidden middle part
293               // do nothing
294             }
295             else if(gbc.gridx + gbc.gridwidth < upperBound) {
296               // x ends in the hidden middle part
297               gbc.gridwidth = (maxTextLength / 2- gbc.gridx + 1;
298             }
299             else {
300               // x ends after the hidden middle part
301               gbc.gridwidth -= upperBound - (maxTextLength / 21;
302             }
303           }
304         }
305         if(gbc.gridwidth == 0) {
306           gbc.gridwidth = 1;
307         }
308 
309         JLabel label = new JLabel();
310         Object object = ann.getFeatures().get(feature);
311         String value = (object == null" " : Strings.toString(object);
312         if(value.length() > maxFeatureValueLength) {
313           // show the full text in the tooltip
314           label.setToolTipText((value.length() 500?
315             "<html><textarea rows=\"30\" cols=\"40\" readonly=\"readonly\">"
316             + value.replaceAll("(.{50,60})\\b""$1\n""</textarea></html>" :
317             ((value.length() 100?
318               "<html><table width=\"500\" border=\"0\" cellspacing=\"0\">"
319                 "<tr><td>" + value.replaceAll("\n""<br>")
320                 "</td></tr></table></html>" :
321               value));
322           if(stackRow.getCrop() == CROP_START) {
323             value = "..." + value.substring(
324               value.length() - maxFeatureValueLength - 1);
325           }
326           else if(stackRow.getCrop() == CROP_END) {
327             value = value.substring(0, maxFeatureValueLength - 2"...";
328           }
329           else {// cut in the middle
330             value = value.substring(0, maxFeatureValueLength / 2"..."
331               + value.substring(value.length() (maxFeatureValueLength / 2));
332           }
333         }
334         label.setText(value);
335         label.setBackground(AnnotationSetsView.getColor(stackRow.getSet(),ann.getType()));
336         label.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
337         label.setOpaque(true);
338 
339         label.addMouseListener(annotationMouseListener.createListener(
340           stackRow.getSet(), type, String.valueOf(ann.getId())));
341 
342         // show the feature values in the tooltip
343         if (!ann.getFeatures().isEmpty()) {
344           String width = (Strings.toString(ann.getFeatures()).length() 100?
345             "500" "100%";
346           String toolTip = "<html><table width=\"" + width
347             "\" border=\"0\" cellspacing=\"0\" cellpadding=\"4\">";
348           Color color = (ColorUIManager.get("ToolTip.background");
349           float[] hsb = Color.RGBtoHSB(
350             color.getRed(), color.getGreen(), color.getBlue()null);
351           color = Color.getHSBColor(hsb[0], hsb[1],
352             Math.max(0f, hsb[2- hsb[2]*0.075f))// darken the color
353           String hexColor = Integer.toHexString(color.getRed()) +
354             Integer.toHexString(color.getGreen()) +
355             Integer.toHexString(color.getBlue());
356           boolean odd = false// alternate background color every other row
357           
358           List<Object> features = new ArrayList<Object>(ann.getFeatures().keySet());
359           //sort the features into alphabetical order
360           Collections.sort(features, new Comparator<Object>() {
361             @Override
362             public int compare(Object o1, Object o2) {
363               return o1.toString().compareToIgnoreCase(o2.toString());
364             }
365           });
366           
367           for (Object key : features) {
368             String fv = Strings.toString(ann.getFeatures().get(key));
369             toolTip +="<tr align=\"left\""
370               (odd?" bgcolor=\"#"+hexColor+"\"":"")+"><td><strong>"
371               + key + "</strong></td><td>"
372               ((fv.length() 500?
373               "<textarea rows=\"20\" cols=\"40\" cellspacing=\"0\">"
374                 + StringEscapeUtils.escapeHtml(fv.replaceAll("(.{50,60})\\b""$1\n"))
375                 "</textarea>" :
376               StringEscapeUtils.escapeHtml(fv).replaceAll("\n""<br>"))
377               "</td></tr>";
378             odd = !odd;
379           }
380           label.setToolTipText(toolTip + "</table></html>");
381         else {
382           label.setToolTipText("No features.");
383         }
384 
385         if(!feature.equals("")) {
386           label.addMouseListener(annotationMouseListener.createListener(
387             stackRow.getSet(), type, feature, Strings.toString(
388               ann.getFeatures().get(feature)), String.valueOf(ann.getId())));
389         }
390         // find the first empty row span for this annotation
391         int oldGridy = gbc.gridy;
392         for(int y = oldGridy; y <= (gridyMax + 1); y++) {
393           // for each cell of this row where spans the annotation
394           boolean xSpanIsEmpty = true;
395           for(int x = gbc.gridx;
396               (x < (gbc.gridx + gbc.gridwidth)) && xSpanIsEmpty; x++) {
397             xSpanIsEmpty = !(gridSet.containsKey(x)
398               && gridSet.get(x).contains(y));
399           }
400           if(xSpanIsEmpty) {
401             gbc.gridy = y;
402             break;
403           }
404         }
405         // save the column x and row y of the current value
406         TreeSet<Integer> ts;
407         for(int x = gbc.gridx; x < (gbc.gridx + gbc.gridwidth); x++) {
408           ts = gridSet.get(x);
409           if(ts == null) {
410             ts = new TreeSet<Integer>();
411           }
412           ts.add(gbc.gridy);
413           gridSet.put(x, ts);
414         }
415         add(label, gbc);
416         gridyMax = Math.max(gridyMax, gbc.gridy);
417         gbc.gridy = oldGridy;
418       }
419 
420       // add a button at the end of the row
421       gbc.gridwidth = 1;
422       if (stackRow.getLastColumnButton() != null) {
423         // last cell of the row
424         gbc.gridx = Math.min(text.length(), maxTextLength1;
425         gbc.insets = new Insets(01030);
426         gbc.fill = GridBagConstraints.NONE;
427         gbc.anchor = GridBagConstraints.WEST;
428         add(stackRow.getLastColumnButton(), gbc);
429         gbc.insets = new Insets(0030);
430         gbc.fill = GridBagConstraints.BOTH;
431         gbc.anchor = GridBagConstraints.CENTER;
432       }
433 
434       // set the new gridy to the maximum row we put a value
435       gbc.gridy = gridyMax;
436     }
437 
438     if (lastRowButton != null) {
439       // add a configuration button on the last row
440       gbc.insets = new java.awt.Insets(010010);
441       gbc.gridx = 0;
442       gbc.gridy++;
443       add(lastRowButton, gbc);
444     }
445 
446     // add an empty cell that takes all remaining space to
447     // align the visible cells at the top-left corner
448     gbc.gridy++;
449     gbc.gridx = Math.min(text.length(), maxTextLength1;
450     gbc.gridwidth = GridBagConstraints.REMAINDER;
451     gbc.gridheight = GridBagConstraints.REMAINDER;
452     gbc.weightx = 1;
453     gbc.weighty = 1;
454     add(new JLabel(""), gbc);
455 
456     validate();
457     updateUI();
458   }
459 
460   /**
461    * Extension of a MouseInputAdapter that adds a method
462    * to create new Listeners from it.<br>
463    * You must overriden the createListener method.
464    */
465   public static class StackMouseListener extends MouseInputAdapter {
466     /**
467      * There is 3 cases for the parameters of createListener:
468      <ol>
469      <li>first line of text -> createListener(word)
470      <li>first column, header -> createListener(type),
471      *   createListener(type, feature)
472      <li>annotation -> createListener(set, type, annotationId),
473      *   createListener(set, type, feature, value, annotationId)
474      </ol>
475      @param parameters see above
476      @return a MouseInputAdapter for the text, header or annotation
477      */
478     public MouseInputAdapter createListener(String... parameters) {
479       return null;
480     }
481   }
482 
483   /**
484    * Annotation that doesn't belong to an annotation set
485    * and with id always equal to -1.<br>
486    * Allows to create an annotation without document, nodes, annotation set,
487    * and keep compatibility with gate.Annotation.
488    <br>
489    * This class is only for AnnotationStack internal use
490    * as it won't work with most of the methods that use gate.Annotation.
491    */
492   private static class StackAnnotation extends gate.annotation.AnnotationImpl {
493     StackAnnotation(Integer id, Node start, Node end, String type,
494                          FeatureMap features) {
495       super(id, start, end, type, features);
496     }
497     static StackAnnotation createAnnotation(int startOffset,
498                   int endOffset, String type, FeatureMap features) {
499       Node startNode = new NodeImpl(-1(longstartOffset);
500       Node endNode = new NodeImpl(-1(longendOffset);
501       return new StackAnnotation(-1, startNode, endNode, type, features);
502     }
503     static StackAnnotation createAnnotation(gate.Annotation annotation) {
504       return new StackAnnotation(annotation.getId(), annotation.getStartNode(),
505         annotation.getEndNode(), annotation.getType(), annotation.getFeatures());
506     }
507   }
508 
509   /**
510    * A row of annotations in the stack.
511    */
512   class StackRow {
513     StackRow(String set, String type, String feature,
514              JButton lastColumnButton, String shortcut, int crop) {
515       this.set = set;
516       this.type = type;
517       this.feature = feature;
518       this.annotations = new HashSet<StackAnnotation>();
519       this.lastColumnButton = lastColumnButton;
520       this.shortcut = shortcut;
521       this.crop = crop;
522     }
523 
524     public String getSet() {
525       return set;
526     }
527     public String getType() {
528       return type;
529     }
530     public String getFeature() {
531       return feature;
532     }
533     public Set<StackAnnotation> getAnnotations() {
534       return annotations;
535     }
536     public JButton getLastColumnButton() {
537       return lastColumnButton;
538     }
539     public String getShortcut() {
540       return shortcut;
541     }
542     public int getCrop() {
543       return crop;
544     }
545     public void addAnnotation(StackAnnotation annotation) {
546       annotations.add(annotation);
547     }
548 
549     String set;
550     String type;
551     String feature;
552     Set<StackAnnotation> annotations;
553     JButton lastColumnButton;
554     String shortcut;
555     int crop;
556   }
557 
558   public void setLastRowButton(JButton lastRowButton) {
559     this.lastRowButton = lastRowButton;
560   }
561 
562   /** @param text first line of text that contains the expression
563    *  and its context */
564   public void setText(String text) {
565     this.text = text;
566   }
567 
568   /** @param expressionStartOffset document offset where starts the expression */
569   public void setExpressionStartOffset(int expressionStartOffset) {
570     this.expressionStartOffset = expressionStartOffset;
571   }
572 
573   /** @param expressionEndOffset document offset where ends the expression */
574   public void setExpressionEndOffset(int expressionEndOffset) {
575     this.expressionEndOffset = expressionEndOffset;
576   }
577 
578   /** @param contextBeforeSize number of characters before the expression */
579   public void setContextBeforeSize(int contextBeforeSize) {
580     this.contextBeforeSize = contextBeforeSize;
581   }
582 
583   /** @param contextAfterSize number of characters after the expression */
584   public void setContextAfterSize(int contextAfterSize) {
585     this.contextAfterSize = contextAfterSize;
586   }
587 
588   /** @param expressionTooltip optional tooltip for the expression */
589   public void setExpressionTooltip(String expressionTooltip) {
590     this.expressionTooltip = expressionTooltip;
591   }
592 
593   /** @param textMouseListener optional listener for the first line of text */
594   public void setTextMouseListener(StackMouseListener textMouseListener) {
595     this.textMouseListener = textMouseListener;
596   }
597 
598   /** @param headerMouseListener optional listener for the first column */
599   public void setHeaderMouseListener(StackMouseListener headerMouseListener) {
600     this.headerMouseListener = headerMouseListener;
601   }
602 
603   /** @param annotationMouseListener optional listener for the annotations */
604   public void setAnnotationMouseListener(StackMouseListener annotationMouseListener) {
605     this.annotationMouseListener = annotationMouseListener;
606   }
607 
608   /** rows of annotations that are displayed in the stack*/
609   ArrayList<StackRow> stackRows;
610   /** maximum number of characters for the text,
611    * if too long an ellipsis is added in the middle */
612   int maxTextLength = 150;
613   /** maximum number of characters for a feature value */
614   int maxFeatureValueLength = 30;
615   JButton lastRowButton;
616   String text = "";
617   int expressionStartOffset = 0;
618   int expressionEndOffset = 0;
619   /** number of characters before the expression */
620   int contextBeforeSize = 10;
621   /** number of characters after the expression */
622   int contextAfterSize = 10;
623   String expressionTooltip = "";
624   StackMouseListener textMouseListener;
625   StackMouseListener headerMouseListener;
626   StackMouseListener annotationMouseListener;
627   public final static int CROP_START = 0;
628   public final static int CROP_MIDDLE = 1;
629   public final static int CROP_END = 2;
630 }