AnnotationStackView.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.event.AnnotationListener;
017 import gate.event.AnnotationEvent;
018 import gate.gui.annedit.AnnotationData;
019 import gate.gui.annedit.AnnotationDataImpl;
020 import gate.*;
021 import gate.util.InvalidOffsetException;
022 import gate.util.OffsetComparator;
023 import gate.gui.docview.AnnotationSetsView.*;
024 import gate.gui.docview.AnnotationStack.*;
025 
026 import javax.swing.*;
027 import javax.swing.text.BadLocationException;
028 import javax.swing.event.*;
029 import java.awt.*;
030 import java.awt.event.*;
031 import java.util.*;
032 import java.util.Timer;
033 import java.util.List;
034 import java.text.Collator;
035 
036 /**
037  * Show a stack view of highlighted annotations in the document
038  * centred on the document caret.
039  *
040  * When double clicked, an annotation is copied to another set in order
041  * to create a gold standard set from several annoatator sets.
042  *
043  * You can choose to display features with annotations by clicking
044  * the first column rectangles.
045  */
046 public class AnnotationStackView  extends AbstractDocumentView
047   implements AnnotationListener {
048 
049   public AnnotationStackView() {
050     typesFeatures = new HashMap<String,String>();
051   }
052 
053   public void cleanup() {
054     super.cleanup();
055     textView = null;
056   }
057 
058   protected void initGUI() {
059 
060     //get a pointer to the text view used to display
061     //the selected annotations
062     Iterator centralViewsIter = owner.getCentralViews().iterator();
063     while(textView == null && centralViewsIter.hasNext()){
064       DocumentView aView = (DocumentViewcentralViewsIter.next();
065       if(aView instanceof TextualDocumentView)
066         textView = (TextualDocumentViewaView;
067     }
068     // find the annotation set view associated with the document
069     Iterator verticalViewsIter = owner.getVerticalViews().iterator();
070     while(annotationSetsView == null && verticalViewsIter.hasNext()){
071       DocumentView aView = (DocumentViewverticalViewsIter.next();
072       if(aView instanceof AnnotationSetsView)
073         annotationSetsView = (AnnotationSetsViewaView;
074     }
075     //get a pointer to the list view
076     Iterator horizontalViewsIter = owner.getHorizontalViews().iterator();
077     while(annotationListView == null && horizontalViewsIter.hasNext()){
078       DocumentView aView = (DocumentView)horizontalViewsIter.next();
079       if(aView instanceof AnnotationListView)
080         annotationListView = (AnnotationListView)aView;
081     }
082     annotationListView.setOwner(owner);
083     document = textView.getDocument();
084 
085     mainPanel = new JPanel();
086     mainPanel.setLayout(new BorderLayout());
087 
088     // toolbar with previous and next annotation buttons
089     JToolBar toolBar = new JToolBar();
090     toolBar.setFloatable(false);
091     toolBar.addSeparator();
092     toolBar.add(previousAnnotationAction = new PreviousAnnotationAction());
093     toolBar.add(nextAnnotationAction = new NextAnnotationAction());
094     toolBar.addSeparator();
095     toolBar.add(targetSetLabel = new JLabel());
096     targetSetLabel.addMouseListener(new MouseAdapter() {
097       public void mouseClicked(MouseEvent e) {
098         askTargetSet();
099       }
100     });
101     targetSetLabel.setToolTipText(
102       "<html>Target set to copy annotation when double clicked.<br>" +
103       "Click to change it.</html>");
104     mainPanel.add(toolBar, BorderLayout.NORTH);
105 
106     stackPanel = new AnnotationStack(10020);
107     scroller = new JScrollPane(stackPanel);
108     scroller.getViewport().setOpaque(true);
109     mainPanel.add(scroller, BorderLayout.CENTER);
110 
111     initListeners();
112   }
113 
114   public Component getGUI(){
115     return mainPanel;
116   }
117 
118   protected void initListeners(){
119 
120     stackPanel.addAncestorListener(new AncestorListener() {
121       public void ancestorAdded(AncestorEvent event) {
122         // when the view becomes visible
123           updateStackView();
124       }
125       public void ancestorMoved(AncestorEvent event) {
126       }
127       public void ancestorRemoved(AncestorEvent event) {
128       }
129     });
130 
131     textView.getTextView().addCaretListener(new CaretListener() {
132       public void caretUpdate(CaretEvent e) {
133         updateStackView();
134       }
135     });
136   }
137 
138   class PreviousAnnotationAction extends AbstractAction {
139     public PreviousAnnotationAction() {
140       super("Previous boundary");
141       putValue(SHORT_DESCRIPTION, "Centre the view on the closest previous " +
142         "annotation boundary among all displayed");
143       putValue(MNEMONIC_KEY, KeyEvent.VK_LEFT);
144     }
145     public void actionPerformed(ActionEvent e) {
146       nextAnnotationAction.setEnabled(true);
147       SortedSet<Annotation> set =
148         new TreeSet<Annotation>(new OffsetComparator());
149       for(SetHandler setHandler : annotationSetsView.setHandlers) {
150         for(TypeHandler typeHandler: setHandler.typeHandlers) {
151           if (typeHandler.isSelected()) {
152             set.addAll(setHandler.set.get(typeHandler.name).getContained(
153               0l(long)textView.getTextView().getCaretPosition()-1));
154           }
155         }
156       }
157       if (set.size() 0) {
158         textView.getTextView().setCaretPosition(
159           set.last().getEndNode().getOffset().intValue());
160         try {
161           textView.getTextView().scrollRectToVisible(
162           textView.getTextView().modelToView(
163             textView.getTextView().getCaretPosition()));
164         catch (BadLocationException exception) {
165           exception.printStackTrace();
166         }
167       }
168       setEnabled(set.size() 1);
169       textView.getTextView().requestFocusInWindow();
170     }
171   }
172 
173   class NextAnnotationAction extends AbstractAction {
174     public NextAnnotationAction() {
175       super("Next boundary");
176       putValue(SHORT_DESCRIPTION, "Centre the view on the closest next " +
177         "annotation boundary among all displayed");
178       putValue(MNEMONIC_KEY, KeyEvent.VK_RIGHT);
179     }
180     public void actionPerformed(ActionEvent e) {
181       previousAnnotationAction.setEnabled(true);
182       SortedSet<Annotation> set = new TreeSet<Annotation>(
183         Collections.reverseOrder(new OffsetComparator()));
184       for(SetHandler setHandler : annotationSetsView.setHandlers) {
185         for(TypeHandler typeHandler: setHandler.typeHandlers) {
186           if (typeHandler.isSelected()) {
187             set.addAll(setHandler.set.get(typeHandler.name).getContained(
188               (long)textView.getTextView().getCaretPosition(),
189               document.getContent().size()-1));
190           }
191         }
192       }
193       if (set.size() 0) {
194         textView.getTextView().setCaretPosition(
195           set.last().getEndNode().getOffset().intValue());
196         try {
197           textView.getTextView().scrollRectToVisible(
198           textView.getTextView().modelToView(
199             textView.getTextView().getCaretPosition()));
200         catch (BadLocationException exception) {
201           exception.printStackTrace();
202         }
203       }
204       setEnabled(set.size() 1);
205       textView.getTextView().requestFocusInWindow();
206     }
207   }
208 
209   protected void registerHooks() { /* do nothing */ }
210 
211   protected void unregisterHooks() { /* do nothing */ }
212 
213   public int getType() {
214     return HORIZONTAL;
215   }
216 
217   public void annotationUpdated(AnnotationEvent e) {
218     updateStackView();
219   }
220 
221   public void updateStackView() {
222     if (textView == null) { return}
223 
224     int caretPosition = textView.getTextView().getCaretPosition();
225 
226     // get the context around the annotation
227     int context = 40;
228     String text = "";
229     try {
230       text = document.getContent().getContent(
231         Math.max(0l, caretPosition - context),
232         Math.min(document.getContent().size(),
233                  caretPosition + + context)).toString();
234     catch (InvalidOffsetException e) {
235       e.printStackTrace();
236     }
237 
238     // initialise the annotation stack
239     stackPanel.setText(text);
240     stackPanel.setExpressionStartOffset(caretPosition);
241     stackPanel.setExpressionEndOffset(caretPosition + 1);
242     stackPanel.setContextBeforeSize(Math.min(caretPosition, context));
243     stackPanel.setContextAfterSize(Math.min(
244       document.getContent().size().intValue()-1-caretPosition, context));
245     stackPanel.setAnnotationMouseListener(new AnnotationMouseListener());
246     stackPanel.setHeaderMouseListener(new HeaderMouseListener());
247     stackPanel.clearAllRows();
248 
249     // add stack rows and annotations for each selected annotation set
250     // in the annotation sets view
251     for(SetHandler setHandler : annotationSetsView.setHandlers) {
252       for(TypeHandler typeHandler: setHandler.typeHandlers) {
253         if (typeHandler.isSelected()) {
254           stackPanel.addRow(setHandler.set.getName(), typeHandler.name,
255             typesFeatures.get(typeHandler.name),
256             null, null, AnnotationStack.CROP_MIDDLE);
257           Set<Annotation> annotations = setHandler.set.get(typeHandler.name)
258             .get(Math.max(0l, caretPosition - context), Math.min(
259               document.getContent().size(), caretPosition + + context));
260           for (Annotation annotation : annotations) {
261             stackPanel.addAnnotation(annotation);
262           }
263         }
264       }
265     }
266 
267     stackPanel.drawStack();
268   }
269 
270   /** @return true if the user input a valid annotation set */
271   boolean askTargetSet() {
272     Object[] messageObjects;
273     final JTextField setsTextField = new JTextField("consensus"15);
274     if (document.getAnnotationSetNames() != null
275      && !document.getAnnotationSetNames().isEmpty()) {
276       Collator collator = Collator.getInstance(Locale.ENGLISH);
277       collator.setStrength(Collator.TERTIARY);
278       SortedSet<String> setNames = new TreeSet<String>(collator);
279       setNames.addAll(document.getAnnotationSetNames());
280       JList list = new JList(setNames.toArray());
281       list.setVisibleRowCount(Math.min(10, setNames.size()));
282       list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
283       list.setSelectedValue(targetSetName, true);
284       JScrollPane scroll = new JScrollPane(list);
285       JPanel vspace = new JPanel();
286       vspace.setSize(05);
287       list.addListSelectionListener(new ListSelectionListener() {
288         public void valueChanged(ListSelectionEvent e) {
289           JList list = (JListe.getSource();
290           if (list.getSelectedValue() != null) {
291             setsTextField.setText((Stringlist.getSelectedValue());
292           }
293         }
294       });
295       messageObjects = new Object[] { "Existing annotation sets:",
296         scroll, vspace, "Target set:", setsTextField };
297     else {
298       messageObjects = new Object[] { "Target set:", setsTextField };
299     }
300     String options[] "Use this target set""Cancel" };
301     JOptionPane optionPane = new JOptionPane(
302       messageObjects, JOptionPane.QUESTION_MESSAGE,
303       JOptionPane.YES_NO_OPTION, null, options, "Cancel");
304     JDialog optionDialog = optionPane.createDialog(
305       owner, "Copy annotation to another set");
306     setsTextField.requestFocus();
307     optionDialog.setVisible(true);
308     Object selectedValue = optionPane.getValue();
309     if (selectedValue == null
310      || selectedValue.equals("Cancel")
311      || setsTextField.getText().trim().length() == 0) {
312       textView.getTextView().requestFocusInWindow();
313       return false;
314     }
315     targetSetName = setsTextField.getText();
316     targetSetLabel.setText("Target set: " + targetSetName);
317     textView.getTextView().requestFocusInWindow();
318     return true;
319   }
320 
321   class AnnotationMouseListener extends StackMouseListener {
322 
323     public AnnotationMouseListener() {
324     }
325 
326     public AnnotationMouseListener(String setName, String annotationId) {
327       AnnotationSet set = document.getAnnotations(setName);
328       annotation = set.get(Integer.valueOf(annotationId));
329       if (annotation != null) {
330         // the annotation has been found by its id
331         annotationData = new AnnotationDataImpl(set, annotation);
332       }
333     }
334 
335     public MouseInputAdapter createListener(String... parameters) {
336       switch(parameters.length) {
337         case 3:
338           return new AnnotationMouseListener(parameters[0], parameters[2]);
339         case 5:
340           return new AnnotationMouseListener(parameters[0], parameters[4]);
341         default:
342           return null;
343       }
344     }
345 
346     public void mousePressed(MouseEvent me) {
347       processMouseEvent(me);
348     }
349     public void mouseReleased(MouseEvent me) {
350       processMouseEvent(me);
351     }
352     public void mouseClicked(MouseEvent me) {
353       processMouseEvent(me);
354     }
355     public void processMouseEvent(MouseEvent me) {
356       if(me.isPopupTrigger()) {
357         // add annotation editors context menu
358         JPopupMenu popup = new JPopupMenu();
359         List<Action> specificEditorActions =
360           annotationListView.getSpecificEditorActions(
361           annotationData.getAnnotationSet(), annotationData.getAnnotation());
362         for (Action action : specificEditorActions) {
363           popup.add(action);
364         }
365         for (Action action : annotationListView.getGenericEditorActions(
366           annotationData.getAnnotationSet(), annotationData.getAnnotation())) {
367           if (specificEditorActions.contains(action)) { continue}
368           popup.add(action);
369         }
370         popup.show(me.getComponent(), me.getX(), me.getY());
371 
372       else if (me.getID() == MouseEvent.MOUSE_CLICKED
373               && me.getButton() == MouseEvent.BUTTON1
374               && me.getClickCount() == 2) {
375         if (targetSetName == null) {
376           if (!askTargetSet()) { return}
377         }
378         // copy the annotation to the target annotation set
379         try {
380           document.getAnnotations(targetSetName).add(
381             annotation.getStartNode().getOffset(),
382             annotation.getEndNode().getOffset(),
383             annotation.getType(),
384             annotation.getFeatures());
385         catch (InvalidOffsetException e) {
386           e.printStackTrace();
387         }
388 
389         // wait some time
390         Date timeToRun = new Date(System.currentTimeMillis() 1000);
391         Timer timer = new Timer("Annotation stack view select type"true);
392         timer.schedule(new TimerTask() {
393           public void run() {
394             SwingUtilities.invokeLater(new Runnable() { public void run() {
395               // select the new annotation and update the stack view
396               annotationSetsView.selectAnnotation(annotation,
397                 document.getAnnotations(targetSetName));
398             }});
399           }
400         }, timeToRun);
401       }
402       textView.getTextView().requestFocusInWindow();
403     }
404 
405     public void mouseEntered(MouseEvent e) {
406       dismissDelay = toolTipManager.getDismissDelay();
407       initialDelay = toolTipManager.getInitialDelay();
408       reshowDelay = toolTipManager.getReshowDelay();
409       enabled = toolTipManager.isEnabled();
410       Component component = e.getComponent();
411       if (!isTooltipSet && component instanceof JLabel) {
412         isTooltipSet = true;
413         JLabel label = (JLabelcomponent;
414         String toolTip = (label.getToolTipText() == null?
415           "" : label.getToolTipText();
416         toolTip = toolTip.replaceAll("</?html>""");
417         toolTip = "<html>" (toolTip.length() == "" : toolTip + "<br>")
418           "Double click to copy this annotation.<br>"
419           "Right click to edit</html>";
420         label.setToolTipText(toolTip);
421       }
422       // make the tooltip indefinitely shown when the mouse is over
423       toolTipManager.setDismissDelay(Integer.MAX_VALUE);
424       toolTipManager.setInitialDelay(0);
425       toolTipManager.setReshowDelay(0);
426       toolTipManager.setEnabled(true);
427     }
428 
429     public void mouseExited(MouseEvent e) {
430       toolTipManager.setDismissDelay(dismissDelay);
431       toolTipManager.setInitialDelay(initialDelay);
432       toolTipManager.setReshowDelay(reshowDelay);
433       toolTipManager.setEnabled(enabled);
434     }
435 
436     ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
437     int dismissDelay, initialDelay, reshowDelay;
438     boolean enabled;
439     Annotation annotation;
440     AnnotationData annotationData;
441     boolean isTooltipSet = false;
442   }
443 
444   protected class HeaderMouseListener extends StackMouseListener {
445 
446     public HeaderMouseListener() {
447     }
448 
449     public HeaderMouseListener(String type, String feature) {
450       this.type = type;
451       this.feature = feature;
452       init();
453     }
454 
455     public HeaderMouseListener(String type) {
456       this.type = type;
457       init();
458     }
459 
460     void init() {
461       mainPanel.addAncestorListener(new AncestorListener() {
462         public void ancestorMoved(AncestorEvent event) {}
463         public void ancestorAdded(AncestorEvent event) {}
464         public void ancestorRemoved(AncestorEvent event) {
465           // no parent so need to be disposed explicitly
466           if (popupWindow != null) { popupWindow.dispose()}
467         }
468       });
469     }
470 
471     public MouseInputAdapter createListener(String... parameters) {
472       switch(parameters.length) {
473         case 1:
474           return new HeaderMouseListener(parameters[0]);
475         case 2:
476           return new HeaderMouseListener(parameters[0], parameters[1]);
477         default:
478           return null;
479       }
480     }
481 
482     // when double clicked shows a list of features for this annotation type
483     public void mouseClicked(MouseEvent e) {
484       if (popupWindow != null && popupWindow.isVisible()) {
485         popupWindow.dispose();
486         return;
487       }
488       if (e.getButton() != MouseEvent.BUTTON1
489        || e.getClickCount() != 2) {
490         return;
491       }
492       // get a list of features for the current annotation type
493       TreeSet<String> features = new TreeSet<String>();
494       Set<String> setNames = new HashSet<String>();
495       if (document.getAnnotationSetNames() != null) {
496         setNames.addAll(document.getAnnotationSetNames());
497       }
498       setNames.add(null)// default set name
499       for (String setName : setNames) {
500         int count = 0;
501         for (Annotation annotation :
502           document.getAnnotations(setName).get(type)) {
503           for (Object feature : annotation.getFeatures().keySet()) {
504             features.add((Stringfeature);
505           }
506           count++; // checks only the 50 first annotations per set
507           if (count == 50) { break// to avoid slowing down
508         }
509       }
510       features.add("          ");
511       // create the list component
512       final JList list = new JList(features.toArray());
513       list.setVisibleRowCount(Math.min(8, features.size()));
514       list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
515       list.setBackground(Color.WHITE);
516       list.addMouseListener(new MouseAdapter() {
517         public void mouseClicked(MouseEvent e) {
518           if (e.getClickCount() == 1) {
519             String feature = (Stringlist.getSelectedValue();
520             if (feature.equals("          ")) {
521               typesFeatures.remove(type);
522             else {
523               typesFeatures.put(type, feature);
524             }
525             popupWindow.setVisible(false);
526             popupWindow.dispose();
527             updateStackView();
528             textView.getTextView().requestFocusInWindow();
529           }
530         }
531       });
532       // create the window that will contain the list
533       popupWindow = new JWindow();
534       popupWindow.addKeyListener(new KeyAdapter() {
535         public void keyPressed(KeyEvent e) {
536           if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
537             popupWindow.setVisible(false);
538             popupWindow.dispose();
539           }
540         }
541       });
542       popupWindow.add(new JScrollPane(list));
543       Component component = e.getComponent();
544       popupWindow.setBounds(
545         component.getLocationOnScreen().x,
546         component.getLocationOnScreen().y + component.getHeight(),
547         component.getWidth(),
548         Math.min(8*component.getHeight(),
549           features.size()*component.getHeight()));
550       popupWindow.pack();
551       popupWindow.setVisible(true);
552       SwingUtilities.invokeLater(new Runnable() {
553       public void run() {
554         String newFeature = typesFeatures.get(type);
555         if (newFeature == null) { newFeature = "          "}
556         list.setSelectedValue(newFeature, true);
557         popupWindow.requestFocusInWindow();
558       }});
559     }
560 
561     public void mouseEntered(MouseEvent e) {
562       Component component = e.getComponent();
563       if (component instanceof JLabel
564       && ((JLabel)component).getToolTipText() == null) {
565         ((JLabel)component).setToolTipText("Double click to choose a feature.");
566       }
567     }
568 
569     String type;
570     String feature;
571     JWindow popupWindow;
572   }
573 
574   JLabel targetSetLabel;
575   AnnotationStack stackPanel;
576   JScrollPane scroller;
577   JPanel mainPanel;
578   TextualDocumentView textView;
579   AnnotationSetsView annotationSetsView;
580   AnnotationListView annotationListView;
581   String targetSetName;
582   Document document;
583   PreviousAnnotationAction previousAnnotationAction;
584   NextAnnotationAction nextAnnotationAction;
585   /** optionally map a type to a feature when the feature value
586    *  must be displayed in the rectangle annotation */
587   Map<String,String> typesFeatures;
588 }