| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.uima.tools.viewer; |
| |
| import java.awt.BorderLayout; |
| import java.awt.Color; |
| import java.awt.Component; |
| import java.awt.ComponentOrientation; |
| import java.awt.Dimension; |
| import java.awt.GridLayout; |
| import java.awt.Insets; |
| import java.awt.Rectangle; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.awt.event.ItemEvent; |
| import java.awt.event.ItemListener; |
| import java.awt.event.MouseEvent; |
| import java.awt.event.MouseListener; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Hashtable; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.StringTokenizer; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import javax.swing.ButtonGroup; |
| import javax.swing.JButton; |
| import javax.swing.JCheckBox; |
| import javax.swing.JComboBox; |
| import javax.swing.JLabel; |
| import javax.swing.JPanel; |
| import javax.swing.JRadioButton; |
| import javax.swing.JScrollPane; |
| import javax.swing.JSplitPane; |
| import javax.swing.JTextPane; |
| import javax.swing.JTree; |
| import javax.swing.Scrollable; |
| import javax.swing.event.TreeExpansionEvent; |
| import javax.swing.event.TreeExpansionListener; |
| import javax.swing.event.TreeWillExpandListener; |
| import javax.swing.text.BadLocationException; |
| import javax.swing.text.DefaultStyledDocument; |
| import javax.swing.text.Document; |
| import javax.swing.text.MutableAttributeSet; |
| import javax.swing.text.SimpleAttributeSet; |
| import javax.swing.text.StyleConstants; |
| import javax.swing.text.StyledDocument; |
| import javax.swing.tree.DefaultMutableTreeNode; |
| import javax.swing.tree.DefaultTreeCellRenderer; |
| import javax.swing.tree.DefaultTreeModel; |
| import javax.swing.tree.ExpandVetoException; |
| import javax.swing.tree.TreeNode; |
| import javax.swing.tree.TreePath; |
| |
| import org.apache.uima.cas.ArrayFS; |
| import org.apache.uima.cas.CAS; |
| import org.apache.uima.cas.CASException; |
| import org.apache.uima.cas.FSIterator; |
| import org.apache.uima.cas.Feature; |
| import org.apache.uima.cas.FeatureStructure; |
| import org.apache.uima.cas.SofaFS; |
| import org.apache.uima.cas.Type; |
| import org.apache.uima.cas.impl.BooleanArrayFSImpl; |
| import org.apache.uima.cas.impl.ByteArrayFSImpl; |
| import org.apache.uima.cas.impl.DoubleArrayFSImpl; |
| import org.apache.uima.cas.impl.FloatArrayFSImpl; |
| import org.apache.uima.cas.impl.IntArrayFSImpl; |
| import org.apache.uima.cas.impl.LongArrayFSImpl; |
| import org.apache.uima.cas.impl.ShortArrayFSImpl; |
| import org.apache.uima.cas.impl.StringArrayFSImpl; |
| import org.apache.uima.cas.text.AnnotationFS; |
| import org.apache.uima.jcas.JCas; |
| import org.apache.uima.jcas.tcas.Annotation; |
| |
| /** |
| * A Swing component that displays annotations in a text pane with highlighting. There is also a |
| * tree view for display details of annotations on which the user clicks. This class extends |
| * {@link JPanel} and so can be reused within any Swing application. |
| * <p> |
| * To launch the viewer, call the {@link #setCAS(CAS)} method with the CAS to be viewed. |
| * <p> |
| * The viewer is configurable via the following methods: |
| * <ul> |
| * <li>{@link #setConsistentColors(boolean)} - if set to true (default), the color assigned to any |
| * annotation type will be the same across all documents. If set to false, colors may vary across |
| * documents.</li> |
| * <li>{@link #setDisplayedTypes(String[])} - specifies a set of types that will be highlighted in |
| * the viewer's text pane.</li> |
| * <li>{@link #setHiddenTypes(String[])} - specifies a set of types that will NOT be highlighted in |
| * the viewer's text pane.</li> |
| * <li>{@link #setHiddenFeatures(String[])} - specifies a set of features that will never shown in |
| * the viewer's annotation details tree.</li> |
| * <li>{@link #setHighFrequencyTypes(String[])} - this can be used to specify a set of types that |
| * occur frequently. These types will the be assigned the most distinguishable colors.</li> |
| * <li>{@link #setInitiallySelectedTypes(String[])} - this can be used to specify a set of types |
| * that will initially be selected (i.e. have their checkboxes checked) in the viewer. The default |
| * is for all types to be initially selected.</li> |
| * <li>{@link #setRightToLeftTextOrientation(boolean)} - switches the text pane from left-to-right |
| * (default) to right-to-left mode. This is needed to support languages such as Arabic and Hebrew, |
| * which are read from right to left.</li> |
| * </ul> |
| */ |
| public class CasAnnotationViewer extends JPanel implements ActionListener, MouseListener, |
| TreeWillExpandListener, TreeExpansionListener, ItemListener { |
| private static final long serialVersionUID = 3559118488371946999L; |
| |
| // Mode constants |
| private static final short MODE_ANNOTATIONS = 0; |
| |
| private static final short MODE_ENTITIES = 1; |
| |
| private ArrayList userTypes = null; |
| |
| /** |
| * @return Returns the userTypes. |
| */ |
| public ArrayList getUserTypes() { |
| return userTypes; |
| } |
| |
| /** |
| * @param userTypes |
| * The userTypes to set. |
| */ |
| public void setUserTypes(ArrayList userTypes) { |
| this.userTypes = userTypes; |
| } |
| |
| // colors to use for highlighting annotations |
| // (use high brightness for best contrast against black text) |
| private static final float BRIGHT = 0.95f; |
| |
| private static final Color[] COLORS = new Color[] { |
| // low saturation colors are best, so put them first |
| Color.getHSBColor(55f / 360, 0.25f, BRIGHT), // butter yellow? |
| Color.getHSBColor(0f / 360, 0.25f, BRIGHT), // pink? |
| Color.getHSBColor(210f / 360, 0.25f, BRIGHT), // baby blue? |
| Color.getHSBColor(120f / 360, 0.25f, BRIGHT), // mint green? |
| Color.getHSBColor(290f / 360, 0.25f, BRIGHT), // lavender? |
| Color.getHSBColor(30f / 360, 0.25f, BRIGHT), // tangerine? |
| Color.getHSBColor(80f / 360, 0.25f, BRIGHT), // celery green? |
| Color.getHSBColor(330f / 360, 0.25f, BRIGHT), // light coral? |
| Color.getHSBColor(160f / 360, 0.25f, BRIGHT), // aqua? |
| Color.getHSBColor(250f / 360, 0.25f, BRIGHT), // light violet? |
| // higher saturation colors |
| Color.getHSBColor(55f / 360, 0.5f, BRIGHT), Color.getHSBColor(0f / 360, 0.5f, BRIGHT), |
| Color.getHSBColor(210f / 360, 0.5f, BRIGHT), Color.getHSBColor(120f / 360, 0.5f, BRIGHT), |
| Color.getHSBColor(290f / 360, 0.5f, BRIGHT), |
| Color.getHSBColor(30f / 360, 0.5f, BRIGHT), |
| Color.getHSBColor(80f / 360, 0.5f, BRIGHT), |
| Color.getHSBColor(330f / 360, 0.5f, BRIGHT), |
| Color.getHSBColor(160f / 360, 0.5f, BRIGHT), |
| Color.getHSBColor(250f / 360, 0.5f, BRIGHT), |
| // even higher saturation colors |
| Color.getHSBColor(55f / 360, 0.75f, BRIGHT), Color.getHSBColor(0f / 360, 0.75f, BRIGHT), |
| Color.getHSBColor(210f / 360, 0.75f, BRIGHT), Color.getHSBColor(120f / 360, 0.75f, BRIGHT), |
| Color.getHSBColor(290f / 360, 0.75f, BRIGHT), Color.getHSBColor(30f / 360, 0.75f, BRIGHT), |
| Color.getHSBColor(80f / 360, 0.75f, BRIGHT), Color.getHSBColor(330f / 360, 0.75f, BRIGHT), |
| Color.getHSBColor(160f / 360, 0.75f, BRIGHT), Color.getHSBColor(250f / 360, 0.75f, BRIGHT) }; |
| |
| private static String[] DEFAULT_HIDDEN_FEATURES = { "sofa" }; |
| |
| private Map mTypeNameToColorMap = new HashMap(); |
| |
| private HashSet noCheckSet = new HashSet(); |
| |
| private List mHighFrequencyTypes = new ArrayList(); |
| |
| private Set mDisplayedTypeNames = null; |
| |
| private Set mHiddenTypeNames = new HashSet(); |
| |
| private Set mInitiallySelectedTypeNames = null; |
| |
| private Map mTypeToCheckboxMap = new HashMap(); |
| |
| private Map mEntityToCheckboxMap = new HashMap(); |
| |
| private CAS mCAS; |
| |
| private Type mStringType; |
| |
| private Type mFsArrayType; |
| |
| private boolean mConsistentColors = true; |
| |
| private Set mHiddenFeatureNames = new HashSet(); |
| |
| private boolean mEntityViewEnabled = false; |
| |
| private short mViewMode = MODE_ANNOTATIONS; |
| |
| private boolean mHideUnselectedCheckboxes = false; |
| |
| // GUI components |
| private JSplitPane horizSplitPane; |
| |
| private JSplitPane vertSplitPane; |
| |
| private JScrollPane textScrollPane; |
| |
| private JTextPane textPane; |
| |
| private JPanel legendPanel; |
| |
| private JLabel legendLabel; |
| |
| private JScrollPane legendScrollPane; |
| |
| private JPanel annotationCheckboxPanel; |
| |
| private JPanel entityCheckboxPanel; |
| |
| private JPanel buttonPanel; |
| |
| private JButton selectAllButton; |
| |
| private JButton deselectAllButton; |
| |
| private JButton showHideUnselectedButton; |
| |
| private JTree selectedAnnotationTree; |
| |
| private DefaultTreeModel selectedAnnotationTreeModel; |
| |
| private JPanel viewModePanel; |
| |
| private JRadioButton annotationModeButton; |
| |
| private JRadioButton entityModeButton; |
| |
| private String[] mBoldfaceKeywords = new String[0]; |
| |
| private int[] mBoldfaceSpans = new int[0]; |
| |
| private JPanel sofaSelectionPanel; |
| |
| private JComboBox sofaSelectionComboBox; |
| |
| private EntityResolver mEntityResolver = new DefaultEntityResolver(); |
| |
| /** |
| * Creates a CAS Annotation Viewer. |
| */ |
| public CasAnnotationViewer() { |
| // create a horizonal JSplitPane |
| horizSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); |
| horizSplitPane.setResizeWeight(0.6); |
| this.setLayout(new BorderLayout()); |
| this.add(horizSplitPane); |
| |
| // create a vertical JSplitPane and add to left of horizSplitPane |
| vertSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); |
| vertSplitPane.setResizeWeight(0.8); |
| vertSplitPane.setPreferredSize(new Dimension(620, 600)); |
| vertSplitPane.setMinimumSize(new Dimension(200, 200)); |
| horizSplitPane.setLeftComponent(vertSplitPane); |
| |
| // add JTextPane to top of vertical split pane |
| textPane = new JTextPane(); |
| textPane.setEditable(false); |
| textPane.setPreferredSize(new Dimension(620, 400)); |
| textPane.setMinimumSize(new Dimension(200, 100)); |
| textScrollPane = new JScrollPane(textPane); |
| vertSplitPane.setTopComponent(textScrollPane); |
| |
| // bottom pane is the legend, with checkboxes |
| legendPanel = new JPanel(); |
| legendPanel.setPreferredSize(new Dimension(620, 200)); |
| legendPanel.setLayout(new BorderLayout()); |
| legendLabel = new JLabel("Legend"); |
| legendPanel.add(legendLabel, BorderLayout.NORTH); |
| // checkboxes are contained in a scroll pane |
| legendScrollPane = new JScrollPane(); |
| legendScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); |
| legendPanel.add(legendScrollPane, BorderLayout.CENTER); |
| // there are two checkbox panels - one for annotations, one for entities |
| annotationCheckboxPanel = new VerticallyScrollablePanel(); |
| annotationCheckboxPanel.setLayout(new GridLayout(0, 5)); |
| entityCheckboxPanel = new VerticallyScrollablePanel(); |
| entityCheckboxPanel.setLayout(new GridLayout(0, 4)); |
| // add annotation panel first, since that is the default |
| legendScrollPane.setViewportView(annotationCheckboxPanel); |
| |
| // at very bottom is the Button Panel, with Select All, Deselect All, |
| // and Hide/Show Unselected Buttons. It also may show the sofa-selection |
| // combo box and/or the Viewer Mode radio group; either are both may |
| // be hidden. |
| buttonPanel = new JPanel(); |
| |
| selectAllButton = new JButton("Select All"); |
| selectAllButton.addActionListener(this); |
| buttonPanel.add(selectAllButton); |
| deselectAllButton = new JButton("Deselect All"); |
| deselectAllButton.addActionListener(this); |
| buttonPanel.add(deselectAllButton); |
| showHideUnselectedButton = new JButton("Hide Unselected"); |
| showHideUnselectedButton.addActionListener(this); |
| buttonPanel.add(showHideUnselectedButton); |
| |
| sofaSelectionPanel = new JPanel(); |
| JLabel sofaSelectionLabel = new JLabel("Sofa:"); |
| sofaSelectionPanel.add(sofaSelectionLabel); |
| sofaSelectionComboBox = new JComboBox(); |
| sofaSelectionPanel.add(sofaSelectionComboBox); |
| sofaSelectionComboBox.addItemListener(this); |
| buttonPanel.add(sofaSelectionPanel); |
| |
| viewModePanel = new JPanel(); |
| viewModePanel.add(new JLabel("Mode: ")); |
| annotationModeButton = new JRadioButton("Annotations"); |
| annotationModeButton.setSelected(true); |
| annotationModeButton.addActionListener(this); |
| viewModePanel.add(annotationModeButton); |
| entityModeButton = new JRadioButton("Entities"); |
| entityModeButton.addActionListener(this); |
| viewModePanel.add(entityModeButton); |
| ButtonGroup group = new ButtonGroup(); |
| group.add(annotationModeButton); |
| group.add(entityModeButton); |
| buttonPanel.add(viewModePanel); |
| viewModePanel.setVisible(false); |
| this.add(buttonPanel, BorderLayout.SOUTH); |
| |
| textPane.setMinimumSize(new Dimension(200, 100)); |
| vertSplitPane.setBottomComponent(legendPanel); |
| |
| // right pane has a JTree |
| selectedAnnotationTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode("Annotations")); |
| selectedAnnotationTree = new JTree(selectedAnnotationTreeModel) { |
| private static final long serialVersionUID = -7882967150283952907L; |
| |
| public Dimension getPreferredScrollableViewportSize() { |
| return new Dimension(230, 500); |
| } |
| }; |
| selectedAnnotationTree.setMinimumSize(new Dimension(50, 100)); |
| selectedAnnotationTree.setScrollsOnExpand(true); |
| selectedAnnotationTree.setRootVisible(true); |
| selectedAnnotationTree.setCellRenderer(new AnnotationTreeCellRenderer()); |
| selectedAnnotationTree.addTreeWillExpandListener(this); |
| selectedAnnotationTree.addTreeExpansionListener(this); |
| JPanel treePanel = new JPanel(); |
| treePanel.setLayout(new BorderLayout()); |
| treePanel.add(new JLabel("Click In Text to See Annotation Detail"), BorderLayout.NORTH); |
| |
| treePanel.add(new JScrollPane(selectedAnnotationTree), BorderLayout.CENTER); |
| horizSplitPane.setRightComponent(treePanel); |
| |
| // add mouse listener to update annotation tree |
| textPane.addMouseListener(this); |
| |
| // initialize hidden feature names map |
| mHiddenFeatureNames.addAll(Arrays.asList(DEFAULT_HIDDEN_FEATURES)); |
| } |
| |
| /** |
| * @deprecated use the zero-argument constructor and call {@link #setEntityViewEnabled(boolean)} |
| */ |
| @Deprecated |
| public CasAnnotationViewer(boolean aEntityViewEnabled) { |
| this(); |
| } |
| |
| /** |
| * Set the list of types that occur most frequently. This method assigns the most distinguishable |
| * colors to these types. |
| * |
| * @param aTypeNames |
| * names of types that are occur frequently. Ideally these should be ordered by |
| * frequency, with the most frequent being first. |
| */ |
| public void setHighFrequencyTypes(String[] aTypeNames) { |
| // store these types for later |
| mHighFrequencyTypes.clear(); |
| mHighFrequencyTypes.addAll(Arrays.asList(aTypeNames)); |
| mTypeNameToColorMap.clear(); |
| assignColors(mHighFrequencyTypes); |
| } |
| |
| /** |
| * Set the list of types that will be highlighted in the viewer. Types not in this list will not |
| * appear in the legend and will never be highlighted. If this method is not called, the default |
| * is to show all types in the CAS (except those specifically hidden by a call to |
| * {@link #setHiddenTypes(String[])}. |
| * |
| * @param aTypeNames |
| * names of types that are to be highlighted. Null indicates that all types in the CAS |
| * should be highlighted. |
| */ |
| public void setDisplayedTypes(String[] aDisplayedTypeNames) { |
| if (aDisplayedTypeNames == null) { |
| mDisplayedTypeNames = null; |
| } else { |
| mDisplayedTypeNames = new HashSet(); |
| mDisplayedTypeNames.addAll(Arrays.asList(aDisplayedTypeNames)); |
| } |
| } |
| |
| /** |
| * Set the list of types that will NOT be highlighted in the viewer. |
| * |
| * @param aTypeNames |
| * names of types that are never to be highlighted. |
| */ |
| public void setHiddenTypes(String[] aTypeNames) { |
| mHiddenTypeNames.clear(); |
| mHiddenTypeNames.addAll(Arrays.asList(aTypeNames)); |
| } |
| |
| /** |
| * Configures the initially selected types in the viewer. If not called, all types will be |
| * initially selected. |
| * |
| * @param aTypeNames |
| * array of fully-qualified names of types to be initially selected |
| */ |
| public void setInitiallySelectedTypes(String[] aTypeNames) { |
| mInitiallySelectedTypeNames = new HashSet(); |
| for (int i = 0; i < aTypeNames.length; i++) { |
| mInitiallySelectedTypeNames.add(aTypeNames[i]); |
| } |
| // apply to existing checkboxes |
| Iterator iterator = mTypeToCheckboxMap.entrySet().iterator(); |
| while (iterator.hasNext()) { |
| Map.Entry entry = (Map.Entry) iterator.next(); |
| String type = ((Type) entry.getKey()).getName(); |
| JCheckBox checkbox = (JCheckBox) entry.getValue(); |
| checkbox.setSelected(typeNamesContains(mInitiallySelectedTypeNames, type)); |
| } |
| |
| // redisplay (if we have a CAS) - this allows this method to be called |
| // either before or after displaying the viewer |
| if (mCAS != null) { |
| display(); |
| } |
| } |
| |
| /** |
| * Configures the viewer to hide certain features in the annotation deatail pane. |
| * |
| * @param aFeatureName |
| * array of (short) feature names to be hidden |
| */ |
| public void setHiddenFeatures(String[] aFeatureNames) { |
| mHiddenFeatureNames.clear(); |
| // add default hidden features |
| mHiddenFeatureNames.addAll(Arrays.asList(DEFAULT_HIDDEN_FEATURES)); |
| // add user-defined hidden features |
| mHiddenFeatureNames.addAll(Arrays.asList(aFeatureNames)); |
| } |
| |
| /** |
| * Configures whether the viewer will allow the user to switch to "Entity" view, which highlight |
| * entities rather than annotations. Entity mode is typically only useful if the |
| * {@link #setEntityResolver(EntityResolver)} method has been called with a user-supplied |
| * class that can determine which annotations refer to the same entity. |
| * |
| * @param aDisplayEntities |
| * true to enable entity viewing mode, false to allow annotation viewing only. |
| * The default is false. |
| */ |
| public void setEntityViewEnabled(boolean aEnabled) { |
| mEntityViewEnabled = aEnabled; |
| this.viewModePanel.setVisible(aEnabled); |
| } |
| |
| /** |
| * Sets the {@link EntityResolver} to use when the viewer is in entity mode. |
| * Entity mode must be turned on using the {@link #setEntityViewEnabled(boolean)} method. |
| * @param aEntityResolver user-supplied class that can determine which annotations correspond |
| * to the same entity. |
| */ |
| public void setEntityResolver(EntityResolver aEntityResolver) { |
| mEntityResolver = aEntityResolver; |
| } |
| |
| /** |
| * Sets whether colors will be consistent in all documents viewed using this viewer. If set to |
| * true, assignments of color to annotation type will persist across documents; if false, colors |
| * will be reassigned in each new document. (Note that if high frequency types are set via |
| * {@link #setHighFrequencyTypes(String[])}, the colors for those types will always be |
| * consistent, regardless of the value passed to this method. |
| * |
| * @param aConsistent |
| * true (the default) causes colors to be consistent across documents, false allows them |
| * to vary |
| */ |
| public void setConsistentColors(boolean aConsistent) { |
| mConsistentColors = aConsistent; |
| } |
| |
| /** |
| * Sets the text orientation. The default is left-to-right, but needs to be set to right-to-left |
| * to properly display some languages, most notably Arabic and Hebrew. |
| * |
| * @param aRightToLeft |
| * true to put the viewer in right-to-left mode, false for left-to-right (the default). |
| */ |
| public void setRightToLeftTextOrientation(boolean aRightToLeft) { |
| textPane.applyComponentOrientation(aRightToLeft ? ComponentOrientation.RIGHT_TO_LEFT |
| : ComponentOrientation.LEFT_TO_RIGHT); |
| } |
| |
| /** |
| * Sets whether unselected (unchecked) checkboxes will be hidden entirely from the legend. This |
| * mode makes for a cleaner legend at the expense of making it more difficult to toggle which |
| * types are selected. There's also a button in the GUI that lets the user change this setting. |
| * |
| * @param aConsistent |
| * true (the default) causes colors to be consistent across documents, false allows them |
| * to vary |
| */ |
| public void setHideUnselectedCheckboxes(boolean aHideUnselected) { |
| mHideUnselectedCheckboxes = aHideUnselected; |
| display(); |
| } |
| |
| /** |
| * Sets the CAS to be viewed. This must be called before {@link #display()}. |
| * |
| * @param aCAS |
| * the CSA to be viewed |
| */ |
| public void setCAS(CAS aCAS) { |
| mCAS = aCAS; |
| mStringType = mCAS.getTypeSystem().getType(CAS.TYPE_NAME_STRING); |
| mFsArrayType = mCAS.getTypeSystem().getType(CAS.TYPE_NAME_FS_ARRAY); |
| // clear checkbox panel so it will be repopulated |
| annotationCheckboxPanel.removeAll(); |
| entityCheckboxPanel.removeAll(); |
| mTypeToCheckboxMap.clear(); |
| mEntityToCheckboxMap.clear(); |
| // clear selected annotation details tree |
| this.updateSelectedAnnotationTree(-1); |
| |
| // clear type to color map if color consistency is off |
| if (!mConsistentColors) { |
| mTypeNameToColorMap.clear(); |
| // but reassign colors to high frequency types |
| assignColors(mHighFrequencyTypes); |
| } |
| |
| // clear boldface |
| mBoldfaceKeywords = new String[0]; |
| mBoldfaceSpans = new int[0]; |
| |
| // enable or disable entity view depending on user's choice |
| this.viewModePanel.setVisible(mEntityViewEnabled); |
| |
| // Populate sofa combo box with the names of all text Sofas in the CAS |
| sofaSelectionComboBox.removeAllItems(); |
| Iterator sofas = aCAS.getSofaIterator(); |
| Feature sofaIdFeat = aCAS.getTypeSystem().getFeatureByFullName(CAS.FEATURE_FULL_NAME_SOFAID); |
| boolean nonDefaultSofaFound = false; |
| while (sofas.hasNext()) { |
| SofaFS sofa = (SofaFS) sofas.next(); |
| if (sofa.getLocalStringData() != null) { |
| String sofaId = sofa.getStringValue(sofaIdFeat); |
| if (CAS.NAME_DEFAULT_SOFA.equals(sofaId)) { |
| sofaId = "DEFAULT"; // make nicer display |
| } else { |
| nonDefaultSofaFound = true; |
| } |
| sofaSelectionComboBox.addItem(sofaId); |
| // if this sofa matches the view passed to this method, select it |
| CAS viewOfSofa = aCAS.getView(sofa); |
| if (viewOfSofa == aCAS) { |
| sofaSelectionComboBox.setSelectedIndex(sofaSelectionComboBox.getItemCount() - 1); |
| } |
| } |
| } |
| if (sofaSelectionComboBox.getItemCount() == 0) { |
| throw new RuntimeException("This CAS contains no document to view."); |
| } |
| // make sofa selector visible if any text sofa other than the default was found |
| sofaSelectionPanel.setVisible(nonDefaultSofaFound); |
| |
| // Note that selection of the Sofa from the combo box happens during |
| // population, and that triggers the call to display() to display |
| // that document and its annotations/entities. |
| // display(); |
| } |
| |
| /** |
| * Causes the specified words to appear in boldface wherever they occur in the document. This is |
| * case-insensitive. Call this method after {@link #setCAS()}. It wil apply only to the current |
| * document, and will be reset on the next call to {@link #setCAS()}. |
| * |
| * @param aWords |
| * array of words to highlight in boldface. |
| */ |
| public void applyBoldfaceToKeywords(String[] aWords) { |
| mBoldfaceKeywords = aWords; |
| doBoldface(); |
| } |
| |
| /** |
| * Causes the specified spans to appear in boldface. This is case-insensitive. Call this method |
| * after {@link #setCAS()}. It wil apply only to the current document, and will be reset on the |
| * next call to {@link #setCAS()}. |
| * |
| * @param aSpans |
| * spans to appear in boldface (begin1, end1, begin2, end2, ...) |
| */ |
| public void applyBoldfaceToSpans(int[] aSpans) { |
| mBoldfaceSpans = aSpans; |
| doBoldface(); |
| } |
| |
| /** |
| * Configures the viewer appropriately for displaying a hit against an XML fragments query. This |
| * does not use a sophisticated algorithm for determining the location of the document that |
| * matched the query. Currently all it does is call {@link #setInitiallySelectedTypes(String[])} |
| * with the list of types mentioned in the query and {@link #applyBoldfaceToKeyword(String[])} on |
| * any keywords mentioned in the query. |
| * |
| * @param aQuery |
| * an XML fragments query |
| * @param aTypeNamespace |
| * namespace to prepend to the element names in the query in order to form |
| * fully-qualified CAS type names. This is optional; if not specified, type namespaces |
| * are ignored and any type whose local name matches the query will be selected. |
| */ |
| public void configureViewForXmlFragmentsQuery(String aQuery, String aTypeNamespace) { |
| // need to parse query and produce type list and keyword list |
| List typeList = new ArrayList(); |
| List keywordList = new ArrayList(); |
| |
| String delims = "<>+-*\" \t\n"; |
| StringTokenizer tokenizer = new StringTokenizer(aQuery, delims, true); |
| boolean inTag = false; |
| while (tokenizer.hasMoreTokens()) { |
| String tok = tokenizer.nextToken(); |
| if ("<".equals(tok)) { |
| inTag = true; |
| } else if (">".equals(tok) && inTag) { |
| inTag = false; |
| } else if (delims.indexOf(tok) == -1) // token is not a delimiter |
| { |
| if (inTag) { |
| if (!tok.startsWith("/")) // ignore end tags |
| { |
| if (tok.endsWith("/")) { |
| tok = tok.substring(0, tok.length() - 1); // strip trailing / from empty tags |
| } |
| typeList.add(aTypeNamespace + '.' + tok); |
| } |
| } else { |
| keywordList.add(tok); |
| } |
| } |
| } |
| |
| // System.out.println(typeList); |
| // System.out.println(keywordList); |
| |
| setInitiallySelectedTypes((String[]) typeList.toArray(new String[0])); |
| display(); |
| applyBoldfaceToKeywords((String[]) keywordList.toArray(new String[0])); |
| } |
| |
| /** |
| * Configures the viewer appropriately for displaying a hit against an XML fragments query. This |
| * does not use a sophisticated algorithm for determining the location of the document that |
| * matched the query. Currently all it does is call {@link #setInitiallySelectedTypes(String[])} |
| * with the list of types mentioned in the query and {@link #applyBoldfaceToKeyword(String[])} on |
| * any keywords mentioned in the query. |
| * |
| * @param aQuery |
| * an XML fragments query |
| */ |
| public void configureViewForXmlFragmentsQuery(String aQuery) { |
| configureViewForXmlFragmentsQuery(aQuery, "*"); |
| } |
| |
| /** |
| * Assign initially checked to the specified types, pairing up down the lists |
| * |
| * @param aNotChecked |
| * list of types not to be initially checked JMP |
| */ |
| public void assignCheckedFromList(ArrayList aNotChecked) { |
| Iterator iterC = aNotChecked.iterator(); |
| while (iterC.hasNext()) { |
| String typeName = (String) iterC.next(); |
| // assign to list of types not to be initially checked |
| noCheckSet.add(typeName); |
| } |
| } |
| |
| /** |
| * Assign colors to the specified types, pairing up down the lists |
| * |
| * @param aColors |
| * list of colors |
| * @param aTypeNames |
| * list of type names JMP |
| */ |
| public void assignColorsFromList(List aColors, ArrayList aTypeNames) { |
| // populate mTypeNameToColorMap |
| Iterator iter = aTypeNames.iterator(); |
| Iterator iterC = aColors.iterator(); |
| while (iter.hasNext()) { |
| if (!iterC.hasNext()) |
| break; |
| String typeName = (String) iter.next(); |
| Color color = (Color) iterC.next(); |
| // assign background color |
| mTypeNameToColorMap.put(typeName, color); |
| } |
| |
| setUserTypes(aTypeNames); |
| |
| // clear checkbox panel so it will be refreshed |
| annotationCheckboxPanel.removeAll(); |
| mTypeToCheckboxMap.clear(); |
| } |
| |
| /** |
| * Assign colors to the specified types |
| * |
| * @param aTypeNames |
| * list of type names |
| */ |
| private void assignColors(List aTypeNames) { |
| // populate mTypeNameToColorMap |
| Iterator iter = aTypeNames.iterator(); |
| while (iter.hasNext()) { |
| String typeName = (String) iter.next(); |
| // assign background color |
| Color c = COLORS[mTypeNameToColorMap.size() % COLORS.length]; |
| mTypeNameToColorMap.put(typeName, c); |
| } |
| |
| // clear checkbox panel so it will be refreshed |
| annotationCheckboxPanel.removeAll(); |
| mTypeToCheckboxMap.clear(); |
| } |
| |
| /** |
| * Creates/updates the display. This is called when setCAS() is called and again each time to |
| * user's mode or checkbox selections change. |
| */ |
| private void display() { |
| // remember split pane divider location so we can restore it later |
| int dividerLoc = vertSplitPane.getDividerLocation(); |
| |
| // remember caret pos and scroll position |
| int caretPos = this.textPane.getCaretPosition(); |
| int verticalScrollPos = this.textScrollPane.getVerticalScrollBar().getValue(); |
| |
| // type of display depends on whether we are in annotation or entity mode |
| switch (mViewMode) { |
| case MODE_ANNOTATIONS: |
| displayAnnotations(); |
| break; |
| case MODE_ENTITIES: |
| displayEntities(); |
| break; |
| } |
| |
| // apply boldface to keywords and spans as indicated by user |
| doBoldface(); |
| |
| // update the label of the Show/Hide Unselected Button |
| if (mHideUnselectedCheckboxes) { |
| showHideUnselectedButton.setText("Show Unselected"); |
| } else { |
| showHideUnselectedButton.setText("Hide Unselected"); |
| } |
| |
| // reset scroll position |
| textPane.setCaretPosition(caretPos); |
| textScrollPane.getVerticalScrollBar().setValue(verticalScrollPos); |
| textScrollPane.revalidate(); |
| |
| // reset split pane divider |
| vertSplitPane.setDividerLocation(dividerLoc); |
| } |
| |
| /** |
| * Creates the annotation display. |
| */ |
| private void displayAnnotations() { |
| // for speed, detach document from text pane before updating |
| StyledDocument doc = (StyledDocument) textPane.getDocument(); |
| Document blank = new DefaultStyledDocument(); |
| textPane.setDocument(blank); |
| |
| // make sure annotationCheckboxPanel is showing |
| if (legendScrollPane.getViewport().getView() != annotationCheckboxPanel) { |
| legendScrollPane.setViewportView(annotationCheckboxPanel); |
| } |
| |
| // add text from CAS |
| try { |
| doc.remove(0, doc.getLength()); |
| doc.insertString(0, mCAS.getDocumentText(), new SimpleAttributeSet()); |
| } catch (BadLocationException e) { |
| throw new RuntimeException(e); |
| } |
| |
| // Iterate over annotations |
| FSIterator iter = mCAS.getAnnotationIndex().iterator(); |
| Hashtable checkBoxes = new Hashtable(); |
| HashSet checkBoxesDone = new HashSet(); |
| while (iter.isValid()) { |
| AnnotationFS fs = (AnnotationFS) iter.get(); |
| iter.moveToNext(); |
| |
| Type type = fs.getType(); |
| |
| // have we seen this type before? |
| JCheckBox checkbox = (JCheckBox) mTypeToCheckboxMap.get(type); |
| if (checkbox == null) { |
| // check that type should be displayed |
| if ((mDisplayedTypeNames == null || typeNamesContains(mDisplayedTypeNames, type.getName())) |
| && !typeNamesContains(mHiddenTypeNames, type.getName())) { |
| // if mTypeNameToColorMap exists, get color from there |
| Color c = (Color) mTypeNameToColorMap.get(type.getName()); |
| if (c == null) // assign next available color |
| { |
| c = COLORS[mTypeNameToColorMap.size() % COLORS.length]; |
| mTypeNameToColorMap.put(type.getName(), c); |
| } |
| // This next section required until priorities work properly |
| // HashSet noCheckSet = new HashSet(); |
| String noCheckArray[] = { |
| // "org.apache.jresporator.PROPER", |
| // "DOCSTRUCT_ANNOT_TYPE", |
| // "VOCAB_ANNOT_TYPE" |
| }; |
| for (int i = 0; i < noCheckArray.length; i++) { |
| noCheckSet.add(noCheckArray[i]); |
| } |
| // end of section |
| |
| // should type be initially selected? |
| boolean selected = ((mInitiallySelectedTypeNames == null && |
| // document annotation is not initially selected in default case |
| !CAS.TYPE_NAME_DOCUMENT_ANNOTATION.equals(type.getName()) && !noCheckSet |
| .contains(type.getName()) // priorities JMP |
| ) || (mInitiallySelectedTypeNames != null && typeNamesContains( |
| mInitiallySelectedTypeNames, type.getName()))); |
| |
| // add checkbox |
| checkbox = new JCheckBox(type.getShortName(), selected); |
| checkbox.setToolTipText(type.getName()); |
| checkbox.addActionListener(this); |
| checkbox.setBackground(c); |
| // annotationCheckboxPanel.add(checkbox); do it later JMP |
| checkBoxes.put(type.getName(), checkbox); |
| checkBoxesDone.add(checkbox); |
| // add to (Type, Checkbox) map |
| mTypeToCheckboxMap.put(type, checkbox); |
| } else { |
| // this type is not hidden, skip it |
| continue; |
| } |
| } |
| // if checkbox is checked, assign color to text |
| if (checkbox.isSelected()) { |
| int begin = fs.getBegin(); |
| int end = fs.getEnd(); |
| |
| // Be careful of 0-length annotations and annotations that span the |
| // entire document. In either of these cases, if we try to set |
| // background color, it will set the input text style, which is not |
| // what we want. |
| if (begin == 0 && end == mCAS.getDocumentText().length()) { |
| end--; |
| } |
| |
| if (begin < end) { |
| MutableAttributeSet attrs = new SimpleAttributeSet(); |
| StyleConstants.setBackground(attrs, checkbox.getBackground()); |
| doc.setCharacterAttributes(begin, end - begin, attrs, false); |
| } |
| } |
| } |
| |
| // now populate panel with checkboxes in order specified in user file. JMP |
| ArrayList aTypeNames = getUserTypes(); |
| if (aTypeNames != null) { |
| Iterator iterT = aTypeNames.iterator(); |
| while (iterT.hasNext()) { |
| String typeName = (String) iterT.next(); |
| JCheckBox cb = (JCheckBox) checkBoxes.get(typeName); |
| if (cb != null) { |
| annotationCheckboxPanel.add(cb); |
| checkBoxesDone.remove(cb); |
| } |
| } |
| } |
| // add additional checkboxes in alphabetical order |
| LinkedList checkboxes = new LinkedList(checkBoxesDone); |
| Collections.sort(checkboxes, new Comparator() { |
| public int compare(Object o1, Object o2) { |
| return ((JCheckBox) o1).getText().toLowerCase().compareTo( |
| ((JCheckBox) o2).getText().toLowerCase()); |
| } |
| }); |
| Iterator iterC = checkboxes.iterator(); |
| while (iterC.hasNext()) { |
| JCheckBox cb = (JCheckBox) iterC.next(); |
| annotationCheckboxPanel.add(cb); |
| } |
| |
| // add/remove checkboxes from display as determined by the |
| // mHideUnselectedCheckboxes toggle |
| Iterator cbIter = mTypeToCheckboxMap.values().iterator(); |
| while (cbIter.hasNext()) { |
| JCheckBox cb = (JCheckBox) cbIter.next(); |
| if (mHideUnselectedCheckboxes && !cb.isSelected()) { |
| if (cb.getParent() == annotationCheckboxPanel) { |
| annotationCheckboxPanel.remove(cb); |
| } |
| } else if (cb.getParent() != annotationCheckboxPanel) { |
| annotationCheckboxPanel.add(cb); |
| } |
| } |
| |
| // reattach document to text pane |
| textPane.setDocument(doc); |
| } |
| |
| /** |
| * Creates the entity display. |
| */ |
| private void displayEntities() { |
| // for speed, detach document from text pane before updating |
| StyledDocument doc = (StyledDocument) textPane.getDocument(); |
| Document blank = new DefaultStyledDocument(); |
| textPane.setDocument(blank); |
| |
| // make sure entityCheckboxPanel is showing |
| if (legendScrollPane.getViewport().getView() != entityCheckboxPanel) { |
| legendScrollPane.setViewportView(entityCheckboxPanel); |
| } |
| |
| // add text from CAS |
| try { |
| doc.remove(0, doc.getLength()); |
| doc.insertString(0, mCAS.getDocumentText(), new SimpleAttributeSet()); |
| } catch (BadLocationException e) { |
| throw new RuntimeException(e); |
| } |
| |
| // Iterate over EntityAnnotations using JCAS, because the EntityResolver interface |
| // uses JCAS as a convenience to the user. |
| JCas jcas; |
| try { |
| // NOTE: for a large type system, this can take a few seconds, which results in a |
| // noticeable delay when the user first switches to Entity mode. |
| jcas = mCAS.getJCas(); |
| } catch (CASException e) { |
| throw new RuntimeException(e); |
| } |
| FSIterator iter = jcas.getAnnotationIndex().iterator(); |
| while (iter.isValid()) { |
| Annotation annot = (Annotation) iter.get(); |
| iter.moveToNext(); |
| |
| // find out what entity this annotation represents |
| EntityResolver.Entity entity = mEntityResolver.getEntity(annot); |
| |
| //if not an entity, skip it |
| if (entity == null) |
| continue; |
| |
| // have we seen this entity before? |
| JCheckBox checkbox = (JCheckBox) mEntityToCheckboxMap.get(entity); |
| if (checkbox == null) { |
| // assign next available color |
| Color c = COLORS[mEntityToCheckboxMap.size() % COLORS.length]; |
| // add checkbox |
| checkbox = new JCheckBox(entity.getCanonicalForm(), true); |
| checkbox.setToolTipText(entity.getCanonicalForm()); |
| checkbox.addActionListener(this); |
| checkbox.setBackground(c); |
| entityCheckboxPanel.add(checkbox); |
| // add to (Entity, Checkbox) map |
| mEntityToCheckboxMap.put(entity, checkbox); |
| } |
| |
| // if checkbox is checked, assign color to text |
| if (checkbox.isSelected()) { |
| int begin = annot.getBegin(); |
| int end = annot.getEnd(); |
| // be careful of 0-length annotation. If we try to set background color when there |
| // is no selection, it will set the input text style, which is not what we want. |
| if (begin != end) { |
| MutableAttributeSet attrs = new SimpleAttributeSet(); |
| StyleConstants.setBackground(attrs, checkbox.getBackground()); |
| doc.setCharacterAttributes(begin, end - begin, attrs, false); |
| } |
| } |
| } |
| |
| // add/remove checkboxes from display as determined by the |
| // mHideUnselectedCheckboxes toggle |
| Iterator cbIter = mEntityToCheckboxMap.values().iterator(); |
| while (cbIter.hasNext()) { |
| JCheckBox cb = (JCheckBox) cbIter.next(); |
| if (mHideUnselectedCheckboxes && !cb.isSelected()) { |
| if (cb.getParent() == entityCheckboxPanel) { |
| entityCheckboxPanel.remove(cb); |
| } |
| } else if (cb.getParent() != entityCheckboxPanel) { |
| entityCheckboxPanel.add(cb); |
| } |
| } |
| |
| // reattach document to text pane |
| textPane.setDocument(doc); |
| } |
| |
| /** |
| * Refreshes the selected annotation tree. |
| * |
| * @param aPosition |
| * the currently selected offset into the document. All annotations overlapping this |
| * point will be rendered in the tree. |
| */ |
| private void updateSelectedAnnotationTree(int aPosition) { |
| DefaultMutableTreeNode root = (DefaultMutableTreeNode) this.selectedAnnotationTreeModel |
| .getRoot(); |
| root.removeAllChildren(); |
| FSIterator annotIter = this.mCAS.getAnnotationIndex().iterator(); |
| while (annotIter.isValid()) { |
| AnnotationFS annot = (AnnotationFS) annotIter.get(); |
| // if (getPanePosition(annot.getBegin()) <= aPosition |
| // && getPanePosition(annot.getEnd()) > aPosition) |
| if (annot.getBegin() <= aPosition && annot.getEnd() > aPosition) { |
| JCheckBox checkbox = (JCheckBox) mTypeToCheckboxMap.get(annot.getType()); |
| if (checkbox != null && checkbox.isSelected()) { |
| addAnnotationToTree(annot); |
| } |
| } |
| // else if (getPanePosition(annot.getBegin()) > aPosition) |
| else if (annot.getBegin() > aPosition) |
| break; |
| annotIter.moveToNext(); |
| } |
| this.selectedAnnotationTreeModel.nodeStructureChanged(root); |
| // expand first level |
| // int row = 0; |
| // while (row < this.selectedAnnotationTree.getRowCount()) |
| // { |
| // if (this.selectedAnnotationTree.getPathForRow(row).getPathCount() <= 2) |
| // { |
| // this.selectedAnnotationTree.expandRow(row); |
| // } |
| // row++; |
| // } |
| |
| // hmmm.. how to get scroll pane to resize properly?? |
| this.selectedAnnotationTree.treeDidChange(); |
| // this.selectedAnnotationTree.setPreferredSize(this.selectedAnnotationTree.getSize()); |
| this.selectedAnnotationTree.revalidate(); |
| this.horizSplitPane.revalidate(); |
| } |
| |
| /** |
| * Adds an annotation to the selected annotations tree. Annotations in the tree are grouped by |
| * type. |
| * |
| * @param aAnnotation |
| * the annotation to add |
| */ |
| protected void addAnnotationToTree(AnnotationFS aAnnotation) { |
| DefaultMutableTreeNode root = (DefaultMutableTreeNode) this.selectedAnnotationTreeModel |
| .getRoot(); |
| // try to find a node for the type |
| DefaultMutableTreeNode typeNode = null; |
| Enumeration typeNodes = root.children(); |
| while (typeNodes.hasMoreElements()) { |
| DefaultMutableTreeNode node = (DefaultMutableTreeNode) typeNodes.nextElement(); |
| if (aAnnotation.getType().equals(((TypeTreeNodeObject) node.getUserObject()).getType())) { |
| typeNode = node; |
| break; |
| } |
| } |
| if (typeNode == null) { |
| typeNode = new DefaultMutableTreeNode(new TypeTreeNodeObject(aAnnotation.getType())); |
| root.insert(typeNode, 0); |
| } |
| |
| // add annotation node |
| DefaultMutableTreeNode annotationNode = new DefaultMutableTreeNode(new FsTreeNodeObject( |
| aAnnotation, null)); |
| typeNode.insert(annotationNode, 0); |
| // add child nodes for features |
| addFeatureTreeNodes(annotationNode, aAnnotation); |
| } |
| |
| private void addFeatureTreeNodes(DefaultMutableTreeNode aParentNode, FeatureStructure aFS) { |
| List aFeatures = aFS.getType().getFeatures(); |
| Iterator iter = aFeatures.iterator(); |
| while (iter.hasNext()) { |
| Feature feat = (Feature) iter.next(); |
| String featName = feat.getShortName(); |
| // skip hidden features |
| if (mHiddenFeatureNames.contains(featName)) { |
| continue; |
| } |
| // how we get feature value depends on feature's range type) |
| String featVal = "null"; |
| Type rangeType = feat.getRange(); |
| String rangeTypeName = rangeType.getName(); |
| if (mCAS.getTypeSystem().subsumes(mStringType, rangeType)) { |
| featVal = aFS.getStringValue(feat); |
| if (featVal == null) { |
| featVal = "null"; |
| } else if (featVal.length() > 64) { |
| featVal = featVal.substring(0, 64) + "..."; |
| } |
| } else if (rangeType.isPrimitive()) { |
| featVal = aFS.getFeatureValueAsString(feat); |
| } else if (mCAS.getTypeSystem().subsumes(mFsArrayType, rangeType)) { |
| ArrayFS arrayFS = (ArrayFS) aFS.getFeatureValue(feat); |
| if (arrayFS != null) { |
| // Add featName = FSArray node, then add each array element as a child |
| DefaultMutableTreeNode arrayNode = new DefaultMutableTreeNode(featName + " = FSArray"); |
| for (int i = 0; i < arrayFS.size(); i++) { |
| FeatureStructure fsVal = arrayFS.get(i); |
| if (fsVal != null) { |
| // Add the FS node and a dummy child, so that user can expand it. |
| // When user expands it, new nodes for feature values will be created. |
| DefaultMutableTreeNode fsValNode = new DefaultMutableTreeNode(new FsTreeNodeObject( |
| fsVal, featName)); |
| if (!fsVal.getType().getFeatures().isEmpty()) { |
| fsValNode.add(new DefaultMutableTreeNode(null)); |
| } |
| arrayNode.add(fsValNode); |
| } else { |
| arrayNode.add(new DefaultMutableTreeNode("null")); |
| } |
| } |
| aParentNode.add(arrayNode); |
| continue; |
| } |
| } else if (rangeType.isArray()) // primitive array |
| { |
| String[] vals = null; |
| if (CAS.TYPE_NAME_STRING_ARRAY.equals(rangeTypeName)) { |
| StringArrayFSImpl arrayFS = (StringArrayFSImpl) aFS.getFeatureValue(feat); |
| if (arrayFS != null) |
| vals = arrayFS.toArray(); |
| } else if (CAS.TYPE_NAME_INTEGER_ARRAY.equals(rangeTypeName)) { |
| IntArrayFSImpl arrayFS = (IntArrayFSImpl) aFS.getFeatureValue(feat); |
| if (arrayFS != null) |
| vals = arrayFS.toStringArray(); |
| } else if (CAS.TYPE_NAME_FLOAT_ARRAY.equals(rangeTypeName)) { |
| FloatArrayFSImpl arrayFS = (FloatArrayFSImpl) aFS.getFeatureValue(feat); |
| if (arrayFS != null) |
| vals = arrayFS.toStringArray(); |
| } else if (CAS.TYPE_NAME_BOOLEAN_ARRAY.equals(rangeTypeName)) { |
| BooleanArrayFSImpl arrayFS = (BooleanArrayFSImpl) aFS.getFeatureValue(feat); |
| if (arrayFS != null) |
| vals = arrayFS.toStringArray(); |
| } else if (CAS.TYPE_NAME_BYTE_ARRAY.equals(rangeTypeName)) { |
| ByteArrayFSImpl arrayFS = (ByteArrayFSImpl) aFS.getFeatureValue(feat); |
| if (arrayFS != null) |
| vals = arrayFS.toStringArray(); |
| } else if (CAS.TYPE_NAME_SHORT_ARRAY.equals(rangeTypeName)) { |
| ShortArrayFSImpl arrayFS = (ShortArrayFSImpl) aFS.getFeatureValue(feat); |
| if (arrayFS != null) |
| vals = arrayFS.toStringArray(); |
| } else if (CAS.TYPE_NAME_LONG_ARRAY.equals(rangeTypeName)) { |
| LongArrayFSImpl arrayFS = (LongArrayFSImpl) aFS.getFeatureValue(feat); |
| if (arrayFS != null) |
| vals = arrayFS.toStringArray(); |
| } |
| if (CAS.TYPE_NAME_DOUBLE_ARRAY.equals(rangeTypeName)) { |
| DoubleArrayFSImpl arrayFS = (DoubleArrayFSImpl) aFS.getFeatureValue(feat); |
| if (arrayFS != null) |
| vals = arrayFS.toStringArray(); |
| } |
| if (vals == null) { |
| featVal = "null"; |
| } else { |
| StringBuffer displayVal = new StringBuffer(); |
| displayVal.append('['); |
| for (int i = 0; i < vals.length - 1; i++) { |
| displayVal.append(vals[i]); |
| displayVal.append(','); |
| } |
| if (vals.length > 0) { |
| displayVal.append(vals[vals.length - 1]); |
| } |
| displayVal.append(']'); |
| featVal = displayVal.toString(); |
| } |
| } else |
| // single feature value |
| { |
| FeatureStructure fsVal = aFS.getFeatureValue(feat); |
| if (fsVal != null) { |
| // Add the FS node and a dummy child, so that user can expand it. |
| // When user expands it, new nodes for feature values will be created. |
| DefaultMutableTreeNode fsValNode = new DefaultMutableTreeNode(new FsTreeNodeObject(fsVal, |
| featName)); |
| if (!fsVal.getType().getFeatures().isEmpty()) { |
| fsValNode.add(new DefaultMutableTreeNode(null)); |
| } |
| aParentNode.add(fsValNode); |
| continue; |
| } |
| } |
| aParentNode.add(new DefaultMutableTreeNode(featName + " = " + featVal)); |
| } |
| } |
| |
| /** |
| * Does wildcard matching to determine if a give type name is "contained" in a Set of type names. |
| * |
| * @param names |
| * Type names, which may include wildcards (e.g, uima.tt.*) |
| * @param name |
| * A type name |
| * @return True iff name matches a name in type names |
| */ |
| private boolean typeNamesContains(Set names, String name) { |
| if (names.contains(name)) |
| return true; |
| else { |
| Iterator namesIterator = names.iterator(); |
| while (namesIterator.hasNext()) { |
| String otherName = (String) namesIterator.next(); |
| if (otherName.indexOf('*') != -1) { |
| if (wildCardMatch(name, otherName)) { |
| return true; |
| } |
| } else { |
| if (otherName.equalsIgnoreCase(name)) { |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Helper for {@link #typeNamesContains(HashSet, String)}. |
| * |
| * @param s |
| * A litteral string |
| * @param pattern |
| * A string that includes one or more *'s as wildcards |
| * @return True iff the string matches the pattern. |
| */ |
| private boolean wildCardMatch(String s, String pattern) { |
| StringBuffer regexpPatternBuffer = new StringBuffer(); |
| for (int i = 0; i < pattern.length(); i++) { |
| char c = pattern.charAt(i); |
| if (c == '*') |
| regexpPatternBuffer.append('.'); |
| else if (c == '.') |
| regexpPatternBuffer.append('\\'); |
| if (Character.isLetter(c)) { |
| regexpPatternBuffer.append('(').append(Character.toLowerCase(c)).append('|').append( |
| Character.toUpperCase(c)).append(')'); |
| } else { |
| regexpPatternBuffer.append(c); |
| } |
| } |
| |
| return s.matches(new String(regexpPatternBuffer)); |
| } |
| |
| /** |
| * @see java.awt.Component#setSize(Dimension) |
| */ |
| public void setSize(Dimension d) { |
| super.setSize(d); |
| Insets insets = getInsets(); |
| Dimension paneSize = new Dimension(d.width - insets.left - insets.right, d.height - insets.top |
| - insets.bottom); |
| |
| horizSplitPane.setPreferredSize(paneSize); |
| horizSplitPane.setSize(paneSize); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) |
| */ |
| public void actionPerformed(ActionEvent e) { |
| if (e.getSource() == selectAllButton) { |
| Iterator cbIter = (mViewMode == MODE_ANNOTATIONS) ? mTypeToCheckboxMap.values().iterator() |
| : mEntityToCheckboxMap.values().iterator(); |
| while (cbIter.hasNext()) { |
| ((JCheckBox) cbIter.next()).setSelected(true); |
| } |
| display(); |
| } else if (e.getSource() == deselectAllButton) { |
| Iterator cbIter = (mViewMode == MODE_ANNOTATIONS) ? mTypeToCheckboxMap.values().iterator() |
| : mEntityToCheckboxMap.values().iterator(); |
| while (cbIter.hasNext()) { |
| ((JCheckBox) cbIter.next()).setSelected(false); |
| } |
| display(); |
| } else if (e.getSource() == annotationModeButton) { |
| mViewMode = MODE_ANNOTATIONS; |
| display(); |
| } else if (e.getSource() == entityModeButton) { |
| mViewMode = MODE_ENTITIES; |
| display(); |
| // make sure we clear the annotation tree when we go into entity mode |
| // this.updateSelectedAnnotationTree(0); |
| } else if (e.getSource() == showHideUnselectedButton) { |
| mHideUnselectedCheckboxes = !mHideUnselectedCheckboxes; |
| display(); |
| } else if (e.getSource() instanceof JCheckBox) { |
| display(); |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent) |
| */ |
| public void mouseClicked(MouseEvent e) { |
| if (mViewMode == MODE_ANNOTATIONS) { |
| int pos = textPane.viewToModel(e.getPoint()); |
| this.updateSelectedAnnotationTree(pos); |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent) |
| */ |
| public void mouseEntered(MouseEvent e) { |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent) |
| */ |
| public void mouseExited(MouseEvent e) { |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent) |
| */ |
| public void mousePressed(MouseEvent e) { |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent) |
| */ |
| public void mouseReleased(MouseEvent e) { |
| } |
| |
| /** |
| * Inner class containing data for a tree node representing a FeatureStructure |
| */ |
| static class FsTreeNodeObject { |
| public FsTreeNodeObject(FeatureStructure aFS, String aFeatureName) { |
| mFS = aFS; |
| mFeatureName = aFeatureName; |
| mCaption = mFS.getType().getShortName(); |
| if (mFS instanceof AnnotationFS) { |
| String coveredText = ((AnnotationFS) mFS).getCoveredText(); |
| if (coveredText.length() > 64) |
| coveredText = coveredText.substring(0, 64) + "..."; |
| mCaption += " (\"" + coveredText + "\")"; |
| } |
| if (mFeatureName != null) { |
| mCaption = mFeatureName + " = " + mCaption; |
| } |
| } |
| |
| public FeatureStructure getFS() { |
| return mFS; |
| } |
| |
| public String toString() { |
| return mCaption; |
| } |
| |
| private FeatureStructure mFS; |
| |
| private String mFeatureName; |
| |
| private String mCaption; |
| } |
| |
| /** |
| * Inner class containing data for a tree node representing a Type |
| */ |
| static class TypeTreeNodeObject { |
| public TypeTreeNodeObject(Type aType) { |
| mType = aType; |
| } |
| |
| public Type getType() { |
| return mType; |
| } |
| |
| public String toString() { |
| return mType.getShortName(); |
| } |
| |
| private Type mType; |
| } |
| |
| class AnnotationTreeCellRenderer extends DefaultTreeCellRenderer { |
| private static final long serialVersionUID = -8661556785397184756L; |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see javax.swing.tree.TreeCellRenderer#getTreeCellRendererComponent(javax.swing.JTree, |
| * java.lang.Object, boolean, boolean, boolean, int, boolean) |
| */ |
| public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, |
| boolean expanded, boolean leaf, int row, boolean aHasFocus) { |
| |
| // set background color if this is an Annotation or a Type |
| Color background = null; |
| if (value instanceof DefaultMutableTreeNode) { |
| Object userObj = ((DefaultMutableTreeNode) value).getUserObject(); |
| Type type = null; |
| if (userObj instanceof FsTreeNodeObject) { |
| FeatureStructure fs = ((FsTreeNodeObject) userObj).getFS(); |
| type = fs.getType(); |
| } else if (userObj instanceof TypeTreeNodeObject) { |
| type = ((TypeTreeNodeObject) userObj).getType(); |
| } |
| if (type != null) { |
| // look up checkbox to get color |
| JCheckBox checkbox = (JCheckBox) mTypeToCheckboxMap.get(type); |
| if (checkbox != null) { |
| background = checkbox.getBackground(); |
| } |
| } |
| } |
| this.setBackgroundNonSelectionColor(background); |
| this.setBackgroundSelectionColor(background); |
| |
| Component component = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, |
| row, aHasFocus); |
| return component; |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see javax.swing.event.TreeWillExpandListener#treeWillCollapse(javax.swing.event.TreeExpansionEvent) |
| */ |
| public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see javax.swing.event.TreeWillExpandListener#treeWillExpand(javax.swing.event.TreeExpansionEvent) |
| */ |
| public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { |
| // if FS node is expanded and it has a dummy child, replace with |
| // feature value nodes (this is what lets us do infinite tree) |
| Object lastPathComponent = event.getPath().getLastPathComponent(); |
| if (lastPathComponent instanceof DefaultMutableTreeNode) { |
| DefaultMutableTreeNode expandedNode = (DefaultMutableTreeNode) lastPathComponent; |
| Object userObj = expandedNode.getUserObject(); |
| if (userObj instanceof FsTreeNodeObject) { |
| TreeNode firstChild = expandedNode.getFirstChild(); |
| if (firstChild instanceof DefaultMutableTreeNode |
| && ((DefaultMutableTreeNode) firstChild).getUserObject() == null) { |
| expandedNode.removeAllChildren(); |
| FeatureStructure fs = ((FsTreeNodeObject) userObj).getFS(); |
| addFeatureTreeNodes(expandedNode, fs); |
| ((JTree) event.getSource()).treeDidChange(); |
| } |
| |
| } |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see javax.swing.event.TreeExpansionListener#treeCollapsed(javax.swing.event.TreeExpansionEvent) |
| */ |
| public void treeCollapsed(TreeExpansionEvent event) { |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see javax.swing.event.TreeExpansionListener#treeExpanded(javax.swing.event.TreeExpansionEvent) |
| */ |
| public void treeExpanded(TreeExpansionEvent event) { |
| // if a Type node is expanded and has only one child, |
| // also expand this child (a usability improvement) |
| Object lastPathComponent = event.getPath().getLastPathComponent(); |
| if (lastPathComponent instanceof DefaultMutableTreeNode) { |
| DefaultMutableTreeNode expandedNode = (DefaultMutableTreeNode) lastPathComponent; |
| Object userObj = expandedNode.getUserObject(); |
| if (userObj instanceof TypeTreeNodeObject && expandedNode.getChildCount() == 1) { |
| TreePath childPath = event.getPath().pathByAddingChild(expandedNode.getFirstChild()); |
| ((JTree) event.getSource()).expandPath(childPath); |
| ((JTree) event.getSource()).treeDidChange(); |
| } |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see java.awt.event.ItemListener#itemStateChanged(java.awt.event.ItemEvent) |
| */ |
| public void itemStateChanged(ItemEvent e) { |
| if (e.getSource() == sofaSelectionComboBox) { |
| // a new sofa was selected. Switch to that view and update display |
| String sofaId = (String) e.getItem(); |
| if ("DEFAULT".equals(sofaId)) { |
| mCAS = mCAS.getView(CAS.NAME_DEFAULT_SOFA); |
| } else { |
| mCAS = mCAS.getView(sofaId); |
| } |
| display(); |
| } |
| |
| } |
| |
| /** |
| * Applies boldface as per the mBoldfaceKeywords and mBoldfaceSpans fields. |
| */ |
| private void doBoldface() { |
| // Keywords |
| if (mBoldfaceKeywords.length > 0) { |
| // build regular expression |
| StringBuffer regEx = new StringBuffer(); |
| for (int i = 0; i < mBoldfaceKeywords.length; i++) { |
| if (i > 0) { |
| regEx.append('|'); |
| } |
| regEx.append("\\b"); |
| String word = mBoldfaceKeywords[i]; |
| for (int j = 0; j < word.length(); j++) { |
| char c = word.charAt(j); |
| if (Character.isLetter(c)) { |
| regEx.append('[').append(Character.toLowerCase(c)).append(Character.toUpperCase(c)) |
| .append(']'); |
| } else if (c == '.' || c == '^' || c == '&' || c == '\\' || c == '(' || c == ')') { |
| regEx.append('\\').append(c); |
| } else { |
| regEx.append('c'); |
| } |
| } |
| regEx.append("\\b"); |
| } |
| // System.out.println("RegEx: " + regEx); |
| Pattern pattern = Pattern.compile(regEx.toString()); |
| Matcher matcher = pattern.matcher(mCAS.getDocumentText()); |
| // match |
| int pos = 0; |
| while (matcher.find(pos)) { |
| MutableAttributeSet attrs = new SimpleAttributeSet(); |
| StyleConstants.setBold(attrs, true); |
| StyledDocument doc = (StyledDocument) textPane.getDocument(); |
| doc.setCharacterAttributes(matcher.start(), matcher.end() - matcher.start(), attrs, false); |
| |
| if (pos == matcher.end()) // infinite loop check |
| break; |
| else |
| pos = matcher.end(); |
| } |
| } |
| // Spans |
| int docLength = mCAS.getDocumentText().length(); |
| int len = mBoldfaceSpans.length; |
| len -= len % 2; // to avoid ArrayIndexOutOfBoundsException if some numbskull passes in an |
| // odd-length array |
| int i = 0; |
| while (i < len) { |
| int begin = mBoldfaceSpans[i]; |
| int end = mBoldfaceSpans[i + 1]; |
| if (begin >= 0 && begin <= docLength && end >= 0 && end <= docLength) { |
| MutableAttributeSet attrs = new SimpleAttributeSet(); |
| StyleConstants.setBold(attrs, true); |
| StyledDocument doc = (StyledDocument) textPane.getDocument(); |
| doc.setCharacterAttributes(begin, end - begin, attrs, false); |
| } |
| i += 2; |
| } |
| } |
| |
| /** |
| * Gets the selected annotation tree component. |
| * |
| * @return the tree that displays annotation details about annotations overlapping the point where |
| * the user last clicked in the text. |
| */ |
| protected JTree getSelectedAnnotationTree() { |
| return this.selectedAnnotationTree; |
| } |
| |
| /** |
| * A panel that is to be placed in a JScrollPane that can only scroll vertically. This panel |
| * should have its width track the viewport's width, and increase its height as necessary to |
| * display all components. |
| * |
| * |
| */ |
| static class VerticallyScrollablePanel extends JPanel implements Scrollable { |
| private static final long serialVersionUID = 1009744410018634511L; |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see javax.swing.Scrollable#getPreferredScrollableViewportSize() |
| */ |
| public Dimension getPreferredScrollableViewportSize() { |
| return getPreferredSize(); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see javax.swing.Scrollable#getScrollableBlockIncrement(java.awt.Rectangle, int, int) |
| */ |
| public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { |
| return 50; |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see javax.swing.Scrollable#getScrollableTracksViewportHeight() |
| */ |
| public boolean getScrollableTracksViewportHeight() { |
| return false; |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see javax.swing.Scrollable#getScrollableTracksViewportWidth() |
| */ |
| public boolean getScrollableTracksViewportWidth() { |
| return true; |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see javax.swing.Scrollable#getScrollableUnitIncrement(java.awt.Rectangle, int, int) |
| */ |
| public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { |
| return 10; |
| } |
| |
| } |
| |
| /** |
| * Trivial entity resolver that's applied if the user turns on entity mode without |
| * specifying their own entity resolver. Returns the covered text as the canonical form, |
| * and treats annotations with equal covered text as belonging to the same entity. |
| */ |
| static class DefaultEntityResolver implements EntityResolver { |
| |
| /* (non-Javadoc) |
| * @see org.apache.uima.tools.viewer.EntityResolver#getCanonicalForm(org.apache.uima.jcas.tcas.Annotation) |
| */ |
| public Entity getEntity(final Annotation aAnnotation) { |
| return new Entity() { |
| |
| public String getCanonicalForm() { |
| return aAnnotation.getCoveredText(); |
| } |
| |
| public boolean equals(Object obj) { |
| if (obj instanceof Entity) { |
| String canon = ((Entity)obj).getCanonicalForm(); |
| return getCanonicalForm().equals(canon); |
| } |
| return false; |
| } |
| |
| public int hashCode() { |
| return getCanonicalForm().hashCode(); |
| } |
| }; |
| } |
| |
| } |
| } |