Log in Help
Print
Homegatesrcgategui 〉 CorpusQualityAssurance.java
 
/*
 *  Copyright (c) 2009-2010, Ontotext AD.
 *  Copyright (c) 1995-2012, The University of Sheffield. See the file
 *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
 *
 *  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 - 10 June 2009
 *
 *  $Id: CorpusQualityAssurance.java 15333 2012-02-07 13:18:33Z ian_roberts $
 */

package gate.gui;

import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.Timer;
import java.text.NumberFormat;
import java.text.Collator;

import javax.swing.*;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.text.Position;

import gate.Annotation;
import gate.AnnotationSet;
import gate.Corpus;
import gate.Document;
import gate.Factory;
import gate.Gate;
import gate.Resource;
import gate.creole.AbstractVisualResource;
import gate.event.CorpusEvent;
import gate.creole.metadata.CreoleResource;
import gate.creole.metadata.GuiType;
import gate.event.CorpusListener;
import gate.swing.XJTable;
import gate.swing.XJFileChooser;
import gate.util.AnnotationDiffer;
import gate.util.ClassificationMeasures;
import gate.util.OntologyMeasures;
import gate.util.ExtensionFileFilter;
import gate.util.OptionsMap;
import gate.util.Strings;

/**
 * Quality assurance corpus view.
 * Compare two sets of annotations with optionally their features
 * globally for each annotation and for each document inside a corpus
 * with different measures notably precision, recall and F1-score.
 */
@CreoleResource(name = "Corpus Quality Assurance", guiType = GuiType.LARGE,
    resourceDisplayed = "gate.Corpus", mainViewer = false,
    helpURL = "http://gate.ac.uk/userguide/sec:eval:corpusqualityassurance")
