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}