1<!DOCTYPE html>
2<!--
3Copyright (c) 2012 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<link rel="import" href="/tracing/base/event.html">
9<link rel="import" href="/tracing/base/settings.html">
10<link rel="import" href="/tracing/base/task.html">
11<link rel="import" href="/tracing/core/filter.html">
12<link rel="import" href="/tracing/model/event.html">
13<link rel="import" href="/tracing/model/event_set.html">
14<link rel="import" href="/tracing/model/x_marker_annotation.html">
15<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
16<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
17<link rel="import" href="/tracing/ui/base/timing_tool.html">
18<link rel="import" href="/tracing/ui/base/ui.html">
19<link rel="import" href="/tracing/ui/timeline_display_transform_animations.html">
20<link rel="import" href="/tracing/ui/timeline_viewport.html">
21<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
22<link rel="import" href="/tracing/ui/tracks/model_track.html">
23<link rel="import" href="/tracing/ui/tracks/ruler_track.html">
24<link rel="import" href="/tracing/value/unit.html">
25
26<!--
27  Interactive visualizaiton of Model objects based loosely on gantt charts.
28  Each thread in the Model is given a set of Tracks, one per subrow in the
29  thread. The TimelineTrackView class acts as a controller, creating the
30  individual tracks, while Tracks do actual drawing.
31
32  Visually, the TimelineTrackView produces (prettier) visualizations like the
33  following:
34    Thread1:  AAAAAAAAAA         AAAAA
35                  BBBB              BB
36    Thread2:     CCCCCC                 CCCCC
37-->
38<polymer-element name='tr-ui-timeline-track-view'>
39  <template>
40    <style>
41    :host {
42      -webkit-box-orient: vertical;
43      display: -webkit-box;
44      position: relative;
45    }
46
47    :host ::content * {
48      -webkit-user-select: none;
49      cursor: default;
50    }
51
52    #drag_box {
53      background-color: rgba(0, 0, 255, 0.25);
54      border: 1px solid rgb(0, 0, 96);
55      font-size: 75%;
56      position: fixed;
57    }
58
59    #hint_text {
60      position: absolute;
61      bottom: 6px;
62      right: 6px;
63      font-size: 8pt;
64    }
65    </style>
66    <content></content>
67
68    <div id='drag_box'></div>
69    <div id='hint_text'></div>
70
71    <tv-ui-b-hotkey-controller id='hotkey_controller'>
72    </tv-ui-b-hotkey-controller>
73  </template>
74
75  <script>
76  'use strict';
77
78  Polymer({
79    ready: function() {
80      this.displayTransform_ = new tr.ui.TimelineDisplayTransform();
81      this.model_ = undefined;
82
83      this.timelineView_ = undefined;
84
85      this.viewport_ = new tr.ui.TimelineViewport(this);
86      this.viewportDisplayTransformAtMouseDown_ = undefined;
87      this.brushingStateController_ = undefined;
88
89      this.rulerTrackContainer_ =
90          new tr.ui.tracks.DrawingContainer(this.viewport_);
91      this.appendChild(this.rulerTrackContainer_);
92      this.rulerTrackContainer_.invalidate();
93
94      this.rulerTrack_ = new tr.ui.tracks.RulerTrack(this.viewport_);
95      this.rulerTrackContainer_.appendChild(this.rulerTrack_);
96
97      this.upperModelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_);
98      this.upperModelTrack_.upperMode = true;
99      this.rulerTrackContainer_.appendChild(this.upperModelTrack_);
100
101      this.modelTrackContainer_ =
102          new tr.ui.tracks.DrawingContainer(this.viewport_);
103      this.appendChild(this.modelTrackContainer_);
104      this.modelTrackContainer_.style.display = 'block';
105      this.modelTrackContainer_.invalidate();
106
107      this.viewport_.modelTrackContainer = this.modelTrackContainer_;
108
109      this.modelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_);
110      this.modelTrackContainer_.appendChild(this.modelTrack_);
111
112      this.timingTool_ = new tr.ui.b.TimingTool(this.viewport_, this);
113
114      this.initMouseModeSelector();
115
116      this.hideDragBox_();
117
118      this.initHintText_();
119
120      this.onSelectionChanged_ = this.onSelectionChanged_.bind(this);
121
122      this.onDblClick_ = this.onDblClick_.bind(this);
123      this.addEventListener('dblclick', this.onDblClick_);
124
125      this.onMouseWheel_ = this.onMouseWheel_.bind(this);
126      this.addEventListener('mousewheel', this.onMouseWheel_);
127
128      this.onMouseDown_ = this.onMouseDown_.bind(this);
129      this.addEventListener('mousedown', this.onMouseDown_);
130
131      this.onMouseMove_ = this.onMouseMove_.bind(this);
132      this.addEventListener('mousemove', this.onMouseMove_);
133
134      this.onTouchStart_ = this.onTouchStart_.bind(this);
135      this.addEventListener('touchstart', this.onTouchStart_);
136
137      this.onTouchMove_ = this.onTouchMove_.bind(this);
138      this.addEventListener('touchmove', this.onTouchMove_);
139
140      this.onTouchEnd_ = this.onTouchEnd_.bind(this);
141      this.addEventListener('touchend', this.onTouchEnd_);
142
143
144      this.addHotKeys_();
145
146      this.mouseViewPosAtMouseDown_ = {x: 0, y: 0};
147      this.lastMouseViewPos_ = {x: 0, y: 0};
148
149      this.lastTouchViewPositions_ = [];
150
151      this.alert_ = undefined;
152
153      this.isPanningAndScanning_ = false;
154      this.isZooming_ = false;
155    },
156
157    initMouseModeSelector: function() {
158      this.mouseModeSelector_ = document.createElement(
159          'tr-ui-b-mouse-mode-selector');
160      this.mouseModeSelector_.targetElement = this;
161      this.appendChild(this.mouseModeSelector_);
162
163      this.mouseModeSelector_.addEventListener('beginpan',
164          this.onBeginPanScan_.bind(this));
165      this.mouseModeSelector_.addEventListener('updatepan',
166          this.onUpdatePanScan_.bind(this));
167      this.mouseModeSelector_.addEventListener('endpan',
168          this.onEndPanScan_.bind(this));
169
170      this.mouseModeSelector_.addEventListener('beginselection',
171          this.onBeginSelection_.bind(this));
172      this.mouseModeSelector_.addEventListener('updateselection',
173          this.onUpdateSelection_.bind(this));
174      this.mouseModeSelector_.addEventListener('endselection',
175          this.onEndSelection_.bind(this));
176
177      this.mouseModeSelector_.addEventListener('beginzoom',
178          this.onBeginZoom_.bind(this));
179      this.mouseModeSelector_.addEventListener('updatezoom',
180          this.onUpdateZoom_.bind(this));
181      this.mouseModeSelector_.addEventListener('endzoom',
182          this.onEndZoom_.bind(this));
183
184      this.mouseModeSelector_.addEventListener('entertiming',
185          this.timingTool_.onEnterTiming.bind(this.timingTool_));
186      this.mouseModeSelector_.addEventListener('begintiming',
187          this.timingTool_.onBeginTiming.bind(this.timingTool_));
188      this.mouseModeSelector_.addEventListener('updatetiming',
189          this.timingTool_.onUpdateTiming.bind(this.timingTool_));
190      this.mouseModeSelector_.addEventListener('endtiming',
191          this.timingTool_.onEndTiming.bind(this.timingTool_));
192      this.mouseModeSelector_.addEventListener('exittiming',
193          this.timingTool_.onExitTiming.bind(this.timingTool_));
194
195      var m = tr.ui.b.MOUSE_SELECTOR_MODE;
196      this.mouseModeSelector_.supportedModeMask =
197          m.SELECTION | m.PANSCAN | m.ZOOM | m.TIMING;
198      this.mouseModeSelector_.settingsKey =
199          'timelineTrackView.mouseModeSelector';
200      this.mouseModeSelector_.setKeyCodeForMode(m.PANSCAN, '2'.charCodeAt(0));
201      this.mouseModeSelector_.setKeyCodeForMode(m.SELECTION, '1'.charCodeAt(0));
202      this.mouseModeSelector_.setKeyCodeForMode(m.ZOOM, '3'.charCodeAt(0));
203      this.mouseModeSelector_.setKeyCodeForMode(m.TIMING, '4'.charCodeAt(0));
204
205      this.mouseModeSelector_.setModifierForAlternateMode(
206          m.SELECTION, tr.ui.b.MODIFIER.SHIFT);
207      this.mouseModeSelector_.setModifierForAlternateMode(
208          m.PANSCAN, tr.ui.b.MODIFIER.SPACE);
209    },
210
211    get brushingStateController() {
212      return this.brushingStateController_;
213    },
214
215    set brushingStateController(brushingStateController) {
216      if (this.brushingStateController_) {
217        this.brushingStateController_.removeEventListener('change',
218                                                      this.onSelectionChanged_);
219      }
220      this.brushingStateController_ = brushingStateController;
221      if (this.brushingStateController_) {
222        this.brushingStateController_.addEventListener('change',
223                                                   this.onSelectionChanged_);
224      }
225    },
226
227    set timelineView(view) {
228      this.timelineView_ = view;
229    },
230
231    onSelectionChanged_: function() {
232      this.showHintText_('Press \'m\' to mark current selection');
233      this.viewport_.dispatchChangeEvent();
234    },
235
236    set selection(selection) {
237      throw new Error('DO NOT CALL THIS');
238    },
239
240    set highlight(highlight) {
241      throw new Error('DO NOT CALL THIS');
242    },
243
244    detach: function() {
245      this.modelTrack_.detach();
246      this.upperModelTrack_.detach();
247
248      this.viewport_.detach();
249    },
250
251    get viewport() {
252      return this.viewport_;
253    },
254
255    get model() {
256      return this.model_;
257    },
258
259    set model(model) {
260      if (!model)
261        throw new Error('Model cannot be undefined');
262
263      var modelInstanceChanged = this.model_ !== model;
264      this.model_ = model;
265      this.modelTrack_.model = model;
266      this.upperModelTrack_.model = model;
267
268      // Set up a reasonable viewport.
269      if (modelInstanceChanged)
270        this.viewport_.setWhenPossible(this.setInitialViewport_.bind(this));
271    },
272
273    get hasVisibleContent() {
274      return this.modelTrack_.hasVisibleContent ||
275          this.upperModelTrack_.hasVisibleContent;
276    },
277
278    setInitialViewport_: function() {
279      // We need the canvas size to be up-to-date at this point. We maybe in
280      // here before the raf fires, so the size may have not been updated since
281      // the canvas was resized.
282      this.modelTrackContainer_.updateCanvasSizeIfNeeded_();
283      var w = this.modelTrackContainer_.canvas.width;
284
285      var min;
286      var range;
287
288      if (this.model_.bounds.isEmpty) {
289        min = 0;
290        range = 1000;
291      } else if (this.model_.bounds.range === 0) {
292        min = this.model_.bounds.min;
293        range = 1000;
294      } else {
295        min = this.model_.bounds.min;
296        range = this.model_.bounds.range;
297      }
298
299      var boost = range * 0.15;
300      this.displayTransform_.set(this.viewport_.currentDisplayTransform);
301      this.displayTransform_.xSetWorldBounds(
302          min - boost, min + range + boost, w);
303      this.viewport_.setDisplayTransformImmediately(this.displayTransform_);
304    },
305
306    /**
307     * @param {Filter} filter The filter to use for finding matches.
308     * @param {Selection} selection The selection to add matches to.
309     * @return {Task} which performs the filtering.
310     */
311    addAllEventsMatchingFilterToSelectionAsTask: function(filter, selection) {
312      var modelTrack = this.modelTrack_;
313      var firstT = modelTrack.addAllEventsMatchingFilterToSelectionAsTask(
314          filter, selection);
315      var lastT = firstT.after(function() {
316        this.upperModelTrack_.addAllEventsMatchingFilterToSelection(
317          filter, selection);
318
319      }, this);
320      return firstT;
321    },
322
323    onMouseMove_: function(e) {
324      // Zooming requires the delta since the last mousemove so we need to avoid
325      // tracking it when the zoom interaction is active.
326      if (this.isZooming_)
327        return;
328
329      this.storeLastMousePos_(e);
330    },
331
332    onTouchStart_: function(e) {
333      this.storeLastTouchPositions_(e);
334      this.focusElements_();
335    },
336
337    onTouchMove_: function(e) {
338      e.preventDefault();
339      this.onUpdateTransformForTouch_(e);
340    },
341
342    onTouchEnd_: function(e) {
343      this.storeLastTouchPositions_(e);
344      this.focusElements_();
345    },
346
347    addHotKeys_: function() {
348      this.addKeyDownHotKeys_();
349      this.addKeyPressHotKeys_();
350    },
351
352    addKeyPressHotKeys_: function() {
353      var addBinding = function(dict) {
354        dict.eventType = 'keypress';
355        dict.useCapture = false;
356        dict.thisArg = this;
357        var binding = new tr.ui.b.HotKey(dict);
358        this.$.hotkey_controller.addHotKey(binding);
359      }.bind(this);
360
361      addBinding({
362        keyCodes: ['w'.charCodeAt(0), ','.charCodeAt(0)],
363        callback: function(e) {
364          this.zoomBy_(1.5, true);
365          e.stopPropagation();
366        }
367      });
368
369      addBinding({
370        keyCodes: ['s'.charCodeAt(0), 'o'.charCodeAt(0)],
371        callback: function(e) {
372          this.zoomBy_(1 / 1.5, true);
373          e.stopPropagation();
374        }
375      });
376
377      addBinding({
378        keyCode: 'g'.charCodeAt(0),
379        callback: function(e) {
380          this.onGridToggle_(true);
381          e.stopPropagation();
382        }
383      });
384
385      addBinding({
386        keyCode: 'G'.charCodeAt(0),
387        callback: function(e) {
388          this.onGridToggle_(false);
389          e.stopPropagation();
390        }
391      });
392
393      addBinding({
394        keyCodes: ['W'.charCodeAt(0), '<'.charCodeAt(0)],
395        callback: function(e) {
396          this.zoomBy_(10, true);
397          e.stopPropagation();
398        }
399      });
400
401      addBinding({
402        keyCodes: ['S'.charCodeAt(0), 'O'.charCodeAt(0)],
403        callback: function(e) {
404          this.zoomBy_(1 / 10, true);
405          e.stopPropagation();
406        }
407      });
408
409      addBinding({
410        keyCode: 'a'.charCodeAt(0),
411        callback: function(e) {
412          this.queueSmoothPan_(this.viewWidth_ * 0.3, 0);
413          e.stopPropagation();
414        }
415      });
416
417      addBinding({
418        keyCodes: ['d'.charCodeAt(0), 'e'.charCodeAt(0)],
419        callback: function(e) {
420          this.queueSmoothPan_(this.viewWidth_ * -0.3, 0);
421          e.stopPropagation();
422        }
423      });
424
425      addBinding({
426        keyCode: 'A'.charCodeAt(0),
427        callback: function(e) {
428          this.queueSmoothPan_(viewWidth * 0.5, 0);
429          e.stopPropagation();
430        }
431      });
432
433      addBinding({
434        keyCode: 'D'.charCodeAt(0),
435        callback: function(e) {
436          this.queueSmoothPan_(viewWidth * -0.5, 0);
437          e.stopPropagation();
438        }
439      });
440
441      addBinding({
442        keyCode: '0'.charCodeAt(0),
443        callback: function(e) {
444          this.setInitialViewport_();
445          e.stopPropagation();
446        }
447      });
448
449      addBinding({
450        keyCode: 'f'.charCodeAt(0),
451        callback: function(e) {
452          this.zoomToSelection();
453          e.stopPropagation();
454        }
455      });
456
457      addBinding({
458        keyCode: 'm'.charCodeAt(0),
459        callback: function(e) {
460          this.setCurrentSelectionAsInterestRange_();
461          e.stopPropagation();
462        }
463      });
464
465      addBinding({
466        keyCode: 'h'.charCodeAt(0),
467        callback: function(e) {
468          this.toggleHighDetails_();
469          e.stopPropagation();
470        }
471      });
472    },
473
474    get viewWidth_() {
475      return this.modelTrackContainer_.canvas.clientWidth;
476    },
477
478    addKeyDownHotKeys_: function() {
479      var addBinding = function(dict) {
480        dict.eventType = 'keydown';
481        dict.useCapture = false;
482        dict.thisArg = this;
483        var binding = new tr.ui.b.HotKey(dict);
484        this.$.hotkey_controller.addHotKey(binding);
485      }.bind(this);
486
487      addBinding({
488        keyCode: 37, // Left arrow.
489        callback: function(e) {
490          var curSel = this.brushingStateController_.selection;
491          var sel = this.viewport.getShiftedSelection(curSel, -1);
492
493          if (sel) {
494            this.brushingStateController.changeSelectionFromTimeline(sel);
495            this.panToSelection();
496          } else {
497            this.queueSmoothPan_(this.viewWidth_ * 0.3, 0);
498          }
499          e.preventDefault();
500          e.stopPropagation();
501        }
502      });
503
504      addBinding({
505        keyCode: 39, // Right arrow.
506        callback: function(e) {
507          var curSel = this.brushingStateController_.selection;
508          var sel = this.viewport.getShiftedSelection(curSel, 1);
509          if (sel) {
510            this.brushingStateController.changeSelectionFromTimeline(sel);
511            this.panToSelection();
512          } else {
513            this.queueSmoothPan_(-this.viewWidth_ * 0.3, 0);
514          }
515          e.preventDefault();
516          e.stopPropagation();
517        }
518      });
519    },
520
521    onDblClick_: function(e) {
522      if (this.mouseModeSelector_.mode !==
523          tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION)
524        return;
525
526      var curSelection = this.brushingStateController_.selection;
527      if (!curSelection.length || !curSelection[0].title)
528        return;
529
530      var selection = new tr.model.EventSet();
531      var filter = new tr.c.ExactTitleFilter(curSelection[0].title);
532      this.modelTrack_.addAllEventsMatchingFilterToSelection(filter,
533                                                              selection);
534
535      this.brushingStateController.changeSelectionFromTimeline(selection);
536    },
537
538    onMouseWheel_: function(e) {
539      if (!e.altKey)
540        return;
541
542      var delta = e.wheelDelta / 120;
543      var zoomScale = Math.pow(1.5, delta);
544      this.zoomBy_(zoomScale);
545      e.preventDefault();
546    },
547
548    onMouseDown_: function(e) {
549      if (this.mouseModeSelector_.mode !==
550          tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION)
551        return;
552
553      // Mouse down must start on ruler track for crosshair guide lines to draw.
554      if (e.target !== this.rulerTrack_)
555        return;
556
557      // Make sure we don't start a selection drag event here.
558      this.dragBeginEvent_ = undefined;
559
560      // Remove nav string marker if it exists, since we're clearing the
561      // find control box.
562      if (this.xNavStringMarker_) {
563        this.model.removeAnnotation(this.xNavStringMarker_);
564        this.xNavStringMarker_ = undefined;
565      }
566
567      var dt = this.viewport_.currentDisplayTransform;
568      tr.ui.b.trackMouseMovesUntilMouseUp(function(e) { // Mouse move handler.
569        // If mouse event is on ruler, don't do anything.
570        if (e.target === this.rulerTrack_)
571          return;
572
573        var relativePosition = this.extractRelativeMousePosition_(e);
574        var loc = tr.model.Location.fromViewCoordinates(
575            this.viewport_, relativePosition.x, relativePosition.y);
576        // Not all points on the timeline represents a valid location.
577        // ex. process header tracks, letter dot tracks.
578        if (!loc)
579          return;
580
581        if (this.guideLineAnnotation_ === undefined) {
582          this.guideLineAnnotation_ =
583              new tr.model.XMarkerAnnotation(loc.xWorld);
584          this.model.addAnnotation(this.guideLineAnnotation_);
585        } else {
586          this.guideLineAnnotation_.timestamp = loc.xWorld;
587          this.modelTrackContainer_.invalidate();
588        }
589
590        // Set the findcontrol's text to nav string of current state.
591        var state = new tr.ui.b.UIState(loc,
592            this.viewport_.currentDisplayTransform.scaleX);
593        this.timelineView_.setFindCtlText(
594            state.toUserFriendlyString(this.viewport_));
595      }.bind(this),
596      undefined, // Mouse up handler.
597      function onKeyUpDuringDrag() {
598        if (this.dragBeginEvent_) {
599          this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
600              this.dragBoxXEnd_, this.dragBoxYEnd_);
601        }
602      }.bind(this));
603    },
604
605    queueSmoothPan_: function(viewDeltaX, deltaY) {
606      var deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld(
607          viewDeltaX);
608      var animation = new tr.ui.TimelineDisplayTransformPanAnimation(
609          deltaX, deltaY);
610      this.viewport_.queueDisplayTransformAnimation(animation);
611    },
612
613    /**
614     * Zoom in or out on the timeline by the given scale factor.
615     * @param {Number} scale The scale factor to apply.  If <1, zooms out.
616     * @param {boolean} Whether to change the zoom level smoothly.
617     */
618    zoomBy_: function(scale, smooth) {
619      if (scale <= 0) {
620        return;
621      }
622
623      smooth = !!smooth;
624      var vp = this.viewport_;
625      var pixelRatio = window.devicePixelRatio || 1;
626
627      var goalFocalPointXView = this.lastMouseViewPos_.x * pixelRatio;
628      var goalFocalPointXWorld = vp.currentDisplayTransform.xViewToWorld(
629          goalFocalPointXView);
630      if (smooth) {
631        var animation = new tr.ui.TimelineDisplayTransformZoomToAnimation(
632            goalFocalPointXWorld, goalFocalPointXView,
633            vp.currentDisplayTransform.panY,
634            scale);
635        vp.queueDisplayTransformAnimation(animation);
636      } else {
637        this.displayTransform_.set(vp.currentDisplayTransform);
638        this.displayTransform_.scaleX *= scale;
639        this.displayTransform_.xPanWorldPosToViewPos(
640            goalFocalPointXWorld, goalFocalPointXView, this.viewWidth_);
641        vp.setDisplayTransformImmediately(this.displayTransform_);
642      }
643    },
644
645    /**
646     * Zoom into the current selection.
647     */
648    zoomToSelection: function() {
649      if (!this.brushingStateController.selectionOfInterest.length)
650        return;
651
652      var bounds = this.brushingStateController.selectionOfInterest.bounds;
653      if (!bounds.range)
654        return;
655
656      var worldCenter = bounds.center;
657      var viewCenter = this.modelTrackContainer_.canvas.width / 2;
658      var adjustedWorldRange = bounds.range * 1.25;
659      var newScale = this.modelTrackContainer_.canvas.width /
660          adjustedWorldRange;
661      var zoomInRatio = newScale /
662          this.viewport_.currentDisplayTransform.scaleX;
663
664      var animation = new tr.ui.TimelineDisplayTransformZoomToAnimation(
665          worldCenter, viewCenter,
666          this.viewport_.currentDisplayTransform.panY,
667          zoomInRatio);
668      this.viewport_.queueDisplayTransformAnimation(animation);
669    },
670
671    /**
672     * Pan the view so the current selection becomes visible.
673     */
674    panToSelection: function() {
675      if (!this.brushingStateController.selectionOfInterest.length)
676        return;
677
678      var bounds = this.brushingStateController.selectionOfInterest.bounds;
679      var worldCenter = bounds.center;
680      var viewWidth = this.viewWidth_;
681
682      var dt = this.viewport_.currentDisplayTransform;
683      if (false && !bounds.range) {
684        if (dt.xWorldToView(bounds.center) < 0 ||
685            dt.xWorldToView(bounds.center) > viewWidth) {
686          this.displayTransform_.set(dt);
687          this.displayTransform_.xPanWorldPosToViewPos(
688              worldCenter, 'center', viewWidth);
689          var deltaX = this.displayTransform_.panX - dt.panX;
690          var animation = new tr.ui.TimelineDisplayTransformPanAnimation(
691              deltaX, 0);
692          this.viewport_.queueDisplayTransformAnimation(animation);
693        }
694        return;
695      }
696
697      this.displayTransform_.set(dt);
698      this.displayTransform_.xPanWorldBoundsIntoView(
699          bounds.min,
700          bounds.max,
701          viewWidth);
702      var deltaX = this.displayTransform_.panX - dt.panX;
703      var animation = new tr.ui.TimelineDisplayTransformPanAnimation(
704          deltaX, 0);
705      this.viewport_.queueDisplayTransformAnimation(animation);
706    },
707
708    navToPosition: function(uiState, showNavLine) {
709      var location = uiState.location;
710      var scaleX = uiState.scaleX;
711      var track = location.getContainingTrack(this.viewport_);
712
713      var worldCenter = location.xWorld;
714      var viewCenter = this.modelTrackContainer_.canvas.width / 5;
715      var zoomInRatio = scaleX /
716          this.viewport_.currentDisplayTransform.scaleX;
717
718      // Vertically scroll so track is in view.
719      track.scrollIntoViewIfNeeded();
720
721      // Perform zoom and panX animation.
722      var animation = new tr.ui.TimelineDisplayTransformZoomToAnimation(
723          worldCenter, viewCenter,
724          this.viewport_.currentDisplayTransform.panY,
725          zoomInRatio);
726      this.viewport_.queueDisplayTransformAnimation(animation);
727
728      if (!showNavLine)
729        return;
730      // Add an X Marker Annotation at the specified timestamp.
731      if (this.xNavStringMarker_)
732        this.model.removeAnnotation(this.xNavStringMarker_);
733      this.xNavStringMarker_ =
734          new tr.model.XMarkerAnnotation(worldCenter);
735      this.model.addAnnotation(this.xNavStringMarker_);
736    },
737
738    setCurrentSelectionAsInterestRange_: function() {
739      var selectionBounds = this.brushingStateController_.selection.bounds;
740      if (selectionBounds.empty) {
741        this.viewport_.interestRange.reset();
742        return;
743      }
744
745      if (this.viewport_.interestRange.min == selectionBounds.min &&
746          this.viewport_.interestRange.max == selectionBounds.max)
747        this.viewport_.interestRange.reset();
748      else
749        this.viewport_.interestRange.set(selectionBounds);
750    },
751
752    toggleHighDetails_: function() {
753      this.viewport_.highDetails = !this.viewport_.highDetails;
754    },
755
756    hideDragBox_: function() {
757      this.$.drag_box.style.left = '-1000px';
758      this.$.drag_box.style.top = '-1000px';
759      this.$.drag_box.style.width = 0;
760      this.$.drag_box.style.height = 0;
761    },
762
763    setDragBoxPosition_: function(xStart, yStart, xEnd, yEnd) {
764      var loY = Math.min(yStart, yEnd);
765      var hiY = Math.max(yStart, yEnd);
766      var loX = Math.min(xStart, xEnd);
767      var hiX = Math.max(xStart, xEnd);
768      var modelTrackRect = this.modelTrack_.getBoundingClientRect();
769      var dragRect = {left: loX, top: loY, width: hiX - loX, height: hiY - loY};
770
771      dragRect.right = dragRect.left + dragRect.width;
772      dragRect.bottom = dragRect.top + dragRect.height;
773
774      var modelTrackContainerRect =
775          this.modelTrackContainer_.getBoundingClientRect();
776      var clipRect = {
777        left: modelTrackContainerRect.left,
778        top: modelTrackContainerRect.top,
779        right: modelTrackContainerRect.right,
780        bottom: modelTrackContainerRect.bottom
781      };
782
783      var headingWidth = window.getComputedStyle(
784          this.querySelector('tr-ui-heading')).width;
785      var trackTitleWidth = parseInt(headingWidth);
786      clipRect.left = clipRect.left + trackTitleWidth;
787
788      var intersectRect_ = function(r1, r2) {
789        if (r2.left > r1.right || r2.right < r1.left ||
790            r2.top > r1.bottom || r2.bottom < r1.top)
791          return false;
792
793        var results = {};
794        results.left = Math.max(r1.left, r2.left);
795        results.top = Math.max(r1.top, r2.top);
796        results.right = Math.min(r1.right, r2.right);
797        results.bottom = Math.min(r1.bottom, r2.bottom);
798        results.width = results.right - results.left;
799        results.height = results.bottom - results.top;
800        return results;
801      };
802
803      // TODO(dsinclair): intersectRect_ can return false (which should actually
804      // be undefined) but we use finalDragBox without checking the return value
805      // which could potentially blowup. Fix this .....
806      var finalDragBox = intersectRect_(clipRect, dragRect);
807
808      this.$.drag_box.style.left = finalDragBox.left + 'px';
809      this.$.drag_box.style.width = finalDragBox.width + 'px';
810      this.$.drag_box.style.top = finalDragBox.top + 'px';
811      this.$.drag_box.style.height = finalDragBox.height + 'px';
812      this.$.drag_box.style.whiteSpace = 'nowrap';
813
814      var pixelRatio = window.devicePixelRatio || 1;
815      var canv = this.modelTrackContainer_.canvas;
816      var dt = this.viewport_.currentDisplayTransform;
817      var loWX = dt.xViewToWorld(
818          (loX - canv.offsetLeft) * pixelRatio);
819      var hiWX = dt.xViewToWorld(
820          (hiX - canv.offsetLeft) * pixelRatio);
821
822      this.$.drag_box.textContent =
823          tr.v.Unit.byName.timeDurationInMs.format(hiWX - loWX);
824
825      var e = new tr.b.Event('selectionChanging');
826      e.loWX = loWX;
827      e.hiWX = hiWX;
828      this.dispatchEvent(e);
829    },
830
831    onGridToggle_: function(left) {
832      var selection = this.brushingStateController_.selection;
833      var tb = left ? selection.bounds.min : selection.bounds.max;
834
835      // Toggle the grid off if the grid is on, the marker position is the same
836      // and the same element is selected (same timebase).
837      if (this.viewport_.gridEnabled &&
838          this.viewport_.gridSide === left &&
839          this.viewport_.gridInitialTimebase === tb) {
840        this.viewport_.gridside = undefined;
841        this.viewport_.gridEnabled = false;
842        this.viewport_.gridInitialTimebase = undefined;
843        return;
844      }
845
846      // Shift the timebase left until its just left of model_.bounds.min.
847      var numIntervalsSinceStart = Math.ceil((tb - this.model_.bounds.min) /
848          this.viewport_.gridStep_);
849
850      this.viewport_.gridEnabled = true;
851      this.viewport_.gridSide = left;
852      this.viewport_.gridInitialTimebase = tb;
853      this.viewport_.gridTimebase = tb -
854          (numIntervalsSinceStart + 1) * this.viewport_.gridStep_;
855    },
856
857    storeLastMousePos_: function(e) {
858      this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
859    },
860
861    storeLastTouchPositions_: function(e) {
862      this.lastTouchViewPositions_ = this.extractRelativeTouchPositions_(e);
863    },
864
865    extractRelativeMousePosition_: function(e) {
866      var canv = this.modelTrackContainer_.canvas;
867      return {
868        x: e.clientX - canv.offsetLeft,
869        y: e.clientY - canv.offsetTop
870      };
871    },
872
873    extractRelativeTouchPositions_: function(e) {
874      var canv = this.modelTrackContainer_.canvas;
875
876      var touches = [];
877      for (var i = 0; i < e.touches.length; ++i) {
878        touches.push({
879          x: e.touches[i].clientX - canv.offsetLeft,
880          y: e.touches[i].clientY - canv.offsetTop
881        });
882      }
883      return touches;
884    },
885
886    storeInitialMouseDownPos_: function(e) {
887
888      var position = this.extractRelativeMousePosition_(e);
889
890      this.mouseViewPosAtMouseDown_.x = position.x;
891      this.mouseViewPosAtMouseDown_.y = position.y;
892    },
893
894    focusElements_: function() {
895      this.$.hotkey_controller.childRequestsGeneralFocus(this);
896    },
897
898    storeInitialInteractionPositionsAndFocus_: function(e) {
899
900      this.storeInitialMouseDownPos_(e);
901      this.storeLastMousePos_(e);
902
903      this.focusElements_();
904    },
905
906    onBeginPanScan_: function(e) {
907      var vp = this.viewport_;
908      this.viewportDisplayTransformAtMouseDown_ =
909          vp.currentDisplayTransform.clone();
910      this.isPanningAndScanning_ = true;
911
912      this.storeInitialInteractionPositionsAndFocus_(e);
913      e.preventDefault();
914    },
915
916    onUpdatePanScan_: function(e) {
917      if (!this.isPanningAndScanning_)
918        return;
919
920      var viewWidth = this.viewWidth_;
921
922      var pixelRatio = window.devicePixelRatio || 1;
923      var xDeltaView = pixelRatio * (this.lastMouseViewPos_.x -
924          this.mouseViewPosAtMouseDown_.x);
925
926      var yDelta = this.lastMouseViewPos_.y -
927          this.mouseViewPosAtMouseDown_.y;
928
929      this.displayTransform_.set(this.viewportDisplayTransformAtMouseDown_);
930      this.displayTransform_.incrementPanXInViewUnits(xDeltaView);
931      this.displayTransform_.panY -= yDelta;
932      this.viewport_.setDisplayTransformImmediately(this.displayTransform_);
933
934      e.preventDefault();
935      e.stopPropagation();
936
937      this.storeLastMousePos_(e);
938    },
939
940    onEndPanScan_: function(e) {
941      this.isPanningAndScanning_ = false;
942
943      this.storeLastMousePos_(e);
944
945      if (!e.isClick)
946        e.preventDefault();
947    },
948
949    onBeginSelection_: function(e) {
950      var canv = this.modelTrackContainer_.canvas;
951      var rect = this.modelTrack_.getBoundingClientRect();
952      var canvRect = canv.getBoundingClientRect();
953
954      var inside = rect &&
955          e.clientX >= rect.left &&
956          e.clientX < rect.right &&
957          e.clientY >= rect.top &&
958          e.clientY < rect.bottom &&
959          e.clientX >= canvRect.left &&
960          e.clientX < canvRect.right;
961
962      if (!inside)
963        return;
964
965      this.dragBeginEvent_ = e;
966
967      this.storeInitialInteractionPositionsAndFocus_(e);
968      e.preventDefault();
969    },
970
971    onUpdateSelection_: function(e) {
972      if (!this.dragBeginEvent_)
973        return;
974
975      // Update the drag box
976      this.dragBoxXStart_ = this.dragBeginEvent_.clientX;
977      this.dragBoxXEnd_ = e.clientX;
978      this.dragBoxYStart_ = this.dragBeginEvent_.clientY;
979      this.dragBoxYEnd_ = e.clientY;
980      this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
981          this.dragBoxXEnd_, this.dragBoxYEnd_);
982
983    },
984
985    onEndSelection_: function(e) {
986      e.preventDefault();
987
988      if (!this.dragBeginEvent_)
989        return;
990
991      // Stop the dragging.
992      this.hideDragBox_();
993      var eDown = this.dragBeginEvent_;
994      this.dragBeginEvent_ = undefined;
995
996      // Figure out extents of the drag.
997      var loY = Math.min(eDown.clientY, e.clientY);
998      var hiY = Math.max(eDown.clientY, e.clientY);
999      var loX = Math.min(eDown.clientX, e.clientX);
1000      var hiX = Math.max(eDown.clientX, e.clientX);
1001
1002      // Convert to worldspace.
1003      var canv = this.modelTrackContainer_.canvas;
1004      var worldOffset = canv.getBoundingClientRect().left;
1005      var loVX = loX - worldOffset;
1006      var hiVX = hiX - worldOffset;
1007
1008      // Figure out what has been selected.
1009      var selection = new tr.model.EventSet();
1010      if (eDown.appendSelection) {
1011        var previousSelection = this.brushingStateController_.selection;
1012        if (previousSelection !== undefined)
1013          selection.addEventSet(previousSelection);
1014      }
1015      this.modelTrack_.addIntersectingEventsInRangeToSelection(
1016          loVX, hiVX, loY, hiY, selection);
1017
1018      // Activate the new selection.
1019      this.brushingStateController_.changeSelectionFromTimeline(selection);
1020    },
1021
1022    onBeginZoom_: function(e) {
1023      this.isZooming_ = true;
1024
1025      this.storeInitialInteractionPositionsAndFocus_(e);
1026      e.preventDefault();
1027    },
1028
1029    onUpdateZoom_: function(e) {
1030      if (!this.isZooming_)
1031        return;
1032      var newPosition = this.extractRelativeMousePosition_(e);
1033
1034      var zoomScaleValue = 1 + (this.lastMouseViewPos_.y -
1035          newPosition.y) * 0.01;
1036
1037      this.zoomBy_(zoomScaleValue, false);
1038      this.storeLastMousePos_(e);
1039    },
1040
1041    onEndZoom_: function(e) {
1042      this.isZooming_ = false;
1043
1044      if (!e.isClick)
1045        e.preventDefault();
1046    },
1047
1048    computeTouchCenter_: function(positions) {
1049      var xSum = 0;
1050      var ySum = 0;
1051      for (var i = 0; i < positions.length; ++i) {
1052        xSum += positions[i].x;
1053        ySum += positions[i].y;
1054      }
1055      return {
1056        x: xSum / positions.length,
1057        y: ySum / positions.length
1058      };
1059    },
1060
1061    computeTouchSpan_: function(positions) {
1062      var xMin = Number.MAX_VALUE;
1063      var yMin = Number.MAX_VALUE;
1064      var xMax = Number.MIN_VALUE;
1065      var yMax = Number.MIN_VALUE;
1066      for (var i = 0; i < positions.length; ++i) {
1067        xMin = Math.min(xMin, positions[i].x);
1068        yMin = Math.min(yMin, positions[i].y);
1069        xMax = Math.max(xMax, positions[i].x);
1070        yMax = Math.max(yMax, positions[i].y);
1071      }
1072      return Math.sqrt((xMin - xMax) * (xMin - xMax) +
1073          (yMin - yMax) * (yMin - yMax));
1074    },
1075
1076    onUpdateTransformForTouch_: function(e) {
1077      var newPositions = this.extractRelativeTouchPositions_(e);
1078      var currentPositions = this.lastTouchViewPositions_;
1079
1080      var newCenter = this.computeTouchCenter_(newPositions);
1081      var currentCenter = this.computeTouchCenter_(currentPositions);
1082
1083      var newSpan = this.computeTouchSpan_(newPositions);
1084      var currentSpan = this.computeTouchSpan_(currentPositions);
1085
1086      var vp = this.viewport_;
1087      var viewWidth = this.viewWidth_;
1088      var pixelRatio = window.devicePixelRatio || 1;
1089
1090      var xDelta = pixelRatio * (newCenter.x - currentCenter.x);
1091      var yDelta = newCenter.y - currentCenter.y;
1092      var zoomScaleValue = currentSpan > 10 ? newSpan / currentSpan : 1;
1093
1094      var viewFocus = pixelRatio * newCenter.x;
1095      var worldFocus = vp.currentDisplayTransform.xViewToWorld(viewFocus);
1096
1097      this.displayTransform_.set(vp.currentDisplayTransform);
1098      this.displayTransform_.scaleX *= zoomScaleValue;
1099      this.displayTransform_.xPanWorldPosToViewPos(
1100          worldFocus, viewFocus, viewWidth);
1101      this.displayTransform_.incrementPanXInViewUnits(xDelta);
1102      this.displayTransform_.panY -= yDelta;
1103      vp.setDisplayTransformImmediately(this.displayTransform_);
1104      this.storeLastTouchPositions_(e);
1105    },
1106
1107    initHintText_: function() {
1108      this.$.hint_text.style.display = 'none';
1109
1110      this.pendingHintTextClearTimeout_ = undefined;
1111    },
1112
1113    showHintText_: function(text) {
1114      if (this.pendingHintTextClearTimeout_) {
1115        window.clearTimeout(this.pendingHintTextClearTimeout_);
1116        this.pendingHintTextClearTimeout_ = undefined;
1117      }
1118      this.pendingHintTextClearTimeout_ = setTimeout(
1119          this.hideHintText_.bind(this), 1000);
1120      this.$.hint_text.textContent = text;
1121      this.$.hint_text.style.display = '';
1122    },
1123
1124    hideHintText_: function() {
1125      this.pendingHintTextClearTimeout_ = undefined;
1126      this.$.hint_text.style.display = 'none';
1127    }
1128  });
1129  </script>
1130</polymer-element>
1131