gate.gui.docview.AnnotationStackView Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gate-core Show documentation
Show all versions of gate-core Show documentation
GATE - general achitecture for text engineering - is open source
software capable of solving almost any text processing problem. This
artifact enables you to embed the core GATE Embedded with its essential
dependencies. You will able to use the GATE Embedded API and load and
store GATE XML documents. This artifact is the perfect dependency for
CREOLE plugins or for applications that need to customize the GATE
dependencies due to confict with their own dependencies or for lower
footprint.
The newest version!
/*
* Copyright (c) 1998-2009, The University of Sheffield and Ontotext.
*
* This file is part of GATE (see http://gate.ac.uk/), and is free
* software, licenced under the GNU Library General Public License,
* Version 2, June 1991 (in the distribution as file licence.html,
* and also available at http://gate.ac.uk/gate/licence.html).
*
* Thomas Heitz - 7 July 2009
*
* $Id$
*/
package gate.gui.docview;
import gate.event.AnnotationListener;
import gate.event.AnnotationEvent;
import gate.gui.MainFrame;
import gate.gui.annedit.AnnotationData;
import gate.gui.annedit.AnnotationDataImpl;
import gate.*;
import gate.util.InvalidOffsetException;
import gate.util.OffsetComparator;
import gate.gui.docview.AnnotationSetsView.*;
import gate.gui.docview.AnnotationStack.*;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.Timer;
import java.util.List;
import java.text.Collator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Show a stack view of highlighted annotations in the document
* centred on the document caret.
*
* When double clicked, an annotation is copied to another set in order
* to create a gold standard set from several annotator sets.
*
* You can choose to display a feature value by double clicking
* the first column rectangles.
*/
@SuppressWarnings("serial")
public class AnnotationStackView extends AbstractDocumentView
implements AnnotationListener {
public AnnotationStackView() {
typesFeatures = new HashMap();
annotationMouseListener = new AnnotationMouseListener();
headerMouseListener = new HeaderMouseListener();
}
@Override
public void cleanup() {
super.cleanup();
textView = null;
}
@Override
protected void initGUI() {
//get a pointer to the text view used to display
//the selected annotations
Iterator centralViewsIter = owner.getCentralViews().iterator();
while(textView == null && centralViewsIter.hasNext()){
DocumentView aView = centralViewsIter.next();
if(aView instanceof TextualDocumentView)
textView = (TextualDocumentView) aView;
}
// find the annotation set view associated with the document
Iterator verticalViewsIter = owner.getVerticalViews().iterator();
while(annotationSetsView == null && verticalViewsIter.hasNext()){
DocumentView aView = verticalViewsIter.next();
if(aView instanceof AnnotationSetsView)
annotationSetsView = (AnnotationSetsView) aView;
}
//get a pointer to the list view
Iterator horizontalViewsIter = owner.getHorizontalViews().iterator();
while(annotationListView == null && horizontalViewsIter.hasNext()){
DocumentView aView = horizontalViewsIter.next();
if(aView instanceof AnnotationListView)
annotationListView = (AnnotationListView)aView;
}
annotationListView.setOwner(owner);
document = textView.getDocument();
mainPanel = new JPanel();
mainPanel.setLayout(new BorderLayout());
// toolbar with previous and next annotation buttons
JToolBar toolBar = new JToolBar();
toolBar.setFloatable(false);
toolBar.addSeparator();
toolBar.add(previousAnnotationAction = new PreviousAnnotationAction());
toolBar.add(nextAnnotationAction = new NextAnnotationAction());
overlappingCheckBox = new JCheckBox("Overlapping");
overlappingCheckBox.setToolTipText("Skip non overlapping annotations.");
toolBar.add(overlappingCheckBox);
toolBar.addSeparator();
toolBar.add(targetSetLabel = new JLabel("Target set: Undefined"));
targetSetLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
askTargetSet();
}
});
targetSetLabel.setToolTipText(
"Target set to copy annotation when double clicked.
" +
"Click to change it.");
mainPanel.add(toolBar, BorderLayout.NORTH);
stackPanel = new AnnotationStack(100, 30);
scroller = new JScrollPane(stackPanel);
scroller.getViewport().setOpaque(true);
mainPanel.add(scroller, BorderLayout.CENTER);
initListeners();
}
@Override
public Component getGUI(){
return mainPanel;
}
protected void initListeners(){
stackPanel.addAncestorListener(new AncestorListener() {
@Override
public void ancestorAdded(AncestorEvent event) {
// when the view becomes visible
updateStackView();
}
@Override
public void ancestorMoved(AncestorEvent event) {
}
@Override
public void ancestorRemoved(AncestorEvent event) {
}
});
textView.getTextView().addCaretListener(new CaretListener() {
@Override
public void caretUpdate(CaretEvent e) {
updateStackView();
}
});
}
class PreviousAnnotationAction extends AbstractAction {
public PreviousAnnotationAction() {
super("Previous boundary");
putValue(SHORT_DESCRIPTION,
"Move to the previous annotation boundary" +
" Alt-Left" +
" ");
putValue(MNEMONIC_KEY, KeyEvent.VK_LEFT);
}
@Override
public void actionPerformed(ActionEvent e) {
nextAnnotationAction.setEnabled(true);
List list = new ArrayList();
for(SetHandler setHandler : annotationSetsView.setHandlers) {
for(TypeHandler typeHandler: setHandler.typeHandlers) {
if (typeHandler.isSelected()) {
// adds all the annotations from the selected type contained
// between the beginning of the document and the caret position - 1
list.addAll(setHandler.set.get(typeHandler.name).getContained(
0l, (long)textView.getTextView().getCaretPosition()-1));
}
}
}
boolean enabled = false;
if (list.size() > 0) {
if (overlappingCheckBox.isSelected()) {
Collections.sort(list, new OffsetComparator());
boolean found = false;
for (int i = list.size()-1; i > 0; i--) {
if (list.get(i).overlaps(list.get(i-1))) {
if (found) { enabled = true; break; }
// set the caret on the previous overlapping annotation found
textView.getTextView().setCaretPosition(
list.get(i).getEndNode().getOffset().intValue());
found = true;
}
}
} else {
SortedSet set =
new TreeSet(new OffsetComparator());
set.addAll(list); // remove same offsets annotations
list = new ArrayList(set);
// set the caret on the previous annotation found
textView.getTextView().setCaretPosition(
list.get(list.size()-1).getEndNode().getOffset().intValue());
enabled = (list.size() > 1);
}
try {
textView.getTextView().scrollRectToVisible(
textView.getTextView().modelToView(
textView.getTextView().getCaretPosition()));
} catch (BadLocationException exception) {
exception.printStackTrace();
}
}
setEnabled(enabled);
textView.getTextView().requestFocusInWindow();
}
}
class NextAnnotationAction extends AbstractAction {
public NextAnnotationAction() {
super("Next boundary");
putValue(SHORT_DESCRIPTION,
"Move to the next annotation boundary" +
" Alt-Right" +
" ");
putValue(MNEMONIC_KEY, KeyEvent.VK_RIGHT);
}
@Override
public void actionPerformed(ActionEvent e) {
previousAnnotationAction.setEnabled(true);
List list = new ArrayList();
for(SetHandler setHandler : annotationSetsView.setHandlers) {
for(TypeHandler typeHandler: setHandler.typeHandlers) {
if (typeHandler.isSelected()) {
// adds all the annotations from the selected type contained
// between the caret position and the end of the document
list.addAll(setHandler.set.get(typeHandler.name).getContained(
(long)textView.getTextView().getCaretPosition(),
document.getContent().size()-1));
}
}
}
boolean enabled = false;
if (list.size() > 0) {
if (overlappingCheckBox.isSelected()) {
Collections.sort(list, new OffsetComparator());
boolean found = false;
for (int i = 0; i < list.size()-1; i++) {
if (list.get(i).overlaps(list.get(i+1))) {
if (found) { enabled = true; break; }
// set the caret on the next overlapping annotation found
textView.getTextView().setCaretPosition(
list.get(i).getEndNode().getOffset().intValue());
found = true;
}
}
} else {
SortedSet set =
new TreeSet(new OffsetComparator());
set.addAll(list); // remove same offsets annotations
list = new ArrayList(set);
// set the caret on the next annotation found
textView.getTextView().setCaretPosition(
list.get(0).getEndNode().getOffset().intValue());
enabled = (list.size() > 1);
}
try {
textView.getTextView().scrollRectToVisible(
textView.getTextView().modelToView(
textView.getTextView().getCaretPosition()));
} catch (BadLocationException exception) {
exception.printStackTrace();
}
}
setEnabled(enabled);
textView.getTextView().requestFocusInWindow();
}
}
@Override
protected void registerHooks() { /* do nothing */ }
@Override
protected void unregisterHooks() { /* do nothing */ }
@Override
public int getType() {
return HORIZONTAL;
}
@Override
public void annotationUpdated(AnnotationEvent e) {
updateStackView();
}
public void updateStackView() {
if (textView == null) { return; }
int caretPosition = textView.getTextView().getCaretPosition();
// get the context around the annotation
int context = 40;
String text = "";
try {
text = document.getContent().getContent(
Math.max(0l, caretPosition - context),
Math.min(document.getContent().size(),
caretPosition + 1 + context)).toString();
} catch (InvalidOffsetException e) {
e.printStackTrace();
}
// initialise the annotation stack
stackPanel.setText(text);
stackPanel.setExpressionStartOffset(caretPosition);
stackPanel.setExpressionEndOffset(caretPosition + 1);
stackPanel.setContextBeforeSize(Math.min(caretPosition, context));
stackPanel.setContextAfterSize(Math.min(
document.getContent().size().intValue()-1-caretPosition, context));
stackPanel.setAnnotationMouseListener(annotationMouseListener);
stackPanel.setHeaderMouseListener(headerMouseListener);
stackPanel.clearAllRows();
// add stack rows and annotations for each selected annotation set
// in the annotation sets view
for(SetHandler setHandler : annotationSetsView.setHandlers) {
for(TypeHandler typeHandler: setHandler.typeHandlers) {
if (typeHandler.isSelected()) {
stackPanel.addRow(setHandler.set.getName(), typeHandler.name,
typesFeatures.get(typeHandler.name),
null, null, AnnotationStack.CROP_MIDDLE);
Set annotations = setHandler.set.get(typeHandler.name)
.get(Math.max(0l, caretPosition - context), Math.min(
document.getContent().size(), caretPosition + 1 + context));
for (Annotation annotation : annotations) {
stackPanel.addAnnotation(annotation);
}
}
}
}
stackPanel.drawStack();
}
/** @return true if the user input a valid annotation set */
boolean askTargetSet() {
Object[] messageObjects;
final JTextField setsTextField = new JTextField("consensus", 15);
if (document.getAnnotationSetNames() != null
&& !document.getAnnotationSetNames().isEmpty()) {
Collator collator = Collator.getInstance(Locale.ENGLISH);
collator.setStrength(Collator.TERTIARY);
SortedSet setNames = new TreeSet(collator);
setNames.addAll(document.getAnnotationSetNames());
JList list = new JList(setNames.toArray(new String[setNames.size()]));
list.setVisibleRowCount(Math.min(10, setNames.size()));
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setSelectedValue(targetSetName, true);
JScrollPane scroll = new JScrollPane(list);
JPanel vspace = new JPanel();
vspace.setSize(0, 5);
list.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
@SuppressWarnings("unchecked")
JList list = (JList) e.getSource();
if (list.getSelectedValue() != null) {
setsTextField.setText(list.getSelectedValue());
}
}
});
messageObjects = new Object[] { "Existing annotation sets:",
scroll, vspace, "Target set:", setsTextField };
} else {
messageObjects = new Object[] { "Target set:", setsTextField };
}
String options[] = { "Use this target set", "Cancel" };
JOptionPane optionPane = new JOptionPane(
messageObjects, JOptionPane.QUESTION_MESSAGE,
JOptionPane.YES_NO_OPTION, null, options, "Cancel");
JDialog optionDialog = optionPane.createDialog(
owner, "Copy annotation to another set");
setsTextField.requestFocus();
optionDialog.setVisible(true);
Object selectedValue = optionPane.getValue();
if (selectedValue == null
|| selectedValue.equals("Cancel")
|| setsTextField.getText().trim().length() == 0) {
textView.getTextView().requestFocusInWindow();
return false;
}
targetSetName = setsTextField.getText();
targetSetLabel.setText("Target set: " + targetSetName);
textView.getTextView().requestFocusInWindow();
return true;
}
class AnnotationMouseListener extends StackMouseListener {
public AnnotationMouseListener() {
}
public AnnotationMouseListener(String setName, String annotationId) {
AnnotationSet set = document.getAnnotations(setName);
annotation = set.get(Integer.valueOf(annotationId));
if (annotation != null) {
// the annotation has been found by its id
annotationData = new AnnotationDataImpl(set, annotation);
}
}
@Override
public MouseInputAdapter createListener(String... parameters) {
switch(parameters.length) {
case 3:
return new AnnotationMouseListener(parameters[0], parameters[2]);
case 5:
return new AnnotationMouseListener(parameters[0], parameters[4]);
default:
return null;
}
}
@Override
public void mousePressed(MouseEvent me) {
processMouseEvent(me);
}
@Override
public void mouseReleased(MouseEvent me) {
processMouseEvent(me);
}
@Override
public void mouseClicked(MouseEvent me) {
processMouseEvent(me);
}
public void processMouseEvent(MouseEvent me) {
if(me.isPopupTrigger()) { // context menu
// add annotation editors to the context menu
JPopupMenu popup = new JPopupMenu();
List specificEditorActions =
annotationListView.getSpecificEditorActions(
annotationData.getAnnotationSet(), annotationData.getAnnotation());
for (Action action : specificEditorActions) {
popup.add(action);
}
List genericEditorActions =
annotationListView.getGenericEditorActions(
annotationData.getAnnotationSet(), annotationData.getAnnotation());
for (Action action : genericEditorActions) {
if (specificEditorActions.contains(action)) { continue; }
popup.add(action);
}
if (specificEditorActions.size() + genericEditorActions.size() > 1) {
popup.show(me.getComponent(), me.getX(), me.getY());
} else { // only one choice so use it directly
if (specificEditorActions.size() == 1) {
specificEditorActions.get(0).actionPerformed(null);
} else {
genericEditorActions.get(0).actionPerformed(null);
}
}
} else if (me.getID() == MouseEvent.MOUSE_CLICKED
&& me.getButton() == MouseEvent.BUTTON1
&& me.getClickCount() == 1
&& (me.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0
&& (me.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0) {
// control + shift + click -> delete the annotation
annotationData.getAnnotationSet().remove(annotation);
} else if (me.getID() == MouseEvent.MOUSE_CLICKED
&& me.getButton() == MouseEvent.BUTTON1
&& me.getClickCount() == 1
&& (me.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0) {
// control + click
String featureDisplayed = typesFeatures.get(annotation.getType());
Object[] features;
if (featureDisplayed != null) {
features = new Object[]{featureDisplayed};
} else {
features = annotation.getFeatures().keySet().toArray();
}
for (Object feature : features) {
String value = (String) annotation.getFeatures().get(feature);
if (value == null) { continue; }
Pattern pattern = Pattern.compile("(https?://[^\\s,;]+)");
Matcher matcher = pattern.matcher(value);
if (matcher.find()) {
// if the feature value contains an url display it in a browser
MainFrame.getInstance().showHelpFrame(
matcher.group(), "Annotation Stack View");
}
}
} else if (me.getID() == MouseEvent.MOUSE_CLICKED
&& me.getButton() == MouseEvent.BUTTON1
&& me.getClickCount() == 2) { // double click
if (targetSetName == null) {
if (!askTargetSet()) { return; }
}
// copy the annotation to the target annotation set
try {
FeatureMap params = Factory.newFeatureMap();
params.putAll(annotation.getFeatures());
document.getAnnotations(targetSetName).add(
annotation.getStartNode().getOffset(),
annotation.getEndNode().getOffset(),
annotation.getType(),
params);
} catch (InvalidOffsetException e) {
e.printStackTrace();
}
// wait some time
Date timeToRun = new Date(System.currentTimeMillis() + 500);
Timer timer = new Timer("Annotation stack view select type", true);
timer.schedule(new TimerTask() {
@Override
public void run() {
SwingUtilities.invokeLater(new Runnable() { @Override
public void run() {
// select the new annotation and update the stack view
annotationSetsView.setTypeSelected(targetSetName,
annotation.getType(), true);
}});
}
}, timeToRun);
}
textView.getTextView().requestFocusInWindow();
}
@Override
public void mouseEntered(MouseEvent e) {
dismissDelay = toolTipManager.getDismissDelay();
initialDelay = toolTipManager.getInitialDelay();
reshowDelay = toolTipManager.getReshowDelay();
enabled = toolTipManager.isEnabled();
Component component = e.getComponent();
if (component instanceof JLabel
&& !((JLabel)component).getToolTipText().contains("Ctr-Sh-click")) {
JLabel label = (JLabel) component;
String toolTip = (label.getToolTipText() == null) ?
"" : label.getToolTipText();
toolTip = toolTip.replaceAll("?html>", "");
toolTip = ""
+ (toolTip.length() == 0 ? "" : ("" + toolTip + "
"))
+ "Double-click to copy. Right-click to edit.
"
+ "Ctr-click to show URL. Ctr-Sh-click to delete.";
label.setToolTipText(toolTip);
}
// make the tooltip indefinitely shown when the mouse is over
toolTipManager.setDismissDelay(Integer.MAX_VALUE);
toolTipManager.setInitialDelay(0);
toolTipManager.setReshowDelay(0);
toolTipManager.setEnabled(true);
}
@Override
public void mouseExited(MouseEvent e) {
toolTipManager.setDismissDelay(dismissDelay);
toolTipManager.setInitialDelay(initialDelay);
toolTipManager.setReshowDelay(reshowDelay);
toolTipManager.setEnabled(enabled);
}
ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
int dismissDelay, initialDelay, reshowDelay;
boolean enabled;
Annotation annotation;
AnnotationData annotationData;
}
protected class HeaderMouseListener extends StackMouseListener {
public HeaderMouseListener() {
}
public HeaderMouseListener(String type, String feature) {
this.type = type;
this.feature = feature;
init();
}
public HeaderMouseListener(String type) {
this.type = type;
init();
}
void init() {
mainPanel.addAncestorListener(new AncestorListener() {
@Override
public void ancestorMoved(AncestorEvent event) {}
@Override
public void ancestorAdded(AncestorEvent event) {}
@Override
public void ancestorRemoved(AncestorEvent event) {
// no parent so need to be disposed explicitly
if (popupWindow != null) { popupWindow.dispose(); }
}
});
}
@Override
public MouseInputAdapter createListener(String... parameters) {
switch(parameters.length) {
case 1:
return new HeaderMouseListener(parameters[0]);
case 2:
return new HeaderMouseListener(parameters[0], parameters[1]);
default:
return null;
}
}
// when double clicked shows a list of features for this annotation type
@Override
public void mouseClicked(MouseEvent e) {
if (popupWindow != null && popupWindow.isVisible()) {
popupWindow.dispose();
return;
}
if (e.getButton() != MouseEvent.BUTTON1
|| e.getClickCount() != 2) {
return;
}
// get a list of features for the current annotation type
TreeSet features = new TreeSet();
Set setNames = new HashSet();
if (document.getAnnotationSetNames() != null) {
setNames.addAll(document.getAnnotationSetNames());
}
setNames.add(null); // default set name
for (String setName : setNames) {
int count = 0;
for (Annotation annotation :
document.getAnnotations(setName).get(type)) {
for (Object feature : annotation.getFeatures().keySet()) {
features.add((String) feature);
}
count++; // checks only the 50 first annotations per set
if (count == 50) { break; } // to avoid slowing down
}
}
features.add(" ");
// create the list component
final JList list = new JList(features.toArray(new String[features.size()]));
list.setVisibleRowCount(Math.min(8, features.size()));
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setBackground(Color.WHITE);
list.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 1) {
String feature = list.getSelectedValue();
if (feature.equals(" ")) {
typesFeatures.remove(type);
} else {
typesFeatures.put(type, feature);
}
popupWindow.setVisible(false);
popupWindow.dispose();
updateStackView();
textView.getTextView().requestFocusInWindow();
}
}
});
// create the window that will contain the list
popupWindow = new JWindow();
popupWindow.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
popupWindow.setVisible(false);
popupWindow.dispose();
}
}
});
popupWindow.add(new JScrollPane(list));
Component component = e.getComponent();
popupWindow.setBounds(
component.getLocationOnScreen().x,
component.getLocationOnScreen().y + component.getHeight(),
component.getWidth(),
Math.min(8*component.getHeight(),
features.size()*component.getHeight()));
popupWindow.pack();
popupWindow.setVisible(true);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
String newFeature = typesFeatures.get(type);
if (newFeature == null) { newFeature = " "; }
list.setSelectedValue(newFeature, true);
popupWindow.requestFocusInWindow();
}});
}
@Override
public void mouseEntered(MouseEvent e) {
Component component = e.getComponent();
if (component instanceof JLabel
&& ((JLabel)component).getToolTipText() == null) {
((JLabel)component).setToolTipText("Double click to choose a feature.");
}
}
String type;
String feature;
JWindow popupWindow;
}
// external objects
TextualDocumentView textView;
AnnotationSetsView annotationSetsView;
AnnotationListView annotationListView;
// user interface elements
JLabel targetSetLabel;
String targetSetName;
JCheckBox overlappingCheckBox;
AnnotationStack stackPanel;
JScrollPane scroller;
JPanel mainPanel;
// actions
PreviousAnnotationAction previousAnnotationAction;
NextAnnotationAction nextAnnotationAction;
// local objects
/** optionally map a type to a feature when the feature value
* must be displayed in the rectangle annotation */
Map typesFeatures;
AnnotationMouseListener annotationMouseListener;
HeaderMouseListener headerMouseListener;
}