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