package edu.cmu.minorthird.text.gui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.io.File;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTextPane;
import javax.swing.ListCellRenderer;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.AttributeSet;
import org.apache.log4j.Logger;
import edu.cmu.minorthird.text.FancyLoader;
import edu.cmu.minorthird.text.MonotonicTextLabels;
import edu.cmu.minorthird.text.Span;
import edu.cmu.minorthird.text.SpanDifference;
import edu.cmu.minorthird.text.TextBase;
import edu.cmu.minorthird.text.TextLabels;
import edu.cmu.minorthird.text.mixup.MixupInterpreter;
import edu.cmu.minorthird.text.mixup.MixupProgram;
/**
* Interactively view the contents of a TextBase and TextLabels.
*
* @author William Cohen
*/
public class TextBaseViewer extends JComponent{
static final long serialVersionUID=200803014L;
public static final String NULL_TRUTH_ENTRY="-compare to-";
public static final String NULL_DISPLAY_TYPE="-top-";
// links to outside
private final StatusMessage statusMsg;
// internal state
private final TextBase base;
private TextLabels labels;
// components
private JList documentList;
private JSlider documentCellHeightSlider;
private JComboBox displayedTypeBox;
private JCheckBox editedOnlyCheckBox;
private JComboBox guessBox;
private JComboBox truthBox;
public HighlightAction highlightAction;
private static Logger log=Logger.getLogger(TextBaseViewer.class);
public JList getDocumentList(){
return documentList;
}
public JComboBox getGuessBox(){
return guessBox;
}
public JComboBox getTruthBox(){
return truthBox;
}
public JComboBox getDisplayedTypeBox(){
return displayedTypeBox;
}
public SpanPainter getSpanPainter(){
return highlightAction;
}
/** change the text labels */
public void updateTextLabels(TextLabels newLabels){
this.labels=newLabels;
// initializeLayout(null);
highlightAction.paintDocument(null); // repaint everything
for(Iterator<String> i=labels.getTypes().iterator();i.hasNext();){
String label=i.next();
boolean contains=false;
for(int j=0;j<truthBox.getItemCount();j++){
String item=(String)truthBox.getItemAt(j);
if(item.equals(label))
contains=true;
}
if(!contains)
truthBox.addItem(label);
}
}
public TextBaseViewer(TextBase base,TextLabels labels,StatusMessage statusMsg){
this(base,labels,null,statusMsg);
}
public TextBaseViewer(TextBase base,TextLabels labels,String displayType,
StatusMessage statusMsg){
this.base=base;
this.labels=labels;
this.statusMsg=statusMsg;
initializeLayout(displayType);
}
// lay out the main window
private void initializeLayout(String displayType){
documentList=new JList();
documentList.setFixedCellWidth(760);
resetDocumentList(labels,displayType,false);
// select 'guess' spans
guessBox=new JComboBox();
guessBox.setEditable(false);
for(Iterator<String> i=labels.getTypes().iterator();i.hasNext();){
String type=i.next();
guessBox.addItem(type);
}
// select 'truth' spans
truthBox=new JComboBox();
// truthBox.setEditable(false);
truthBox.addItem(NULL_TRUTH_ENTRY);
log.debug("types: "+labels.getTypes());
for(Iterator<String> i=labels.getTypes().iterator();i.hasNext();){
truthBox.addItem(i.next());
}
// select spans to display in the documentList
displayedTypeBox=new JComboBox();
displayedTypeBox.setEditable(false);
displayedTypeBox.addItem(NULL_DISPLAY_TYPE);
for(Iterator<String> i=labels.getTypes().iterator();i.hasNext();){
displayedTypeBox.addItem(i.next().toString());
if(displayType!=null)
displayedTypeBox.setSelectedItem(displayType);
}
ResetDocumentListAction resetDocumentListAction=
new ResetDocumentListAction();
displayedTypeBox.addActionListener(resetDocumentListAction);
editedOnlyCheckBox=new JCheckBox("Unlabeled");
editedOnlyCheckBox.addActionListener(resetDocumentListAction);
// create the highlightAction
highlightAction=
new HighlightAction("Highlight",guessBox,truthBox,documentList);
// action for highlighting happens when guessBox, truthBox changes or button
// pressed
guessBox.addActionListener(highlightAction);
truthBox.addActionListener(highlightAction);
JButton highlightButton=new JButton(highlightAction);
documentCellHeightSlider=new DocumentCellHeightSlider(documentList);
//
// layout stuff
//
setPreferredSize(new Dimension(800,600));
setBorder(BorderFactory.createEmptyBorder(4,4,4,4)); // 4 = border width
setLayout(new GridBagLayout());
GridBagConstraints gbc;
// row 2 - the toolbar
int col=0;
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.HORIZONTAL;
gbc.weightx=1.0;
gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(highlightButton,gbc);
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.HORIZONTAL;
gbc.weightx=1.0;
gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(guessBox,gbc);
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.HORIZONTAL;
gbc.weightx=1.0;
gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(truthBox,gbc);
gbc=new GridBagConstraints();
gbc.weightx=gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(new JLabel("H:"),gbc);
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.HORIZONTAL;
gbc.weightx=1.0;
gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(documentCellHeightSlider,gbc);
gbc=new GridBagConstraints();
gbc.weightx=gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(new JLabel("W:"),gbc);
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.HORIZONTAL;
gbc.weightx=1.0;
gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(new DocumentCellWidthSlider(documentList),gbc);
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.HORIZONTAL;
gbc.weightx=1.0;
gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(new JButton(new ZoomAction("Font-2",-2,documentList)),gbc);
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.HORIZONTAL;
gbc.weightx=1.0;
gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(new JButton(new ZoomAction("Font+2",+2,documentList)),gbc);
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.HORIZONTAL;
gbc.weightx=1.0;
gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(displayedTypeBox,gbc);
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.HORIZONTAL;
gbc.weightx=1.0;
gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(editedOnlyCheckBox,gbc);
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.HORIZONTAL;
gbc.weightx=1.0;
gbc.weighty=0.0;
gbc.gridx=++col;
gbc.gridy=2;
add(new ContextWidthSlider(documentList),gbc);
// row 1
gbc=new GridBagConstraints();
gbc.fill=GridBagConstraints.BOTH;
gbc.weightx=gbc.weighty=2.0;
gbc.gridx=1;
gbc.gridy=1;
gbc.gridwidth=col;
add(new JScrollPane(documentList),gbc);
}
synchronized private void resetDocumentList(TextLabels labels,
String displayType,boolean onlyEditedSpans){
// collect all the top-level spans, and put them in a JList
log.debug("reset for type "+displayType);
List<Span> spans=new ArrayList<Span>();
Iterator<Span> i=null;
if(displayType==null||NULL_DISPLAY_TYPE.equals(displayType)){
i=base.documentSpanIterator();
}else{
i=labels.instanceIterator(displayType);
}
// decide what type to filter on, if any
String truthType=NULL_TRUTH_ENTRY;
if(truthBox!=null)
truthType=(String)truthBox.getSelectedItem();
if(NULL_TRUTH_ENTRY.equals(truthType))
onlyEditedSpans=false;
while(i.hasNext()){
Span s=i.next();
if(!onlyEditedSpans){
spans.add(s);
}else{
// see if this span has been edited
boolean wasEdited=false;
for(Iterator<Span> j=
labels.closureIterator(truthType,s.getDocumentId());j.hasNext();){
Span t=j.next();
if(t.contains(s))
wasEdited=true;
}
if(!wasEdited)
spans.add(s);
}
}
if(spans.size()==0){
statusMsg.display("no"+(onlyEditedSpans?" unedited":"")+
" spans of type "+displayType);
}else{
synchronized(documentList){
statusMsg.display("Displaying "+spans.size()+
(onlyEditedSpans?" unedited":"")+" spans of type "+displayType);
Span[] spanArray=spans.toArray(new Span[spans.size()]);
if(documentList==null)
documentList=new JList();
documentList.setVisible(false);
documentList.setListData(spanArray);
SpanRenderer renderer=new SpanRenderer(spanArray);
documentList.setCellRenderer(renderer);
documentList.setVisible(true);
documentList.repaint();
if(highlightAction!=null)
highlightAction.paintDocument(null); // repaint everything
statusMsg.display("Displaying "+spans.size()+
(onlyEditedSpans?" unedited":"")+" spans of type "+displayType);
}
}
}
/**
* highlights text in the spans displayed in documentList based on the options
* in truthBox, guessBox
*/
public class HighlightAction extends AbstractAction implements SpanPainter{
static final long serialVersionUID=200803014L;
private JComboBox guessBox,truthBox;
private JList documentList;
private SpanDifference spanDifference;
public HighlightAction(String msg,JComboBox guessBox,JComboBox truthBox,
JList documentList){
super(msg);
this.guessBox=guessBox;
this.truthBox=truthBox;
this.documentList=documentList;
}
@Override
public void actionPerformed(ActionEvent event){
synchronized(documentList){
paintDocument(null); // paint everything
}
DecimalFormat fmt=new DecimalFormat("##0.000");
double tr=spanDifference.tokenRecall()*100;
double tp=spanDifference.tokenPrecision()*100;
double sr=spanDifference.spanRecall()*100;
double sp=spanDifference.spanPrecision()*100;
statusMsg.display("Token recall: "+fmt.format(tr)+" precision: "+
fmt.format(tp)+" Span recall: "+fmt.format(sr)+" precision: "+
fmt.format(sp));
}
@Override
public void paintDocument(String documentId){
synchronized(documentList){
try{
SpanRenderer renderer=(SpanRenderer)documentList.getCellRenderer();
// System.out.println("TBV: call differenceIterator "+documentId);
renderer.highlightDiffs(differenceIterator(documentId),documentId,
fpColor(),fnColor(),tpColor(),mpColor());
documentList.repaint(); // seems to be needed
}catch(Exception e){
System.out.println("error: "+e);
e.printStackTrace();
}
}
}
// colors for false pos, false neg, true pos, "maybe" pos
@Override
public AttributeSet fpColor(){
return nullTruthType()?HiliteColors.yellow:HiliteColors.red;
}
@Override
public AttributeSet fnColor(){
return nullTruthType()?HiliteColors.yellow:HiliteColors.blue;
}
@Override
public AttributeSet tpColor(){
return nullTruthType()?HiliteColors.yellow:HiliteColors.green;
}
@Override
public AttributeSet mpColor(){
return HiliteColors.yellow;
}
// figure out what spans to update...
@Override
public SpanDifference.Looper differenceIterator(String documentId){
String guessType=(String)guessBox.getSelectedItem();
String truthType=(String)truthBox.getSelectedItem();
Iterator<Span> guessLooper=
documentId==null?labels.instanceIterator(guessType):labels
.instanceIterator(guessType,documentId);
if(nullTruthType()){
Iterator<Span> nullLooper=new HashSet<Span>().iterator();
spanDifference=new SpanDifference(guessLooper,nullLooper);
return spanDifference.differenceIterator();
}else{
// System.out.println("TBV: truthType: "+truthType+" document:" +
// documentId);
Iterator<Span> truthSpanLooper=
documentId==null?labels.instanceIterator(truthType):labels
.instanceIterator(truthType,documentId);
Iterator<Span> closureSpanLooper=
documentId==null?labels.closureIterator(truthType):labels
.closureIterator(truthType,documentId);
/*
* System.out.println("TBV: iterating over "+truthType+" spans"); for
* (Span.Looper ii=labels.instanceIterator(truthType,documentId);
* ii.hasNext(); ) { System.out.println("TBV: span type "+truthType+":
* "+ii.nextSpan()); } for (Span.Looper
* ii=labels.closureIterator(truthType,documentId); ii.hasNext(); ) {
* System.out.println("TBV: closure span "+ii.nextSpan()); }
*/
spanDifference=
new SpanDifference(guessLooper,truthSpanLooper,closureSpanLooper);
return spanDifference.differenceIterator();
}
}
private boolean nullTruthType(){
return NULL_TRUTH_ENTRY.equals(truthBox.getSelectedItem());
}
}
private class ResetDocumentListAction extends AbstractAction{
static final long serialVersionUID=200803014L;
public ResetDocumentListAction(){
super("Display");
}
@Override
public void actionPerformed(ActionEvent event){
synchronized(documentList){
String type=(String)displayedTypeBox.getSelectedItem();
resetDocumentList(labels,type,editedOnlyCheckBox.isSelected());
documentList.repaint();
}
}
}
private class ZoomAction extends AbstractAction{
static final long serialVersionUID=200803014L;
private JList documentList;
private int sizeDelta;
public ZoomAction(String msg,int sizeDelta,JList documentList){
super(msg);
this.sizeDelta=sizeDelta;
this.documentList=documentList;
}
@Override
public void actionPerformed(ActionEvent event){
synchronized(documentList){
SpanRenderer renderer=(SpanRenderer)documentList.getCellRenderer();
renderer.zoomFont(sizeDelta);
documentList.repaint();
}
}
}
private class ContextWidthSlider extends JSlider{
static final long serialVersionUID=200803014L;
final private JList documentList;
public ContextWidthSlider(final JList documentList){
super(0,10,0);
this.documentList=documentList;
addChangeListener(new ChangeListener(){
@Override
public void stateChanged(ChangeEvent e){
synchronized(documentList){
ContextWidthSlider slider=(ContextWidthSlider)e.getSource();
if(!slider.getValueIsAdjusting()){
int value=slider.getValue();
SpanRenderer renderer=
(SpanRenderer)slider.documentList.getCellRenderer();
renderer.setContextWidth(value);
slider.documentList.repaint();
}
}
}
});
}
}
private class DocumentCellHeightSlider extends JSlider{
static final long serialVersionUID=200803014L;
public DocumentCellHeightSlider(final JList documentList){
super(-1,100,-1);
addChangeListener(new ChangeListener(){
@Override
public void stateChanged(ChangeEvent e){
synchronized(documentList){
DocumentCellHeightSlider slider=
(DocumentCellHeightSlider)e.getSource();
if(!slider.getValueIsAdjusting()){
documentList.setFixedCellHeight(slider.getValue());
}
}
}
});
}
}
private class DocumentCellWidthSlider extends JSlider{
static final long serialVersionUID=200803014L;
public DocumentCellWidthSlider(final JList vList){
super(-1,760,760);
addChangeListener(new ChangeListener(){
@Override
public void stateChanged(ChangeEvent e){
synchronized(vList){
DocumentCellWidthSlider slider=
(DocumentCellWidthSlider)e.getSource();
if(!slider.getValueIsAdjusting()){
vList.setFixedCellWidth(slider.getValue());
}
}
}
});
}
}
/**
* ListCellRenderer for the List of Documents. This has some additional
* methods for highlighting spans.
*/
private class SpanRenderer implements ListCellRenderer{
// a span plus a color to show it in
private class SpanMarkup implements Comparable<SpanMarkup>{
public Span span;
public AttributeSet color;
public SpanMarkup(Span span,AttributeSet color){
this.span=span;
this.color=color;
}
@Override
public int compareTo(SpanMarkup cspan){
return span.compareTo(cspan.span);
}
}
private JComponent spanComponents[]; // component for each span
private SpanDocument spanDocs[]; // doc to hold each span
private SortedSet<SpanMarkup>[] spanMarkups; // markup for each span
private boolean[] spanIsDirty; // true if document needs to be updated
private Span[] spans; // action spans
private Map<Span,Integer> indexOfSpanMap; // maps span to index in arrays above
private Map<String,List<Span>> spansWithDocumentMap; // find all spans with a particular
// documentId
private Font currentFont; // font being used
private int contextWidth=0; // how much context to show
public SpanRenderer(Span[] spans){
this.spans=spans;
// work out default font
currentFont=new JTextPane().getFont();
synchronized(documentList){
// cache out renderers and rendered documents for each span
spanComponents=new JComponent[spans.length];
spanDocs=new SpanDocument[spans.length];
spanMarkups=new TreeSet[spans.length];
spanIsDirty=new boolean[spans.length];
spansWithDocumentMap=new HashMap<String,List<Span>>();
indexOfSpanMap=new HashMap<Span,Integer>();
for(int i=0;i<spans.length;i++){
// log.debug("render span loop on span: " + spans[i].asString());
spanDocs[i]=new SpanDocument(spans[i],contextWidth);
spanMarkups[i]=new TreeSet<SpanMarkup>();
spanIsDirty[i]=false;
refreshSpanComponent(i);
// update indexOfSpanMap
indexOfSpanMap.put(spans[i],new Integer(i));
// update spansWithDocumentMap
String documentId=spans[i].getDocumentId();
List<Span> spansWithDocument=spansWithDocumentMap.get(documentId);
if(spansWithDocument==null){
spansWithDocumentMap.put(documentId,(spansWithDocument=
new ArrayList<Span>()));
}
spansWithDocument.add(spans[i]);
}
}
}
// used to force repaint of a component
private void refreshSpanComponent(int i){
synchronized(documentList){
JTextPane pane=new JTextPane(spanDocs[i]);
pane.setFont(currentFont);
// spanComponents[i] = new JScrollPane(pane);
spanComponents[i]=pane;
spanComponents[i]
.setBorder(BorderFactory.createLineBorder(Color.black));
}
}
public void setContextWidth(int contextWidth){
synchronized(documentList){
documentList.setVisible(false);
this.contextWidth=contextWidth;
for(int i=0;i<spanComponents.length;i++){
spanDocs[i]=new SpanDocument(spans[i],contextWidth);
spanIsDirty[i]=true;
refreshSpanComponent(i);
}
documentList.setVisible(true);
documentList.repaint();
}
}
/** Implement the ListCellRenderer interface. */
@Override
public Component getListCellRendererComponent(JList el,Object v,int index,
boolean sel,boolean focus){
// handle synchronization errors? there oughta be a better way!
if(spanIsDirty==null||index>=spanIsDirty.length){
return new JLabel("sync error?");
}
synchronized(documentList){
if(spanIsDirty[index]){
SortedSet<SpanMarkup> marks=spanMarkups[index];
spanDocs[index].resetHighlights();
for(Iterator<SpanMarkup> i=marks.iterator();i.hasNext();){
SpanMarkup m=i.next();
spanDocs[index].highlight(m.span,m.color);
}
refreshSpanComponent(index);
spanIsDirty[index]=false;
}
if(sel)
spanComponents[index].setBorder(BorderFactory.createLineBorder(
Color.blue,2));
else
spanComponents[index].setBorder(BorderFactory.createLineBorder(
Color.black,2));
return spanComponents[index];
}
}
/** Highlight differences between two sets of spans. */
public void highlightDiffs(SpanDifference.Looper i,String documentId,
AttributeSet fp,AttributeSet fn,AttributeSet tp,AttributeSet mp){
// to decode status into a color
AttributeSet[] colorByStatus=
new AttributeSet[SpanDifference.MAX_STATUS+1];
colorByStatus[SpanDifference.FALSE_POS]=fp;
colorByStatus[SpanDifference.FALSE_NEG]=fn;
colorByStatus[SpanDifference.TRUE_POS]=tp;
colorByStatus[SpanDifference.UNKNOWN_POS]=mp;
synchronized(documentList){
if(documentId!=null){
// clear old markup from these spans
List<Span> spansWithDocument=spansWithDocumentMap.get(documentId);
for(Iterator<Span> j=spansWithDocument.iterator();j.hasNext();){
int index=indexOfSpanMap.get(j.next()).intValue();
spanDocs[index].resetHighlights();
spanMarkups[index]=new TreeSet<SpanMarkup>();
spanIsDirty[index]=true;
}
}else{
// clear old markup from all spans
for(int j=0;j<spanDocs.length;j++){
if(!spanMarkups[j].isEmpty()){
spanDocs[j].resetHighlights();
spanMarkups[j]=new TreeSet<SpanMarkup>();
spanIsDirty[j]=true;
}
}
}
// highlight the differences
while(i.hasNext()){
Span diffSpan=i.next();
int status=i.getStatus();
String documentIdOfS=diffSpan.getDocumentId();
List<Span> spansWithDocument=spansWithDocumentMap.get(documentIdOfS);
if(spansWithDocument!=null){
for(Iterator<Span> j=spansWithDocument.iterator();j.hasNext();){
// t is a span with the same document as diffSpan
Span t=j.next();
int indexOfT=indexOfSpanMap.get(t).intValue();
SortedSet<SpanMarkup> marks=spanMarkups[indexOfT];
marks.add(new SpanMarkup(diffSpan,colorByStatus[status]));
spanIsDirty[indexOfT]=true;
}
}
}
}
}
/** Increase/decrease font size by delta */
public void zoomFont(int delta){
synchronized(documentList){
int currentSize=currentFont.getSize();
String newFont=currentFont.getFamily()+"-plain-"+(currentSize+delta);
currentFont=Font.decode(newFont);
statusMsg.display("current font is "+newFont);
for(int i=0;i<spanComponents.length;i++){
refreshSpanComponent(i);
}
}
}
}
/** Pop up a frame for viewing the labels. */
public static void view(TextLabels labels){
JFrame frame=new JFrame("TextBaseViewer");
TextBase base=labels.getTextBase();
StatusMessage statusMsg=new StatusMessage();
TextBaseViewer viewer=new TextBaseViewer(base,labels,statusMsg);
JComponent main=new StatusMessagePanel(viewer,statusMsg);
frame.getContentPane().add(main,BorderLayout.CENTER);
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args){
try{
TextLabels labels=FancyLoader.loadTextLabels(args[0]);
if(args.length>1){
MixupProgram p=new MixupProgram(new File(args[1]));
MixupInterpreter interp=new MixupInterpreter(p);
interp.eval((MonotonicTextLabels)labels);
}
view(labels);
}catch(Exception e){
e.printStackTrace();
System.out.println("usage: TextBaseViewer key [mixupProgram]");
}
}
}