public class CorpusQualityAssurance extends AbstractVisualResource
  implements CorpusListener {

  public Resource init(){
    initLocalData();
    initGuiComponents();
    initListeners();
    return this;
  }

  protected void initLocalData(){
    collator = Collator.getInstance(Locale.ENGLISH);
    collator.setStrength(Collator.TERTIARY);
    documentTableModel = new DefaultTableModel();
    documentTableModel.addColumn("Document");
    documentTableModel.addColumn("Match");
    documentTableModel.addColumn("Only A");
    documentTableModel.addColumn("Only B");
    documentTableModel.addColumn("Overlap");
    annotationTableModel = new DefaultTableModel();
    annotationTableModel.addColumn("Annotation");
    annotationTableModel.addColumn("Match");
    annotationTableModel.addColumn("Only A");
    annotationTableModel.addColumn("Only B");
    annotationTableModel.addColumn("Overlap");
    document2TableModel = new DefaultTableModel();
    document2TableModel.addColumn("Document");
    document2TableModel.addColumn("Agreed");
    document2TableModel.addColumn("Total");
    confusionTableModel = new DefaultTableModel();
    types = new TreeSet<String>(collator);
    corpusChanged = false;
    measuresType = FSCORE_MEASURES;
    doubleComparator = new Comparator<String>() {
      public int compare(String s1, String s2) {
        if (s1 == null || s2 == null) {
          return 0;
        } else if (s1.equals("")) {
          return 1;
        } else if (s2.equals("")) {
          return -1;
        } else {
          return Double.valueOf(s1).compareTo(Double.valueOf(s2));
        }
      }
    };
    totalComparator = new Comparator<String>() {
      public int compare(String s1, String s2) {
        if (s1 == null || s2 == null) {
          return 0;
        } else if (s1.equals("Micro summary")) {
          return s2.equals("Macro summary") ? -1 : 1;
        } else if (s1.equals("Macro summary")) {
          return s2.equals("Micro summary") ? -1 : 1;
        } else if (s2.equals("Micro summary")) {
          return s1.equals("Macro summary") ? 1 : -1;
        } else if (s2.equals("Macro summary")) {
          return s1.equals("Micro summary") ? 1 : -1;
        } else {
          return s1.compareTo(s2);
        }
      }
    };
  }

  protected void initGuiComponents() {
    setLayout(new BorderLayout());

    JPanel sidePanel = new JPanel(new GridBagLayout());
    GridBagConstraints gbc = new GridBagConstraints();
    gbc.gridx = 0;
    sidePanel.add(Box.createVerticalStrut(5), gbc);

    // toolbar
    JToolBar toolbar = new JToolBar();
    toolbar.setFloatable(false);
    toolbar.add(openDocumentAction = new OpenDocumentAction());
    openDocumentAction.setEnabled(false);
    toolbar.add(openAnnotationDiffAction = new OpenAnnotationDiffAction());
    openAnnotationDiffAction.setEnabled(false);
    toolbar.add(exportToHtmlAction = new ExportToHtmlAction());
    toolbar.add(reloadCacheAction = new ReloadCacheAction());
    toolbar.add(new HelpAction());
    gbc.anchor = GridBagConstraints.NORTHWEST;
    sidePanel.add(toolbar, gbc);
    gbc.anchor = GridBagConstraints.NORTH;
    sidePanel.add(Box.createVerticalStrut(5), gbc);

    // annotation sets list
    JLabel label = new JLabel("Annotation Sets A/Key & B/Response");
    label.setToolTipText("aka 'Key & Response sets'");
    gbc.fill = GridBagConstraints.BOTH;
    sidePanel.add(label, gbc);
    sidePanel.add(Box.createVerticalStrut(2), gbc);
    setList = new JList();
    setList.setSelectionModel(new ToggleSelectionABModel(setList));
    setList.setPrototypeCellValue("present in every document");
    setList.setVisibleRowCount(4);
    gbc.weighty = 1;
    sidePanel.add(new JScrollPane(setList), gbc);
    gbc.weighty = 0;
    sidePanel.add(Box.createVerticalStrut(2), gbc);
    setCheck = new JCheckBox("present in every document", false);
    setCheck.addActionListener(new AbstractAction(){
      public void actionPerformed(ActionEvent e) {
        updateSetList();
      }
    });
    sidePanel.add(setCheck, gbc);
    sidePanel.add(Box.createVerticalStrut(5), gbc);

    // annotation types list
    label = new JLabel("Annotation Types");
    label.setToolTipText("Annotation types to compare");
    sidePanel.add(label, gbc);
    sidePanel.add(Box.createVerticalStrut(2), gbc);
    typeList = new JList();
    typeList.setSelectionModel(new ToggleSelectionModel());
    typeList.setPrototypeCellValue("present in every document");
    typeList.setVisibleRowCount(4);
    gbc.weighty = 1;
    sidePanel.add(new JScrollPane(typeList), gbc);
    gbc.weighty = 0;
    sidePanel.add(Box.createVerticalStrut(2), gbc);
    typeCheck = new JCheckBox("present in every selected set", false);
    typeCheck.addActionListener(new AbstractAction(){
      public void actionPerformed(ActionEvent e) {
        setList.getListSelectionListeners()[0].valueChanged(null);
      }
    });
    sidePanel.add(typeCheck, gbc);
    sidePanel.add(Box.createVerticalStrut(5), gbc);

    // annotation features list
    label = new JLabel("Annotation Features");
    label.setToolTipText("Annotation features to compare");
    sidePanel.add(label, gbc);
    sidePanel.add(Box.createVerticalStrut(2), gbc);
    featureList = new JList();
    featureList.setSelectionModel(new ToggleSelectionModel());
    featureList.setPrototypeCellValue("present in every document");
    featureList.setVisibleRowCount(4);
    gbc.weighty = 1;
    sidePanel.add(new JScrollPane(featureList), gbc);
    gbc.weighty = 0;
    sidePanel.add(Box.createVerticalStrut(2), gbc);
    featureCheck = new JCheckBox("present in every selected type", false);
    featureCheck.addActionListener(new AbstractAction(){
      public void actionPerformed(ActionEvent e) {
        typeList.getListSelectionListeners()[0].valueChanged(null);
      }
    });
    sidePanel.add(featureCheck, gbc);
    sidePanel.add(Box.createVerticalStrut(5), gbc);

    // measures tabbed panes
    label = new JLabel("Measures");
    label.setToolTipText("Measures used to compare annotations");
    optionsButton = new JToggleButton("Options");
    optionsButton.setMargin(new Insets(1, 1, 1, 1));
    JPanel labelButtonPanel = new JPanel(new BorderLayout());
    labelButtonPanel.add(label, BorderLayout.WEST);
    labelButtonPanel.add(optionsButton, BorderLayout.EAST);
    sidePanel.add(labelButtonPanel, gbc);
    sidePanel.add(Box.createVerticalStrut(2), gbc);
    final JScrollPane measureScrollPane = new JScrollPane();
    measureList = new JList();
    measureList.setSelectionModel(new ToggleSelectionModel());
    String prefix = getClass().getName() + '.';
    double beta = (userConfig.getDouble(prefix+"fscorebeta") == null) ?
      1.0 : userConfig.getDouble(prefix+"fscorebeta");
    double beta2 = (userConfig.getDouble(prefix+"fscorebeta2") == null) ?
      0.5 : userConfig.getDouble(prefix+"fscorebeta2");
    String fscore = "F" + beta + "-score ";
    String fscore2 = "F" + beta2 + "-score ";
    measureList.setModel(new ExtendedListModel(new String[]{
      fscore+"strict", fscore+"lenient", fscore+"average",
      fscore+"strict BDM", fscore+"lenient BDM", fscore+"average BDM",
      fscore2+"strict", fscore2+"lenient", fscore2+"average",
      fscore2+"strict BDM", fscore2+"lenient BDM", fscore2+"average BDM"}));
    measureList.setPrototypeCellValue("present in every document");
    measureList.setVisibleRowCount(4);
    measureScrollPane.setViewportView(measureList);
    final JScrollPane measure2ScrollPane = new JScrollPane();
    measure2List = new JList();
    measure2List.setSelectionModel(new ToggleSelectionModel());
    measure2List.setModel(new ExtendedListModel(new String[]{
      "Observed agreement", "Cohen's Kappa" , "Pi's Kappa"}));
    measure2List.setPrototypeCellValue("present in every document");
    measure2List.setVisibleRowCount(4);
    measure2ScrollPane.setViewportView(measure2List);
    measureTabbedPane = new JTabbedPane();
    measureTabbedPane.addTab("F-Score", null,
      measureScrollPane, "Inter-annotator agreement");
    measureTabbedPane.addTab("Classification", null,
      measure2ScrollPane, "Classification agreement");
    gbc.weighty = 1;
    sidePanel.add(measureTabbedPane, gbc);
    gbc.weighty = 0;
    sidePanel.add(Box.createVerticalStrut(5), gbc);
    sidePanel.add(Box.createVerticalGlue(), gbc);

    // options panel for fscore measures
    final JPanel measureOptionsPanel = new JPanel();
    measureOptionsPanel.setLayout(
      new BoxLayout(measureOptionsPanel, BoxLayout.Y_AXIS));
    JPanel betaPanel = new JPanel();
    betaPanel.setLayout(new BoxLayout(betaPanel, BoxLayout.X_AXIS));
    JLabel betaLabel = new JLabel("Fscore Beta 1:");
    final JSpinner betaSpinner =
      new JSpinner(new SpinnerNumberModel(beta, 0, 1, 0.1));
    betaSpinner.setToolTipText(
      "<html>Relative weight of precision and recall." +
      "<ul><li>1 weights equally precision and recall" +
      "<li>0.5 weights precision twice as much as recall" +
      "<li>2 weights recall twice as much as precision</ul></html>");
    betaPanel.add(betaLabel);
    betaPanel.add(Box.createHorizontalStrut(5));
    betaPanel.add(betaSpinner);
    betaPanel.add(Box.createHorizontalGlue());
    measureOptionsPanel.add(betaPanel);
    betaSpinner.setMaximumSize(new Dimension(Integer.MAX_VALUE,
      Math.round(betaLabel.getPreferredSize().height*1.5f)));
    JPanel beta2Panel = new JPanel();
    beta2Panel.setLayout(new BoxLayout(beta2Panel, BoxLayout.X_AXIS));
    JLabel beta2Label = new JLabel("Fscore Beta 2:");
    final JSpinner beta2Spinner =
      new JSpinner(new SpinnerNumberModel(beta2, 0, 1, 0.1));
    beta2Spinner.setToolTipText(betaSpinner.getToolTipText());
    beta2Panel.add(beta2Label);
    beta2Panel.add(Box.createHorizontalStrut(5));
    beta2Panel.add(beta2Spinner);
    beta2Panel.add(Box.createHorizontalGlue());
    measureOptionsPanel.add(beta2Panel);
    beta2Spinner.setMaximumSize(new Dimension(Integer.MAX_VALUE,
      Math.round(beta2Label.getPreferredSize().height*1.5f)));
    JPanel bdmFilePanel = new JPanel();
    bdmFilePanel.setLayout(new BoxLayout(bdmFilePanel, BoxLayout.X_AXIS));
    JLabel bdmFileLabel = new JLabel("BDM file:");
    JButton bdmFileButton = new JButton(new SetBdmFileAction());
    bdmFilePanel.add(bdmFileLabel);
    bdmFilePanel.add(Box.createHorizontalStrut(5));
    bdmFilePanel.add(bdmFileButton);
    bdmFilePanel.add(Box.createHorizontalGlue());
    measureOptionsPanel.add(bdmFilePanel);

    // options panel for classification measures
    final JPanel measure2OptionsPanel = new JPanel();
    measure2OptionsPanel.setLayout(
      new BoxLayout(measure2OptionsPanel, BoxLayout.Y_AXIS));
    JPanel verbosePanel = new JPanel();
    verbosePanel.setLayout(new BoxLayout(verbosePanel, BoxLayout.X_AXIS));
    boolean verbose = (userConfig.getBoolean(prefix+"verbose") == null) ?
      false : userConfig.getBoolean(prefix+"verbose");
    verboseOptionCheckBox = new JCheckBox("Output ignored annotations",verbose);
    verbosePanel.add(verboseOptionCheckBox);
    verbosePanel.add(Box.createHorizontalGlue());
    measure2OptionsPanel.add(verbosePanel);

    // options button action
    optionsButton.setAction(new AbstractAction("Options") {
      int[] selectedIndices;
      public void actionPerformed(ActionEvent e) {
        JToggleButton button = (JToggleButton) e.getSource();
        // switch measure options panel and measure list
        if (button.isSelected()) {
          if (measuresType == FSCORE_MEASURES) {
            selectedIndices = measureList.getSelectedIndices();
            measureScrollPane.setViewportView(measureOptionsPanel);
          } else if (measuresType == CLASSIFICATION_MEASURES) {
            selectedIndices = measure2List.getSelectedIndices();
            measure2ScrollPane.setViewportView(measure2OptionsPanel);
          }
        } else {
          String prefix = getClass().getEnclosingClass().getName() + '.';
          if (measuresType == FSCORE_MEASURES) {
            // update beta with new values
            String fscore = "F" + betaSpinner.getValue() + "-score ";
            String fscore2 = "F" + beta2Spinner.getValue() + "-score ";
            measureList.setModel(new ExtendedListModel(new String[]{
              fscore+"strict", fscore+"lenient", fscore+"average",
              fscore+"strict BDM", fscore+"lenient BDM", fscore+"average BDM",
              fscore2+"strict", fscore2+"lenient", fscore2+"average",
              fscore2+"strict BDM", fscore2+"lenient BDM", fscore2+"average BDM"}));
            // save in GATE preferences
            userConfig.put(prefix+"fscorebeta", betaSpinner.getValue());
            userConfig.put(prefix+"fscorebeta2", beta2Spinner.getValue());
            // put back the list and its selection
            measureScrollPane.setViewportView(measureList);
            measureList.setSelectedIndices(selectedIndices);
          } else if (measuresType == CLASSIFICATION_MEASURES) {
            userConfig.put(prefix+"verbose",verboseOptionCheckBox.isSelected());
            measure2ScrollPane.setViewportView(measure2List);
            measure2List.setSelectedIndices(selectedIndices);
          }
        }
      }
    });

    // compare button and progress bar
    JButton compareButton = new JButton(compareAction = new CompareAction());
    compareAction.setEnabled(false);
    sidePanel.add(compareButton, gbc);
    sidePanel.add(Box.createVerticalStrut(5), gbc);
    progressBar = new JProgressBar();
    progressBar.setStringPainted(true);
    progressBar.setString("");
    sidePanel.add(progressBar, gbc);
    sidePanel.add(Box.createVerticalStrut(5), gbc);

    // tables
    annotationTable = new XJTable() {
      public boolean isCellEditable(int rowIndex, int vColIndex) {
        return false;
      }
      protected JTableHeader createDefaultTableHeader() {
        return new JTableHeader(columnModel) {
          public String getToolTipText(MouseEvent event) {
            int index = columnModel.getColumnIndexAtX(event.getPoint().x);
            if (index == -1) { return null; }
            int modelIndex = columnModel.getColumn(index).getModelIndex();
            String columnName = this.table.getModel().getColumnName(modelIndex);
            return createToolTipFromColumnName(columnName);
          }
        };
      }
    };
    annotationTable.setModel(annotationTableModel);
    annotationTable.setSortable(false);
    annotationTable.setEnableHidingColumns(true);
    annotationTable.setAutoResizeMode(XJTable.AUTO_RESIZE_ALL_COLUMNS);
    documentTable = new XJTable() {
      public boolean isCellEditable(int rowIndex, int vColIndex) {
        return false;
      }
      protected JTableHeader createDefaultTableHeader() {
        return new JTableHeader(columnModel) {
          public String getToolTipText(MouseEvent event) {
            int index = columnModel.getColumnIndexAtX(event.getPoint().x);
            if (index == -1) { return null; }
            int modelIndex = columnModel.getColumn(index).getModelIndex();
            String columnName = this.table.getModel().getColumnName(modelIndex);
            return createToolTipFromColumnName(columnName);
          }
        };
      }
    };
    documentTable.setModel(documentTableModel);
    documentTable.setSortable(false);
    documentTable.setEnableHidingColumns(true);
    documentTable.setAutoResizeMode(XJTable.AUTO_RESIZE_ALL_COLUMNS);
    document2Table = new XJTable() {
      public boolean isCellEditable(int rowIndex, int vColIndex) {
        return false;
      }
    };
    document2Table.setModel(document2TableModel);
    confusionTable = new XJTable() {
      public boolean isCellEditable(int rowIndex, int vColIndex) {
        return false;
      }
    };
    confusionTable.setModel(confusionTableModel);
    confusionTable.setSortable(false);

    tableTabbedPane = new JTabbedPane();
    tableTabbedPane.addTab("Corpus statistics", null,
      new JScrollPane(annotationTable),
      "Compare each annotation type for the whole corpus");
    tableTabbedPane.addTab("Document statistics", null,
      new JScrollPane(documentTable),
      "Compare each documents in the corpus with theirs annotations");

    JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
    splitPane.setContinuousLayout(true);
    splitPane.setOneTouchExpandable(true);
    splitPane.setResizeWeight(0.80);
    splitPane.setLeftComponent(tableTabbedPane);
    splitPane.setRightComponent(new JScrollPane(sidePanel));

    add(splitPane);
  }

  protected void initListeners() {

    // when the view is shown update the tables if the corpus has changed
    addAncestorListener(new AncestorListener() {
      public void ancestorAdded(AncestorEvent event) {
        if (!isShowing() || !corpusChanged) { return; }
        if (timerTask != null) { timerTask.cancel(); }
        Date timeToRun = new Date(System.currentTimeMillis() + 1000);
        timerTask = new TimerTask() { public void run() {
          readSetsTypesFeatures(0);
        }};
        timer.schedule(timerTask, timeToRun); // add a delay before updating
      }
      public void ancestorRemoved(AncestorEvent event) { /* do nothing */ }
      public void ancestorMoved(AncestorEvent event) { /* do nothing */ }
    });

    // when set list selection change
    setList.addListSelectionListener(new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        if (typesSelected == null) {
          typesSelected = typeList.getSelectedValues();
        }
        typeList.setModel(new ExtendedListModel());
        keySetName = ((ToggleSelectionABModel)
          setList.getSelectionModel()).getSelectedValueA();
        responseSetName = ((ToggleSelectionABModel)
          setList.getSelectionModel()).getSelectedValueB();
        if (keySetName == null
         || responseSetName == null
         || setList.getSelectionModel().getValueIsAdjusting()) {
          compareAction.setEnabled(false);
          return;
        }
        setList.setEnabled(false);
        setCheck.setEnabled(false);
        // update type UI list
        TreeSet<String> someTypes = new TreeSet<String>();
        TreeMap<String, TreeSet<String>> typesFeatures;
        boolean firstLoop = true; // needed for retainAll to work
        synchronized(docsSetsTypesFeatures) {
          for (TreeMap<String, TreeMap<String, TreeSet<String>>>
              setsTypesFeatures : docsSetsTypesFeatures.values()) {
            typesFeatures = setsTypesFeatures.get(
              keySetName.equals("[Default set]") ? "" : keySetName);
            if (typesFeatures != null) {
              if (typeCheck.isSelected() && !firstLoop) {
                someTypes.retainAll(typesFeatures.keySet());
              } else {
                someTypes.addAll(typesFeatures.keySet());
              }
            } else if (typeCheck.isSelected()) {
              // empty set no types to display
              break;
            }
            typesFeatures = setsTypesFeatures.get(
              responseSetName.equals("[Default set]") ? "" : responseSetName);
            if (typesFeatures != null) {
              if (typeCheck.isSelected()) {
                someTypes.retainAll(typesFeatures.keySet());
              } else {
                someTypes.addAll(typesFeatures.keySet());
              }
            } else if (typeCheck.isSelected()) {
              break;
            }
            firstLoop = false;
          }
        }
        typeList.setModel(new ExtendedListModel(someTypes.toArray()));
        if (someTypes.size() > 0) {
          for (Object value : typesSelected) {
            // put back the selection if possible
            int index = typeList.getNextMatch(
              (String) value, 0, Position.Bias.Forward);
            if (index != -1) {
              typeList.setSelectedIndex(index);
            }
          }
        }
        typesSelected = null;
        setList.setEnabled(true);
        setCheck.setEnabled(true);
        if (measuresType == FSCORE_MEASURES) {
          compareAction.setEnabled(true);
        }
      }
    });

    // when type list selection change
    typeList.addListSelectionListener(new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        // update feature UI list
        if (featuresSelected == null) {
          featuresSelected = featureList.getSelectedValues();
        }
        featureList.setModel(new ExtendedListModel());
        if (typeList.getSelectedValues().length == 0
         || typeList.getSelectionModel().getValueIsAdjusting()) {
          return;
        }
        final Set<String> typeNames = new HashSet<String>();
        for (Object type : typeList.getSelectedValues()) {
          typeNames.add((String) type);
        }
        typeList.setEnabled(false);
        typeCheck.setEnabled(false);
        TreeSet<String> features = new TreeSet<String>(collator);
        TreeMap<String, TreeSet<String>> typesFeatures;
        boolean firstLoop = true; // needed for retainAll to work
        synchronized(docsSetsTypesFeatures) {
          for (TreeMap<String, TreeMap<String, TreeSet<String>>> sets :
               docsSetsTypesFeatures.values()) {
            typesFeatures = sets.get(keySetName.equals("[Default set]") ?
              "" : keySetName);
            if (typesFeatures != null) {
              for (String typeName : typesFeatures.keySet()) {
                if (typeNames.contains(typeName)) {
                  if (featureCheck.isSelected() && !firstLoop) {
                    features.retainAll(typesFeatures.get(typeName));
                  } else {
                    features.addAll(typesFeatures.get(typeName));
                  }
                }
              }
            } else if (featureCheck.isSelected()) {
              // empty type no features to display
              break;
            }
            typesFeatures = sets.get(responseSetName.equals("[Default set]") ?
              "" : responseSetName);
            if (typesFeatures != null) {
              for (String typeName : typesFeatures.keySet()) {
                if (typeNames.contains(typeName)) {
                  if (featureCheck.isSelected()) {
                    features.retainAll(typesFeatures.get(typeName));
                  } else {
                    features.addAll(typesFeatures.get(typeName));
                  }
                }
              }
            } else if (featureCheck.isSelected()) {
              break;
            }
            firstLoop = false;
          }
        }
        featureList.setModel(new ExtendedListModel(features.toArray()));
        if (features.size() > 0) {
          for (Object value : featuresSelected) {
            // put back the selection if possible
            int index = featureList.getNextMatch(
              (String) value, 0, Position.Bias.Forward);
            if (index != -1) {
              featureList.setSelectedIndex(index);
            }
          }
        }
        featuresSelected = null;
        typeList.setEnabled(true);
        typeCheck.setEnabled(true);
      }
    });

    // when type list selection change
    featureList.addListSelectionListener(new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        if (measuresType == CLASSIFICATION_MEASURES) {
          if (typeList.getSelectedIndices().length == 1
           && featureList.getSelectedIndices().length == 1) {
            compareAction.setEnabled(true);
            compareAction.putValue(Action.SHORT_DESCRIPTION,
              "Compare annotations between sets A and B");
          } else {
            compareAction.setEnabled(false);
            compareAction.putValue(Action.SHORT_DESCRIPTION,
              "You must select exactly one type and one feature");
          }
        }
      }
    });

    // when the measure tab selection change
    measureTabbedPane.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        JTabbedPane tabbedPane = (JTabbedPane) e.getSource();
        int selectedTab = tabbedPane.getSelectedIndex();
        tableTabbedPane.removeAll();
        openDocumentAction.setEnabled(false);
        openAnnotationDiffAction.setEnabled(false);
        if (optionsButton.isSelected()) {
          optionsButton.doClick(); // hide the options panel if shown
        }
        if (tabbedPane.getTitleAt(selectedTab).equals("F-Score")) {
          measuresType = FSCORE_MEASURES;
          compareAction.setEnabled(keySetName != null
                           && responseSetName != null);
          compareAction.putValue(Action.SHORT_DESCRIPTION,
            "Compare annotations between sets A and B");
          tableTabbedPane.addTab("Corpus statistics", null,
            new JScrollPane(annotationTable),
            "Compare each annotation type for the whole corpus");
          tableTabbedPane.addTab("Document statistics", null,
            new JScrollPane(documentTable),
            "Compare each documents in the corpus with theirs annotations");
        } else {
          measuresType = CLASSIFICATION_MEASURES;
          featureList.getListSelectionListeners()[0].valueChanged(null);
          tableTabbedPane.addTab("Document statistics", null,
            new JScrollPane(document2Table),
            "Compare each documents in the corpus with theirs annotations");
          tableTabbedPane.addTab("Confusion Matrices", null,
            new JScrollPane(confusionTable), "Describe how annotations in" +
              " one set are classified in the other and vice versa.");
        }
      }
    });

    // enable/disable toolbar icons according to the document table selection
    documentTable.getSelectionModel().addListSelectionListener(
      new ListSelectionListener() {
        public void valueChanged(ListSelectionEvent e) {
          if (e.getValueIsAdjusting()) { return; }
          boolean enabled = documentTable.getSelectedRow() != -1
            && !((String)documentTableModel.getValueAt(
            documentTable.getSelectedRow(), 0)).endsWith("summary");
          openDocumentAction.setEnabled(enabled);
          openAnnotationDiffAction.setEnabled(enabled);
        }
      }
    );

    // enable/disable toolbar icons according to the document 2 table selection
    document2Table.getSelectionModel().addListSelectionListener(
      new ListSelectionListener() {
        public void valueChanged(ListSelectionEvent e) {
          if (e.getValueIsAdjusting()) { return; }
          boolean enabled = document2Table.getSelectedRow() != -1
            && !((String)document2TableModel.getValueAt(
              document2Table.getSelectedRow(),
              0)).endsWith("summary");
          openDocumentAction.setEnabled(enabled);
          openAnnotationDiffAction.setEnabled(enabled);
        }
      }
    );

    // double click on a document loads it in the document editor
    documentTable.addMouseListener(new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        if (!e.isPopupTrigger()
          && e.getClickCount() == 2
          && openDocumentAction.isEnabled()) {
          openDocumentAction.actionPerformed(null);
        }
      }
    });

    // double click on a document loads it in the document editor
    document2Table.addMouseListener(new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        if (!e.isPopupTrigger()
          && e.getClickCount() == 2
          && openDocumentAction.isEnabled()) {
          openDocumentAction.actionPerformed(null);
        }
      }
    });

    InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
    ActionMap actionMap = getActionMap();
    inputMap.put(KeyStroke.getKeyStroke("F1"), "help");
    actionMap.put("help", new HelpAction());
  }

  /**
   * Create a table header tool tips from the column name.
   * @param columnName name used for creating the tooltip
   * @return tooltip value
   */
  protected String createToolTipFromColumnName(String columnName) {
    String tooltip;
    if (columnName.equals("Document")
     || columnName.equals("Annotation")) {
      tooltip = null;
    } else if (columnName.equals("Match")) {
      tooltip = "aka Correct";
    } else if (columnName.equals("Only A")) {
      tooltip = "aka Missing";
    } else if (columnName.equals("Only B")) {
      tooltip = "aka Spurious";
    } else if (columnName.equals("Overlap")) {
      tooltip = "aka Partial";
    } else if (columnName.equals("Rec.B/A")) {
      tooltip = "Recall for B relative to A";
    } else if (columnName.equals("Prec.B/A")) {
      tooltip = "Precision for B relative to A";
    } else {
      tooltip = columnName
        .replaceFirst("s.", "score strict")
        .replaceFirst("l.", "score lenient")
        .replaceFirst("a.", "score average")
        .replaceFirst("B.", " BDM");
    }
    return tooltip;
  }

  protected static class ExtendedListModel extends DefaultListModel {
    public ExtendedListModel() {
      super();
    }
    public ExtendedListModel(Object[] elements) {
      super();
      for (Object element : elements) {
        super.addElement(element);
      }
    }
  }

  protected static class ToggleSelectionModel extends DefaultListSelectionModel {
    boolean gestureStarted = false;
    public void setSelectionInterval(int index0, int index1) {
      if (isSelectedIndex(index0) && !gestureStarted) {
        super.removeSelectionInterval(index0, index1);
      } else {
        super.addSelectionInterval(index0, index1);
      }
      gestureStarted = true;
    }
    public void setValueIsAdjusting(boolean isAdjusting) {
      if (!isAdjusting) {
        gestureStarted = false;
      }
    }
  }

  /**
   * Add a suffix A and B for the first and second selected item.
   * Allows only 2 items to be selected.
   */
  protected static class ToggleSelectionABModel extends DefaultListSelectionModel {
    public ToggleSelectionABModel(JList list) {
      this.list = list;
    }
    public void setSelectionInterval(int index0, int index1) {
      ExtendedListModel model = (ExtendedListModel) list.getModel();
      String value = (String) model.getElementAt(index0);
      if (value.endsWith(" (A)") || value.endsWith(" (B)")) {
        // if ends with ' (A)' or ' (B)' then remove the suffix
        model.removeElementAt(index0);
        model.insertElementAt(value.substring(0,
          value.length() - " (A)".length()), index0);
        if (value.endsWith(" (A)")) {
          selectedValueA = null;
        } else {
          selectedValueB = null;
        }
        removeSelectionInterval(index0, index1);
      } else {
        // suffix with ' (A)' or ' (B)' if not already existing
        if (selectedValueA == null) {
          model.removeElementAt(index0);
          model.insertElementAt(value + " (A)", index0);
          selectedValueA = value;
          addSelectionInterval(index0, index1);
        } else if (selectedValueB == null) {
          model.removeElementAt(index0);
          model.insertElementAt(value + " (B)", index0);
          selectedValueB = value;
          addSelectionInterval(index0, index1);
        }
      }
    }
    public void clearSelection() {
      selectedValueA = null;
      selectedValueB = null;
      super.clearSelection();
    }
    public String getSelectedValueA() {
      return selectedValueA;
    }
    public String getSelectedValueB() {
      return selectedValueB;
    }
    JList list;
    String selectedValueA, selectedValueB;
  }

  public void cleanup(){
    super.cleanup();
    corpus = null;
  }

  public void setTarget(Object target){
    if(corpus != null && corpus != target){
      //we already had a different corpus
      corpus.removeCorpusListener(this);
    }
    if(!(target instanceof Corpus)){
      throw new IllegalArgumentException(
        "This view can only be used with a GATE corpus!\n" +
        target.getClass().toString() + " is not a GATE corpus!");
    }
    this.corpus = (Corpus) target;
    corpus.addCorpusListener(this);

    corpusChanged = true;
    if (!isShowing()) { return; }
    if (timerTask != null) { timerTask.cancel(); }
    Date timeToRun = new Date(System.currentTimeMillis() + 2000);
    timerTask = new TimerTask() { public void run() {
      readSetsTypesFeatures(0);
    }};
    timer.schedule(timerTask, timeToRun); // add a delay before updating
  }

  public void documentAdded(final CorpusEvent e) {
    corpusChanged = true;
    if (!isShowing()) { return; }
    if (timerTask != null) { timerTask.cancel(); }
    Date timeToRun = new Date(System.currentTimeMillis() + 2000);
    timerTask = new TimerTask() { public void run() {
      readSetsTypesFeatures(0);
    }};
    timer.schedule(timerTask, timeToRun); // add a delay before updating
  }

  public void documentRemoved(final CorpusEvent e) {
    corpusChanged = true;
    if (!isShowing()) { return; }
    if (timerTask != null) { timerTask.cancel(); }
    Date timeToRun = new Date(System.currentTimeMillis() + 2000);
    timerTask = new TimerTask() { public void run() {
      readSetsTypesFeatures(0);
    }};
    timer.schedule(timerTask, timeToRun); // add a delay before updating
  }

  /**
   * Update set lists.
   * @param documentStart first document to read in the corpus,
   * the first document of the corpus is 0.
   */
  protected void readSetsTypesFeatures(final int documentStart) {
    if (!isShowing()) { return; }
    corpusChanged = false;
    SwingUtilities.invokeLater(new Runnable(){ public void run() {
      progressBar.setMaximum(corpus.size() - 1);
      progressBar.setString("Read sets, types, features");
      reloadCacheAction.setEnabled(false);
    }});
    CorpusQualityAssurance.this.setCursor(
      Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    Runnable runnable = new Runnable() { public void run() {
    if (docsSetsTypesFeatures.size() != corpus.getDocumentNames().size()
    || !docsSetsTypesFeatures.keySet().containsAll(corpus.getDocumentNames())) {
      if (documentStart == 0) { docsSetsTypesFeatures.clear(); }
      TreeMap<String, TreeMap<String, TreeSet<String>>> setsTypesFeatures;
      TreeMap<String, TreeSet<String>> typesFeatures;
      TreeSet<String> features;
      for (int i = documentStart; i < corpus.size(); i++) {
        // fill in the lists of document, set, type and feature names
        boolean documentWasLoaded = corpus.isDocumentLoaded(i);
        Document document = (Document) corpus.get(i);
        if (document != null && document.getAnnotationSetNames() != null) {
          setsTypesFeatures =
            new TreeMap<String, TreeMap<String, TreeSet<String>>>(collator);
          HashSet<String> setNames =
            new HashSet<String>(document.getAnnotationSetNames());
          setNames.add("");
          for (String set : setNames) {
            typesFeatures = new TreeMap<String, TreeSet<String>>(collator);
            AnnotationSet annotations = document.getAnnotations(set);
            for (String type : annotations.getAllTypes()) {
              features = new TreeSet<String>(collator);
              for (Annotation annotation : annotations.get(type)) {
                for (Object featureKey : annotation.getFeatures().keySet()) {
                  features.add((String) featureKey);
                }
              }
              typesFeatures.put(type, features);
            }
            setsTypesFeatures.put(set, typesFeatures);
          }
          docsSetsTypesFeatures.put(document.getName(), setsTypesFeatures);
        }
        if (!documentWasLoaded) {
          corpus.unloadDocument(document);
          Factory.deleteResource(document);
        }
        final int progressValue = i + 1;
        SwingUtilities.invokeLater(new Runnable(){ public void run() {
          progressBar.setValue(progressValue);
          if ((progressValue+1) % 5 == 0) {
            // update the set list every 5 documents read
            updateSetList();
          }
        }});
        if (Thread.interrupted()) { return; }
      }
    }
    updateSetList();
    SwingUtilities.invokeLater(new Runnable(){ public void run(){
      progressBar.setValue(progressBar.getMinimum());
      progressBar.setString("");
      CorpusQualityAssurance.this.setCursor(
        Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
      reloadCacheAction.setEnabled(true);
    }});
    }};
    readSetsTypesFeaturesThread = new Thread(runnable, "readSetsTypesFeatures");
    readSetsTypesFeaturesThread.setPriority(Thread.MIN_PRIORITY);
    readSetsTypesFeaturesThread.start();
  }

  protected void updateSetList() {
    final TreeSet<String> setsNames = new TreeSet<String>(collator);
    Set<String> sets;
    boolean firstLoop = true; // needed for retainAll to work
    synchronized(docsSetsTypesFeatures) {
      for (String document : docsSetsTypesFeatures.keySet()) {
        // get the list of set names
        sets = docsSetsTypesFeatures.get(document).keySet();
        if (!sets.isEmpty()) {
          if (setCheck.isSelected() && !firstLoop) {
            setsNames.retainAll(sets);
          } else {
            setsNames.addAll(sets);
          }
        } else if (setCheck.isSelected()) {
          break;
        }
        firstLoop = false;
      }
    }
    SwingUtilities.invokeLater(new Runnable(){ public void run() {
      // update the UI lists of sets
      setsNames.remove("");
      setsNames.add("[Default set]");
      String keySetNamePrevious = keySetName;
      String responseSetNamePrevious = responseSetName;
      setList.setModel(new ExtendedListModel(setsNames.toArray()));
      if (setsNames.size() > 0) {
        if (keySetNamePrevious != null) {
          // put back the selection if possible
          int index = setList.getNextMatch(
            keySetNamePrevious, 0, Position.Bias.Forward);
          if (index != -1) {
            setList.setSelectedIndex(index);
          }
        }
        if (responseSetNamePrevious != null) {
          // put back the selection if possible
          int index = setList.getNextMatch(
            responseSetNamePrevious, 0, Position.Bias.Forward);
          if (index != -1) {
            setList.setSelectedIndex(index);
          }
        }
      }
    }});
  }

  protected void compareAnnotation() {
    int progressValuePrevious = -1;
    if (readSetsTypesFeaturesThread != null
     && readSetsTypesFeaturesThread.isAlive()) {
      // stop the thread that reads the sets, types and features
      progressValuePrevious = progressBar.getValue();
      readSetsTypesFeaturesThread.interrupt();
    }
    SwingUtilities.invokeLater(new Runnable() { public void run() {
      progressBar.setMaximum(corpus.size() - 1);
      progressBar.setString("Compare annotations");
      setList.setEnabled(false);
      setCheck.setEnabled(false);
      typeList.setEnabled(false);
      typeCheck.setEnabled(false);
      featureList.setEnabled(false);
      featureCheck.setEnabled(false);
      optionsButton.setEnabled(false);
      measureTabbedPane.setEnabled(false);
      measureList.setEnabled(false);
      exportToHtmlAction.setEnabled(false);
      reloadCacheAction.setEnabled(false);
    }});

    boolean useBdm = false;
    if (measuresType == FSCORE_MEASURES) {
      differsByDocThenType.clear();
      documentNames.clear();
      for (Object measure : measureList.getSelectedValues()) {
        if (((String) measure).contains("BDM")) { useBdm = true; break; }
      }
    }
    List<ClassificationMeasures> classificationMeasuresList =
      new ArrayList<ClassificationMeasures>();
    List<OntologyMeasures> documentOntologyMeasuresList =
      new ArrayList<OntologyMeasures>();
    List<OntologyMeasures> annotationOntologyMeasuresList =
      new ArrayList<OntologyMeasures>();

    // for each document
    for (int row = 0; row < corpus.size(); row++) {
      boolean documentWasLoaded = corpus.isDocumentLoaded(row);
      Document document = (Document) corpus.get(row);
      documentNames.add(document.getName());
      Set<Annotation> keys = new HashSet<Annotation>();
      Set<Annotation> responses = new HashSet<Annotation>();
      // get annotations from selected annotation sets
      if (keySetName.equals("[Default set]")) {
        keys = document.getAnnotations();
      } else if (document.getAnnotationSetNames() != null
      && document.getAnnotationSetNames().contains(keySetName)) {
        keys = document.getAnnotations(keySetName);
      }
      if (responseSetName.equals("[Default set]")) {
        responses = document.getAnnotations();
      } else if (document.getAnnotationSetNames() != null
      && document.getAnnotationSetNames()
        .contains(responseSetName)) {
        responses = document.getAnnotations(responseSetName);
      }
      if (!documentWasLoaded) { // in case of datastore
        corpus.unloadDocument(document);
        Factory.deleteResource(document);
      }

      // add data to the fscore document table
      if (measuresType == FSCORE_MEASURES) {
        types.clear();
        for (Object type : typeList.getSelectedValues()) {
          types.add((String) type);
        }
        if (typeList.isSelectionEmpty()) {
          for (int i = 0; i < typeList.getModel().getSize(); i++) {
            types.add((String) typeList.getModel().getElementAt(i));
          }
        }
        Set<String> featureSet = new HashSet<String>();
        for (Object feature : featureList.getSelectedValues()) {
          featureSet.add((String) feature);
        }
        HashMap<String, AnnotationDiffer> differsByType =
          new HashMap<String, AnnotationDiffer>();
        AnnotationDiffer differ;
        Set<Annotation> keysIter = new HashSet<Annotation>();
        Set<Annotation> responsesIter = new HashSet<Annotation>();
        for (String type : types) {
          if (!keys.isEmpty() && !types.isEmpty()) {
            keysIter = ((AnnotationSet)keys).get(type);
          }
          if (!responses.isEmpty() && !types.isEmpty()) {
            responsesIter = ((AnnotationSet)responses).get(type);
          }
          differ = new AnnotationDiffer();
          differ.setSignificantFeaturesSet(featureSet);
          differ.calculateDiff(keysIter, responsesIter); // compare
          differsByType.put(type, differ);
        }
        differsByDocThenType.add(differsByType);
        differ = new AnnotationDiffer(differsByType.values());
        List<String> measuresRow;
        if (useBdm) {
          OntologyMeasures ontologyMeasures = new OntologyMeasures();
          ontologyMeasures.setBdmFile(bdmFileUrl);
          ontologyMeasures.calculateBdm(differsByType.values());
          documentOntologyMeasuresList.add(ontologyMeasures);
          measuresRow = ontologyMeasures.getMeasuresRow(
            measureList.getSelectedValues(),
            documentNames.get(documentNames.size()-1));
        } else {
          measuresRow = differ.getMeasuresRow(measureList.getSelectedValues(),
            documentNames.get(documentNames.size()-1));
        }
        documentTableModel.addRow(measuresRow.toArray());

        // add data to the classification document table
      } else if (measuresType == CLASSIFICATION_MEASURES
             && !keys.isEmpty() && !responses.isEmpty()) {
        ClassificationMeasures classificationMeasures =
          new ClassificationMeasures();
        classificationMeasures.calculateConfusionMatrix(
          (AnnotationSet) keys, (AnnotationSet) responses,
          (String) typeList.getSelectedValue(),
          (String) featureList.getSelectedValue(),
          verboseOptionCheckBox.isSelected());
        classificationMeasuresList.add(classificationMeasures);
        List<String> measuresRow = classificationMeasures.getMeasuresRow(
          measure2List.getSelectedValues(),
          documentNames.get(documentNames.size()-1));
        document2TableModel.addRow(measuresRow.toArray());
        List<List<String>> matrix = classificationMeasures
          .getConfusionMatrix(documentNames.get(documentNames.size()-1));
        for (List<String> matrixRow : matrix) {
          while (confusionTableModel.getColumnCount() < matrix.size()) {
            confusionTableModel.addColumn(" ");
          }
          confusionTableModel.addRow(matrixRow.toArray());
        }
      }
      final int progressValue = row + 1;
      SwingUtilities.invokeLater(new Runnable(){ public void run() {
        progressBar.setValue(progressValue);
      }});
    } // for (int row = 0; row < corpus.size(); row++)

    // add data to the fscore annotation table
    if (measuresType == FSCORE_MEASURES) {
      for (String type : types) {
        ArrayList<AnnotationDiffer> differs = new ArrayList<AnnotationDiffer>();
        for (HashMap<String, AnnotationDiffer> differsByType :
              differsByDocThenType) {
          differs.add(differsByType.get(type));
        }
        List<String> measuresRow;
        if (useBdm) {
          OntologyMeasures ontologyMeasures = new OntologyMeasures();
          ontologyMeasures.setBdmFile(bdmFileUrl);
          ontologyMeasures.calculateBdm(differs);
          annotationOntologyMeasuresList.add(ontologyMeasures);
          measuresRow = ontologyMeasures.getMeasuresRow(
            measureList.getSelectedValues(), type);
        } else {
          AnnotationDiffer differ = new AnnotationDiffer(differs);
          measuresRow = differ.getMeasuresRow(
            measureList.getSelectedValues(), type);
        }
        annotationTableModel.addRow(measuresRow.toArray());
      }
    }

    // add summary rows to the fscore tables
    if (measuresType == FSCORE_MEASURES) {
      if (useBdm) {
        OntologyMeasures ontologyMeasures =
          new OntologyMeasures(documentOntologyMeasuresList);
        printSummary(ontologyMeasures, documentTableModel, 5,
          documentTableModel.getRowCount(), measureList.getSelectedValues());
        ontologyMeasures = new OntologyMeasures(annotationOntologyMeasuresList);
        printSummary(ontologyMeasures, annotationTableModel, 5,
          annotationTableModel.getRowCount(), measureList.getSelectedValues());
      } else {
        List<AnnotationDiffer> differs = new ArrayList<AnnotationDiffer>();
        for (Map<String, AnnotationDiffer> differsByType :
              differsByDocThenType) {
          differs.addAll(differsByType.values());
        }
        AnnotationDiffer differ = new AnnotationDiffer(differs);
        printSummary(differ, documentTableModel, 5,
          documentTableModel.getRowCount(), measureList.getSelectedValues());
        printSummary(differ, annotationTableModel, 5,
          annotationTableModel.getRowCount(), measureList.getSelectedValues());
      }

      // add summary rows to the classification tables
    } else if (measuresType == CLASSIFICATION_MEASURES) {
      ClassificationMeasures classificationMeasures =
        new ClassificationMeasures(classificationMeasuresList);
      printSummary(classificationMeasures, document2TableModel, 3,
        document2TableModel.getRowCount(), measure2List.getSelectedValues());
      List<List<String>> matrix = classificationMeasures
        .getConfusionMatrix("Whole corpus");
      int insertionRow = 0;
      for (List<String> row : matrix) {
        while (confusionTableModel.getColumnCount() < matrix.size()) {
          confusionTableModel.addColumn(" ");
        }
        confusionTableModel.insertRow(insertionRow++, row.toArray());
      }
    }

    SwingUtilities.invokeLater(new Runnable(){ public void run(){
      progressBar.setValue(progressBar.getMinimum());
      progressBar.setString("");
      setList.setEnabled(true);
      setCheck.setEnabled(true);
      typeList.setEnabled(true);
      typeCheck.setEnabled(true);
      featureList.setEnabled(true);
      featureCheck.setEnabled(true);
      optionsButton.setEnabled(true);
      measureTabbedPane.setEnabled(true);
      measureList.setEnabled(true);
      exportToHtmlAction.setEnabled(true);
      reloadCacheAction.setEnabled(true);
    }});
    if (progressValuePrevious > -1) {
      // restart the thread where it was interrupted
      readSetsTypesFeatures(progressValuePrevious);
    }
  }

  protected void printSummary(Object measureObject,
                              DefaultTableModel tableModel, int columnGroupSize,
                              int insertionRow, Object[] measures) {
    AnnotationDiffer differ = null;
    ClassificationMeasures classificationMeasures = null;
    OntologyMeasures ontologyMeasures = null;
    if (measureObject instanceof AnnotationDiffer) {
      differ = (AnnotationDiffer) measureObject;
    } else if (measureObject instanceof ClassificationMeasures) {
      classificationMeasures = (ClassificationMeasures) measureObject;
    } else if (measureObject instanceof OntologyMeasures) {
      ontologyMeasures = (OntologyMeasures) measureObject;
    }
    NumberFormat f = NumberFormat.getInstance(Locale.ENGLISH);
    f.setMaximumFractionDigits(2);
    f.setMinimumFractionDigits(2);
    List<Object> values = new ArrayList<Object>();

    // average measures by document
    values.add("Macro summary");
    for (int col = 1; col < tableModel.getColumnCount(); col++) {
      if (col < columnGroupSize) {
        values.add("");
      } else {
        float sumF = 0;
        for (int row = 0; row < tableModel.getRowCount(); row++) {
          try {
            sumF += Float.parseFloat((String) tableModel.getValueAt(row, col));
          } catch(NumberFormatException e) {
            // do nothing
          }
        }
        values.add(f.format(sumF / tableModel.getRowCount()));
      }
    }
    tableModel.insertRow(insertionRow, values.toArray());

    // sum counts and recalculate measures like the corpus is one document
    values.clear();
    values.add("Micro summary");
    for (int col = 1; col < columnGroupSize; col++) {
      int sum = 0;
      for (int row = 0; row < tableModel.getRowCount()-1; row++) {
        try {
          sum += Integer.valueOf((String) tableModel.getValueAt(row, col));
        } catch(NumberFormatException e) {
          // do nothing
        }
      }
      values.add(Integer.toString(sum));
    }
    if (measureObject instanceof OntologyMeasures) {
      List<AnnotationDiffer> differs = new ArrayList<AnnotationDiffer>(
        ontologyMeasures.getDifferByTypeMap().values());
      differ = new AnnotationDiffer(differs);
    }
    for (Object object : measures) {
      String measure = (String) object;
      int index = measure.indexOf('-');
      double beta = (index == -1) ?
        1 : Double.valueOf(measure.substring(1, index));
      if (measure.endsWith("strict")) {
        values.add(f.format(differ.getPrecisionStrict()));
        values.add(f.format(differ.getRecallStrict()));
        values.add(f.format(differ.getFMeasureStrict(beta)));
      } else if (measure.endsWith("strict BDM")) {
        values.add(f.format(ontologyMeasures.getPrecisionStrictBdm()));
        values.add(f.format(ontologyMeasures.getRecallStrictBdm()));
        values.add(f.format(ontologyMeasures.getFMeasureStrictBdm(beta)));
      } else if (measure.endsWith("lenient")) {
        values.add(f.format(differ.getPrecisionLenient()));
        values.add(f.format(differ.getRecallLenient()));
        values.add(f.format(differ.getFMeasureLenient(beta)));
      } else if (measure.endsWith("lenient BDM")) {
        values.add(f.format(ontologyMeasures.getPrecisionLenientBdm()));
        values.add(f.format(ontologyMeasures.getRecallLenientBdm()));
        values.add(f.format(ontologyMeasures.getFMeasureLenientBdm(beta)));
      } else if (measure.endsWith("average")) {
        values.add(f.format(differ.getPrecisionAverage()));
        values.add(f.format(differ.getRecallAverage()));
        values.add(f.format(differ.getFMeasureAverage(beta)));
      } else if (measure.endsWith("average BDM")) {
        values.add(f.format(ontologyMeasures.getPrecisionAverageBdm()));
        values.add(f.format(ontologyMeasures.getRecallAverageBdm()));
        values.add(f.format(ontologyMeasures.getFMeasureAverageBdm(beta)));
      } else if (measure.equals("Observed agreement")) {
        values.add(f.format(classificationMeasures.getObservedAgreement()));
      } else if (measure.equals("Cohen's Kappa")) {
        float result = classificationMeasures.getKappaCohen();
        values.add(Float.isNaN(result) ? "" : f.format(result));
      } else if (measure.equals("Pi's Kappa")) {
        float result = classificationMeasures.getKappaPi();
        values.add(Float.isNaN(result) ? "" : f.format(result));
      }
    }
    tableModel.insertRow(insertionRow + 1, values.toArray());
  }

  protected class SetBdmFileAction extends AbstractAction {
    public SetBdmFileAction() {
      super("Browse");
      putValue(SHORT_DESCRIPTION, "Choose a BDM file to compute BDM measures");
    }
    public void actionPerformed(ActionEvent evt) {
      XJFileChooser fileChooser = MainFrame.getFileChooser();
      fileChooser.setAcceptAllFileFilterUsed(true);
      fileChooser.setDialogTitle("Choose a BDM file");
      fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
      fileChooser.setResource(
        CorpusQualityAssurance.class.getName() + ".BDMfile");
      int res = fileChooser.showOpenDialog(CorpusQualityAssurance.this);
      if (res != JFileChooser.APPROVE_OPTION) { return; }
      try {
        bdmFileUrl = fileChooser.getSelectedFile().toURI().toURL();
      } catch (MalformedURLException e) {
        e.printStackTrace();
      }
    }
  }

  /**
   * Update document table.
   */
  protected class CompareAction extends AbstractAction {
    public CompareAction() {
      super("Compare");
      putValue(SHORT_DESCRIPTION, "Compare annotations between sets A and B");
      putValue(MNEMONIC_KEY, KeyEvent.VK_ENTER);
      putValue(SMALL_ICON, MainFrame.getIcon("crystal-clear-action-run"));
    }
    public void actionPerformed(ActionEvent evt) {
      boolean useBdm = false;
      for (Object measure : measureList.getSelectedValues()) {
        if (((String) measure).contains("BDM")) { useBdm = true; break; }
      }
      if (useBdm && measuresType == FSCORE_MEASURES && bdmFileUrl == null) {
        new SetBdmFileAction().actionPerformed(null);
        if (bdmFileUrl == null) { return; }
      }

      CorpusQualityAssurance.this.setCursor(
        Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
      setEnabled(false);

      Runnable runnable = new Runnable() { public void run() {
      if (measuresType == FSCORE_MEASURES) {
        documentTableModel = new DefaultTableModel();
        annotationTableModel = new DefaultTableModel();
        documentTableModel.addColumn("Document");
        annotationTableModel.addColumn("Annotation");
        documentTableModel.addColumn("Match");
        annotationTableModel.addColumn("Match");
        documentTableModel.addColumn("Only A");
        annotationTableModel.addColumn("Only A");
        documentTableModel.addColumn("Only B");
        annotationTableModel.addColumn("Only B");
        documentTableModel.addColumn("Overlap");
        annotationTableModel.addColumn("Overlap");
        for (Object measure : measureList.getSelectedValues()) {
          String measureString = ((String) measure)
            .replaceFirst("score strict", "s.")
            .replaceFirst("score lenient", "l.")
            .replaceFirst("score average", "a.")
            .replaceFirst(" BDM", "B.");
          documentTableModel.addColumn("Prec.B/A");
          annotationTableModel.addColumn("Prec.B/A");
          documentTableModel.addColumn("Rec.B/A");
          annotationTableModel.addColumn("Rec.B/A");
          documentTableModel.addColumn(measureString);
          annotationTableModel.addColumn(measureString);
        }
        compareAnnotation(); // do all the computation
        // update data

        SwingUtilities.invokeLater(new Runnable() { public void run() {
          // redraw document table
          documentTable.setModel(documentTableModel);
          for (int col = 0; col < documentTable.getColumnCount(); col++) {
            documentTable.setComparator(col, doubleComparator);
          }
          documentTable.setComparator(0, totalComparator);
          documentTable.setSortedColumn(0);
          // redraw annotation table
          annotationTable.setModel(annotationTableModel);
          for (int col = 0; col < annotationTable.getColumnCount(); col++) {
            annotationTable.setComparator(col, doubleComparator);
          }
          annotationTable.setComparator(0, totalComparator);
          annotationTable.setSortedColumn(0);
          CorpusQualityAssurance.this.setCursor(
            Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
          setEnabled(true);
        }});

      } else if (measuresType == CLASSIFICATION_MEASURES) {
        document2TableModel = new DefaultTableModel();
        document2TableModel.addColumn("Document");
        document2TableModel.addColumn("Agreed");
        document2TableModel.addColumn("Total");
        for (Object measure : measure2List.getSelectedValues()) {
          document2TableModel.addColumn(measure);
        }
        confusionTableModel = new DefaultTableModel();
        compareAnnotation(); // do all the computation
        SwingUtilities.invokeLater(new Runnable() { public void run() {
          document2Table.setSortable(false);
          document2Table.setModel(document2TableModel);
          document2Table.setComparator(0, totalComparator);
          document2Table.setComparator(1, doubleComparator);
          document2Table.setSortedColumn(0);
          document2Table.setSortable(true);
          confusionTable.setModel(confusionTableModel);
          CorpusQualityAssurance.this.setCursor(
            Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
          setEnabled(true);
        }});
      }
      }};
      Thread thread = new Thread(runnable,  "CompareAction");
      thread.setPriority(Thread.MIN_PRIORITY);
      thread.start();
    }
  }

  class OpenDocumentAction extends AbstractAction{
    public OpenDocumentAction(){
      super("Open documents", MainFrame.getIcon("document"));
      putValue(SHORT_DESCRIPTION,
        "Opens document for the selected row in a document editor");
      putValue(MNEMONIC_KEY, KeyEvent.VK_UP);
    }
    public void actionPerformed(ActionEvent e){
      final Document document = (Document)
        corpus.get(measuresType == FSCORE_MEASURES ?
          documentTable.rowViewToModel(documentTable.getSelectedRow())
        : document2Table.rowViewToModel(document2Table.getSelectedRow()));
      SwingUtilities.invokeLater( new Runnable() { public void run() {
        MainFrame.getInstance().select(document);
      }});
    }
  }

  class OpenAnnotationDiffAction extends AbstractAction{
    public OpenAnnotationDiffAction(){
      super("Open annotation diff", MainFrame.getIcon("annDiff"));
      putValue(SHORT_DESCRIPTION,
        "Opens annotation diff for the selected row in the document table");
      putValue(MNEMONIC_KEY, KeyEvent.VK_RIGHT);
    }
    public void actionPerformed(ActionEvent e){
      Document document = (Document)
        corpus.get(measuresType == FSCORE_MEASURES ?
          documentTable.rowViewToModel(documentTable.getSelectedRow())
        : document2Table.rowViewToModel(document2Table.getSelectedRow()));
      String documentName = document.getName();
      String annotationType = (String) typeList.getSelectedValue();
      Set<String> featureSet = new HashSet<String>();
      for (Object feature : featureList.getSelectedValues()) {
        featureSet.add((String) feature);
      }
      AnnotationDiffGUI frame = new AnnotationDiffGUI("Annotation Difference",
        documentName, documentName, keySetName,
        responseSetName, annotationType, featureSet);
      frame.pack();
      frame.setLocationRelativeTo(MainFrame.getInstance());
      frame.setVisible(true);
    }
  }

  protected class ExportToHtmlAction extends AbstractAction{
    public ExportToHtmlAction(){
      super("Export to HTML");
      putValue(SHORT_DESCRIPTION, "Export the tables to HTML");
      putValue(SMALL_ICON,
        MainFrame.getIcon("crystal-clear-app-download-manager"));
    }
    public void actionPerformed(ActionEvent evt){
      XJFileChooser fileChooser = MainFrame.getFileChooser();
      fileChooser.setAcceptAllFileFilterUsed(true);
      fileChooser.setDialogTitle("Choose a file where to export the tables");
      fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
      ExtensionFileFilter filter = new ExtensionFileFilter("HTML files","html");
      fileChooser.addChoosableFileFilter(filter);
      String title = corpus.getName();
        title += "_" + keySetName;
        title += "-" + responseSetName;
      fileChooser.setFileName(title + ".html");
      fileChooser.setResource(CorpusQualityAssurance.class.getName());
      int res = fileChooser.showSaveDialog(CorpusQualityAssurance.this);
      if (res != JFileChooser.APPROVE_OPTION) { return; }

      File saveFile = fileChooser.getSelectedFile();
      Writer fw = null;
      try{
        fw = new BufferedWriter(new FileWriter(saveFile));

        // Header, Title
        fw.write(BEGINHTML + nl);
        fw.write(BEGINHEAD);
        fw.write(title);
        fw.write(ENDHEAD + nl);
        fw.write("<H1>Corpus Quality Assurance</H1>" + nl);
        fw.write("<P>Corpus: " + corpus.getName() + "<BR>" + nl);
        fw.write("Key set: " + keySetName + "<BR>" + nl);
        fw.write("Response set: " + responseSetName + "<BR>" + nl);
        fw.write("Types: "
          + Strings.toString(typeList.getSelectedValues()) + "<BR>" + nl);
        fw.write("Features: "
          + Strings.toString(featureList.getSelectedValues()) + "</P>" + nl);
        fw.write("<P>&nbsp;</P>" + nl);

        ArrayList<JTable> tablesToExport = new ArrayList<JTable>();
        tablesToExport.add(annotationTable);
        tablesToExport.add(documentTable);
        tablesToExport.add(document2Table);
        tablesToExport.add(confusionTable);
        for (JTable table : tablesToExport) {
          fw.write(BEGINTABLE + nl + "<TR>" + nl);
          for(int col = 0; col < table.getColumnCount(); col++){
            fw.write("<TH align=\"left\">"
              + table.getColumnName(col) + "</TH>" + nl);
          }
          fw.write("</TR>" + nl);
          for(int row = 0; row < table.getRowCount(); row ++){
            fw.write("<TR>" + nl);
            for(int col = 0; col < table.getColumnCount(); col++){
              String value = (String) table.getValueAt(row, col);
              if (value == null) { value = ""; }
              fw.write("<TD>" + value  + "</TD>" + nl);
            }
            fw.write("</TR>" + nl);
          }
          fw.write(ENDTABLE + nl);
          fw.write("<P>&nbsp;</P>" + nl);
        }

        fw.write(ENDHTML + nl);
        fw.flush();

      } catch(IOException ioe){
        JOptionPane.showMessageDialog(CorpusQualityAssurance.this,
          ioe.toString(), "GATE", JOptionPane.ERROR_MESSAGE);
        ioe.printStackTrace();

      } finally {
        if (fw != null) {
          try {
            fw.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }

    final String nl = Strings.getNl();
    static final String BEGINHTML =
      "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">" +
      "<html>";
    static final String ENDHTML = "</body></html>";
    static final String BEGINHEAD = "<head>" +
      "<meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\">"
      + "<title>";
    static final String ENDHEAD = "</title></head><body>";
    static final String BEGINTABLE = "<table cellpadding=\"0\" border=\"1\">";
    static final String ENDTABLE = "</table>";
  }

  class ReloadCacheAction extends AbstractAction{
    public ReloadCacheAction(){
      super("Reload cache", MainFrame.getIcon("crystal-clear-action-reload"));
      putValue(SHORT_DESCRIPTION,
        "Reload cache for set, type and feature names list");
    }
    public void actionPerformed(ActionEvent e){
      docsSetsTypesFeatures.clear();
      readSetsTypesFeatures(0);
    }
  }

  protected class HelpAction extends AbstractAction {
    public HelpAction() {
      super();
      putValue(SHORT_DESCRIPTION, "User guide for this tool");
      putValue(SMALL_ICON, MainFrame.getIcon("crystal-clear-action-info"));
      putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("F1"));
    }
    public void actionPerformed(ActionEvent e) {
      MainFrame.getInstance().showHelpFrame(
        "sec:eval:corpusqualityassurance",
        CorpusQualityAssurance.class.getName());
    }
  }

  // local variables
  protected Corpus corpus;
  protected boolean corpusChanged;
  protected TreeSet<String> types;
  /** cache for document*set*type*feature names */
  final protected Map<String, TreeMap<String, TreeMap<String, TreeSet<String>>>>
    docsSetsTypesFeatures = Collections.synchronizedMap(new LinkedHashMap
      <String, TreeMap<String, TreeMap<String, TreeSet<String>>>>());
  /** ordered by document as in the <code>corpus</code>
   *  then contains (annotation type * AnnotationDiffer) */
  protected ArrayList<HashMap<String, AnnotationDiffer>> differsByDocThenType =
    new ArrayList<HashMap<String, AnnotationDiffer>>();
  protected ArrayList<String> documentNames = new ArrayList<String>();
  protected String keySetName;
  protected String responseSetName;
  protected Object[] typesSelected;
  protected Object[] featuresSelected;
  protected Timer timer = new Timer("CorpusQualityAssurance", true);
  protected TimerTask timerTask;
  protected Thread readSetsTypesFeaturesThread;
  /** FSCORE_MEASURES or CLASSIFICATION_MEASURES */
  protected int measuresType;
  protected static final int FSCORE_MEASURES = 0;
  protected static final int CLASSIFICATION_MEASURES = 1;
  protected Collator collator;
  protected Comparator<String> doubleComparator;
  protected Comparator<String> totalComparator;
  protected OptionsMap userConfig = Gate.getUserConfig();
  protected URL bdmFileUrl;

  // user interface components
  protected XJTable documentTable;
  protected DefaultTableModel documentTableModel;
  protected XJTable annotationTable;
  protected DefaultTableModel annotationTableModel;
  protected XJTable document2Table;
  protected DefaultTableModel document2TableModel;
  protected XJTable confusionTable;
  protected DefaultTableModel confusionTableModel;
  protected JTabbedPane tableTabbedPane;
  protected JList setList;
  protected JList typeList;
  protected JList featureList;
  protected JToggleButton optionsButton;
  protected JTabbedPane measureTabbedPane;
  protected JList measureList;
  protected JList measure2List;
  protected JCheckBox setCheck;
  protected JCheckBox typeCheck;
  protected JCheckBox featureCheck;
  protected JProgressBar progressBar;
  protected JCheckBox verboseOptionCheckBox;

  // actions
  protected OpenDocumentAction openDocumentAction;
  protected OpenAnnotationDiffAction openAnnotationDiffAction;
  protected ExportToHtmlAction exportToHtmlAction;
  protected ReloadCacheAction reloadCacheAction;
  protected CompareAction compareAction;
}