ctakes-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From seanfi...@apache.org
Subject svn commit: r1660963 [14/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/a...
Date Thu, 19 Feb 2015 18:06:17 GMT
Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/search/TimelineSearchUtil.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/search/TimelineSearchUtil.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/search/TimelineSearchUtil.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/search/TimelineSearchUtil.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,434 @@
+package org.chboston.cnlp.timeline.gui.search;
+
+import com.mdimension.jchronic.Options;
+import com.mdimension.jchronic.tags.Pointer;
+import org.chboston.cnlp.nlp.annotation.classtype.SemanticClassType;
+import org.chboston.cnlp.nlp.annotation.entity.Entity;
+import org.chboston.cnlp.nlp.annotation.relation.Relation;
+import org.chboston.cnlp.timeline.chronic.parser.AtParser;
+import org.chboston.cnlp.timeline.timeline.Timeline;
+import org.chboston.cnlp.timeline.timespan.TimeSpan;
+import org.chboston.cnlp.timeline.timespan.plus.PointedTimeSpan;
+import org.chboston.cnlp.timeline.timespan.plus.TimeSpanPlus;
+
+import java.util.*;
+import java.util.regex.Pattern;
+
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 8/14/13
+ */
+final public class TimelineSearchUtil {
+
+   private TimelineSearchUtil() {
+   }
+
+   /**
+    * really primitive exchange of query term for search char
+    */
+   static public enum TermTimePointer {
+      EQUAL( '=', "during", "contains", "contained by" ),
+      BEFORE( '<', "before" ),
+      AFTER( '>', "after" ),
+      OVERLAP( '~', "around", "overlaps", "overlapping" );
+      final private char __searchChar;
+      final private Collection<String> __searchTerms;
+
+      private TermTimePointer( final char searchChar,
+                               final String... searchTerms ) {
+         __searchChar = searchChar;
+         __searchTerms = new HashSet<>( Arrays.asList( searchTerms ) );
+      }
+
+      public char getSearchChar() {
+         return __searchChar;
+      }
+
+      public boolean isTermTimePointer( final String term ) {
+         return __searchTerms.contains( term.toLowerCase() );
+      }
+   }
+
+   static private TermTimePointer getTermTimePointer( final String term ) {
+      for ( TermTimePointer termTimePointer : TermTimePointer.values() ) {
+         if ( termTimePointer.isTermTimePointer( term ) ) {
+            return termTimePointer;
+         }
+      }
+      return null;
+   }
+
+
+   /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+   //                                  Search text to terms parsing
+   /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+   static public char getSearchCommand( final String searchText ) {
+      if ( searchText.startsWith( "-" ) ) {
+         return '-';
+      } else if ( searchText.startsWith( "+" ) ) {
+         return '+';
+      }
+      return ' ';
+   }
+
+   static public List<String> getSearchTerms( final String searchText ) {
+      String lowerText = searchText.trim().toLowerCase();
+      final char command = getSearchCommand( lowerText );
+      if ( command != ' ' ) {
+         lowerText = lowerText.substring( 1 );
+      }
+//      final String[] searchTerms = lowerText.split( "\\|" );
+      final String[] searchTerms = BAR_PATTERN.split( lowerText );
+
+      final List<String> termList = new ArrayList<>( searchTerms.length );
+      for ( String searchTerm : searchTerms ) {
+//         searchTerm = searchTerm.trim().replaceAll( "\\s+", " " );
+         searchTerm = WHITESPACE_PATTERN.matcher( searchTerm.trim() ).replaceAll( " " );
+         if ( !searchTerm.isEmpty() ) {
+            termList.add( searchTerm );
+         }
+      }
+      Collections.sort( termList );
+      return termList;
+   }
+
+
+   /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+   //                                 Entity Searching
+   /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+   //   static public Map<SemanticClassType, Collection<Entity>> getSemanticEventMap( final DefaultTimeline timeline,
+   //                                                                                 final TimeSpanPlus timeSpan,
+   //                                                                                 final boolean addMisc ) {
+   //      final Collection<Entity> entities = timeline.getEntities( timeSpan );
+   //      if ( entities == null || entities.isEmpty() ) {
+   //         return Collections.emptyMap();
+   //      }
+   //      return getSemanticEventMap( entities, addMisc );
+   //   }
+
+
+   // TODO refactor with CollectionMap
+   static public Map<SemanticClassType, Collection<Entity>> getSemanticEventMap( final Collection<Entity> entities,
+                                                                                 final boolean addMisc ) {
+      final Map<SemanticClassType, Collection<Entity>> classTypeEvents = new HashMap<>();
+      for ( SemanticClassType semanticType : SemanticClassType.values() ) {
+         final Collection<Entity> semanticEntities = new HashSet<>();
+         if ( addMisc && semanticType == SemanticClassType.MISC ) {
+            semanticEntities.addAll( entities );
+         } else {
+            for ( Entity entity : entities ) {
+               if ( entity.isClassType( semanticType ) ) {
+                  semanticEntities.add( entity );
+               }
+            }
+         }
+         if ( !semanticEntities.isEmpty() ) {
+            classTypeEvents.put( semanticType, semanticEntities );
+         }
+      }
+      return classTypeEvents;
+   }
+
+   static public Map<SemanticClassType, Collection<Entity>> getSemanticEventMap( final Timeline timeline,
+                                                                                 final PointedTimeSpan timeSpan,
+                                                                                 final Collection<String> searchTerms,
+                                                                                 final boolean requireSearchTerms,
+                                                                                 final Collection<String> miscTerms ) {
+      final Collection<Entity> entities = timeline.getEntities( timeSpan );
+      if ( entities == null || entities.isEmpty() ) {
+         return Collections.emptyMap();
+      }
+      return getSemanticEventMap( entities, searchTerms, requireSearchTerms, miscTerms );
+   }
+
+   static public Map<SemanticClassType, Collection<Entity>> getSemanticEventMap( final Collection<Entity> entities,
+                                                                                 final Collection<String> searchTerms,
+                                                                                 final boolean requireSearchTerms,
+                                                                                 final Collection<String> miscTerms ) {
+      final boolean addMisc = miscTerms != null && !miscTerms.isEmpty();
+      final Map<SemanticClassType, Collection<Entity>> semanticEntities = getSemanticEventMap( entities, addMisc );
+      if ( !requireSearchTerms ) {
+         return semanticEntities;
+      }
+      final Collection<SemanticClassType> semanticTypesToRemove = EnumSet.noneOf( SemanticClassType.class );
+      final Collection<Entity> entitiesToRemove = new HashSet<>();
+      for ( Map.Entry<SemanticClassType, Collection<Entity>> semanticEntry : semanticEntities.entrySet() ) {
+         final SemanticClassType semanticType = semanticEntry.getKey();
+         final Collection<Entity> classTypeEntities = semanticEntry.getValue();
+         entitiesToRemove.clear();
+         for ( Entity entity : classTypeEntities ) {
+            if ( !hasSearchTerm( entity.getSpannedText(),
+                  searchTerms ) ) { //&& !tLinksHaveTerm( entity, searchTerms ) ) {
+               if ( semanticType == SemanticClassType.MISC
+                    && TimelineSearchUtil.hasSearchTerm( entity.getSpannedText(), miscTerms ) ) {
+                  continue;
+               }
+               entitiesToRemove.add( entity );
+            }
+         }
+         classTypeEntities.removeAll( entitiesToRemove );
+         if ( classTypeEntities.isEmpty() ) {
+            semanticTypesToRemove.add( semanticType );
+         }
+      }
+      for ( SemanticClassType semanticType : semanticTypesToRemove ) {
+         semanticEntities.remove( semanticType );
+      }
+      return semanticEntities;
+   }
+
+   /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+   //                                 Term Searching
+   /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+   static private final Pattern WHITESPACE_PATTERN = Pattern.compile( "\\s+" );
+   static private final Pattern EQUAL_PATTERN = Pattern.compile( "=" );
+   static private final Pattern BAR_PATTERN = Pattern.compile( "\\|" );
+
+   static private String getSearchableText( final String text ) {
+      if ( text == null || text.trim().isEmpty() ) {
+         return "";
+      }
+      final String lowerText = text.trim().toLowerCase();
+      return " "
+             + EQUAL_PATTERN.matcher( WHITESPACE_PATTERN.matcher( lowerText ).replaceAll( " " ) ).replaceAll( " " )
+             + " ";
+//      return " " + text.trim().replaceAll( "\\s+", " " ).replaceAll( "=", " " ).toLowerCase() + " ";
+   }
+
+   static public int getSearchTermIndex( final String text, final Collection<String> searchTerms ) {
+      if ( searchTerms == null || searchTerms.isEmpty() ) {
+         return -1;
+      }
+      final String searchableText = getSearchableText( text );
+      if ( searchableText.isEmpty() ) {
+         return -1;
+      }
+      // substitute
+      for ( String searchTerm : searchTerms ) {
+         final int index = searchableText.indexOf( " " + searchTerm.toLowerCase() + " " );
+         if ( index >= 0 ) {
+            return index;
+         }
+      }
+      return -1;
+   }
+
+   static public int getSearchTermIndex( final String text, final String searchTerm ) {
+      if ( searchTerm == null ) {
+         return -1;
+      }
+      final String searchableText = getSearchableText( text );
+      return searchableText.indexOf( " " + searchTerm.toLowerCase() + " " );
+   }
+
+
+   static public boolean hasSearchTerm( final String text, final Collection<String> searchTerms ) {
+      return getSearchTermIndex( text, searchTerms ) >= 0;
+   }
+
+
+   static public boolean hasSearchTerm( final Timeline timeline,
+                                        final PointedTimeSpan timeSpan,
+                                        final String searchTerm ) {
+      if ( searchTerm.equals( "*" ) ) {
+         return true;
+      }
+      for ( Entity entity : timeline.getEntities( timeSpan ) ) {
+         if ( hasSearchTerm( entity.getSpannedText(), searchTerm ) ) {
+            return true;
+         }
+         for ( Relation tlink : timeline.getEntityEntityRelations( entity ) ) {
+            if ( hasSearchTerm( tlink.getSecondEntity().getSpannedText(), searchTerm ) ) {
+               return true;
+            }
+         }
+      }
+      return false;
+   }
+
+   static public boolean hasSearchTerm( final String text, final String searchTerm ) {
+      return getSearchTermIndex( text, searchTerm ) >= 0;
+   }
+
+
+   /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+   //                                  Time Searching
+   /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+   //   static public boolean hasTermTimeSpan( final long referenceMillis, final String searchTerm,
+   //                                          final Collection<TimeSpanPlus> timeSpans ) {
+   //      final TimeSpan termTimeSpan = getTimeSpan( referenceMillis, searchTerm );
+   //      if ( termTimeSpan == null ) {
+   //         return false;
+   //      }
+   //      final long startMillis = termTimeSpan.getStartMillis();
+   //      final long stopMillis = termTimeSpan.getStopMillis();
+   //      for ( TimeSpanPlus timeSpan : timeSpans ) {
+   //         if ( timeSpan.getStartMillis() == startMillis && timeSpan.getStopMillis() == stopMillis ) {
+   //            return true;
+   //         }
+   //      }
+   //      return false;
+   //   }
+
+
+   static public boolean isTimeTerm( final long referenceMillis, final String searchTerm ) {
+      return getTimeSpan( referenceMillis, searchTerm ) != null;
+   }
+
+   //   static public Collection<TimeSpanPlus> getFilteredTimeSpans( final DefaultTimeline timeline,
+   //                                                                 final Collection<String> searchTerms ) {
+   //      final Collection<TimeSpanPlus> allValidTimeSpans = new HashSet<TimeSpanPlus>( timeline.getTimeSpans() );
+   //      for ( String searchTerm : searchTerms ) {
+   //         final TimeSpan termTimeSpan = getTimeSpan( timeline.getReferenceMillis(), searchTerm );
+   //         if ( termTimeSpan == null ) {
+   //            continue;
+   //         }
+   //         final TermTimePointer termTimePointer = getTermTimePointer( searchTerm );
+   //         final Collection<TimeSpanPlus> validTimeSpans = getFilteredTimeSpans( termTimePointer,
+   //                                                                                termTimeSpan,
+   //                                                                                allValidTimeSpans );
+   //         allValidTimeSpans.retainAll( validTimeSpans );
+   //         if ( allValidTimeSpans.isEmpty() ) {
+   //            return Collections.emptySet();
+   //         }
+   //      }
+   //      return allValidTimeSpans;
+   //   }
+
+
+   static public final String ALL_FILTER_TIME_TERMS = "ALL_FILTER_TIME_TERMS";
+
+   static public Map<String, Collection<PointedTimeSpan>> getFilteredTimeSpansMap( final Timeline timeline,
+                                                                                   final Iterable<String> searchTerms ) {
+      final Map<String, Collection<PointedTimeSpan>> validTimeSpanMap = new HashMap<>();
+      final Collection<PointedTimeSpan> allValidTimeSpans = new HashSet<>( timeline.getTimeSpans() );
+      for ( String searchTerm : searchTerms ) {
+         final TimeSpan termTimeSpan = getTimeSpan( timeline.getReferenceMillis(), searchTerm );
+         if ( termTimeSpan == null ) {
+            continue;
+         }
+         TermTimePointer termTimePointer = null;
+         final int firstSpaceIndex = searchTerm.indexOf( ' ' );
+         if ( firstSpaceIndex > 0 && firstSpaceIndex < searchTerm.length() - 2 ) {
+            termTimePointer = getTermTimePointer( searchTerm.substring( 0, firstSpaceIndex ) );
+         }
+         final Collection<PointedTimeSpan> validTimeSpans = getFilteredTimeSpans( termTimePointer,
+               termTimeSpan,
+               timeline.getTimeSpans() );
+         allValidTimeSpans.retainAll( validTimeSpans );
+         if ( !validTimeSpans.isEmpty() ) {
+            validTimeSpanMap.put( searchTerm, validTimeSpans );
+         }
+      }
+      allValidTimeSpans.add( TimeSpanPlus.UNKNOWN_TIMESPAN_PLUS );
+      validTimeSpanMap.put( ALL_FILTER_TIME_TERMS, allValidTimeSpans );
+      return validTimeSpanMap;
+   }
+
+
+   //   static public Collection<TimeSpanPlus> getTimeSpans( final DefaultTimeline timeline, final String searchTerm ) {
+   //      final TimeSpan termTimeSpan = getTimeSpan( timeline.getReferenceMillis(), searchTerm );
+   //      if ( termTimeSpan == null ) {
+   //         return Collections.emptySet();
+   //      }
+   //      final TermTimePointer termTimePointer = getTermTimePointer( searchTerm );
+   //      return getFilteredTimeSpans( termTimePointer, termTimeSpan, timeline.getTimeSpans() );
+   //   }
+
+   static public TimeSpan getTimeSpan( final long referenceMillis, final String searchTerm ) {
+      String possibleTimeTerm = searchTerm;
+      TermTimePointer termTimePointer = null;
+      final int firstSpaceIndex = possibleTimeTerm.indexOf( ' ' );
+      if ( firstSpaceIndex > 0 && firstSpaceIndex < searchTerm.length() - 2 ) {
+         termTimePointer = getTermTimePointer( searchTerm.substring( 0, firstSpaceIndex ) );
+         if ( termTimePointer != null ) {
+            possibleTimeTerm = searchTerm.substring( firstSpaceIndex + 1 );
+         }
+      }
+      final Calendar referenceCalendar = Calendar.getInstance();
+      referenceCalendar.setTimeInMillis( referenceMillis );
+      final Options chronicOptions = new Options( Pointer.PointerType.NONE, referenceCalendar, false, 0 );
+      return AtParser.createTimeSpan( possibleTimeTerm, chronicOptions );
+   }
+
+   static private Collection<PointedTimeSpan> getFilteredTimeSpans( final TermTimePointer termTimePointer,
+                                                                    final TimeSpan searchTimeSpan,
+                                                                    final Iterable<PointedTimeSpan> timeSpansToSearch ) {
+      final Collection<PointedTimeSpan> validTimeSpans = new HashSet<>();
+      for ( PointedTimeSpan timeSpan : timeSpansToSearch ) {
+         if ( termTimePointer == TermTimePointer.EQUAL && searchTimeSpan.contains( timeSpan ) ) {
+            validTimeSpans.add( timeSpan );
+         } else if ( termTimePointer == TermTimePointer.BEFORE
+                     && timeSpan.getStartMillis() < searchTimeSpan.getStartMillis() ) {
+            validTimeSpans.add( timeSpan );
+         } else if ( termTimePointer == TermTimePointer.AFTER
+                     && timeSpan.getStopMillis() > searchTimeSpan.getStopMillis() ) {
+            validTimeSpans.add( timeSpan );
+         } else if ( termTimePointer == TermTimePointer.OVERLAP && searchTimeSpan.overlaps( timeSpan ) ) {
+            validTimeSpans.add( timeSpan );
+//         } else if ( termTimePointer == null && !searchTimeSpan.contains( timeSpan ) ) {
+         } else if ( termTimePointer == null && searchTimeSpan.overlaps( timeSpan ) ) {
+            validTimeSpans.add( timeSpan );
+         }
+      }
+      return validTimeSpans;
+   }
+
+
+   static public Map<PointedTimeSpan, Map<SemanticClassType, Collection<Entity>>> search(
+         final boolean requireSearchTerms,
+         final Timeline timeline,
+         final Map<String, Collection<PointedTimeSpan>> validTimeSpanMap,
+         final Collection<String> searchTerms ) {
+      final Map<PointedTimeSpan, Map<SemanticClassType, Collection<Entity>>> timeSpanSemanticEntityMap
+            = new HashMap<>();
+      final Collection<String> nonDateTerms = new HashSet<>( searchTerms );
+      nonDateTerms.removeAll( validTimeSpanMap.keySet() );
+      final Collection<Entity> usedEntities = new HashSet<>();
+      final Collection<PointedTimeSpan> validSearchTimeSpans = validTimeSpanMap.get( ALL_FILTER_TIME_TERMS );
+      for ( PointedTimeSpan timeSpan : validSearchTimeSpans ) {
+         if ( timeSpan.equals( TimeSpanPlus.UNKNOWN_TIMESPAN_PLUS ) ) {
+            continue;
+         }
+         final Map<SemanticClassType, Collection<Entity>> semanticEventMap
+               = TimelineSearchUtil.getSemanticEventMap( timeline, timeSpan, searchTerms,
+               requireSearchTerms, nonDateTerms );
+         if ( semanticEventMap != null && !semanticEventMap.isEmpty() ) {
+            timeSpanSemanticEntityMap.put( timeSpan, semanticEventMap );
+            for ( Collection<Entity> entities : semanticEventMap.values() ) {
+               usedEntities.addAll( entities );
+//               for ( Entity entity : entities ) {
+//                  if ( entity instanceof CoreferenceChain ) {
+//                     for ( Entity chainLink : (CoreferenceChain)entity ) {
+//                        usedEntities.add( chainLink );
+//                     }
+//                  }
+//               }
+            }
+         }
+      }
+      // Add unknown time terms
+      final Map<SemanticClassType, Collection<Entity>> semanticEventMap
+            = TimelineSearchUtil.getSemanticEventMap( timeline, TimeSpanPlus.UNKNOWN_TIMESPAN_PLUS, searchTerms,
+            requireSearchTerms, nonDateTerms );
+      if ( semanticEventMap != null && !semanticEventMap.isEmpty() ) {
+         for ( Collection<Entity> entities : semanticEventMap.values() ) {
+            entities.removeAll( usedEntities );
+         }
+         timeSpanSemanticEntityMap.put( TimeSpanPlus.UNKNOWN_TIMESPAN_PLUS, semanticEventMap );
+      }
+      return timeSpanSemanticEntityMap;
+   }
+
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/spiral/JSpiralBar.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/spiral/JSpiralBar.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/spiral/JSpiralBar.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/spiral/JSpiralBar.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,718 @@
+package org.chboston.cnlp.timeline.gui.spiral;
+
+import org.chboston.cnlp.gui.SmoothToolTip;
+import org.chboston.cnlp.gui.ZoomSlider;
+import org.chboston.cnlp.nlp.annotation.entity.Entity;
+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.oldUI.SpiralBarUIDelegate;
+import org.chboston.cnlp.timeline.oldUI.SpiralBarUtil;
+import org.chboston.cnlp.timeline.oldUI.SpiralledScrollPaneLayout;
+import org.chboston.cnlp.timeline.oldUI.TimelineDrawUtil;
+import org.chboston.cnlp.timeline.timeline.Timeline;
+import org.chboston.cnlp.timeline.timespan.plus.PointedTimeSpan;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.geom.GeneralPath;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.chboston.cnlp.timeline.oldUI.SpiralBarUtil.*;
+
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 9/5/12
+ */
+// TODO extract a generic spiralBar and "spanCollectionModel" and SpiralSpanRenderer, create TimelineSpiralBar
+public class JSpiralBar extends JComponent {
+
+   /**
+    * Bound property name for <code>DefaultTimeline</code>.
+    */
+   static public final String MODEL_PROPERTY = "model";
+   static public final String SELECTION_MODEL_PROPERTY = "selectionModel";
+
+   final private SpiralBarUIDelegate _spiralUI = new SpiralBarUIDelegate();
+   private BoundedRangeModel _boundedRangeModel;
+
+   /**
+    * The model that defines the timeline displayed by this object.
+    */
+   private Timeline _model;
+   private TimeSpanSelectionModel _selectionModel;
+   private final TimeSpanSelectionListener _selectionListener;
+
+   private ZoomSlider _zoomSlider;
+   private JButton _undoZoomButton;
+
+   private boolean _isZoomDragging;
+   private double _zoomStartX;
+   private double _zoomStopX;
+   private boolean _zoomToSelection;
+
+   final private Map<Shape, PointedTimeSpan> _shapeToSpanMap;
+
+
+   public JSpiralBar( final Timeline model ) {
+      final Dimension size = new Dimension(
+            SpiralBarUtil.SPIRAL_CIRCUMFERENCE * 2 + SpiralBarUtil.SCROLLBAR_TRACK_SPACER,
+            SpiralBarUtil.SPIRAL_CIRCUMFERENCE );
+      setSize( size );
+      setMinimumSize( size );
+      _shapeToSpanMap = new HashMap<>();
+
+      _zoomToSelection = true;
+      // We want quick reaction in the tooltips on the spiral
+      //      ToolTipManager.sharedInstance().setInitialDelay( 100 );
+      ToolTipManager.sharedInstance().registerComponent( this );
+
+      //      setLayout( null );
+      //      setOpaque( true );
+      //      updateUI();
+      setModel( model );
+      _selectionListener = new PrivateSelectionListener();
+      setSelectionModel( new DefaultTimeSpanSelectionModel() );
+   }
+
+   /**
+    * @return the <code>AnnotationTimeline</code> that is providing the data
+    */
+   final public Timeline getModel() {
+      return _model;
+   }
+
+   public void setModel( final Timeline newModel ) {
+      if ( newModel == null ) {
+         return;
+      }
+      final Timeline oldModel = _model;
+      _model = newModel;
+      _beginDrawTime = TimelineDrawUtil.getBufferedBeginTime( newModel.getStartCalendar(), newModel.getEndCalendar() );
+      _endDrawTime = TimelineDrawUtil.getBufferedEndTime( newModel.getStartCalendar(), newModel.getEndCalendar() );
+      firePropertyChange( MODEL_PROPERTY, oldModel, newModel );
+      repaint();
+   }
+
+   /**
+    * 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;
+   }
+
+   public TimeSpanSelectionListener getSelectionListener() {
+      return _selectionListener;
+   }
+
+   public void setZoomSlider( final ZoomSlider zoomSlider ) {
+      _zoomSlider = zoomSlider;
+      final SpiralMouseListener listener = new SpiralMouseListener();
+      addMouseListener( listener );
+      addMouseMotionListener( listener );
+      add( zoomSlider );
+      _undoZoomButton = new JButton( new UndoZoomAction( zoomSlider ) );
+      _undoZoomButton.setFocusPainted( false );
+      add( _undoZoomButton );
+      // rezoom
+      _zoomSlider.zoomIn( 1d );
+   }
+
+   public ZoomSlider getZoomSlider() {
+      return _zoomSlider;
+   }
+
+   public void setZoomToSelection( final boolean zoomToSelection ) {
+      _zoomToSelection = zoomToSelection;
+   }
+
+   public boolean isZoomToSelection() {
+      return _zoomToSelection;
+   }
+
+   public void setBounds( final int x, final int y, final int width, final int height ) {
+      if ( _zoomSlider != null ) {
+//         final int zoomX = SpiralBarUtil.SPIRAL_CIRCUMFERENCE + SpiralBarUtil.SCROLLBAR_TRACK_SPACER;
+//         final int zoomY = 10;
+//         final int zoomWidth = width - 2 * zoomX;
+//         final int zoomHeight = _zoomSlider.getPreferredSize().height;
+         final int zoomX = SpiralBarUtil.SPIRAL_CIRCUMFERENCE + SpiralBarUtil.SCROLLBAR_TRACK_SPACER;
+         final int zoomY = 10;
+         final int zoomWidth = width - 3 * zoomX;
+         final int zoomHeight = _zoomSlider.getPreferredSize().height;
+         _zoomSlider.setBounds( zoomX, zoomY, zoomWidth, zoomHeight );
+         if ( _undoZoomButton != null ) {
+            final int undoX = zoomX + zoomWidth + 5;
+            final int undoY = zoomY + 10;
+            final int undoWidth = zoomX - 10;
+            final int undoHeight = zoomHeight - 15;
+            _undoZoomButton.setBounds( undoX, undoY, undoWidth, undoHeight );
+         }
+      }
+      super.setBounds( x, y, width, height );
+   }
+
+   public void paint( final Graphics g ) {
+      super.paint( g );
+      if ( !canPaintTimeline() ) {
+         return;
+      }
+      _spiralUI.setSpiralBounds( getBounds() );
+      final Graphics2D myG = (Graphics2D)g.create();
+      myG.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
+      paintZoomDragLine( myG );
+      paintSpiralTrack( myG );
+      paintSpiral( myG );
+      myG.dispose();
+   }
+
+   private Calendar _beginDrawTime;
+   private Calendar _endDrawTime;
+
+   private Calendar getBeginDrawTime() {
+      return _beginDrawTime;
+   }
+
+   private Calendar getEndDrawTime() {
+      return _endDrawTime;
+   }
+
+   final protected long getBeginDrawMillis() {
+      return getBeginDrawTime().getTimeInMillis();
+   }
+
+   final protected long getEndDrawMillis() {
+      return getEndDrawTime().getTimeInMillis();
+   }
+
+   final protected long getDrawTimeDelta() {
+      return getEndDrawMillis() - getBeginDrawMillis();
+   }
+
+   protected double getWidthDrawXform() {
+      final long drawTimeDelta = getDrawTimeDelta();
+      final int panelWidth = getWidth();
+      return drawTimeDelta / (double)panelWidth;
+   }
+
+   private boolean canPaintTimeline() {
+      return _model != null && !_model.isSingleDate();
+   }
+
+
+   protected void paintSpiralTrack( final Graphics2D g ) {
+      final Rectangle spiralBounds = getBounds();
+      final GeneralPath fullSpiral = SpiralBarUtil.createCompleteSpiralPath( _spiralUI,
+            spiralBounds.width,
+            START_RADIAN, START_RADIAN );
+      final int ty = 3;
+      g.translate( 0, ty );
+      final Stroke oldStroke = g.getStroke();
+
+      g.setStroke( STROKE_ONE );
+
+      g.translate( 1, 1 );
+      g.setColor( TRACK_HIGHLIGHT );
+      g.draw( fullSpiral );
+      g.translate( -1, -1 );
+
+      g.setColor( TRACK_SHADOW );
+      g.draw( fullSpiral );
+
+      g.setStroke( oldStroke );
+      g.translate( 0, -ty );
+   }
+
+   protected void paintSpiral( final Graphics2D g ) {
+      if ( _boundedRangeModel == null ) {
+         return;
+      }
+      final int ty = 3;
+      g.translate( 0, ty );
+      final Stroke oldStroke = g.getStroke();
+      g.setColor( TIMELINE_COLOR );
+      g.setStroke( STROKE_SIX );
+      final Rectangle spiralBounds = getBounds();
+      if ( _boundedRangeModel.getMaximum() <= _boundedRangeModel.getExtent() ) {
+         paintSpiralAsLine( g );
+      } else {
+         paintSpiralAsSpiral( g, spiralBounds );
+      }
+      g.setStroke( oldStroke );
+
+      paintDateSpans( g, spiralBounds );
+      g.translate( 0, -ty );
+   }
+
+   private void paintSpiralAsLine( final Graphics2D g ) {
+      final double end = getWidth() - _spiralUI.getCircumference() / 2 - 1;
+      final double start = _spiralUI.getCircumference() / 2 + 1;
+      // Paint Track
+      g.setStroke( STROKE_SEVEN );
+      g.setColor( THUMB_SHADOW );
+      g.drawLine( (int)start, 1, (int)end, 1 );
+      g.setStroke( STROKE_FIVE );
+      g.setColor( THUMB_COLOR );
+      g.drawLine( (int)start, 1, (int)end, 1 );
+      // Paint EndPoints
+      g.setColor( TIMELINE_ENDPOINT_COLOR );
+      g.setStroke( STROKE_ROUND_SEVEN );
+      g.drawLine( (int)start, 1, (int)start + 1, 1 );
+      g.drawLine( (int)end - 1, 1, (int)end, 1 );
+   }
+
+   private void paintSpiralAsSpiral( final Graphics2D g, final Rectangle spiralBounds ) {
+      // Paint Track
+      final int invisible = _boundedRangeModel.getMaximum() - _boundedRangeModel.getExtent();
+      final double lineStart = _boundedRangeModel.getValue();
+      final double stop = _spiralUI.getStopRadian();
+      final double end = lineStart * stop / invisible;
+      final double start = stop - end;
+      final GeneralPath fullSpiral = SpiralBarUtil.createCompleteSpiralPath( _spiralUI,
+            spiralBounds.width,
+            start, end );
+      g.setStroke( STROKE_SEVEN );
+      g.setColor( THUMB_SHADOW );
+      g.draw( fullSpiral );
+      g.setStroke( STROKE_FIVE );
+      g.setColor( THUMB_COLOR );
+      g.draw( fullSpiral );
+      paintSpiralEndPoints( g, spiralBounds );
+   }
+
+   private void paintSpiralEndPoints( final Graphics2D g, final Rectangle spiralBounds ) {
+      final int visibleX = _boundedRangeModel.getValue();
+      final int visibleWidth = _boundedRangeModel.getExtent();
+      final int totalWidth = _boundedRangeModel.getMaximum();
+      final long drawTimeDelta = getDrawTimeDelta();
+      final double xform = drawTimeDelta / totalWidth;
+      g.setColor( TIMELINE_ENDPOINT_COLOR );
+      g.setStroke( STROKE_ROUND_SEVEN );
+      // Draw Start
+      final double x1 = 0;//getBeginDrawMillis() / xform;
+      final Point startPoint = getPointOnCompleteSpiral( _spiralUI, spiralBounds.width,
+            (int)x1, visibleX, visibleWidth, totalWidth );
+      g.drawLine( startPoint.x, startPoint.y, startPoint.x + 1, startPoint.y );
+      // Draw End
+      final double x2 = (getEndDrawMillis() - getBeginDrawMillis()) / xform;
+      final Point endPoint = getPointOnCompleteSpiral( _spiralUI, spiralBounds.width,
+            (int)x2, visibleX, visibleWidth, totalWidth );
+      g.drawLine( endPoint.x, endPoint.y, endPoint.x + 1, endPoint.y );
+   }
+
+
+   protected void paintZoomDragLine( final Graphics2D g ) {
+      if ( !_isZoomDragging || _boundedRangeModel == null ) {
+         return;
+      }
+      final Rectangle spiralBounds = getBounds();
+      final int visibleX = _boundedRangeModel.getValue();
+      final int visibleWidth = _boundedRangeModel.getExtent();
+      final int totalWidth = _boundedRangeModel.getMaximum();
+      final Stroke oldStroke = g.getStroke();
+      final double start = Math.min( _zoomStartX, _zoomStopX );
+      final double stop = Math.max( _zoomStartX, _zoomStopX );
+      final GeneralPath path = getPathOnCompleteSpiral( _spiralUI, spiralBounds.width,
+            (int)start, (int)stop, visibleX, visibleWidth, totalWidth );
+      final int ty = 3;
+      g.translate( 0, ty );
+      g.setColor( ZOOM_DRAG_SHADOW );
+      g.setStroke( ZOOM_DRAG_STROKE_1 );
+      g.draw( path );
+
+      g.setColor( ZOOM_DRAG_COLOR );
+      g.setStroke( ZOOM_DRAG_STROKE );
+      g.draw( path );
+      g.setStroke( oldStroke );
+      g.translate( 0, -ty );
+   }
+
+
+   protected void paintDateSpans( final Graphics2D g, final Rectangle spiralBounds ) {
+      if ( _boundedRangeModel == null || !canPaintTimeline() ) {
+         return;
+      }
+      final int leftSpiralCenter = _spiralUI.getCircumference() / 2;
+      final int rightSpiralCenter = spiralBounds.width - leftSpiralCenter;
+      final int visibleX = _boundedRangeModel.getValue();
+      final int visibleWidth = _boundedRangeModel.getExtent();
+      final int totalWidth = _boundedRangeModel.getMaximum();
+      final long beginDrawMillis = getBeginDrawMillis();
+      final long drawTimeDelta = getDrawTimeDelta();
+      final double xform = drawTimeDelta / totalWidth;
+      final Stroke oldStroke = g.getStroke();
+      _shapeToSpanMap.clear();
+      //      final Collection<TimeSpanPlus> selections = getSelectionModel().getSelectedTimeSpans();
+      final Collection<PointedTimeSpan> selections = getSelectionModel().getSelectedEntities().keySet();
+      for ( PointedTimeSpan timeSpan : _model ) {
+         if ( selections.contains( timeSpan ) ) {
+            // paint selections last so they are on top and visible
+            continue;
+         }
+         final long begin = timeSpan.getStartMillis();
+         final long end = timeSpan.getStopMillis();
+         final double x1 = (begin - beginDrawMillis) / xform;
+         final double x2 = (end - beginDrawMillis) / xform;
+         final GeneralPath path = getPathOnCompleteSpiral( _spiralUI, spiralBounds.width,
+               (int)x1, (int)x2, visibleX, visibleWidth, totalWidth );
+         final Shape dateOutlineShape = paintDatePath( g, path, leftSpiralCenter, rightSpiralCenter,
+               selections.contains( timeSpan ) );
+         _shapeToSpanMap.put( dateOutlineShape, timeSpan );
+      }
+      for ( PointedTimeSpan timeSpan : _model ) {
+         if ( !selections.contains( timeSpan ) ) {
+            // already painted non-selections
+            continue;
+         }
+         final long begin = timeSpan.getStartMillis();
+         final long end = timeSpan.getStopMillis();
+         final double x1 = (begin - beginDrawMillis) / xform;
+         final double x2 = (end - beginDrawMillis) / xform;
+         final GeneralPath path = getPathOnCompleteSpiral( _spiralUI, spiralBounds.width,
+               (int)x1, (int)x2, visibleX, visibleWidth, totalWidth );
+         final Shape dateOutlineShape = paintDatePath( g, path, leftSpiralCenter, rightSpiralCenter,
+               selections.contains( timeSpan ) );
+         _shapeToSpanMap.put( dateOutlineShape, timeSpan );
+      }
+
+      g.setStroke( oldStroke );
+   }
+
+   static private Shape paintDatePath( final Graphics2D g,
+                                       final GeneralPath path,
+                                       final int leftSpiralCenter,
+                                       final int rightSpiralCenter, final boolean selected ) {
+      final Rectangle pathBounds = path.getBounds();
+      if ( pathBounds.width < 1 ) {
+         g.setColor( DATE_SHADOW );
+         g.setStroke( STROKE_SEVEN );
+         g.drawLine( pathBounds.x, pathBounds.y, pathBounds.x + 1, pathBounds.y );
+         g.setColor( selected ? DATE_SELECTED : DATE_COLOR );
+         g.setStroke( STROKE_FIVE );
+         g.drawLine( pathBounds.x, pathBounds.y, pathBounds.x + 1, pathBounds.y );
+         return STROKE_SEVEN.createStrokedShape( path );
+      } else if ( pathBounds.y > 5 || pathBounds.x + pathBounds.width < leftSpiralCenter
+                  || pathBounds.x > rightSpiralCenter ) {
+         g.setColor( DATE_SHADOW );
+         g.setStroke( STROKE_ROUND_SEVEN );
+         g.draw( path );
+         g.setColor( selected ? DATE_SELECTED : DATE_COLOR );
+         g.setStroke( STROKE_ROUND_FIVE );
+         g.draw( path );
+         return STROKE_ROUND_SEVEN.createStrokedShape( path );
+      }
+      g.setColor( DATE_SHADOW );
+      g.setStroke( STROKE_SEVEN );
+      g.draw( path );
+      g.setColor( selected ? DATE_SELECTED : DATE_COLOR );
+      g.setStroke( STROKE_FIVE );
+      g.draw( path );
+      return STROKE_SEVEN.createStrokedShape( path );
+   }
+
+
+   //////////////////////////////////////////////////////////////////////////////////////////
+
+
+   public JScrollPane createScrollPane() {
+      final JScrollPane scrollPane = new SpiralBarScrollPane();
+      final SpiralledScrollPaneLayout layout = new SpiralledScrollPaneLayout();
+      layout.addLayoutComponent( SpiralledScrollPaneLayout.SPIRAL_COMPONENT, this );
+      scrollPane.setLayout( layout );
+      scrollPane.add( this, SpiralledScrollPaneLayout.SPIRAL_COMPONENT );
+      _boundedRangeModel = scrollPane.getHorizontalScrollBar().getModel();
+      _boundedRangeModel.addChangeListener( createBoundsListener() );
+      return scrollPane;
+   }
+
+
+   private ChangeListener createBoundsListener() {
+      return new ChangeListener() {
+         @Override
+         public void stateChanged( final ChangeEvent e ) {
+            SwingUtilities.invokeLater( new Runnable() {
+               @Override
+               public void run() {
+                  repaint();
+               }
+            } );
+         }
+      };
+   }
+
+   public JToolTip createToolTip() {
+      final SmoothToolTip tip = new SmoothToolTip();
+      tip.setComponent( this );
+      return tip;
+   }
+
+   private Shape getShapeForPoint( final Point p ) {
+      Shape smallestShape = null;
+      Rectangle smallestRectangle = null;
+      for ( Shape shape : _shapeToSpanMap.keySet() ) {
+         // go through all shapes and find the smallest for the most exact tooltip
+         if ( shape.contains( p ) ) {
+            final Rectangle rectangle = shape.getBounds();
+            if ( smallestRectangle == null || smallestRectangle.contains( rectangle ) ) {
+               smallestShape = shape;
+               smallestRectangle = rectangle;
+            }
+         }
+      }
+      return smallestShape;
+   }
+
+   private PointedTimeSpan getTimeSpanForPoint( final Point p ) {
+      final Shape shape = getShapeForPoint( p );
+      if ( shape == null ) {
+         return null;
+      }
+      return _shapeToSpanMap.get( shape );
+   }
+
+   public String getToolTipText( final MouseEvent event ) {
+      if ( event == null ) {
+         return null;
+      }
+      final Point p = event.getPoint();
+      final PointedTimeSpan timeSpan = getTimeSpanForPoint( p );
+      if ( timeSpan == null ) {
+         return null;
+      }
+      return timeSpan.toString();
+   }
+
+
+   private void paintLater() {
+      SwingUtilities.invokeLater( new Runnable() {
+         @Override
+         public void run() {
+            repaint();
+         }
+      } );
+   }
+
+   private class SpiralMouseListener extends MouseAdapter {
+
+      private boolean __mousePressed;
+
+      public void mousePressed( final MouseEvent event ) {
+         if ( event == null ) {
+            return;
+         }
+         final Point p = event.getPoint();
+         final Rectangle spiralBounds = getBounds();
+         final int circumference = _spiralUI.getCircumference();
+         if ( p.x > circumference && p.x < spiralBounds.width - circumference && p.y > 10 ) {
+            // not on a spiral or the visible line
+            return;
+         }
+         final double lineX = SpiralBarUtil.getLineLociForPoint( p.x, p.y, _spiralUI,
+               spiralBounds.width,
+               _boundedRangeModel.getValue(),
+               _boundedRangeModel.getExtent(),
+               _boundedRangeModel.getMaximum() );
+         _zoomStartX = lineX;
+         _zoomStopX = lineX;
+         __mousePressed = true;
+      }
+
+      public void mouseReleased( final MouseEvent event ) {
+         __mousePressed = false;
+         if ( _isZoomDragging ) {
+            final double start = Math.min( _zoomStartX, _zoomStopX );
+            final double stop = Math.max( _zoomStartX, _zoomStopX );
+            final boolean repainted = _zoomSlider.zoomInTo( start, stop );
+//            paintLater();   // removed 2/11/14 as it doesn't seem to be needed
+         }
+         _isZoomDragging = false;
+      }
+
+      public void mouseDragged( final MouseEvent event ) {
+         if ( event == null || !__mousePressed ) {
+            return;
+         }
+         _isZoomDragging = true;
+         final Point p = event.getPoint();
+         final Rectangle spiralBounds = getBounds();
+         final int circumference = _spiralUI.getCircumference();
+         if ( p.x > circumference / 2 && p.x < spiralBounds.width - circumference / 2 ) {
+            // ensure that the drag registers on the visible line
+            p.setLocation( p.x, 5 );
+         }
+         _zoomStopX = SpiralBarUtil.getLineLociForPoint( p.x, p.y, _spiralUI,
+               spiralBounds.width,
+               _boundedRangeModel.getValue(),
+               _boundedRangeModel.getExtent(),
+               _boundedRangeModel.getMaximum() );
+         paintLater();
+      }
+
+      public void mouseClicked( final MouseEvent event ) {
+         if ( event == null ) {
+            return;
+         }
+         __mousePressed = false;
+         _isZoomDragging = false;
+         final Point p = event.getPoint();
+         final PointedTimeSpan timeSpan = getTimeSpanForPoint( p );
+         if ( timeSpan == null ) {
+            return;
+         }
+         final TimeSpanSelectionModel selectionModel = getSelectionModel();
+         selectionModel.setValueIsAdjusting( selectionModel, true );
+         final Map<PointedTimeSpan, Collection<Entity>> timeSpanEntities = new HashMap<>(
+               1 );
+         timeSpanEntities.put( timeSpan, getModel().getEntities( timeSpan ) );
+         selectionModel.setSelectedEntities( selectionModel, timeSpanEntities );
+         selectionModel.setValueIsAdjusting( selectionModel, false );
+      }
+   }
+
+
+   static private class SpiralBarScrollPane extends JScrollPane {
+      public void setCorner( final String key, final Component component ) {
+         if ( key.equals( LOWER_RIGHT_CORNER ) ) {
+            final JViewport footer = createRowFooter( component );
+            super.setCorner( key, footer );
+         } else {
+            super.setCorner( key, component );
+         }
+      }
+
+      // The Row footer is like a row header - it tracks the scrolled view
+      private JViewport createRowFooter( final Component component ) {
+         final JViewport viewport = new JViewport();
+         viewport.setView( component );
+         final JScrollBar scrollBar = getVerticalScrollBar();
+         if ( scrollBar != null ) {
+            final BoundedRangeModel rangeModel = scrollBar.getModel();
+            if ( rangeModel != null ) {
+               rangeModel.addChangeListener( new ChangeListener() {
+                  @Override
+                  public void stateChanged( final ChangeEvent event ) {
+                     final Object source = event.getSource();
+                     if ( source instanceof BoundedRangeModel ) {
+                        final Point p = viewport.getViewPosition();
+                        p.y = ((BoundedRangeModel)source).getValue();
+                        viewport.setViewPosition( p );
+                     }
+                  }
+               } );
+            }
+         }
+         return viewport;
+      }
+
+      public void paint( final Graphics g ) {
+         super.paint( g );
+         //  Paint a mock  border outside the viewport bounds
+         final Rectangle portBounds = getViewport().getBounds();
+         final int y1 = portBounds.y;
+         final int x1 = portBounds.x - 1;
+         g.setColor( Color.GRAY );
+         g.drawRect( x1, y1, portBounds.width + 2, portBounds.height + 1 );
+      }
+   }
+
+   private class PrivateSelectionListener implements TimeSpanSelectionListener {
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public void valueChanged( final TimeSpanSelectionEvent event ) {
+         if ( event == null || event.getValueIsAdjusting() ) {
+            return;
+         }
+         if ( _zoomToSelection ) {
+            zoomToSelection();
+         } else {
+            revalidate();
+            // The repaint call is necessary as revalidate doesn't always know/clear the current state of the spiral
+            repaint();
+            // paintLater();  // Paint Later does not work as component tree doesn't play nicely
+         }
+      }
+   }
+
+   private void zoomToSelection() {
+      //      final Collection<TimeSpanPlus> selections = getSelectionModel().getSelectedTimeSpans();
+      final Collection<PointedTimeSpan> selections = getSelectionModel().getSelectedEntities().keySet();
+      if ( selections.isEmpty() ) {
+         _zoomSlider.zoomIn( 1d );
+         paintLater();
+         return;
+      }
+      long minMillis = Long.MAX_VALUE;
+      long maxMillis = 0;
+      for ( PointedTimeSpan timeSpan : selections ) {
+         minMillis = Math.min( minMillis, timeSpan.getStartMillis() );
+         maxMillis = Math.max( maxMillis, timeSpan.getStopMillis() );
+      }
+      final double totalWidth = _boundedRangeModel.getMaximum();
+      final long drawTimeDelta = getDrawTimeDelta();
+      final double xform = drawTimeDelta / totalWidth;
+      final long beginDrawMillis = getBeginDrawMillis();
+      final double x1 = (minMillis - beginDrawMillis) / xform;
+      final double x2 = (maxMillis - beginDrawMillis) / xform;
+      final double xBuffer = (x2 - x1) / 20d;
+      final boolean repainted = _zoomSlider.zoomInTo( x1 - xBuffer, x2 + xBuffer );
+      //      if ( !repainted ) {
+//      paintLater();      // removed 2/11/14 as it doesn't seem to be needed
+      //      }
+   }
+
+   private class UndoZoomAction extends AbstractAction {
+      final private ZoomSlider __zoomSlider;
+
+      private UndoZoomAction( final ZoomSlider zoomSlider ) {
+         super( "Undo" );
+         putValue( LONG_DESCRIPTION, "Undo Zoom" );
+         putValue( Action.SHORT_DESCRIPTION, "Undo Zoom" );
+         __zoomSlider = zoomSlider;
+      }
+
+      public void actionPerformed( final ActionEvent event ) {
+         __zoomSlider.undoZoomLevel();
+      }
+   }
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/spiral/TimeZoomSlider.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/spiral/TimeZoomSlider.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/spiral/TimeZoomSlider.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/spiral/TimeZoomSlider.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,169 @@
+package org.chboston.cnlp.timeline.gui.spiral;
+
+import org.chboston.cnlp.gui.ZoomSlider;
+import org.chboston.cnlp.timeline.oldUI.TimelineDrawUtil;
+import org.chboston.cnlp.timeline.timeline.Timeline;
+import org.chboston.cnlp.timeline.timespan.CalendarUtil;
+import org.chboston.cnlp.timeline.timespan.DefaultTimeSpan;
+import org.chboston.cnlp.timeline.timespan.TimeSpan;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Calendar;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 2/7/14
+ */
+public class TimeZoomSlider extends ZoomSlider {
+
+   static private enum TIME_TICK {
+      LIFE( -70, null, "Life" ),
+      DECADE( -60, CalendarUtil.TIME_MILLIS.DECADE, "Decade" ),
+      YEAR( -50, CalendarUtil.TIME_MILLIS.YEAR, "Year" ),
+      MONTH( -40, CalendarUtil.TIME_MILLIS.MONTH, "Month" ),
+      WEEK( -30, CalendarUtil.TIME_MILLIS.WEEK, "Week" ),
+      DAY( -20, CalendarUtil.TIME_MILLIS.DAY, "Day" ),
+      //      HOUR( -20, CalendarUtil.TIME_MILLIS.HOUR, "Hour" ),
+      INF( -10, null, "Inf" );
+      final private int __tick;
+      final private long __millis;
+      final private String __label;
+
+      private TIME_TICK( final int tick, final CalendarUtil.TIME_MILLIS timeMillis, final String label ) {
+         __tick = tick;
+         __millis = (timeMillis == null) ? 0 : timeMillis.__millis;
+         __label = label;
+      }
+   }
+
+
+   static private final Dictionary<Integer, Component> LABEL_TABLE;
+
+   static {
+      LABEL_TABLE = new Hashtable<>();
+      for ( TIME_TICK timeTick : TIME_TICK.values() ) {
+         LABEL_TABLE.put( timeTick.__tick, new JLabel( timeTick.__label ) );
+      }
+   }
+
+   final private TimeSpan _timeSpan;
+   // doc time
+   final private TimeSpan _referenceDay;
+   // the buffered total time of the timeline gui components.  Necessary for dic time centering
+   final private TimeSpan _drawSpan;
+
+
+   public TimeZoomSlider( final JComponent viewComponent, final Timeline model ) {
+      super( viewComponent );
+      _timeSpan = new DefaultTimeSpan( model.getStartMillis(), model.getStopMillis() );
+      final long docTimeStart = model.getReferenceMillis();
+      final long docTimeStop = docTimeStart + 1000 * 60 * 60 * 24 - 1;
+      _referenceDay = new DefaultTimeSpan( docTimeStart, docTimeStop );
+      final Calendar beginDrawTime = TimelineDrawUtil.getBufferedBeginTime(
+            model.getStartCalendar(),
+            model.getEndCalendar() );
+      final Calendar endDrawTime = TimelineDrawUtil
+            .getBufferedEndTime( model.getStartCalendar(), model.getEndCalendar() );
+      _drawSpan = new DefaultTimeSpan( beginDrawTime.getTimeInMillis(), endDrawTime.getTimeInMillis() );
+      // value, extent, min, max, isAdjusting
+      setModel( new DefaultBoundedRangeModel( TIME_TICK.LIFE.__tick, 10, TIME_TICK.LIFE.__tick, TIME_TICK.INF.__tick ) {
+         public void setValue( int n ) {
+            if ( n == TIME_TICK.INF.__tick ) {
+               // kludge because the Java DefaultBoundedRangeModel does not handle negatives - bug
+               setRangeProperties( TIME_TICK.INF.__tick, 10, TIME_TICK.LIFE.__tick, TIME_TICK.INF.__tick, false );
+            } else {
+               super.setValue( n );
+            }
+         }
+      } );
+      setLabelTable( LABEL_TABLE );
+   }
+
+   private long getMillisWidth( final int zoomTick ) {
+      if ( zoomTick == TIME_TICK.LIFE.__tick ) {
+         return _timeSpan.getWidthMillis();
+      } else if ( zoomTick == TIME_TICK.INF.__tick ) {
+         return zoomTick;
+      }
+      for ( TIME_TICK timeTick : TIME_TICK.values() ) {
+         if ( zoomTick == timeTick.__tick ) {
+            return timeTick.__millis;
+         }
+      }
+      return TIME_TICK.INF.__tick;
+   }
+
+   private double getTimeZoom( final long millis ) {
+      if ( millis >= _timeSpan.getWidthMillis() ) {
+         return 1d;
+      }
+      return (double)_timeSpan.getWidthMillis() / (double)millis;
+   }
+
+   /**
+    * Called by setValue( int value )
+    *
+    * @param zoom -
+    * @return -
+    */
+   protected int getSetValueValue( final int zoom ) {
+      if ( !isZooming() ) {
+         if ( zoom > TIME_TICK.DAY.__tick ) {
+            return TIME_TICK.DAY.__tick;
+         }
+         return zoom;
+      } else {
+         if ( zoom < 0 ) {
+            return zoom;
+         }
+      }
+      return getSliderValue( zoom );
+   }
+
+   protected double getStateChangeZoom( final int value ) {
+      if ( value == 0 ) {
+         return 0;
+      }
+      if ( value < 0 ) {
+         return getTimeZoom( getMillisWidth( value ) );
+      }
+      return (double)value;
+   }
+
+   protected double getZoomInCenterX( final double zoom, final double oldZoom,
+                                      final int visibleRectX, final double halfWidth ) {
+      if ( getSliderValue( zoom ) >= TIME_TICK.INF.__tick || halfWidth == 0 ) {
+         return super.getZoomInCenterX( zoom, oldZoom, visibleRectX, halfWidth );
+      }
+      // Calculate new zoom size
+      final Dimension viewportSize = getViewportSize();
+      final double viewportWidth = viewportSize.getWidth();
+      // because of Component sizing maximums, we have to limit the zoom
+      final double newViewWidth = getBestWidth( zoom * viewportWidth );
+      final double pixelsPerMilli = newViewWidth / (double)_drawSpan.getWidthMillis();
+      final long referenceCenter = (_referenceDay.getStartMillis() + _referenceDay.getWidthMillis() / 2l) -
+                                   _drawSpan.getStartMillis();
+      return (double)referenceCenter * pixelsPerMilli;
+   }
+
+   protected int getSliderValue( final double zoom ) {
+      if ( zoom == 1 ) {
+         return TIME_TICK.LIFE.__tick;
+      }
+      final double timeSpanMillis = _timeSpan.getWidthMillis();
+      for ( TIME_TICK timeTick : TIME_TICK.values() ) {
+         if ( timeTick != TIME_TICK.LIFE && timeTick != TIME_TICK.INF
+              && zoom == timeSpanMillis / (double)timeTick.__millis ) {
+            return timeTick.__tick;
+         }
+      }
+      return TIME_TICK.INF.__tick;
+   }
+
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/AbstractTimelineComponent.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/AbstractTimelineComponent.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/AbstractTimelineComponent.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/AbstractTimelineComponent.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,172 @@
+package org.chboston.cnlp.timeline.gui.timeline;
+
+import org.chboston.cnlp.timeline.gui.timeline.ui.TimelineUI;
+import org.chboston.cnlp.timeline.oldUI.TimelineDrawUtil;
+import org.chboston.cnlp.timeline.timeline.Timeline;
+
+import javax.swing.*;
+import java.util.Calendar;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 8/2/13
+ */
+abstract public class AbstractTimelineComponent extends JComponent implements TimelineComponent {
+
+   /**
+    * Bound property name for <code>DefaultTimeline</code>.
+    */
+   static public final String MODEL_PROPERTY = "model";
+
+   /**
+    * The model that defines the timeline displayed by this object.
+    */
+   private Timeline _model;
+
+   /**
+    * This is set to true for the life of the <code>setUI</code> call.
+    */
+   private boolean _settingUI;
+   static private final Object UI_LOCK = new Object();
+
+   private Calendar _beginDrawTime;
+   private Calendar _endDrawTime;
+
+
+   public AbstractTimelineComponent( final Timeline model ) {
+      this.ui = createTimelineUI();
+      this.ui.installUI( this );
+      setLayout( null );
+      setModel( model );
+   }
+
+   abstract protected TimelineUI createTimelineUI();
+
+   /**
+    * @return the <code>TimelineUI</code> object that renders this component
+    */
+   final public TimelineUI getUI() {
+      return (TimelineUI)ui;
+   }
+
+   final public void setUI( final TimelineUI ui ) {
+      // if the UI ever needs to be reloaded with different default settings then remove the == check
+      if ( _settingUI || this.ui == ui ) {
+         // faster check outside lock
+         return;
+      }
+      synchronized ( UI_LOCK ) {
+         // slower check within lock
+         if ( _settingUI || this.ui == ui ) {
+            return;
+         }
+         _settingUI = true;
+         try {
+            super.setUI( ui );
+         } finally {
+            _settingUI = false;
+         }
+      }
+   }
+
+   /**
+    * Notification from the <code>UIManager</code> that the L&F has changed.
+    * Replaces the current UI object with the latest version from the
+    * <code>UIManager</code>.
+    *
+    * @see javax.swing.JComponent#updateUI
+    */
+   final public void updateUI() {
+      setUI( createTimelineUI() );
+   }
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   final public Timeline getModel() {
+      return _model;
+   }
+
+   public void setModel( final Timeline newModel ) {
+      if ( newModel == null ) {
+         throw new IllegalArgumentException( "Cannot set a null DefaultTimeline Model" );
+      }
+      if ( newModel.equals( _model ) ) {
+         return;
+      }
+      // TODO Removed for New
+      //      if ( _model != null ) {
+      //         _model.removeTimelineListener( this );
+      //      }
+      //      newModel.addTimelineListener( this );
+      _beginDrawTime = TimelineDrawUtil.getBufferedBeginTime( newModel.getStartCalendar(), newModel.getEndCalendar() );
+      _endDrawTime = TimelineDrawUtil.getBufferedEndTime( newModel.getStartCalendar(), newModel.getEndCalendar() );
+      final Timeline oldModel = _model;
+      _model = newModel;
+      firePropertyChange( MODEL_PROPERTY, oldModel, _model );
+      invalidate();
+   }
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   final public Calendar getBeginDrawTime() {
+      return _beginDrawTime;
+   }
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   final public Calendar getEndDrawTime() {
+      return _endDrawTime;
+   }
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   final public long getBeginDrawMillis() {
+      return getBeginDrawTime().getTimeInMillis();
+   }
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   final public long getEndDrawMillis() {
+      return getEndDrawTime().getTimeInMillis();
+   }
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   final public long getDrawTimeDelta() {
+      return getEndDrawMillis() - getBeginDrawMillis();
+   }
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   public double getWidthDrawXform() {
+      final long drawTimeDelta = getDrawTimeDelta();
+      final int panelWidth = getWidth();
+      return drawTimeDelta / (double)panelWidth;
+   }
+
+   final protected void paintLater() {
+      SwingUtilities.invokeLater( new Runnable() {
+         @Override
+         public void run() {
+            repaint();
+         }
+      } );
+   }
+
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/JTimelineComponent.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/JTimelineComponent.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/JTimelineComponent.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/JTimelineComponent.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,320 @@
+package org.chboston.cnlp.timeline.gui.timeline;
+
+import org.chboston.cnlp.gui.SmoothToolTip;
+import org.chboston.cnlp.nlp.annotation.entity.Entity;
+import org.chboston.cnlp.timeline.gui.timeline.ui.BasicTimelineUI;
+import org.chboston.cnlp.timeline.gui.timeline.ui.TimelineUI;
+import org.chboston.cnlp.timeline.gui.timespan.plus.DefaultRelationRenderer2;
+import org.chboston.cnlp.timeline.gui.timespan.plus.DefaultTimeSpanRenderer;
+import org.chboston.cnlp.timeline.gui.timespan.plus.TimeSpanRenderer;
+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.TimeSpan;
+import org.chboston.cnlp.timeline.timespan.plus.PointedTimeSpan;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 8/2/13
+ */
+public class JTimelineComponent extends AbstractTimelineComponent {
+
+   /**
+    * @see #getUIClassID
+    * @see #readObject
+    */
+   private static final String UI_CLASS_ID = "BasicTimelineUI";
+
+   /**
+    * Bound property name for <code>cellRenderer</code>.
+    */
+   static public final String CELL_RENDERER_PROPERTY = "cellRenderer";
+   static public final String SELECTION_MODEL_PROPERTY = "selectionModel";
+
+   static final private TimeSpanRenderer DEFAULT_RENDERER = new DefaultTimeSpanRenderer();
+
+   private TimeSpanSelectionModel _selectionModel;
+   private final TimeSpanSelectionListener _selectionListener;
+
+   private Timeline _gridModel;
+
+   /**
+    * The cell used to draw nodes. If <code>null</code>, the UI uses a default
+    * <code>cellRenderer</code>.
+    */
+   final private Map<Class<? extends TimeSpan>, TimeSpanRenderer> _rendererMap;
+
+
+   public JTimelineComponent( final Timeline model, final Timeline gridModel ) {
+      super( model );
+      _gridModel = gridModel;
+      _rendererMap = new HashMap<>();
+//      setTimeSpanRenderer( TimeSpan.class, new DefaultRelationRenderer() );
+      setTimeSpanRenderer( TimeSpan.class, new DefaultRelationRenderer2() );
+      setOpaque( true );
+      ToolTipManager.sharedInstance().registerComponent( this );
+      setBackground( Color.WHITE );
+      _selectionListener = new PrivateSelectionListener();
+      setSelectionModel( new DefaultTimeSpanSelectionModel() );
+      addMouseListener( new TimelineMouseHandler() );
+      addMouseMotionListener( new TimelineMouseHandler() );
+      //      setMinimumSize( new Dimension( 10, TimeSpanDrawUtil.TIMESPAN_CELL_HEIGHT ) );
+   }
+
+
+   final protected TimelineUI createTimelineUI() {
+      return new BasicTimelineUI();
+   }
+
+   /**
+    * Returns the name of the L&F class that renders this component.
+    *
+    * @return the string "TimelineUI"
+    * @see JComponent#getUIClassID
+    * @see UIDefaults#getUI
+    */
+   public String getUIClassID() {
+      return UI_CLASS_ID;
+   }
+
+   /**
+    * Returns the <code>Rectangle</code> that the specified node will be drawn
+    * into. Returns <code>null</code> if any component in the path is hidden
+    * (under a collapsed parent).
+    * <p/>
+    * Note:<br>
+    * This method returns a valid rectangle, even if the specified
+    * node is not currently displayed.
+    *
+    * @param timeSpan the <code>TimeSpan</code> identifying the node
+    * @return the <code>Rectangle</code> the node is drawn in,
+    * or <code>null</code>
+    */
+   public Rectangle getTimeSpanBounds( final TimeSpan timeSpan ) {
+      final TimelineUI ui = getUI();
+      if ( ui != null ) {
+         return ui.getTimeSpanBounds( this, timeSpan );
+      }
+      return null;
+   }
+
+   public Timeline getGridModel() {
+      return _gridModel;
+   }
+
+   public void setGridModel( final Timeline timeline ) {
+      _gridModel = timeline;
+   }
+
+   /**
+    * 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;
+   }
+
+   public Collection<PointedTimeSpan> getSelectedTimeSpans() {
+      final TimeSpanSelectionModel selectionModel = getSelectionModel();
+      return selectionModel.getSelectedEntities().keySet();
+   }
+
+//
+//   public Collection<TimeSpanPlus> getFocusedTimeSpans() {
+//      final TimeSpanSelectionModel selectionModel = getSelectionModel();
+//      return selectionModel();
+//   }
+//
+
+   /**
+    * @return the <code>TimeSpanRenderer</code> that is rendering each span
+    */
+   public TimeSpanRenderer getTimeSpanRenderer( final Class<? extends TimeSpan> timeSpanClass ) {
+      if ( timeSpanClass == null ) {
+         return DEFAULT_RENDERER;
+      }
+      final TimeSpanRenderer renderer = _rendererMap.get( timeSpanClass );
+      if ( renderer == null ) {
+         final Class<?>[] interfaces = timeSpanClass.getInterfaces();
+         for ( Class<?> intrfc : interfaces ) {
+            if ( intrfc != null && TimeSpan.class.isAssignableFrom( intrfc ) ) {
+               if ( _rendererMap.containsKey( intrfc ) ) {
+                  return _rendererMap.get( intrfc );
+               }
+            }
+         }
+         final Class superClass = timeSpanClass.getSuperclass();
+         if ( superClass != null && TimeSpan.class.isAssignableFrom( superClass ) ) {
+            return getTimeSpanRenderer( superClass );
+         }
+      }
+      return DEFAULT_RENDERER;
+   }
+
+   /**
+    * @param renderer the <code>TimeSpanRenderer</code> that is to render each cell
+    */
+   public void setTimeSpanRenderer( final Class<? extends TimeSpan> timeSpanClass,
+                                    final TimeSpanRenderer renderer ) {
+      final TimeSpanRenderer oldValue = getTimeSpanRenderer( timeSpanClass );
+      if ( renderer != null ) {
+         _rendererMap.put( timeSpanClass, renderer );
+      } else {
+         _rendererMap.remove( timeSpanClass );
+      }
+      firePropertyChange( CELL_RENDERER_PROPERTY, oldValue, renderer );
+      invalidate();
+   }
+
+   @Override
+   public JToolTip createToolTip() {
+      final SmoothToolTip tip = new SmoothToolTip();
+      tip.setComponent( this );
+      return tip;
+   }
+
+   @Override
+   public String getToolTipText( final MouseEvent event ) {
+      if ( event != null ) {
+         final Point p = event.getPoint();
+         final TimeSpan timeSpan = getTimeSpanForLocation( p.x, p.y );
+         if ( timeSpan == null ) {
+            return null;
+         }
+         final TimeSpanRenderer renderer = getTimeSpanRenderer( timeSpan.getClass() );
+         if ( renderer == null ) {
+            return null;
+         }
+         final JComponent component = renderer.getTimeSpanRendererComponent( this,
+               (PointedTimeSpan)timeSpan,
+               false,
+               false,
+               false );
+         if ( component != null ) {
+            return component.getToolTipText();
+         }
+      }
+      // No tip from the renderer get our own tip
+      return getToolTipText();
+   }
+
+
+   /**
+    * Returns the path for the node at the specified location.
+    *
+    * @param x an integer giving the number of pixels horizontally from
+    *          the left edge of the display area, minus any left margin
+    * @param y an integer giving the number of pixels vertically from
+    *          the top of the display area, minus any top margin
+    * @return the <code>Span</code> for the node at that location
+    */
+   public PointedTimeSpan getTimeSpanForLocation( final int x, final int y ) {
+      final PointedTimeSpan closestTimeSpan = getClosestTimeSpanForLocation( x, y );
+      if ( closestTimeSpan != null ) {
+         final Rectangle pathBounds = getTimeSpanBounds( closestTimeSpan );
+         if ( pathBounds != null && pathBounds.contains( x, y ) ) {
+            return closestTimeSpan;
+         }
+      }
+      return null;
+   }
+
+   /**
+    * Returns the path to the node that is closest to x,y.  If
+    * no nodes are currently viewable, or there is no model, returns
+    * <code>null</code>, otherwise it always returns a valid path.  To test if
+    * the node is exactly at x, y, get the node's bounds and
+    * test x, y against that.
+    *
+    * @param x an integer giving the number of pixels horizontally from
+    *          the left edge of the display area, minus any left margin
+    * @param y an integer giving the number of pixels vertically from
+    *          the top of the display area, minus any top margin
+    * @return the <code>Span</code> for the node closest to that location,
+    * <code>null</code> if nothing is viewable or there is no model
+    * @see #getTimeSpanForLocation
+    * @see #getTimeSpanBounds
+    */
+   public PointedTimeSpan getClosestTimeSpanForLocation( final int x, final int y ) {
+      final TimelineUI ui = getUI();
+      if ( ui != null ) {
+         return (PointedTimeSpan)ui.getClosestTimeSpanForLocation( this, x, y );
+      }
+      return null;
+   }
+
+
+   private class TimelineMouseHandler extends MouseAdapter {
+      @Override
+      public void mouseClicked( final MouseEvent event ) {
+         if ( event == null ) {
+            return;
+         }
+         final Point p = event.getPoint();
+         final PointedTimeSpan timeSpan = getTimeSpanForLocation( p.x, p.y );
+         if ( timeSpan == null ) {
+            return;
+         }
+         final TimeSpanSelectionModel selectionModel = getSelectionModel();
+         selectionModel.setValueIsAdjusting( selectionModel, true );
+         selectionModel.setSelectedTerms( selectionModel, Collections.singletonList( "*" ) );
+         final Map<PointedTimeSpan, Collection<Entity>> timeSpanEntities = new HashMap<>( 1 );
+         timeSpanEntities.put( timeSpan, getModel().getEntities( timeSpan ) );
+         selectionModel.setSelectedEntities( selectionModel, timeSpanEntities );
+         selectionModel.setValueIsAdjusting( selectionModel, false );
+      }
+   }
+
+   private class PrivateSelectionListener implements TimeSpanSelectionListener {
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public void valueChanged( final TimeSpanSelectionEvent event ) {
+         if ( event == null || event.getValueIsAdjusting() ) {
+            return;
+         }
+         paintLater();
+      }
+   }
+
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/TimelineComponent.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/TimelineComponent.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/TimelineComponent.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/TimelineComponent.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,48 @@
+package org.chboston.cnlp.timeline.gui.timeline;
+
+import org.chboston.cnlp.timeline.timeline.Timeline;
+import org.chboston.cnlp.timeline.timespan.TimeSpan;
+
+import java.awt.*;
+import java.util.Calendar;
+
+/**
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 8/2/13
+ */
+public interface TimelineComponent {
+
+   /**
+    * @return the <code>DefaultTimeline</code> that is providing the data
+    */
+   public Timeline getModel();
+
+   public Calendar getBeginDrawTime();
+
+   public Calendar getEndDrawTime();
+
+   public long getBeginDrawMillis();
+
+   public long getEndDrawMillis();
+
+   public long getDrawTimeDelta();
+
+   public double getWidthDrawXform();
+
+   /**
+    * Returns the <code>Rectangle</code> that the specified node will be drawn
+    * into. Returns <code>null</code> if any component in the path is hidden
+    * (under a collapsed parent).
+    * <p/>
+    * Note:<br>
+    * This method returns a valid rectangle, even if the specified
+    * node is not currently displayed.
+    *
+    * @param timeSpan the <code>TimeSpan</code> identifying the node
+    * @return the <code>Rectangle</code> the node is drawn in,
+    * or <code>null</code>
+    */
+   public Rectangle getTimeSpanBounds( final TimeSpan timeSpan );
+
+}

Added: ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/stack/SemanticTimelinesStack.java
URL: http://svn.apache.org/viewvc/ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/stack/SemanticTimelinesStack.java?rev=1660963&view=auto
==============================================================================
--- ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/stack/SemanticTimelinesStack.java (added)
+++ ctakes/sandbox/timelanes/org/chboston/cnlp/timeline/gui/timeline/stack/SemanticTimelinesStack.java Thu Feb 19 18:06:13 2015
@@ -0,0 +1,44 @@
+package org.chboston.cnlp.timeline.gui.timeline.stack;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * Uses a single panel to wrap the Box containing stacks of components so that a single resize will propagate
+ * Author: SPF
+ * Affiliation: CHIP-NLP
+ * Date: 11/12/12
+ */
+public class SemanticTimelinesStack extends JPanel {
+
+   final Box _typeStack = Box.createVerticalBox();
+
+   public SemanticTimelinesStack() {
+      super( new BorderLayout() );
+      // make the background a little brighter than Color.LIGHT_GRAY, but darker than the standard JPanel background
+      setBackground( new Color( 224, 224, 224 ) );
+      // Use color of white to match background of timelines
+//      setBackground( Color.WHITE );
+      add( _typeStack, BorderLayout.CENTER );
+   }
+
+   public Container getStack() {
+      return _typeStack;
+   }
+
+   public void addTimelineComponent( final JComponent component ) {
+      _typeStack.add( component );
+   }
+
+   public void addSpacer() {
+      _typeStack.add( Box.createVerticalStrut( 10 ) );
+   }
+
+   public Dimension getPreferredSize() {
+      // The box will absorb the vertical changes and won't pass them up the component tree,
+      // So we have to "pass" the height manually
+      return new Dimension( super.getPreferredSize().width, _typeStack.getPreferredSize().height );
+   }
+
+
+}



Mime
View raw message