You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ctakes.apache.org by se...@apache.org on 2015/02/19 19:06:17 UTC

svn commit: r1660963 [9/19] - in /ctakes/sandbox/timelanes: META-INF/ edu/ edu/mayo/ edu/mayo/bmi/ edu/mayo/bmi/annotation/ edu/mayo/bmi/annotation/knowtator/ org/ org/chboston/ org/chboston/cnlp/ org/chboston/cnlp/anafora/ org/chboston/cnlp/anafora/an...

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/eventSelection/EventSelectionEvent.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/eventSelection/EventSelectionEvent.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/eventSelection/EventSelectionEvent.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/eventSelection/EventSelectionEvent.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,75 @@
+package org.chboston.cnlp.timeline.eventSelection;
+
+import org.chboston.cnlp.nlp.annotation.entity.Entity;
+
+import java.util.Collection;
+import java.util.EventObject;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 7/8/13
+ */
+public class EventSelectionEvent extends EventObject {
+   private final Set<Entity> _selectionSet;
+   private final Set<String> _selectionTexts;
+   private final boolean _isAdjusting;
+
+   /**
+    * Represents a change in selection status between {@code firstIndex} and
+    * {@code lastIndex}, inclusive. {@code firstIndex} is less than or equal to
+    * {@code lastIndex}. The selection of at least one index within the range will
+    * have changed.
+    *
+    * @param selections  all selections to which this event applies
+    * @param isAdjusting whether or not this is one in a series of
+    *                    multiple events, where changes are still being made
+    */
+   public EventSelectionEvent( final Object source, final Collection<Entity> selections,
+                               final Collection<String> selectionTexts, final boolean isAdjusting ) {
+      super( source );
+      _selectionSet = new HashSet<>( selections );
+      _selectionTexts = new HashSet<>( selectionTexts );
+      _isAdjusting = isAdjusting;
+   }
+
+   public Collection<Entity> getSelectedEvents() {
+      return _selectionSet;
+   }
+
+   public Collection<String> getSelectionTexts() {
+      return _selectionTexts;
+   }
+
+   /**
+    * Returns whether or not this is one in a series of multiple events,
+    * where changes are still being made. See the documentation for
+    * {@link javax.swing.ListSelectionModel#setValueIsAdjusting} for
+    * more details on how this is used.
+    *
+    * @return {@code true} if this is one in a series of multiple events,
+    * where changes are still being made
+    */
+   public boolean getValueIsAdjusting() {
+      return _isAdjusting;
+   }
+
+   /**
+    * Returns a {@code String} that displays and identifies this
+    * object's properties.
+    *
+    * @return a String representation of this object
+    */
+   public String toString() {
+      String properties =
+            " source=" + getSource() +
+            " selections= " + _selectionSet.toString() +
+            " isAdjusting= " + _isAdjusting +
+            " ";
+      return getClass().getName() + "[" + properties + "]";
+   }
+
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/eventSelection/EventSelectionListener.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/eventSelection/EventSelectionListener.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/eventSelection/EventSelectionListener.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/eventSelection/EventSelectionListener.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,20 @@
+package org.chboston.cnlp.timeline.eventSelection;
+
+
+import java.util.EventListener;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 7/8/13
+ */
+public interface EventSelectionListener extends EventListener {
+
+   /**
+    * Called whenever the value of the selection changes.
+    *
+    * @param event the event that characterizes the change.
+    */
+   void valueChanged( final EventSelectionEvent event );
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/TextHighlightPanel.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/TextHighlightPanel.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/TextHighlightPanel.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/TextHighlightPanel.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,533 @@
+package org.chboston.cnlp.timeline.gui;
+
+import org.chboston.cnlp.nlp.annotation.classtype.SemanticClassType;
+import org.chboston.cnlp.nlp.annotation.classtype.TemporalClassType;
+import org.chboston.cnlp.nlp.annotation.entity.Entity;
+import org.chboston.cnlp.nlp.annotation.textspan.DefaultDiscontiguousTextSpan;
+import org.chboston.cnlp.nlp.annotation.textspan.DefaultTextSpan;
+import org.chboston.cnlp.nlp.annotation.textspan.DiscontiguousTextSpan;
+import org.chboston.cnlp.nlp.annotation.textspan.TextSpan;
+import org.chboston.cnlp.nlp.util.HashSetMap;
+import org.chboston.cnlp.timeline.gui.event.EventColor;
+import org.chboston.cnlp.timeline.gui.event.EventStatus;
+import org.chboston.cnlp.timeline.gui.event.list.EventListElement;
+import org.chboston.cnlp.timeline.gui.event.text.*;
+import org.chboston.cnlp.timeline.gui.timespan.selection.DefaultTimeSpanSelectionModel;
+import org.chboston.cnlp.timeline.gui.timespan.selection.TimeSpanSelectionEvent;
+import org.chboston.cnlp.timeline.gui.timespan.selection.TimeSpanSelectionListener;
+import org.chboston.cnlp.timeline.gui.timespan.selection.TimeSpanSelectionModel;
+import org.chboston.cnlp.timeline.timeline.Timeline;
+import org.chboston.cnlp.timeline.timespan.plus.PointedTimeSpan;
+import org.chboston.cnlp.timeline.timespan.plus.TimeSpanPlus;
+
+import javax.swing.*;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.plaf.basic.BasicScrollBarUI;
+import javax.swing.text.*;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 9/2/13
+ */
+final public class TextHighlightPanel extends JTextPane {
+
+   static private final Logger LOGGER = Logger.getLogger( "TextHighlightPanel" );
+
+   static private final boolean DEBUG = false;
+
+   // TODO - swim lane for images, video
+   // TODO - swim lane for icons: radiation, bloodwork, surgery?
+   // TODO - swim lane for locations (body) (s/s, d/d)
+   // TODO - swim lane for geographic locations (test/proc, med)
+   // TODO - swim lane for stacked medication rise and fall
+
+   static public final String SELECTION_MODEL_PROPERTY = "selectionModel";
+
+
+   static private final HighlightPainter FILL_PAINTER = new FillPainter();
+   static private final HighlightPainter UNDERLINE_PAINTER = new UnderlinePainter();
+   static private final HighlightPainter TWO_UNDERLINE_PAINTER = new TwoUnderliner();
+   static private final HighlightPainter WAVE_PAINTER = new WaveUnderliner();
+   static private final HighlightPainter DASH_PAINTER = new DashUnderliner();
+   static private final HighlightPainter DASH_WAVE_PAINTER = new DashWaveUnderliner();
+
+   //   static private final TrianglePainter TRIANGLE_PAINTER = new TrianglePainter();
+// X : 0,4 UL ; 0,4  LL ; 4,-4 UR ; 4,-4 LR
+// Y : 0,4 UL ; 4,-4 LL ; 0,4  UR ; 4,-4 LR
+//   static private final HighlightPainter SYMPTOM_PAINTER = new TrianglePainter( 0, 4, 0, 4 );
+//   static private final HighlightPainter PROCEDURE_PAINTER = new TrianglePainter( 0, 4, 4, -4 );
+//   static private final HighlightPainter DISEASE_PAINTER = new TrianglePainter( 4, -4, 4, -4 );
+//   static private final HighlightPainter MEDICATION_PAINTER = new TrianglePainter( 4, -4, 0, 4 );
+   static private final HighlightPainter ASTERISK_PAINTER = new AsteriskPainter();
+
+
+   private final Highlighter.HighlightPainter _allLayersPainter = new AllPainter();
+
+
+   private Collection<Integer> _selectedRowYs = new HashSet<>();
+   private int _rowHeight = 0;
+
+   private final ListModel<EventListElement> _eventListModel;
+   private final ListSelectionModel _eventListSelectionModel;
+   private TimeSpanSelectionModel _selectionModel;
+   private final TimeSpanSelectionListener _selectionListener;
+   private JScrollBar _verticalScrollBar;
+
+   private final Collection<TextSpan> _focusTextSpans;
+   private final Collection<TextSpan> _selectedTextSpans;
+   private final Map<TextSpan, SemanticClassType> _semanticTypeMap;
+   private final Map<TextSpan, Entity> _eventMap;
+
+
+   public TextHighlightPanel( final Timeline timeline, final ListModel<EventListElement> eventListModel,
+                              final ListSelectionModel eventListSelectionModel ) {
+      setEditable( false );
+//      setFont( new Font( "Monospaced", Font.PLAIN, getFont().getSize() ) );  // Didn't help as spacing is already bad
+      _eventListModel = eventListModel;
+      eventListSelectionModel.addListSelectionListener( new PrivateListSelectionListener() );
+      _eventListSelectionModel = eventListSelectionModel;
+      _selectionListener = new PrivateSelectionListener();
+      _focusTextSpans = new HashSet<>();
+      _selectedTextSpans = new HashSet<>();
+      _semanticTypeMap = new HashMap<>();
+      _eventMap = new HashMap<>();
+      final Collection<Entity> events = timeline.getEntities();
+      for ( Entity event : events ) {
+         final TextSpan textSpan = event.getTextSpan();
+         addEvent( textSpan, event );
+         // TODO refactor to use event.isClassType()
+         if ( event.isClassType( SemanticClassType.SIGN_OR_SYMPTOM ) ) {
+            addSemanticHighlight( textSpan, SemanticClassType.SIGN_OR_SYMPTOM );
+         } else if ( event.isClassType( SemanticClassType.PROCEDURE ) ) {
+            addSemanticHighlight( textSpan, SemanticClassType.PROCEDURE );
+         } else if ( event.isClassType( SemanticClassType.DISEASE_DISORDER ) ) {
+            addSemanticHighlight( textSpan, SemanticClassType.DISEASE_DISORDER );
+         } else if ( event.isClassType( SemanticClassType.MEDICATION ) ) {
+            addSemanticHighlight( textSpan, SemanticClassType.MEDICATION );
+            // Also mark text from other semantic types even though they aren't of our 4 main event types
+         } else if ( event.isClassType( SemanticClassType.ANATOMICAL_SITE ) ) {
+            addSemanticHighlight( textSpan, SemanticClassType.ANATOMICAL_SITE );
+         } else if ( event.isClassType( SemanticClassType.CLINICAL_ATTRIBUTE ) ) {
+            addSemanticHighlight( textSpan, SemanticClassType.CLINICAL_ATTRIBUTE );
+         } else if ( event.isClassType( SemanticClassType.PHENOMENA ) ) {
+            addSemanticHighlight( textSpan, SemanticClassType.PHENOMENA );
+         } else if ( event.isClassType( SemanticClassType.MISC ) ) {
+            addSemanticHighlight( textSpan, SemanticClassType.MISC );
+         }
+      }
+      final Collection<Entity> times = timeline.getTimes();
+      for ( Entity timex : times ) {
+         final TextSpan textSpan = timex.getTextSpan();
+         addEvent( textSpan, timex );
+      }
+      setSelectionModel( new DefaultTimeSpanSelectionModel() );
+      addMouseListener( new TimelineMouseHandler( timeline ) );
+      addMouseMotionListener( new TimelineMouseHandler( timeline ) );
+   }
+
+   // TODO enable Timeline Switching
+//   public void setTimeline( final Timeline timeline, final EventListModel eventListModel ) {
+//      _eventColorMap.clear();
+//      _selectedRowYs.clear();
+//      _selectedTextSpans.clear();
+//      _focusTextSpans.clear();
+//      _eventListModel = eventListModel;
+//      final Collection<TimeSpanPlus> timeSpans = timeline.getTimeSpans();
+//      for ( TimeSpanPlus timeSpan : timeSpans ) {
+//         final Collection<Entity> events = timeline.getEntities( timeSpan );
+//         for ( Entity event : events ) {
+//            final TextSpan textSpan = event.getTextSpan();
+//            final boolean isNegated = AttributeUtil.hasNegativePolarity( event );
+//            final boolean isUncertain = AttributeUtil.hasUncertainty( event );
+//            addEventHighlight( textSpan, isNegated, isUncertain );
+//         }
+//      }
+//      repaint();
+//   }
+
+   public void setVerticalScrollBar( final JScrollBar scrollBar ) {
+      _verticalScrollBar = scrollBar;
+      _verticalScrollBar.setUI( new HighlightScrollBarUI() );
+   }
+
+   /**
+    * Sets the TimeSpan selection model for this OldJTimeline to <code>newModel</code>
+    * and registers for listener notifications from the new selection model.
+    *
+    * @param newModel the new selection model
+    * @throws IllegalArgumentException if <code>newModel</code> is <code>null</code>
+    * @see #getSelectionModel
+    */
+   public void setSelectionModel( final TimeSpanSelectionModel newModel ) {
+      if ( newModel == null ) {
+         throw new IllegalArgumentException( "Cannot set a null SelectionModel" );
+      }
+      if ( newModel.equals( _selectionModel ) ) {
+         return;
+      }
+      if ( _selectionModel != null ) {
+         _selectionModel.removeSelectionListener( _selectionListener );
+      }
+      final TimeSpanSelectionModel oldModel = _selectionModel;
+      _selectionModel = newModel;
+      _selectionModel.addSelectionListener( _selectionListener );
+      firePropertyChange( SELECTION_MODEL_PROPERTY, oldModel, newModel );
+      repaint();
+   }
+
+   /**
+    * @return the object that provides TimeSpan selection state, <code>null</code>
+    * if selection is not allowed
+    * @see #setSelectionModel
+    */
+   public TimeSpanSelectionModel getSelectionModel() {
+      return _selectionModel;
+   }
+
+   private void addSelectedHighlight( final TextSpan textSpan ) {
+      if ( textSpan instanceof DefaultDiscontiguousTextSpan ) {
+         for ( TextSpan textSpan1 : (DiscontiguousTextSpan)textSpan ) {
+            addSelectedHighlight( textSpan1 );
+         }
+         return;
+      }
+      try {
+         final Rectangle bounds = modelToView( textSpan.getStartIndex() );
+         _selectedRowYs.add( bounds.y );
+         _selectedTextSpans.add( textSpan );
+         if ( DEBUG ) {
+            System.out.println( textSpan.getStartIndex() + " , " + textSpan.getEndIndex() );
+         }
+      } catch ( BadLocationException blE ) {
+         LOGGER.warning( blE.getMessage() );
+      }
+   }
+
+   private void addEvent( final TextSpan textSpan, final Entity event ) {
+      if ( textSpan instanceof DefaultDiscontiguousTextSpan ) {
+         for ( TextSpan textSpan1 : (DiscontiguousTextSpan)textSpan ) {
+            addEvent( textSpan1, event );
+         }
+         return;
+      }
+      _eventMap.put( textSpan, event );
+   }
+
+
+   private void addSemanticHighlight( final TextSpan textSpan, final SemanticClassType semanticType ) {
+      if ( textSpan instanceof DefaultDiscontiguousTextSpan ) {
+         for ( TextSpan textSpan1 : (DiscontiguousTextSpan)textSpan ) {
+            addSemanticHighlight( textSpan1, semanticType );
+         }
+         return;
+      }
+      _semanticTypeMap.put( textSpan, semanticType );
+   }
+
+   private void addFocusTextSpan( final TextSpan textSpan ) {
+      if ( textSpan instanceof DefaultDiscontiguousTextSpan ) {
+         for ( TextSpan textSpan1 : (DiscontiguousTextSpan)textSpan ) {
+            addFocusTextSpan( textSpan1 );
+         }
+         return;
+      }
+      _focusTextSpans.add( textSpan );
+   }
+
+   private void selectTimeSpans( final TimeSpanSelectionEvent event ) {
+      final String text = getText();
+      if ( text == null || text.isEmpty() ) {
+         return;
+      }
+      _selectedRowYs.clear();
+      _selectedTextSpans.clear();
+      _focusTextSpans.clear();
+
+      int firstOffset = 0;
+      final Map<PointedTimeSpan, Collection<Entity>> selectedEntities = event.getSelectedEntities();
+      if ( selectedEntities != null && !selectedEntities.isEmpty() ) {
+         firstOffset = text.length() - 1;
+         for ( Collection<Entity> entityCollection : selectedEntities.values() ) {
+            for ( Entity entity : entityCollection ) {
+               if ( entity.isClassType( TemporalClassType.TIMEX ) || entity.isClassType( TemporalClassType.DOCTIME ) ) {
+                  // This should not happen
+                  continue;
+               }
+               final TextSpan textSpan = entity.getTextSpan();
+               addSelectedHighlight( textSpan );
+               firstOffset = Math.min( firstOffset, textSpan.getStartIndex() );
+               //            final Collection<Relation> eventEventRelations = _timeline.getEntityEntityRelations( entity );
+               //            final EventListElement element = new EventListElement( timeSpan, entity, eventEventRelations,
+               //                                                                   semanticType, selectionTexts, isFirst );
+            }
+         }
+      }
+      scrollToOffset( firstOffset );
+   }
+
+   /**
+    * {@inheritDoc}
+    * also repaints the vertical scroll bar
+    */
+   @Override
+   public void paint( final Graphics g ) {
+      super.paint( g );
+      // repaint the vertical scroll bar
+      _verticalScrollBar.repaint();
+   }
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   public void setText( final String text ) {
+      super.setText( text );
+      try {
+         for ( TextSpan textSpan : _eventMap.keySet() ) {
+            getHighlighter().addHighlight( textSpan.getStartIndex(), textSpan.getEndIndex(), _allLayersPainter );
+         }
+         for ( TextSpan textSpan : _semanticTypeMap.keySet() ) {
+            getHighlighter().addHighlight( textSpan.getStartIndex(), textSpan.getEndIndex(), _allLayersPainter );
+         }
+      } catch ( BadLocationException blE ) {
+         LOGGER.warning( blE.getMessage() );
+      }
+   }
+
+   // Painter for underlined highlights
+   private class AllPainter extends LayeredHighlighter.LayerPainter {
+      @Override
+      public void paint( final Graphics g, final int offs0, final int offs1, final Shape bounds,
+                         final JTextComponent comp ) {
+         // Do nothing: this method will never be called
+      }
+
+      @Override
+      public Shape paintLayer( final Graphics g, final int startIndex, final int stopIndex, final Shape bounds,
+                               final JTextComponent comp, final View view ) {
+         Rectangle alloc;
+         if ( startIndex == view.getStartOffset() && stopIndex == view.getEndOffset() ) {
+            if ( bounds instanceof Rectangle ) {
+               alloc = (Rectangle)bounds;
+            } else {
+               alloc = bounds.getBounds();
+            }
+         } else {
+            try {
+               final Shape shape = view.modelToView( startIndex,
+                     Position.Bias.Forward, stopIndex,
+                     Position.Bias.Backward, bounds );
+               alloc = (shape instanceof Rectangle) ? (Rectangle)shape
+                                                    : shape.getBounds();
+            } catch ( BadLocationException blE ) {
+               return null;
+            }
+         }
+         _rowHeight = alloc.height;
+         final TextSpan textSpan = new DefaultTextSpan( startIndex, stopIndex );
+         if ( _focusTextSpans.contains( textSpan ) ) {
+            FILL_PAINTER.paintHighlight( g, alloc, EventColor.FOCUS.getColor() );
+         } else if ( _selectedTextSpans.contains( textSpan ) ) {
+            boolean intersectsFocus = false;
+            for ( TextSpan focusSpan : _focusTextSpans ) {
+               if ( textSpan.intersects( focusSpan ) ) {
+                  intersectsFocus = true;
+                  break;
+               }
+            }
+            if ( !intersectsFocus ) {
+               FILL_PAINTER.paintHighlight( g, alloc, EventColor.SELECTED.getColor() );
+            }
+         }
+         final Entity event = _eventMap.get( textSpan );
+         if ( event != null ) {
+            if ( event.isClassType( TemporalClassType.TIMEX ) || event.isClassType( TemporalClassType.DOCTIME ) ) {
+               TWO_UNDERLINE_PAINTER.paintHighlight( g, alloc, EventColor.TIMEX.getColor() );
+            } else if ( event.isClassType( TemporalClassType.EVENT ) ) {
+               final EventStatus eventStatus = EventStatus.getEventStatus( event );
+               final Color color = EventColor.getEventColor( eventStatus ).getColor();
+               if ( eventStatus == EventStatus.UNCERTAIN_NEGATED ) {
+                  DASH_WAVE_PAINTER.paintHighlight( g, alloc, color );
+               } else if ( eventStatus == EventStatus.UNCERTAIN ) {
+                  WAVE_PAINTER.paintHighlight( g, alloc, color );
+               } else if ( eventStatus == EventStatus.NEGATED ) {
+                  DASH_PAINTER.paintHighlight( g, alloc, color );
+               } else {
+                  UNDERLINE_PAINTER.paintHighlight( g, alloc, color );
+               }
+            }
+         }
+         final SemanticClassType semanticType = _semanticTypeMap.get( textSpan );
+         if ( semanticType != null ) {
+            final Color semanticColor = EventColor.getSemanticColor( semanticType ).getColor();
+            ASTERISK_PAINTER.paintHighlight( g, alloc, semanticColor );
+         }
+         return alloc;
+      }
+   }
+
+
+   private class HighlightScrollBarUI extends BasicScrollBarUI {
+      protected void paintTrack( final Graphics g, final JComponent comp, final Rectangle trackBounds ) {
+         super.paintTrack( g, comp, trackBounds );
+         final double xForm = trackBounds.getHeight() / TextHighlightPanel.this.getHeight();
+         final double xxForm = trackBounds.getWidth() / TextHighlightPanel.this.getWidth();
+         final int rowHeight = Math.max( 2, (int)(_rowHeight * xForm) );
+         final HashSetMap<Color, Rectangle> eventTrackBounds = new HashSetMap<>();
+         try {
+//            for ( Map.Entry<TextSpan,Color> eventColor : _eventColorMap.entrySet() ) {
+//               final Rectangle bounds = modelToView( eventColor.getKey().getStartIndex() );
+//               final int trackX = trackBounds.x + (int)(bounds.x * xxForm);
+//               final int trackY = trackBounds.y + (int)(bounds.y * xForm);
+//               final Rectangle endBounds = modelToView( eventColor.getKey().getEndIndex() );
+//               final int trackWidth = Math.max(1, (int)((endBounds.x + endBounds.width - bounds.x ) * xxForm) );
+//               eventTrackBounds.place( eventColor.getValue(),
+//                                       new Rectangle( trackX, trackY, trackWidth, rowHeight ) );
+//            }
+            for ( Map.Entry<TextSpan, Entity> eventEntry : _eventMap.entrySet() ) {
+               final Rectangle bounds = modelToView( eventEntry.getKey().getStartIndex() );
+               final int trackX = trackBounds.x + (int)(bounds.x * xxForm);
+               final int trackY = trackBounds.y + (int)(bounds.y * xForm);
+               final Rectangle endBounds = modelToView( eventEntry.getKey().getEndIndex() );
+               final int trackWidth = Math.max( 1, (int)((endBounds.x + endBounds.width - bounds.x) * xxForm) );
+               final EventStatus eventStatus = EventStatus.getEventStatus( eventEntry.getValue() );
+               Color color;
+               if ( eventEntry.getValue().isClassType( TemporalClassType.TIMEX )
+                    || eventEntry.getValue().isClassType( TemporalClassType.DOCTIME ) ) {
+                  color = EventColor.TIMEX.getColor();
+               } else {
+                  color = EventColor.getEventColor( eventStatus ).getColor();
+               }
+               eventTrackBounds.place( color, new Rectangle( trackX, trackY, trackWidth, rowHeight ) );
+            }
+         } catch ( BadLocationException blE ) {
+            LOGGER.warning( blE.getMessage() );
+         }
+         paintEventYs( g, eventTrackBounds );
+         if ( _selectedRowYs.isEmpty() ) {
+            return;
+         }
+         g.setColor( EventColor.SELECTED.getColor() );
+         for ( int y : _selectedRowYs ) {
+            final int trackY = trackBounds.y + (int)(y * xForm);
+            g.fillRect( trackBounds.x, trackY, trackBounds.width, rowHeight );
+         }
+      }
+
+      private void paintEventYs( final Graphics g, final HashSetMap<Color, Rectangle> eventRowBounds ) {
+         for ( Map.Entry<Color, Set<Rectangle>> colorEvents : eventRowBounds.entrySet() ) {
+            g.setColor( colorEvents.getKey() );
+            for ( Rectangle bounds : colorEvents.getValue() ) {
+               g.fillRect( bounds.x, bounds.y, bounds.width, bounds.height );
+            }
+         }
+      }
+   }
+
+
+   private class PrivateSelectionListener implements TimeSpanSelectionListener {
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public void valueChanged( final TimeSpanSelectionEvent event ) {
+         if ( event == null || event.getSource() == null || event.getValueIsAdjusting() ) {
+            return;
+         }
+         selectTimeSpans( event );
+      }
+   }
+
+   private void scrollToOffset( final int offset ) {
+      final Rectangle visibleBounds = getVisibleRect();
+      try {
+         final Rectangle textBounds = modelToView( offset );
+         visibleBounds.y = Math.max( 0, textBounds.y - (visibleBounds.height - textBounds.height) / 2 );
+         scrollRectToVisible( visibleBounds );
+      } catch ( BadLocationException blE ) {
+         LOGGER.warning( blE.getMessage() );
+      }
+      final Rectangle bounds = getVisibleRect();
+      repaint( bounds.x, bounds.y, bounds.width, bounds.height );
+   }
+
+   private class PrivateListSelectionListener implements ListSelectionListener {
+      public void valueChanged( final ListSelectionEvent event ) {
+         if ( event == null || event.getValueIsAdjusting() ) {
+            return;
+         }
+         if ( _eventListSelectionModel.isSelectionEmpty() ) {
+            return;
+         }
+         final int leadIndex = _eventListSelectionModel.getLeadSelectionIndex();
+         if ( leadIndex < 0 || leadIndex >= _eventListModel.getSize() ) {
+            return;
+         }
+         final Object value = _eventListModel.getElementAt( leadIndex );
+         if ( value == null ) {
+            return;
+         }
+         _focusTextSpans.clear();
+         final EventListElement element = (EventListElement)value;
+         final Entity entity = element.getEntity();
+         final TextSpan textSpan = entity.getTextSpan();
+         addFocusTextSpan( textSpan );
+         scrollToOffset( textSpan.getStartIndex() );
+      }
+   }
+
+
+   private class TimelineMouseHandler extends MouseAdapter {
+      private final Timeline __timeline;
+
+      private TimelineMouseHandler( final Timeline timeline ) {
+         __timeline = timeline;
+      }
+
+      @Override
+      public void mouseClicked( final MouseEvent event ) {
+         if ( event == null ) {
+            return;
+         }
+         final Point p = event.getPoint();
+         final int textIndex = viewToModel( event.getPoint() );
+         if ( textIndex < 0 ) {
+            return;
+         }
+         Entity selection = null;
+         for ( Map.Entry<TextSpan, Entity> eventEntry : _eventMap.entrySet() ) {
+            if ( eventEntry.getKey().getStartIndex() <= textIndex && textIndex <= eventEntry.getKey().getEndIndex() ) {
+               selection = eventEntry.getValue();
+               break;
+            }
+         }
+         if ( selection == null ) {
+            return;
+         }
+         final Map<PointedTimeSpan, Collection<Entity>> timeSpanEntities = new HashMap<>();
+         final Collection<Entity> selections = Collections.singletonList( selection );
+         for ( PointedTimeSpan timeSpan : __timeline ) {
+            if ( __timeline.getEntities( timeSpan ).contains( selection ) ) {
+               timeSpanEntities.put( timeSpan, selections );
+            }
+         }
+         if ( timeSpanEntities.isEmpty() ) {
+            timeSpanEntities.put( TimeSpanPlus.UNKNOWN_TIMESPAN_PLUS, selections );
+         }
+         final TimeSpanSelectionModel selectionModel = getSelectionModel();
+         selectionModel.setValueIsAdjusting( selectionModel, true );
+         selectionModel.setSelectedTerms( selectionModel, Collections.singletonList( "*" ) );
+         selectionModel.setSelectedEntities( selectionModel, timeSpanEntities );
+         selectionModel.setValueIsAdjusting( selectionModel, false );
+      }
+   }
+
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/calendar/JTimelineCalendar.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/calendar/JTimelineCalendar.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/calendar/JTimelineCalendar.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/calendar/JTimelineCalendar.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,398 @@
+package org.chboston.cnlp.timeline.gui.calendar;
+
+import org.chboston.cnlp.timeline.oldUI.TimelineDrawUtil;
+import org.chboston.cnlp.timeline.timespan.TimeSpan;
+
+import javax.swing.*;
+import java.awt.*;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+import static org.chboston.cnlp.timeline.timespan.CalendarUtil.TIME_MILLIS.*;
+
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 11/14/12
+ */
+public class JTimelineCalendar extends JLabel {
+
+   static private final int CALENDER_TICK_HEIGHT = 10;
+   static private final int TEXT_WIDTH_MIN = 75;
+   static private final int TEXT_WIDTH_MAX = 300;
+   static private final int TEXT_WIDTH_INCR = 25;
+
+   static private final DateFormat YEAR_DATE_FORMAT = new SimpleDateFormat( "yyyy" );
+   static private final DateFormat SHORT_MONTH_DATE_FORMAT = new SimpleDateFormat( "M/yyyy" );
+   static private final DateFormat SHORT_DATE_FORMAT = new SimpleDateFormat( "M/d/yyyy" );
+   static private final DateFormat SHORT_DATE_TIME_FORMAT = new SimpleDateFormat( "M/d/yyyy h:mm a" );
+
+   private TimeSpan _timeline;
+
+   public JTimelineCalendar( final TimeSpan timeline ) {
+      _timeline = timeline;
+   }
+
+   public void setTimeline( final TimeSpan timeline ) {
+      _timeline = timeline;
+   }
+
+   private int getFontHeight() {
+      final FontMetrics fm = getFontMetrics( getFont() );
+      return fm.getHeight();
+   }
+
+   @Override
+   public Dimension getPreferredSize() {
+      final FontMetrics fm = getFontMetrics( getFont() );
+      fm.getHeight();
+
+      return new Dimension( super.getPreferredSize().width, getFontHeight() + CALENDER_TICK_HEIGHT );
+   }
+
+   @Override
+   protected void paintComponent( final Graphics g ) {
+      int totalWidth = getPreferredSize().width;
+      if ( totalWidth == 0 ) {
+         // preferred width is 0 on startup
+         totalWidth = getBounds().width;
+      }
+      if ( totalWidth == 0 ) {
+         return;
+      }
+      final long beginDrawMillis = TimelineDrawUtil.getBufferedBeginTime( _timeline.getStartCalendar(),
+            _timeline.getEndCalendar() ).getTimeInMillis();
+      final long endDrawMillis = TimelineDrawUtil.getBufferedEndTime( _timeline.getStartCalendar(),
+            _timeline.getEndCalendar() ).getTimeInMillis();
+
+      final long drawTimeDelta = endDrawMillis - beginDrawMillis;
+      final double xform = drawTimeDelta / (double)totalWidth;
+
+      final long startMillis = _timeline.getStartMillis();
+      final long endMillis = _timeline.getStopMillis();
+      final long deltaMillis = endMillis - startMillis;
+      // Total number of draw pixels
+      final double pixelDelta = deltaMillis / xform;
+
+      for ( int textWidth = TEXT_WIDTH_MIN; textWidth <= TEXT_WIDTH_MAX; textWidth += TEXT_WIDTH_INCR ) {
+         if ( pixelDelta < textWidth ) {
+            // too small
+            continue;
+         }
+         // get back to Millis
+         final double perDateMillis = textWidth * xform;
+         final Calendar checkCalendar = getCalendar( startMillis, perDateMillis );
+         final int calendarField = getCalendarField( perDateMillis );
+         final int calendarIncrement = getCalendarIncrement( perDateMillis );
+         final DateFormat dateFormat = getDateFormat( perDateMillis, calendarIncrement );
+         if ( checkTextWidth( g, dateFormat, checkCalendar, calendarField, calendarIncrement, xform ) ) {
+            final Calendar calendar = getCalendar( startMillis, perDateMillis );
+            if ( calendarIncrement == 1 ) {
+               paintCenteredText( g,
+                     dateFormat,
+                     calendar,
+                     calendarField,
+                     calendarIncrement,
+                     beginDrawMillis,
+                     xform,
+                     endDrawMillis );
+            } else {
+               paintStartingText( g,
+                     dateFormat,
+                     calendar,
+                     calendarField,
+                     calendarIncrement,
+                     beginDrawMillis,
+                     xform,
+                     endDrawMillis );
+            }
+            break;
+         }
+      }
+   }
+
+
+   static private DateFormat getDateFormat( final double perDateMillis ) {
+      if ( perDateMillis >= DECADE.__millis ) {
+         return YEAR_DATE_FORMAT;
+      } else if ( perDateMillis >= 5 * YEAR.__millis ) {
+         return YEAR_DATE_FORMAT;
+      } else if ( perDateMillis >= 2 * YEAR.__millis ) {
+         return YEAR_DATE_FORMAT;
+      } else if ( perDateMillis >= YEAR.__millis ) {
+         return YEAR_DATE_FORMAT;
+      } else if ( perDateMillis >= 6 * MONTH.__millis ) {
+         return SHORT_MONTH_DATE_FORMAT;
+      } else if ( perDateMillis >= MONTH.__millis ) {
+         return SHORT_MONTH_DATE_FORMAT;
+      } else if ( perDateMillis >= WEEK.__millis ) {
+         return SHORT_DATE_FORMAT;
+      } else if ( perDateMillis >= 2 * DAY.__millis ) {
+         return SHORT_DATE_FORMAT;
+      } else if ( perDateMillis >= DAY.__millis ) {
+         return SHORT_DATE_FORMAT;
+      } else if ( perDateMillis >= DAY.__millis / 2 ) {
+         return SHORT_DATE_TIME_FORMAT;
+      } else if ( perDateMillis >= DAY.__millis / 4 ) {
+         return SHORT_DATE_TIME_FORMAT;
+      }
+      return SHORT_DATE_TIME_FORMAT;
+   }
+
+   static private DateFormat getDateFormat( final double perDateMillis, final int calendarIncrement ) {
+      if ( perDateMillis >= DECADE.__millis ) {
+         return YEAR_DATE_FORMAT;
+      } else if ( perDateMillis >= 5 * YEAR.__millis ) {
+         return YEAR_DATE_FORMAT;
+      } else if ( perDateMillis >= 2 * YEAR.__millis ) {
+         return YEAR_DATE_FORMAT;
+      } else if ( perDateMillis >= YEAR.__millis ) {
+         return YEAR_DATE_FORMAT;
+      } else if ( perDateMillis >= 6 * MONTH.__millis ) {
+         //         return SHORT_MONTH_DATE_FORMAT;
+         return SHORT_DATE_FORMAT;
+      } else if ( perDateMillis >= MONTH.__millis ) {
+         if ( calendarIncrement == 1 ) {
+            return SHORT_MONTH_DATE_FORMAT;
+         }
+         return SHORT_DATE_FORMAT;
+      } else if ( perDateMillis >= WEEK.__millis ) {
+         return SHORT_DATE_FORMAT;
+      } else if ( perDateMillis >= 2 * DAY.__millis ) {
+         return SHORT_DATE_FORMAT;
+      } else if ( perDateMillis >= DAY.__millis ) {
+         return SHORT_DATE_FORMAT;
+      } else if ( perDateMillis >= DAY.__millis / 2 ) {
+         return SHORT_DATE_TIME_FORMAT;
+      } else if ( perDateMillis >= DAY.__millis / 4 ) {
+         return SHORT_DATE_TIME_FORMAT;
+      }
+      return SHORT_DATE_TIME_FORMAT;
+   }
+
+   static private int getCalendarField( final double perDateMillis ) {
+      if ( perDateMillis >= DECADE.__millis ) {
+         return Calendar.YEAR;
+      } else if ( perDateMillis >= 5 * YEAR.__millis ) {
+         return Calendar.YEAR;
+      } else if ( perDateMillis >= 2 * YEAR.__millis ) {
+         return Calendar.YEAR;
+      } else if ( perDateMillis >= YEAR.__millis ) {
+         return Calendar.YEAR;
+      } else if ( perDateMillis >= 6 * MONTH.__millis ) {
+         return Calendar.MONTH;
+      } else if ( perDateMillis >= MONTH.__millis ) {
+         return Calendar.MONTH;
+      } else if ( perDateMillis >= WEEK.__millis ) {
+         return Calendar.WEEK_OF_YEAR;
+      } else if ( perDateMillis >= 2 * DAY.__millis ) {
+         return Calendar.DAY_OF_YEAR;
+      } else if ( perDateMillis >= DAY.__millis ) {
+         return Calendar.DAY_OF_YEAR;
+      } else if ( perDateMillis >= DAY.__millis / 2 ) {
+         return Calendar.HOUR_OF_DAY;
+      } else if ( perDateMillis >= DAY.__millis / 4 ) {
+         return Calendar.HOUR_OF_DAY;
+      }
+      return Calendar.HOUR_OF_DAY;
+   }
+
+   final int getCalendarIncrement( final double perDateMillis ) {
+      if ( perDateMillis >= DECADE.__millis ) {
+         return 10;
+      } else if ( perDateMillis >= 5 * YEAR.__millis ) {
+         return 5;
+      } else if ( perDateMillis >= 2 * YEAR.__millis ) {
+         return 2;
+      } else if ( perDateMillis >= YEAR.__millis ) {
+         return 1;
+      } else if ( perDateMillis >= 6 * MONTH.__millis ) {
+         return 6;
+      } else if ( perDateMillis >= MONTH.__millis ) {
+         return 1;
+      } else if ( perDateMillis >= WEEK.__millis ) {
+         return 1;
+      } else if ( perDateMillis >= 2 * DAY.__millis ) {
+         return 2;
+      } else if ( perDateMillis >= DAY.__millis ) {
+         return 1;
+      } else if ( perDateMillis >= DAY.__millis / 2 ) {
+         return 12;
+      } else if ( perDateMillis >= DAY.__millis / 4 ) {
+         return 6;
+      } else if ( perDateMillis >= HOUR.__millis / 6 ) {
+         return 4;
+      } else if ( perDateMillis >= HOUR.__millis / 12 ) {
+         return 2;
+      }
+      return 1;
+   }
+
+   static private Calendar getCalendar( final long startMillis, final double perDateMillis ) {
+      final Calendar calendar = Calendar.getInstance();
+      calendar.setTimeInMillis( startMillis );
+      if ( perDateMillis >= DECADE.__millis ) {
+         calendar.add( Calendar.YEAR, -calendar.get( Calendar.YEAR ) % 10 );
+         calendar.add( Calendar.DAY_OF_YEAR, -calendar.get( Calendar.DAY_OF_YEAR ) + 1 );
+      } else if ( perDateMillis >= 5 * YEAR.__millis ) {
+         calendar.add( Calendar.YEAR, -calendar.get( Calendar.YEAR ) % 5 );
+         calendar.add( Calendar.DAY_OF_YEAR, -calendar.get( Calendar.DAY_OF_YEAR ) + 1 );
+      } else if ( perDateMillis >= 2 * YEAR.__millis ) {
+         calendar.add( Calendar.YEAR, -calendar.get( Calendar.YEAR ) % 2 );
+         calendar.add( Calendar.DAY_OF_YEAR, -calendar.get( Calendar.DAY_OF_YEAR ) + 1 );
+      } else if ( perDateMillis >= YEAR.__millis ) {
+         calendar.add( Calendar.DAY_OF_YEAR, -calendar.get( Calendar.DAY_OF_YEAR ) + 1 );
+      } else if ( perDateMillis >= 6 * MONTH.__millis ) {
+         calendar.add( Calendar.MONTH, -calendar.get( Calendar.MONTH ) % 6 );
+         calendar.add( Calendar.DAY_OF_MONTH, -calendar.get( Calendar.DAY_OF_MONTH ) + 1 );
+      } else if ( perDateMillis >= MONTH.__millis ) {
+         calendar.add( Calendar.DAY_OF_MONTH, -calendar.get( Calendar.DAY_OF_MONTH ) + 1 );
+      } else if ( perDateMillis >= WEEK.__millis ) {
+         calendar.add( Calendar.DAY_OF_WEEK_IN_MONTH, -calendar.get( Calendar.DAY_OF_WEEK_IN_MONTH ) + 1 );
+         calendar.add( Calendar.HOUR_OF_DAY, -calendar.get( Calendar.HOUR_OF_DAY ) );
+      } else if ( perDateMillis >= 2 * DAY.__millis ) {
+         calendar.add( Calendar.DAY_OF_YEAR, -calendar.get( Calendar.DAY_OF_YEAR ) % 2 );
+      } else if ( perDateMillis >= DAY.__millis ) {
+         calendar.add( Calendar.HOUR_OF_DAY, -calendar.get( Calendar.HOUR_OF_DAY ) );
+      } else if ( perDateMillis >= DAY.__millis / 2 ) {
+         calendar.add( Calendar.HOUR_OF_DAY, -calendar.get( Calendar.HOUR_OF_DAY ) % 12 - 1 );
+      } else if ( perDateMillis >= DAY.__millis / 4 ) {
+         calendar.add( Calendar.HOUR_OF_DAY, -calendar.get( Calendar.HOUR_OF_DAY ) % 6 - 1 );
+      } else if ( perDateMillis >= HOUR.__millis / 6 ) {
+         calendar.add( Calendar.HOUR_OF_DAY, -calendar.get( Calendar.HOUR_OF_DAY ) % 4 - 1 );
+      } else if ( perDateMillis >= HOUR.__millis / 12 ) {
+         calendar.add( Calendar.HOUR_OF_DAY, -calendar.get( Calendar.HOUR_OF_DAY ) % 2 - 1 );
+      } else if ( perDateMillis >= HOUR.__millis ) {
+         calendar.add( Calendar.HOUR_OF_DAY, -1 );
+      }
+      return calendar;
+   }
+
+//   final Map<String,Integer> _textWidths = new HashMap<>();
+//
+//   private int getTextWidth( final Graphics g, final String text ) {
+//      Integer width = _textWidths.get( text );
+//      if ( width != null ) {
+//         return width;
+//      }
+//      final FontMetrics fm = g.getFontMetrics();
+//      final int textWidth = (int) fm.getStringBounds( text, g ).getWidth();
+//      _textWidths.put( text, textWidth );
+//      return textWidth;
+//   }
+
+   // Didn't improve speed
+//   final Map<String,BufferedImage> _dateTextImages = new HashMap<>();
+//
+//   private BufferedImage getDateTextImage( final Graphics g, final String text ) {
+//      BufferedImage dateTextImage = _dateTextImages.get( text );
+//      if ( dateTextImage != null ) {
+//          return dateTextImage;
+//      }
+//      final FontMetrics fm = g.getFontMetrics();
+//      final Rectangle2D bounds = fm.getStringBounds( text, g );
+//      final BufferedImage dateTextImage = new BufferedImage( (int)bounds.getWidth()+10, (int)bounds.getHeight(),
+//                                                         BufferedImage.TYPE_INT_ARGB );
+//      final Graphics2D g2d = dateTextImage.createGraphics();
+//      g2d.setColor( Color.BLACK );
+//      g2d.drawString( text, 0, fm.getAscent() );
+//      g2d.dispose();
+//      _dateTextImages.put( text, dateTextImage );
+//      return dateTextImage;
+//   }
+
+
+   static private String getDateText( final long millis, final DateFormat dateFormat ) {
+      final Date date = new Date( millis );
+      return dateFormat.format( date );
+   }
+
+   static private int getTextWidth( final Graphics g, final String text ) {
+      final FontMetrics fm = g.getFontMetrics();
+      return (int)fm.getStringBounds( text, g ).getWidth();
+   }
+
+   /**
+    * Make certain that the formatted text dates/times won't be overlapping on screen
+    *
+    * @param g                 -
+    * @param dateFormat        -
+    * @param calendar          -
+    * @param calendarField     -
+    * @param calendarIncrement -
+    * @param xform             -
+    * @return true if the dates will not overlap each other on screen
+    */
+   protected boolean checkTextWidth( final Graphics g, final DateFormat dateFormat, final Calendar calendar,
+                                     final int calendarField, final int calendarIncrement, final double xform ) {
+      long millis = calendar.getTimeInMillis() + 1;
+      final String text = getDateText( millis, dateFormat );
+      final int textWidth = getTextWidth( g, text ) + 10;
+      final int x1 = (int)(millis / xform);
+      calendar.add( calendarField, calendarIncrement );
+      millis = calendar.getTimeInMillis();
+      final int x2 = (int)(millis / xform);
+      return x2 - x1 > textWidth;
+   }
+
+
+   protected void paintStartingText( final Graphics g, final DateFormat dateFormat, final Calendar calendar,
+                                     final int calendarField, final int calendarIncrement,
+                                     final long beginDrawMillis, final double xform, final long endMillis ) {
+      // Clipbounds seem to be ok
+      final Rectangle clipBounds = g.getClipBounds();
+      final int fontHeight = getFontHeight();
+      final int trackHeight = fontHeight + CALENDER_TICK_HEIGHT;
+      long millis = calendar.getTimeInMillis() + 1;
+      int incrementCount = 0;
+      while ( millis <= endMillis + 10 ) {
+         int x = (int)((millis - beginDrawMillis) / xform);
+         if ( clipBounds.contains( x, fontHeight ) ) {
+            if ( incrementCount % calendarIncrement == 0 ) {
+               final String text = getDateText( millis, dateFormat );
+//               BufferedImage textImage = getDateTextImage( g, text );
+//               g.drawImage( textImage, x-textImage.getWidth()/2, 0, null );
+               final int textWidth = getTextWidth( g, text );
+               final int offset = textWidth / 4;
+               g.drawString( text, x - offset, fontHeight );
+               g.drawLine( x, fontHeight + 1, x, trackHeight );
+            } else {
+               g.drawLine( x, fontHeight + 6, x, trackHeight );
+            }
+         }
+         calendar.add( calendarField, 1 );
+         millis = calendar.getTimeInMillis();
+         incrementCount++;
+      }
+   }
+
+   protected void paintCenteredText( final Graphics g, final DateFormat dateFormat, final Calendar calendar,
+                                     final int calendarField, final int calendarIncrement,
+                                     final long beginDrawMillis, final double xform, final long endMillis ) {
+      // Clipbounds seem to be ok
+      final Rectangle clipBounds = g.getClipBounds();
+      final int fontHeight = getFontHeight();
+      final int trackHeight = fontHeight + CALENDER_TICK_HEIGHT;
+      long millis = calendar.getTimeInMillis() + 1;
+      long nextMillis;
+      while ( millis <= endMillis + 10 ) {
+         calendar.add( calendarField, calendarIncrement );
+         nextMillis = calendar.getTimeInMillis();
+         int x = (int)((millis - beginDrawMillis) / xform);
+         int xCenter = (int)(((nextMillis + millis) / 2l - beginDrawMillis) / xform);
+         if ( clipBounds.contains( x, fontHeight ) || clipBounds.contains( xCenter, fontHeight ) ) {
+            final String text = getDateText( millis, dateFormat );
+            final int textWidth = getTextWidth( g, text );
+            final int offset = textWidth / 2;
+            g.drawString( text, xCenter - offset, fontHeight );
+            g.drawLine( x, fontHeight, x, trackHeight );
+         }
+         millis = nextMillis;
+      }
+   }
+
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/corpus/NoteListRenderer.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/corpus/NoteListRenderer.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/corpus/NoteListRenderer.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/corpus/NoteListRenderer.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,30 @@
+package org.chboston.cnlp.timeline.gui.corpus;
+
+
+import org.chboston.cnlp.nlp.corpus.DefaultNote;
+import org.chboston.cnlp.nlp.corpus.Note;
+
+import javax.swing.*;
+import java.awt.*;
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 3/25/14
+ */
+public class NoteListRenderer extends DefaultListCellRenderer {
+
+   public Component getListCellRendererComponent( final JList list, final Object value, final int index,
+                                                  final boolean isSelected, final boolean cellHasFocus ) {
+      if ( value == null || !(value instanceof DefaultNote) ) {
+         return super.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus );
+      }
+      final Note note = (Note)value;
+      final String date = DateFormat.getDateInstance().format( new Date( note.getTimestampMillis() ) );
+      final String textValue = note.getName() + " : " + date;
+      return super.getListCellRendererComponent( list, textValue, index, isSelected, cellHasFocus );
+   }
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/corpus/NoteNavigator.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/corpus/NoteNavigator.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/corpus/NoteNavigator.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/corpus/NoteNavigator.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,232 @@
+package org.chboston.cnlp.timeline.gui.corpus;
+
+import org.chboston.cnlp.gui.GlobalHotkeyManager;
+import org.chboston.cnlp.nlp.corpus.DefaultNote;
+import org.chboston.cnlp.nlp.corpus.Note;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import javax.swing.event.EventListenerList;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import java.awt.*;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.dnd.DnDConstants;
+import java.awt.dnd.DropTarget;
+import java.awt.dnd.DropTargetDropEvent;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.io.File;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 3/25/14
+ */
+final public class NoteNavigator extends JPanel {
+
+   final boolean DEBUG = false;
+   static private final String TEXT_DOC_PATH
+         = "C:\\Spiffy\\Data\\corpus\\SampleTemporal\\ID000_clinic_demo_gold\\ID000_clinic_demo.txt";
+
+   final private JComboBox<Note> _noteBox;
+   private Note _selectedNote;
+
+   public NoteNavigator( final String defaultDirectory ) {
+      super( new BorderLayout( 10, 10 ) );
+      setBorder( new EmptyBorder( 5, 5, 5, 5 ) );
+      final JLabel label = new JLabel( "Selected Note:" );
+      _noteBox = new JComboBox<>();
+      _noteBox.addActionListener( new NoteSelectionForwarder() );
+      _noteBox.setRenderer( new NoteListRenderer() );
+
+      if ( DEBUG ) {
+         _noteBox.addItem( new DefaultNote( TEXT_DOC_PATH ) );
+      }
+
+      final JButton openChooserButton = new JButton( new OpenFileAction( defaultDirectory ) );
+      add( label, BorderLayout.WEST );
+      add( _noteBox, BorderLayout.CENTER );
+      add( openChooserButton, BorderLayout.EAST );
+
+      setDropTarget( new FileDropTarget() );
+
+      final GlobalHotkeyManager hotKeyManager = GlobalHotkeyManager.getInstance();
+      hotKeyManager.addHotKey( "PreviousNote",
+            KeyStroke.getKeyStroke( KeyEvent.VK_P, KeyEvent.CTRL_MASK, false ),
+            new PreviousNoteAction() );
+      hotKeyManager.addHotKey( "NextNote",
+            KeyStroke.getKeyStroke( KeyEvent.VK_N, KeyEvent.CTRL_MASK, false ),
+            new NextNoteAction() );
+   }
+
+   public Note getSelectedNote() {
+      return _selectedNote;
+   }
+
+   /**
+    * mocks the navigator as a list
+    *
+    * @param listener the {@code ListSelectionListener} to add
+    */
+   public void addListSelectionListener( final ListSelectionListener listener ) {
+      listenerList.add( ListSelectionListener.class, listener );
+   }
+
+   /**
+    * mocks the navigator as a list
+    *
+    * @param listener the {@code ListSelectionListener} to remove
+    */
+   public void removeListSelectionListener( final ListSelectionListener listener ) {
+      listenerList.remove( ListSelectionListener.class, listener );
+   }
+
+
+   /**
+    * Opens the JFileChooser
+    */
+   private class OpenFileAction extends AbstractAction {
+      private final JFileChooser __chooser;
+
+      private OpenFileAction( final String defaultDirectory ) {
+         super( "Open Note" );
+         __chooser = new JFileChooser();
+         if ( defaultDirectory != null ) {
+            final File startingDir = new File( defaultDirectory );
+            if ( startingDir.exists() ) {
+               __chooser.setCurrentDirectory( startingDir );
+            }
+         }
+         __chooser.setFileSelectionMode( JFileChooser.FILES_ONLY );
+         __chooser.addChoosableFileFilter( new FileNameExtensionFilter( "Text File", ".txt" ) );
+         __chooser.addChoosableFileFilter( new FileNameExtensionFilter( "Xmi File", ".xmi", ".xmi.old" ) );
+//         __chooser.setAcceptAllFileFilterUsed( false );
+         __chooser.setMultiSelectionEnabled( true );
+      }
+
+      @Override
+      public void actionPerformed( final ActionEvent event ) {
+         final int option = __chooser.showOpenDialog( null );
+         if ( option != JFileChooser.APPROVE_OPTION ) {
+            return;
+         }
+         final File[] files = __chooser.getSelectedFiles();
+         final MutableComboBoxModel<Note> noteListModel = (MutableComboBoxModel<Note>)_noteBox.getModel();
+         for ( File file : files ) {
+            boolean add = true;
+            for ( int i = 0; i < _noteBox.getItemCount(); i++ ) {
+               if ( _noteBox.getItemAt( i ).getPath().equals( file.getPath() ) ) {
+                  add = false;
+                  break;
+               }
+            }
+            if ( add ) {
+               noteListModel.addElement( new DefaultNote( file.getPath() ) );
+            }
+         }
+      }
+      // TODO implement timeline display
+   }
+
+
+   private class FileDropTarget extends DropTarget {
+      public synchronized void drop( final DropTargetDropEvent event ) {
+         event.acceptDrop( DnDConstants.ACTION_COPY );
+         try {
+            final Object values = event.getTransferable().getTransferData( DataFlavor.javaFileListFlavor );
+            if ( values instanceof Iterable ) {
+               final MutableComboBoxModel<Note> noteListModel = (MutableComboBoxModel<Note>)_noteBox.getModel();
+               for ( Object value : (Iterable)values ) {
+                  if ( value instanceof File ) {
+                     boolean add = true;
+                     for ( int i = 0; i < _noteBox.getItemCount(); i++ ) {
+                        if ( _noteBox.getItemAt( i ).getPath().equals( ((File)value).getPath() ) ) {
+                           add = false;
+                           break;
+                        }
+                     }
+                     if ( add ) {
+                        noteListModel.addElement( new DefaultNote( ((File)value).getPath() ) );
+                        _noteBox.setSelectedIndex( noteListModel.getSize() - 1 );
+                     }
+                  }
+               }
+            }
+         } catch ( Exception ex ) {
+            ex.printStackTrace();
+         }
+      }
+   }
+
+
+   private class PreviousNoteAction extends AbstractAction {
+      @Override
+      public void actionPerformed( final ActionEvent event ) {
+         final int index = _noteBox.getSelectedIndex();
+         if ( index > 0 ) {
+            _noteBox.setSelectedIndex( index - 1 );
+         }
+      }
+   }
+
+   private class NextNoteAction extends AbstractAction {
+      @Override
+      public void actionPerformed( final ActionEvent event ) {
+         final int index = _noteBox.getSelectedIndex();
+         if ( index < _noteBox.getItemCount() - 1 ) {
+            _noteBox.setSelectedIndex( index + 1 );
+         }
+      }
+   }
+
+   /**
+    * Notifies {@code ListSelectionListener}s added directly to the list
+    * of selection changes made to the selection model. {@code JList}
+    * listens for changes made to the selection in the selection model,
+    * and forwards notification to listeners added to the list directly,
+    * by calling this method.
+    * <p/>
+    * This method constructs a {@code ListSelectionEvent} with this list
+    * as the source, and the specified arguments, and sends it to the
+    * registered {@code ListSelectionListeners}.
+    *
+    * @param firstIndex  the first index in the range, {@code <= lastIndex}
+    * @param lastIndex   the last index in the range, {@code >= firstIndex}
+    * @param isAdjusting whether or not this is one in a series of
+    *                    multiple events, where changes are still being made
+    * @see #addListSelectionListener
+    * @see #removeListSelectionListener
+    * @see javax.swing.event.ListSelectionEvent
+    * @see EventListenerList
+    */
+   private void fireSelectionValueChanged( final int firstIndex, final int lastIndex, final boolean isAdjusting ) {
+      final Object[] listeners = listenerList.getListenerList();
+      final ListSelectionEvent event = new ListSelectionEvent( this, firstIndex, lastIndex, isAdjusting );
+      for ( int i = listeners.length - 2; i >= 0; i -= 2 ) {
+         if ( listeners[ i ] == ListSelectionListener.class ) {
+            ((ListSelectionListener)listeners[ i + 1 ]).valueChanged( event );
+         }
+      }
+   }
+
+   final private class NoteSelectionForwarder implements ActionListener {
+      @Override
+      public void actionPerformed( final ActionEvent event ) {
+         final Object selected = _noteBox.getSelectedItem();
+         if ( selected == null ) {
+            // do something to clear selected timeline
+            return;
+         }
+         if ( selected != _selectedNote ) {
+            // the note selection has changed, forward it to list selection listeners
+            _selectedNote = (Note)selected;
+            final int index = _noteBox.getSelectedIndex();
+            fireSelectionValueChanged( index, index, false );
+         }
+      }
+   }
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventColor.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventColor.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventColor.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventColor.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,127 @@
+package org.chboston.cnlp.timeline.gui.event;
+
+
+import org.chboston.cnlp.nlp.annotation.classtype.SemanticClassType;
+
+import java.awt.*;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 9/6/13
+ */
+public enum EventColor {
+
+   NORMAL( new Color( 0x7C, 0xE4, 0x40 ), "Green" ),
+   //   UNCERTAIN( new Color( 255, 200, 0 ), "#CC6600" ),
+   UNCERTAIN( new Color( 255, 127, 0 ), "#FF8000" ),
+   NEGATED( new Color( 0xEC, 0x45, 0x39 ), "Red" ),
+   SELECTED( new Color( 0, 178, 178 ), "#008B8B" ),
+   //   SELECTED( (UIManager.getColor( "Table.selectionBackground" ) != null )
+//             ? UIManager.getColor( "Table.selectionBackground" )
+//             : UIManager.getColor( "textHighlight" ), "#008B8B" ),
+   FOCUS( new Color( 102, 255, 255 ), "Cyan" ),
+   FUZZY_NORMAL( new Color( 0x6A, 0xC0, 0x37, 95 ), "Green" ),
+   FUZZY_UNCERTAIN( new Color( 255, 200, 0, 95 ), "Orange" ),
+   FUZZY_NEGATED( new Color( 0xEC, 0x45, 0x39, 95 ), "Red" ),
+   FUZZY_SELECTED( new Color( 0, 178, 178, 95 ), "#008B8B" ),
+   FUZZY_FOCUS( new Color( 102, 255, 255, 95 ), "Cyan" ),
+   REFERENCE( new Color( 216, 216, 216, 85 ), "#D8D8D8" ),
+
+   TIMEX( Color.GRAY, "NULL" ),
+
+   // TODO Move Semantic Types to another enum
+//   SIGN_SYMPTOM( new Color( 247, 129, 243 ), "#F781F3" ),
+//   TEST_PROCEDURE( new Color( 129, 159, 247 ), "#819FF7" ),
+//   DISEASE_DISORDER( new Color( 127, 0, 0 ), "#7F0008" ),
+//   MEDICATION( new Color( 1, 58, 223 ), "#013ADF" );
+   SIGN_SYMPTOM( new Color( 255, 0, 179 ), "NULL" ),
+   TEST_PROCEDURE( Color.BLACK, "NULL" ),
+   DISEASE_DISORDER( new Color( 7, 135, 7 ), "NULL" ),
+   MEDICATION( Color.RED, "NULL" ),
+   //   ANATOMICAL_SITE( Color.BLUE, "NULL" ),
+   UNKNOWN( Color.GRAY, "NULL" ),
+   NONE( Color.WHITE, "NULL" );
+
+
+   static public EventColor getEventColor( final EventStatus eventStatus ) {
+      switch ( eventStatus ) {
+         case NEGATED:
+            return NEGATED;
+         case UNCERTAIN_NEGATED:
+            return NEGATED;
+         case UNCERTAIN:
+            return UNCERTAIN;
+         case NORMAL:
+            return NORMAL;
+      }
+      return NORMAL;
+   }
+
+   static public EventColor getEventColor( final EventStatus eventStatus, final boolean isFuzzy ) {
+      if ( !isFuzzy ) {
+         return getEventColor( eventStatus );
+      }
+      switch ( eventStatus ) {
+         case NEGATED:
+            return FUZZY_NEGATED;
+         case UNCERTAIN_NEGATED:
+            return FUZZY_NEGATED;
+         case UNCERTAIN:
+            return FUZZY_UNCERTAIN;
+         case NORMAL:
+            return FUZZY_NORMAL;
+      }
+      return FUZZY_NORMAL;
+   }
+
+
+   static public EventColor getSemanticColor( final SemanticClassType semanticType ) {
+      if ( semanticType == null || semanticType == SemanticClassType.UNKNOWN ) {
+         return NONE;
+      }
+      switch ( semanticType ) {
+         case SIGN_OR_SYMPTOM:
+            return SIGN_SYMPTOM;
+         case PROCEDURE:
+            return TEST_PROCEDURE;
+         case DISEASE_DISORDER:
+            return DISEASE_DISORDER;
+         case MEDICATION:
+            return MEDICATION;
+//            case ANATOMICAL_SITE:
+//               return ANATOMICAL_SITE;
+      }
+      return UNKNOWN;
+   }
+
+
+   static public EventColor getTimeSpanColor( final boolean isSelected, final boolean isFocused,
+                                              final boolean isFuzzy ) {
+      if ( isSelected ) {
+         if ( isFocused ) {
+            return isFuzzy ? FUZZY_FOCUS : FOCUS;
+         }
+         return isFuzzy ? FUZZY_SELECTED : SELECTED;
+      }
+      return isFuzzy ? FUZZY_NORMAL : NORMAL;
+   }
+
+
+   private final Color _color;
+   private final String _htmlCode;
+
+   private EventColor( final Color color, final String htmlCode ) {
+      _color = color;
+      _htmlCode = htmlCode;
+   }
+
+   public Color getColor() {
+      return _color;
+   }
+
+   public String getHtmlCode() {
+      return _htmlCode;
+   }
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventHtmlFactory.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventHtmlFactory.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventHtmlFactory.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventHtmlFactory.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,411 @@
+package org.chboston.cnlp.timeline.gui.event;
+
+import org.chboston.cnlp.nlp.annotation.annotation.Annotation;
+import org.chboston.cnlp.nlp.annotation.attribute.Attribute;
+import org.chboston.cnlp.nlp.annotation.attribute.AttributeUtil;
+import org.chboston.cnlp.nlp.annotation.classtype.SemanticClassType;
+import org.chboston.cnlp.nlp.annotation.coreference.CoreferenceChain;
+import org.chboston.cnlp.nlp.annotation.entity.Entity;
+import org.chboston.cnlp.nlp.annotation.relation.Relation;
+import org.chboston.cnlp.timeline.gui.search.TimelineSearchUtil;
+import org.chboston.cnlp.timeline.timespan.plus.PointedTimeSpan;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 8/27/13
+ */
+final public class EventHtmlFactory {
+
+   static private final Pattern SPACE_PATTERN = Pattern.compile( "\\s\\s\\s+" );
+
+   private EventHtmlFactory() {
+   }
+
+   static private enum HTML_COLOR {
+      DATE_SPAN( "Black" ), DATE_TEXT( "Blue" ),
+      //      EVENT_TEXT( "Green" ), EVENT_NEGATED_TEXT( "Red" ), EVENT_UNCERTAIN_TEXT( "#CC6600" ),
+      RELATION( "#7A2770" ), MISC_UMLS( "Gray" ), MODIFIER_UMLS( "Dark Gray" ),
+//      SEARCH( "#008B8B" )
+      ;
+      private final String __htmlCode;
+
+      private HTML_COLOR( final String htmlCode ) {
+         __htmlCode = htmlCode;
+      }
+
+      public String toString() {
+         return __htmlCode;
+      }
+   }
+
+   static private enum HTML_TAG {
+      HEADER( "<HTML>" ), FOOTER( "</HTML>" ), EOL( "<BR>" ), LINE( "<HR>" ), BULLET( "&#8226 " ),
+      COLOR( "COLOR_TAG" ), FONT_START( "<p style=\"color:" + COLOR.toString() + "\">" ), FONT_END( "</p>" ),
+      BOLD( "<b>" ), END_BOLD( "</b>" ), ITALIC( "<i>" ), END_ITALIC( "</i>" );
+      private final String __htmlCode;
+
+      private HTML_TAG( final String htmlCode ) {
+         __htmlCode = htmlCode;
+      }
+
+      public String toString() {
+         return __htmlCode;
+      }
+   }
+
+
+   static public String createColorLegend() {
+      final StringBuilder sb = new StringBuilder();
+      sb.append( HTML_TAG.HEADER );
+      setFontColor( sb, HTML_COLOR.DATE_SPAN );
+      sb.append( HTML_TAG.BOLD );
+      sb.append( "<CENTER>LEGEND</CENTER>" );
+      sb.append( HTML_TAG.END_BOLD );
+      sb.append( HTML_TAG.LINE );
+      // legend for timespan
+      sb.append( HTML_TAG.BOLD );
+      sb.append( "Event Date/Time" );
+      sb.append( HTML_TAG.END_BOLD );
+      endFontColor( sb );
+      // legend for events
+      appendEventColor( sb, false, false, false );
+      sb.append( HTML_TAG.BULLET );
+      appendEventPrefix( sb, false, false, false, true );
+      sb.append( "Normal Event" );
+      appendEventSuffix( sb, false, false, false, true );
+      endFontColor( sb );
+      // uncertain
+      appendEventColor( sb, false, false, true );
+      sb.append( HTML_TAG.BULLET );
+      appendEventPrefix( sb, false, false, true, true );
+      sb.append( "Uncertain Event" );
+      appendEventSuffix( sb, false, false, true, true );
+      endFontColor( sb );
+      // negated
+      appendEventColor( sb, false, true, false );
+      sb.append( HTML_TAG.BULLET );
+      appendEventPrefix( sb, false, true, false, true );
+      sb.append( "Negated Event" );
+      appendEventSuffix( sb, false, true, false, true );
+      endFontColor( sb );
+      // selected
+      appendEventColor( sb, true, false, false );
+      sb.append( HTML_TAG.BULLET );
+      appendEventPrefix( sb, true, false, false, true );
+      sb.append( "Selected Event" );
+      appendEventSuffix( sb, true, false, false, true );
+      endFontColor( sb );
+      // relation
+      setFontColor( sb, HTML_COLOR.RELATION );
+      sb.append( "&#160;&#160;&#160;&#160;-" );
+      sb.append( HTML_TAG.ITALIC ).append( "Event Relation" );
+      sb.append( HTML_TAG.END_ITALIC );
+      sb.append( HTML_TAG.FOOTER );
+      return sb.toString();
+   }
+
+   static public String createEventHtml( final PointedTimeSpan timeSpan,
+                                         final Entity entity, final Collection<Relation> eventEvents,
+                                         final SemanticClassType semanticType, final Collection<String> searchTerms ) {
+      final StringBuilder sb = new StringBuilder();
+      sb.append( HTML_TAG.HEADER );
+      appendEventHtml( sb, timeSpan, entity, eventEvents, semanticType, searchTerms );
+      sb.append( HTML_TAG.FOOTER );
+      return sb.toString();
+   }
+
+
+   static public String createEventHtml( final Entity entity, final Collection<Relation> eventEvents,
+                                         final SemanticClassType semanticType, final Collection<String> searchTerms ) {
+      final StringBuilder sb = new StringBuilder();
+      sb.append( HTML_TAG.HEADER );
+      appendEventHtml( sb, entity, eventEvents, semanticType, searchTerms );
+      sb.append( HTML_TAG.FOOTER );
+      return sb.toString();
+   }
+
+   static public String createRelationHtml( final Relation eventEvent ) {
+      final StringBuilder sb = new StringBuilder();
+      sb.append( HTML_TAG.HEADER );
+      appendRelationHtml( sb, eventEvent );
+      sb.append( HTML_TAG.FOOTER );
+      return sb.toString();
+   }
+
+   static private void appendRelationHtml( final StringBuilder sb, final Relation tlink ) {
+      setFontColor( sb, HTML_COLOR.RELATION );
+      final String tlinkTypeText = createTlinkTypeText( tlink );
+      if ( !tlinkTypeText.isEmpty() ) {
+         final Entity entity = tlink.getSecondEntity();
+         sb.append( "&#160;&#160;&#160;&#160;-" );
+         sb.append( HTML_TAG.ITALIC ).append( tlinkTypeText ).append( " " );
+//         final boolean isSearchResult = TimelineSearchUtil.hasSearchTerm( entity.getSpannedText(), searchTerms );
+         final boolean isSearchResult = false;
+         final boolean isNegative = AttributeUtil.hasNegativePolarity( entity );
+         final boolean isUncertain = AttributeUtil.hasUncertainty( entity );
+         appendEventPrefix( sb, isSearchResult, isNegative, isUncertain, false );
+         sb.append( getNiceText( entity, null ) );
+         appendEventSuffix( sb, isSearchResult, isNegative, isUncertain, false );
+         sb.append( HTML_TAG.END_ITALIC );
+         sb.append( HTML_TAG.EOL );
+      }
+      endFontColor( sb );
+   }
+
+
+   static public void appendEventHtml( final StringBuilder sb, final PointedTimeSpan timeSpan,
+                                       final Entity entity, final Collection<Relation> eventEvents,
+                                       final SemanticClassType semanticType, final Collection<String> searchTerms ) {
+      appendTimeSpan( sb, timeSpan );
+      appendEventHtml( sb, entity, eventEvents, semanticType, searchTerms );
+   }
+
+
+   static public void appendEventHtml( final StringBuilder sb,
+                                       final Entity entity, final Collection<Relation> eventEvents,
+                                       final SemanticClassType semanticType, final Collection<String> searchTerms ) {
+      appendEvent( sb, entity, semanticType, searchTerms );
+//      if ( eventEvents != null && !eventEvents.isEmpty() ) {
+//         appendEventEvents( sb, semanticType, eventEvents, searchTerms );
+//      }
+   }
+
+
+   static private void appendTimeSpan( final StringBuilder sb, final PointedTimeSpan timeSpan ) {
+      final String dateSpanText = timeSpan.toString();
+      if ( dateSpanText != null && !dateSpanText.isEmpty() ) {
+         setFontColor( sb, HTML_COLOR.DATE_SPAN );
+         sb.append( HTML_TAG.BOLD ).append( dateSpanText ).append( HTML_TAG.END_BOLD );
+         endFontColor( sb );
+      }
+   }
+
+   static private void appendEvent( final StringBuilder sb, final Entity entity, final SemanticClassType semanticType,
+                                    final Collection<String> searchTerms ) {
+      final boolean isSearchResult = TimelineSearchUtil.hasSearchTerm( entity.getSpannedText(), searchTerms );
+      final boolean isNegative = AttributeUtil.hasNegativePolarity( entity );
+      final boolean isUncertain = AttributeUtil.hasUncertainty( entity );
+      //      final boolean isModifier = isModifierUmls( entity );
+      //      final boolean isMiscUmls = isMiscUmls( entity );
+      appendEventColor( sb, isSearchResult, isNegative, isUncertain );
+      sb.append( HTML_TAG.BULLET );
+      appendEventPrefix( sb, isSearchResult, isNegative, isUncertain, true );
+      sb.append( getNiceText( entity, semanticType ) );
+      appendEventSuffix( sb, isSearchResult, isNegative, isUncertain, true );
+      endFontColor( sb );
+   }
+
+   static private void appendEventEvents( final StringBuilder sb, final SemanticClassType semanticType,
+                                          final Collection<Relation> tlinks,
+                                          final Collection<String> searchTerms ) {
+      final java.util.List<Relation> tlinkList = new ArrayList<>( tlinks );
+      Collections.sort( tlinkList, TlinkComparator.INSTANCE );
+      setFontColor( sb, HTML_COLOR.RELATION );
+      for ( Relation tlink : tlinkList ) {
+         final String tlinkTypeText = createTlinkTypeText( tlink );
+         if ( tlinkTypeText.isEmpty() ) {
+            continue;
+         }
+         final Entity entity = tlink.getSecondEntity();
+         sb.append( "&#160;&#160;&#160;&#160;-" );
+         sb.append( HTML_TAG.ITALIC ).append( tlinkTypeText ).append( " " );
+         final boolean isSearchResult = TimelineSearchUtil.hasSearchTerm( entity.getSpannedText(), searchTerms );
+         final boolean isNegative = AttributeUtil.hasNegativePolarity( entity );
+         final boolean isUncertain = AttributeUtil.hasUncertainty( entity );
+         appendEventPrefix( sb, isSearchResult, isNegative, isUncertain, false );
+         sb.append( getNiceText( entity, semanticType ) );
+         appendEventSuffix( sb, isSearchResult, isNegative, isUncertain, false );
+//         if ( isNegative ) {
+//            sb.append( "(No) " );
+//         }
+//         if ( isSearchResult ) {
+//            sb.append( HTML_TAG.BOLD );
+//         }
+//         sb.append( getNiceText( entity, semanticType ) );
+//         if ( isUncertain ) {
+//            sb.append( " (??)" );
+//         }
+//         if ( isSearchResult ) {
+//            sb.append( HTML_TAG.END_BOLD );
+//         }
+         sb.append( HTML_TAG.END_ITALIC );
+         sb.append( HTML_TAG.EOL );
+      }
+      endFontColor( sb );
+   }
+
+
+   //   static private boolean isMiscUmls( final Annotation event ) {
+//      return false;
+//   }
+
+//   static private boolean isModifierUmls( final Annotation event ) {
+//      return false;
+//   }
+
+   //   static private void appendDate( final StringBuilder sb, final Annotation date ) {
+   //      sb.append( "\"" ).append( date.getSpannedText() ).append( "\"" ).append( HTML_TAG.EOL );
+   //   }
+
+//   static private void appendEvent( final StringBuilder sb,
+//                                    final Annotation event,
+//                                    final SemanticClassType semanticType,
+//                                    final boolean isSearchResult,
+//                                    final boolean isNegative,
+//                                    final boolean isUncertain ) {
+//      if ( isNegative ) {
+//         sb.append( HTML_TAG.ITALIC ).append( "(No) " ).append( HTML_TAG.END_ITALIC );
+//      }
+//      if ( isSearchResult ) {
+//         sb.append( HTML_TAG.BOLD );
+//      }
+//      sb.append( getNiceText( event, semanticType ) );
+//      if ( isUncertain ) {
+//         sb.append( HTML_TAG.ITALIC ).append( " (??)" ).append( HTML_TAG.END_ITALIC );
+//      }
+//      if ( isSearchResult ) {
+//         sb.append( HTML_TAG.END_BOLD );
+//      }
+//   }
+
+   static private void appendEventColor( final StringBuilder sb,
+                                         final boolean isSearchResult,
+                                         final boolean isNegative,
+                                         final boolean isUncertain ) {
+      if ( isSearchResult ) {
+//         setFontColor( sb, HTML_COLOR.SEARCH );
+         setFontColor( sb, EventColor.SELECTED );
+      } else if ( isNegative ) {
+//         setFontColor( sb, HTML_COLOR.EVENT_NEGATED_TEXT );
+         setFontColor( sb, EventColor.NEGATED );
+      } else if ( isUncertain ) {
+//         setFontColor( sb, HTML_COLOR.EVENT_UNCERTAIN_TEXT );
+         setFontColor( sb, EventColor.UNCERTAIN );
+         //      } else if ( needModifierColor ) {
+         //         setFontColor( sb, HTML_COLOR.MODIFIER_UMLS );
+         //      } else if ( needMiscUmlsColor ) {
+         //         setFontColor( sb, HTML_COLOR.MISC_UMLS );
+      } else {
+//         setFontColor( sb, HTML_COLOR.EVENT_TEXT );
+         setFontColor( sb, EventColor.NORMAL );
+      }
+   }
+
+   static private void appendEventPrefix( final StringBuilder sb,
+                                          final boolean isSearchResult,
+                                          final boolean isNegative,
+                                          final boolean isUncertain, final boolean useItalics ) {
+      if ( isNegative ) {
+         if ( useItalics ) {
+            sb.append( HTML_TAG.ITALIC ).append( "(No) " ).append( HTML_TAG.END_ITALIC );
+         } else {
+            sb.append( "(No) " );
+         }
+      }
+      if ( isSearchResult ) {
+         sb.append( HTML_TAG.BOLD );
+      }
+   }
+
+   static private void appendEventSuffix( final StringBuilder sb,
+                                          final boolean isSearchResult,
+                                          final boolean isNegative,
+                                          final boolean isUncertain, final boolean useItalics ) {
+      if ( isUncertain ) {
+         if ( useItalics ) {
+            sb.append( HTML_TAG.ITALIC ).append( " (??)" ).append( HTML_TAG.END_ITALIC );
+         } else {
+            sb.append( " (??)" );
+         }
+      }
+      if ( isSearchResult ) {
+         sb.append( HTML_TAG.END_BOLD );
+      }
+   }
+
+   static private void setFontColor( final StringBuilder sb, final HTML_COLOR htmlColor ) {
+      final String fontColorStart = HTML_TAG.FONT_START.toString().replace( HTML_TAG.COLOR.toString(),
+            htmlColor.toString() );
+      sb.append( fontColorStart );
+   }
+
+   static private void setFontColor( final StringBuilder sb, final EventColor eventColor ) {
+      final String fontColorStart = HTML_TAG.FONT_START.toString().replace( HTML_TAG.COLOR.toString(),
+            eventColor.getHtmlCode() );
+      sb.append( fontColorStart );
+   }
+
+   static private void endFontColor( final StringBuilder sb ) {
+      sb.append( HTML_TAG.FONT_END );
+   }
+
+   static private String getNiceText( final String fullText ) {
+      return SPACE_PATTERN.matcher( fullText ).replaceAll( " ... " );
+   }
+
+
+   static private String getNiceText( final Annotation annotation, final SemanticClassType semanticType ) {
+      if ( semanticType != null && annotation instanceof CoreferenceChain ) {
+         return getCoreferenceText( (CoreferenceChain)annotation, semanticType );
+      }
+      return getNiceText( annotation.getSpannedText() );
+   }
+
+   static private String getCoreferenceText( final CoreferenceChain chain, final SemanticClassType semanticType ) {
+      String semanticText = null;
+      for ( Entity entity : chain ) {
+         if ( entity.getClassType() == semanticType ) {
+            if ( semanticText == null || entity.getSpannedText().length() > semanticText.length() ) {
+               semanticText = entity.getSpannedText();
+            }
+         }
+      }
+      if ( semanticText == null ) {
+         return getNiceText( chain.getSpannedText() );
+      }
+      final String fullText = chain.getSpannedText();
+      final int index = fullText.indexOf( semanticText );
+      if ( index < 0 || index + semanticText.length() > fullText.length() ) {
+         return getNiceText( chain.getSpannedText() );
+      }
+      // Removed for reading clarity (rather than excessive info) 7/8/14 SPF
+//      final String notedText = fullText.replace( semanticText, "[" + semanticText + "]" );
+//      return getNiceText( notedText );
+      return getNiceText( fullText );
+   }
+
+
+   static private String createTlinkTypeText( final Relation tlink ) {
+      final Attribute relationTypeAttribute = tlink.getAttribute( "Relation Type" );
+      if ( relationTypeAttribute == null ) {
+         // assume equality
+         return "occurs at the same time as";
+      }
+      // There is currently no default relation type in the thyme schema, but we must have one, so use overlap
+      final String relationType = relationTypeAttribute.getValue();
+      if ( relationType.equals( "BEFORE" ) ) {
+         return "occurs before";
+      } else if ( relationType.equals( "AFTER" ) ) {
+         return "occurs after";
+      } else if ( relationType.equals( "OVERLAP" ) ) {
+         return "overlaps";
+      } else if ( relationType.equals( "CONTAINS" ) ) {
+         return "contains";
+      } else if ( relationType.equals( "CONTAINED-BY" ) ) {
+         return "is within";
+      } else if ( relationType.equals( "BEGINS-ON" ) ) {
+         return "begins on";
+      } else if ( relationType.equals( "ENDS-ON" ) ) {
+         return "ends on";
+      }
+      return "overlaps";
+   }
+
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventStatus.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventStatus.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventStatus.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/EventStatus.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,25 @@
+package org.chboston.cnlp.timeline.gui.event;
+
+import org.chboston.cnlp.nlp.annotation.annotation.Annotation;
+import org.chboston.cnlp.nlp.annotation.attribute.AttributeUtil;
+
+/**
+ * @author SPF , chip-nlp
+ * @version %I%
+ * @since 9/24/2014
+ */
+public enum EventStatus {
+   NORMAL, UNCERTAIN, NEGATED, UNCERTAIN_NEGATED, UNKNOWN;
+
+   static public EventStatus getEventStatus( final Annotation event ) {
+      final boolean isNegated = AttributeUtil.hasNegativePolarity( event );
+      final boolean isUncertain = AttributeUtil.hasUncertainty( event );
+      if ( isNegated ) {
+         return isUncertain ? UNCERTAIN_NEGATED : NEGATED;
+      } else if ( isUncertain ) {
+         return UNCERTAIN;
+      }
+      return NORMAL;
+   }
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/SemanticShape.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/SemanticShape.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/SemanticShape.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/event/SemanticShape.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,75 @@
+package org.chboston.cnlp.timeline.gui.event;
+
+import org.chboston.cnlp.nlp.annotation.classtype.SemanticClassType;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 9/10/14
+ */
+public enum SemanticShape {
+   // TODO Move Semantic Types to another enum
+   // X : 0,4 UL ; 0,4  LL ; 4,-4 UR ; 4,-4 LR
+   // Y : 0,4 UL ; 4,-4 LL ; 0,4  UR ; 4,-4 LR
+//   SIGN_SYMPTOM( 0, 4, 0, 4 ),
+//   TEST_PROCEDURE( 0, 4, 4, -4 ),
+//   DISEASE_DISORDER( 4, -4, 0, 4 ),
+//   MEDICATION( 4, -4, 4, -4 ),
+//   // square
+//   UNKNOWN( 0, 0, 4, 4 );
+
+   SIGN_SYMPTOM( 0, 6, 0, 6 ),
+   TEST_PROCEDURE( 0, 6, 6, -6 ),
+   DISEASE_DISORDER( 6, 0, 0, 6 ),
+   MEDICATION( 6, 0, 6, -6 ),
+   // square
+   UNKNOWN( 0, 0, 6, 6 );
+
+   static public SemanticShape getShape( final SemanticClassType semanticType ) {
+      if ( semanticType != null ) {
+         switch ( semanticType ) {
+            case SIGN_OR_SYMPTOM:
+               return SemanticShape.SIGN_SYMPTOM;
+            case PROCEDURE:
+               return SemanticShape.TEST_PROCEDURE;
+            case DISEASE_DISORDER:
+               return SemanticShape.DISEASE_DISORDER;
+            case MEDICATION:
+               return SemanticShape.MEDICATION;
+         }
+      }
+      return SemanticShape.UNKNOWN;
+   }
+
+   private final int _xStart;
+   private final int _yStart;
+   private final int _xOffset;
+   private final int _yOffset;
+
+   // X : 0,4 UL ; 0,4  LL ; 4,-4 UR ; 4,-4 LR
+   // Y : 0,4 UL ; 4,-4 LL ; 0,4  UR ; 4,-4 LR
+   private SemanticShape( final int xStart, final int yStart, final int xOffset, final int yOffset ) {
+      _xStart = xStart;
+      _yStart = yStart;
+      _xOffset = xOffset;
+      _yOffset = yOffset;
+   }
+
+   public int getX() {
+      return _xStart;
+   }
+
+   public int getY() {
+      return _yStart;
+   }
+
+   public int getXoffset() {
+      return _xOffset;
+   }
+
+   public int getYoffset() {
+      return _yOffset;
+   }
+
+
+}