001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030/** 031 * 032 */ 033package org.openimaj.vis.timeline; 034 035import java.awt.Dimension; 036import java.util.ArrayList; 037import java.util.HashMap; 038import java.util.List; 039 040import org.openimaj.image.MBFImage; 041import org.openimaj.image.colour.RGBColour; 042import org.openimaj.math.geometry.point.Point2d; 043import org.openimaj.math.geometry.point.Point2dImpl; 044import org.openimaj.math.geometry.shape.Rectangle; 045import org.openimaj.math.geometry.shape.Triangle; 046import org.openimaj.vis.AnimatedVisualisationProvider; 047import org.openimaj.vis.general.AxesRenderer2D; 048import org.openimaj.vis.general.DiversityAxis; 049import org.openimaj.vis.general.ItemPlotter; 050import org.openimaj.vis.general.LabelTransformer; 051 052/** 053 * A timeline visualisation. The timeline consists of a set of {@link TimelineTrack}s and you 054 * can add these with {@link #addTrack(TimelineTrack)} and add objects to those tracks using 055 * {@link #addTimelineObject(TimelineTrack, TimelineObject)}. 056 * <p> 057 * This class is an implementation of the {@link DiversityAxis} visualisation. 058 * 059 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 060 * @created 3 Jul 2012 / 25 Jun 2013 061 */ 062public class Timeline extends DiversityAxis<TimelineObject> 063{ 064 /** */ 065 private static final long serialVersionUID = 1L; 066 067 /** 068 * This is an item plotter for {@link TimelineObject}s. The size of each band needs 069 * to be updated here when the diversity axis band size changes. 070 * 071 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 072 * @created 25 Jun 2013 073 * @version $Author$, $Revision$, $Date$ 074 */ 075 protected static class TimelineObjectPlotter implements ItemPlotter<TimelineObject, Float[], MBFImage> 076 { 077 /** The size of the bands in the diversity axis */ 078 private int bandSize = 100; 079 080 /** 081 * {@inheritDoc} 082 * @see org.openimaj.vis.general.ItemPlotter#renderRestarting() 083 */ 084 @Override 085 public void renderRestarting() 086 { 087 } 088 089 /** 090 * {@inheritDoc} 091 * @see org.openimaj.vis.general.ItemPlotter#plotObject(org.openimaj.image.Image, org.openimaj.vis.general.XYPlotVisualisation.LocatedObject, org.openimaj.vis.general.AxesRenderer2D) 092 */ 093 @Override 094 public void plotObject( final MBFImage visImage, 095 final org.openimaj.vis.general.XYPlotVisualisation.LocatedObject<TimelineObject> object, 096 final AxesRenderer2D<Float[], MBFImage> renderer ) 097 { 098 // Work out where we're going to plot this timeline object. 099 final Point2d p = renderer.calculatePosition( object.x, object.y ); 100 101 // Reset its size, if we need to then update the visualisation 102 object.object.setRequiredSize( new Dimension( 103 object.object.getRequiredSize().width, this.bandSize ) ); 104 object.object.updateVis(); 105 106 // Now get the image and draw it in the correct place. 107 final MBFImage i = object.object.getVisualisationImage(); 108 if( i != null ) 109 visImage.drawImage( i, (int)p.getX(), (int)p.getY() ); 110 } 111 112 /** 113 * Set the size of a band. 114 * @param bandSize The size of a band. 115 */ 116 public void setBandSize( final int bandSize ) 117 { 118 this.bandSize = bandSize; 119 } 120 } 121 122 /** 123 * Represents a track in the timeline. A track has a name and a number. 124 * 125 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 126 * @created 3 Jul 2012 127 * @version $Author$, $Revision$, $Date$ 128 */ 129 public static class TimelineTrack 130 { 131 /** The number of this track */ 132 private int number = 0; 133 134 /** The label */ 135 private final String label; 136 137 /** Markers for the track */ 138 private final List<TimelineMarker> markers = new ArrayList<TimelineMarker>(); 139 140 /** 141 * Instantiate a new track with the given label. 142 * 143 * @param label The label of the track 144 * @param number The track number 145 */ 146 public TimelineTrack( final String label, final int number ) 147 { 148 this.number = number; 149 this.label = label; 150 } 151 152 /** 153 * Return the number of this track. 154 * @return The track number 155 */ 156 public int getTrackNumber() 157 { 158 return this.number; 159 } 160 161 /** 162 * Add a track marker to this time. 163 * 164 * @param time The time to add the track marker 165 */ 166 public void addTrackMarker( final long time ) 167 { 168 final TimelineMarker tm = new TimelineMarker(); 169 tm.time = time; 170 tm.colour = RGBColour.YELLOW; 171 this.markers.add( tm ); 172 } 173 174 /** 175 * Get the name of the track 176 * @return the label 177 */ 178 public String getLabel() 179 { 180 return this.label; 181 } 182 } 183 184 /** 185 * Timeline markers that are drawn onto a timeline 186 * 187 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 188 * @created 3 Jul 2012 189 * @version $Author$, $Revision$, $Date$ 190 */ 191 public static class TimelineMarker 192 { 193 /** The marker type */ 194 public TimelineMarkerType type = TimelineMarkerType.FLAG; 195 196 /** The time of the marker */ 197 public long time = 0; 198 199 /** The label of the marker, if it has one */ 200 public String label; 201 202 /** The colour of the marker */ 203 public Float[] colour = RGBColour.BLACK; 204 } 205 206 /** 207 * Different type of markers. 208 * 209 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 210 * @created 3 Jul 2012 211 * @version $Author$, $Revision$, $Date$ 212 */ 213 public static enum TimelineMarkerType 214 { 215 /** 216 * Draws a marker with a little flag showing some text 217 */ 218 LABEL 219 { 220 @Override 221 public void drawMarker( final TimelineMarker m, final MBFImage image, final int x, final int h ) 222 { 223 image.drawLine( x, 0, x, h, m.colour ); 224 225 final int fw = 20; 226 final int fh = 10; 227 image.drawShapeFilled( new Rectangle( x, 4, fw+4, fh + 4 ), m.colour ); 228 229 // TODO: Need to draw the label 230 } 231 }, 232 /** 233 * Draws a marker with a little flag at the top 234 */ 235 FLAG 236 { 237 private final int fs = 10; 238 239 @Override 240 public void drawMarker( final TimelineMarker m, final MBFImage image, final int x, final int h ) 241 { 242 image.drawLine( x, 0, x, h, m.colour ); 243 image.drawShapeFilled( 244 new Triangle( 245 new Point2dImpl( x, 0 ), 246 new Point2dImpl( x+this.fs*2, this.fs/2 ), 247 new Point2dImpl( x, this.fs ) ), 248 m.colour ); 249 } 250 }; 251 252 /** 253 * Draws this marker into the given graphics context at the given 254 * position and with the given length. 255 * 256 * @param m The marker 257 * @param image The image to draw to 258 * @param xPos The position 259 * @param height The length to draw the marker 260 */ 261 public abstract void drawMarker( TimelineMarker m, MBFImage image, 262 int xPos, int height ); 263 264 /** 265 * Returns an opposing colour 266 * 267 * @param c The colour to oppose 268 * @return An opposing colour 269 */ 270 static protected Float[] getOpposingColour( final Float[] c ) 271 { 272 // TODO: This isn't really getting a good colour 273 return new Float[]{ 1-c[0], 1-c[1], 1-c[2], 1f }; 274 } 275 } 276 277 /** Width of the sidebar in pixels */ 278 private final int sidebarWidth = 150; 279 280 /** List of markers on this timeline */ 281 private final List<TimelineMarker> markers = new ArrayList<Timeline.TimelineMarker>(); 282 283 /** The sidebar panel */ 284 private MBFImage sidebarPanel = null; 285 286 /** The default time scalar is 100 milliseconds per pixel */ 287 private double timeScalar = 100; 288 289 /** The number of tracks */ 290 private int nTracks = 1; 291 292 /** The tracks */ 293 private final HashMap<Integer,TimelineTrack> tracks = new HashMap<Integer, Timeline.TimelineTrack>(); 294 295 /** The plotter used to plot timeline objects */ 296 private final TimelineObjectPlotter plotter = new TimelineObjectPlotter(); 297 298 /** 299 * Default constructor 300 * @param w Width of the visualisation 301 * @param h Height of the visualisation 302 */ 303 public Timeline( final int w, final int h ) 304 { 305 super( w, h, null ); 306 super.setItemPlotter( this.plotter ); 307 this.init(); 308 } 309 310 /** 311 * 312 */ 313 private void init() 314 { 315 this.sidebarPanel = new MBFImage( this.sidebarWidth, 316 this.visImage.getHeight(), 4 ); 317 318 // Setup the axis renderer (the timeline's ruler); 319 this.axesRenderer2D.setAxisPaddingLeft( this.sidebarWidth ); 320 this.axesRenderer2D.setDrawYAxis( false ); 321 this.axesRenderer2D.setDrawXAxisName( false ); 322 this.axesRenderer2D.setMajorTickColour( RGBColour.WHITE ); 323 this.axesRenderer2D.setMinorTickColour( RGBColour.WHITE ); 324 this.axesRenderer2D.setMajorTickColour( RGBColour.WHITE ); 325 this.axesRenderer2D.setMinorTickColour( RGBColour.WHITE ); 326 this.axesRenderer2D.setxAxisNameColour( RGBColour.WHITE ); 327 this.axesRenderer2D.setxTickLabelColour( RGBColour.WHITE ); 328 this.axesRenderer2D.setxAxisColour( RGBColour.WHITE ); 329 this.axesRenderer2D.setxMajorTickSpacing( 10000 ); 330 this.axesRenderer2D.setxMinorTickSpacing( 1000 ); 331 this.axesRenderer2D.setxLabelSpacing( 10000 ); 332 this.axesRenderer2D.setxAxisLabelTransformer( new LabelTransformer() 333 { 334 @Override 335 public String transform( final double value ) 336 { 337 // Convert milliseconds to seconds 338 return ""+Math.round(value/1000d); 339 } 340 } ); 341 342 // Set the default time scalar 343 this.setTimeScalar( 50 ); 344 345 // Do the precalcs for the axes renderer 346 this.axesRenderer2D.precalc( ); 347 } 348 349 /** 350 * Add a new track to the timeline. Will be given the label "Track n" where 351 * n is the number of the track. 352 * 353 * @return The timeline track that was added. 354 */ 355 public TimelineTrack addTrack() 356 { 357 return this.addTrack( "Track " + this.data.size() + 1 ); 358 } 359 360 /** 361 * Add a track with the given label. 362 * 363 * @param label The label 364 * @return The timeline track that was added 365 */ 366 public TimelineTrack addTrack( final String label ) 367 { 368 return this.addTrack( new TimelineTrack( label, this.nTracks++ ) ); 369 } 370 371 /** 372 * Add a new track to the timeline. 373 * 374 * @param tt The track to add. 375 * @return The timeline track that was added 376 */ 377 public TimelineTrack addTrack( final TimelineTrack tt ) 378 { 379 this.tracks.put( tt.getTrackNumber(), tt ); 380 return tt; 381 } 382 383 /** 384 * Add an object to a track. 385 * 386 * @param tt The track to add the object to 387 * @param obj The timeline objec to add 388 * @return The timeline track 389 */ 390 public TimelineTrack addTimelineObject( final TimelineTrack tt, final TimelineObject obj ) 391 { 392 super.addObject( tt.getTrackNumber(), obj.getStartTimeMilliseconds(), obj ); 393 394 if( obj instanceof AnimatedVisualisationProvider ) 395 ((AnimatedVisualisationProvider)obj).addAnimatedVisualisationListener( this ); 396 397 obj.setDataPixelTransformer( 398 this.axesRenderer2D.getRelativePixelTransformer( 399 (int)this.axesRenderer2D.calculatePosition( 400 obj.getStartTimeMilliseconds(), 0 ).getX(), 401 tt.getTrackNumber() ) ); 402 403 return tt; 404 } 405 406 /** 407 * Add a new marker with a label. 408 * 409 * @param timeMilliseconds The time at which the marker should be added. 410 * @param label The label to put on the marker. 411 * @return The created timeline marker object. 412 */ 413 public TimelineMarker addMarker( final long timeMilliseconds, final String label ) 414 { 415 final TimelineMarker m = new TimelineMarker(); 416 m.type = TimelineMarkerType.LABEL; 417 m.time = timeMilliseconds; 418 m.label = label; 419 this.markers.add( m ); 420 this.repaint(); 421 return m; 422 } 423 424 /** 425 * Add a new marker 426 * 427 * @param timeMilliseconds The time at which to add the marker 428 * @return The created timeline marker object 429 */ 430 public TimelineMarker addMarker( final long timeMilliseconds ) 431 { 432 final TimelineMarker m = new TimelineMarker(); 433 m.type = TimelineMarkerType.FLAG; 434 m.time = timeMilliseconds; 435 this.markers.add( m ); 436 this.repaint(); 437 return m; 438 } 439 440 /** 441 * Returns the scalar between milliseconds and pixels. If this was to return 442 * 1 then it would mean one pixel represented 1 millisecond; if this was to 443 * return 1000 then it would mean one pixel represented one second. 444 * 445 * @return The time scalar (milliseconds per pixel) 446 */ 447 public double getTimeScalar() 448 { 449 return this.timeScalar; 450 } 451 452 /** 453 * Set the time scalar of this timeline. The units are milliseconds per 454 * pixel. 455 * 456 * @param ts The new time scalar 457 */ 458 public void setTimeScalar( final double ts ) 459 { 460 this.timeScalar = ts; 461 this.axesRenderer2D.setMaxXValue( this.timeScalar * (this.visImage.getWidth()-this.sidebarWidth) ); 462 463 this.updateVis(); 464 } 465 466 /** 467 * Find the position of the given time on the panel. 468 * 469 * @param milliseconds the number of milliseconds 470 * @return The pixel position 471 */ 472 public int getTimePosition( final long milliseconds ) 473 { 474 return (int) (milliseconds / this.getTimeScalar()); 475 } 476 477 /** 478 * {@inheritDoc} 479 * @see org.openimaj.vis.VisualisationImpl#update() 480 */ 481 @Override 482 public void update() 483 { 484 // Draw the sidebar 485 this.visImage.drawImage( this.sidebarPanel, 0, 0 ); 486 487 super.update(); 488 489 // Draw all the timeline markers 490 for( final TimelineMarker m : this.markers ) 491 { 492 final int x = this.getTimePosition( m.time ); 493 m.type.drawMarker( m, this.visImage, x, this.getHeight() ); 494 } 495 } 496 497 /** 498 * {@inheritDoc} 499 * @see org.openimaj.vis.general.DiversityAxis#bandSizeKnown(int) 500 */ 501 @Override 502 public void bandSizeKnown( final int bandSize ) 503 { 504 this.plotter.setBandSize( bandSize ); 505 super.bandSizeKnown( bandSize ); 506 } 507 508 /** 509 * Main method test for the timeline 510 * @param args The command-line args (unused) 511 */ 512 public static void main( final String[] args ) 513 { 514 final Timeline t = new Timeline( 1200, 400 ); 515 t.showWindow( "Timeline" ); 516 } 517}