12da489cd246702bee5938545b18a6f710ed214bcJamie Gennis// Copyright (c) 2012 The Chromium Authors. All rights reserved.
22da489cd246702bee5938545b18a6f710ed214bcJamie Gennis// Use of this source code is governed by a BSD-style license that can be
32da489cd246702bee5938545b18a6f710ed214bcJamie Gennis// found in the LICENSE file.
42da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
52da489cd246702bee5938545b18a6f710ed214bcJamie Gennis'use strict';
62da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
72da489cd246702bee5938545b18a6f710ed214bcJamie Gennis/**
888448d9ae4dfff1805045790ef5f32495d62abccJeff Brown * @fileoverview Code for the viewport.
92da489cd246702bee5938545b18a6f710ed214bcJamie Gennis */
1066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennisbase.require('base.events');
112da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
122da489cd246702bee5938545b18a6f710ed214bcJamie Gennisbase.exportTo('tracing', function() {
132da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
142da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  /**
152da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   * The TimelineViewport manages the transform used for navigating
162da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   * within the timeline. It is a simple transform:
172da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   *   x' = (x+pan) * scale
182da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   *
192da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   * The timeline code tries to avoid directly accessing this transform,
202da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   * instead using this class to do conversion between world and viewspace,
212da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   * as well as the math for centering the viewport in various interesting
222da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   * ways.
232da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   *
242da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   * @constructor
252da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   * @extends {base.EventTarget}
262da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   */
272da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  function TimelineViewport(parentEl) {
282da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.parentEl_ = parentEl;
2966a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    this.modelTrackContainer_ = null;
302da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.scaleX_ = 1;
312da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.panX_ = 0;
3266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    this.panY_ = 0;
332da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.gridTimebase_ = 0;
342da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.gridStep_ = 1000 / 60;
352da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.gridEnabled_ = false;
362da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.hasCalledSetupFunction_ = false;
372da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
3866a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    this.onResize_ = this.onResize_.bind(this);
396833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis    this.onModelTrackControllerScroll_ =
406833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis        this.onModelTrackControllerScroll_.bind(this);
412da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
422da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    // The following code uses an interval to detect when the parent element
432da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    // is attached to the document. That is a trigger to run the setup function
442da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    // and install a resize listener.
452da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.checkForAttachInterval_ = setInterval(
462da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.checkForAttach_.bind(this), 250);
472da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
482da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.markers = [];
492da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  }
502da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
512da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  TimelineViewport.prototype = {
522da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    __proto__: base.EventTarget.prototype,
532da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
542da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    /**
552da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * Allows initialization of the viewport when the viewport's parent element
562da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * has been attached to the document and given a size.
572da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * @param {Function} fn Function to call when the viewport can be safely
582da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * initialized.
592da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     */
602da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    setWhenPossible: function(fn) {
612da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.pendingSetFunction_ = fn;
622da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
632da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
642da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    /**
652da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * @return {boolean} Whether the current timeline is attached to the
662da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * document.
672da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     */
682da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    get isAttachedToDocument_() {
692da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      var cur = this.parentEl_;
7066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      // Allow not providing a parent element, used by tests.
7166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      if (cur === undefined)
7266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        return;
732da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      while (cur.parentNode)
742da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        cur = cur.parentNode;
752da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return cur == this.parentEl_.ownerDocument;
762da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
772da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
782da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    onResize_: function() {
792da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.dispatchChangeEvent();
802da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
812da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
822da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    /**
832da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * Checks whether the parentNode is attached to the document.
842da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * When it is, it installs the iframe-based resize detection hook
852da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * and then runs the pendingSetFunction_, if present.
862da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     */
872da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    checkForAttach_: function() {
882da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (!this.isAttachedToDocument_ || this.clientWidth == 0)
892da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        return;
902da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
912da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (!this.iframe_) {
922da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.iframe_ = document.createElement('iframe');
932da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.iframe_.style.cssText =
942da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            'position:absolute;width:100%;height:0;border:0;visibility:hidden;';
952da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.parentEl_.appendChild(this.iframe_);
962da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
9766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        this.iframe_.contentWindow.addEventListener('resize', this.onResize_);
982da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
992da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
10066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      var curSize = this.parentEl_.clientWidth + 'x' +
10166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis          this.parentEl_.clientHeight;
1022da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (this.pendingSetFunction_) {
1032da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.lastSize_ = curSize;
1042da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        try {
1052da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          this.pendingSetFunction_();
1062da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        } catch (ex) {
10766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis          console.log('While running setWhenPossible:',
10866a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis              ex.message ? ex.message + '\n' + ex.stack : ex.stack);
1092da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        }
1102da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.pendingSetFunction_ = undefined;
1112da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
1122da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1132da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      window.clearInterval(this.checkForAttachInterval_);
1142da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.checkForAttachInterval_ = undefined;
1152da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
1162da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1172da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    /**
1182da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * Fires the change event on this viewport. Used to notify listeners
1192da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     * to redraw when the underlying model has been mutated.
1202da489cd246702bee5938545b18a6f710ed214bcJamie Gennis     */
1212da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    dispatchChangeEvent: function() {
1222da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      base.dispatchSimpleEvent(this, 'change');
1232da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
1242da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1252da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    dispatchMarkersChangeEvent_: function() {
1262da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      base.dispatchSimpleEvent(this, 'markersChange');
1272da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
1282da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1292da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    detach: function() {
1302da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (this.checkForAttachInterval_) {
1312da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        window.clearInterval(this.checkForAttachInterval_);
1322da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.checkForAttachInterval_ = undefined;
1332da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
1342da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (this.iframe_) {
13566a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        this.iframe_.removeEventListener('resize', this.onResize_);
1362da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.parentEl_.removeChild(this.iframe_);
1372da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
1382da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
1392da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
14066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    getStateInViewCoordinates: function() {
14166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      return {
14266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        panX: this.xWorldVectorToView(this.panX),
14366a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        panY: this.panY,
14466a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        scaleX: this.scaleX
14566a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      };
14666a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    },
14766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
14866a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    setStateInViewCoordinates: function(state) {
14966a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      this.panX = this.xViewVectorToWorld(state.panX);
15066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      this.panY = state.panY;
15166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    },
15266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
1536833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis    onModelTrackControllerScroll_: function(e) {
1546833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis      this.panY_ = this.modelTrackContainer_.scrollTop;
1556833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis    },
1566833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis
15766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    set modelTrackContainer(m) {
1586833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis
1596833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis      if (this.modelTrackContainer_)
1606833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis        this.modelTrackContainer_.removeEventListener('scroll',
1616833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis            this.onModelTrackControllerScroll_);
1626833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis
16366a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      this.modelTrackContainer_ = m;
1646833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis      this.modelTrackContainer_.addEventListener('scroll',
1656833e18b1d4077bf3a727b4422cc2acdbeee35a7Jamie Gennis          this.onModelTrackControllerScroll_);
16666a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    },
16766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
1682da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    get scaleX() {
1692da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return this.scaleX_;
1702da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
1712da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    set scaleX(s) {
1722da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      var changed = this.scaleX_ != s;
1732da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (changed) {
1742da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.scaleX_ = s;
1752da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.dispatchChangeEvent();
1762da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
1772da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
1782da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1792da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    get panX() {
1802da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return this.panX_;
1812da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
1822da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    set panX(p) {
1832da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      var changed = this.panX_ != p;
1842da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (changed) {
1852da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.panX_ = p;
1862da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.dispatchChangeEvent();
1872da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
1882da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
1892da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
19066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    get panY() {
19166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      return this.panY_;
19266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    },
19366a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    set panY(p) {
19466a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      this.panY_ = p;
19566a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      this.modelTrackContainer_.scrollTop = p;
19666a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    },
19766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
1982da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    setPanAndScale: function(p, s) {
1992da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      var changed = this.scaleX_ != s || this.panX_ != p;
2002da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (changed) {
2012da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.scaleX_ = s;
2022da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.panX_ = p;
2032da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.dispatchChangeEvent();
2042da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
2052da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2062da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2072da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    xWorldToView: function(x) {
2082da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return (x + this.panX_) * this.scaleX_;
2092da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2102da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2112da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    xWorldVectorToView: function(x) {
2122da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return x * this.scaleX_;
2132da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2142da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2152da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    xViewToWorld: function(x) {
2162da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return (x / this.scaleX_) - this.panX_;
2172da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2182da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2192da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    xViewVectorToWorld: function(x) {
2202da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return x / this.scaleX_;
2212da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2222da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2232da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    xPanWorldPosToViewPos: function(worldX, viewX, viewWidth) {
2242da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (typeof viewX == 'string') {
2252da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if (viewX == 'left') {
2262da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          viewX = 0;
2272da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        } else if (viewX == 'center') {
2282da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          viewX = viewWidth / 2;
2292da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        } else if (viewX == 'right') {
2302da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          viewX = viewWidth - 1;
2312da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        } else {
2322da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          throw new Error('unrecognized string for viewPos. left|center|right');
2332da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        }
2342da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
2352da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.panX = (viewX / this.scaleX_) - worldX;
2362da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2372da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
23888448d9ae4dfff1805045790ef5f32495d62abccJeff Brown    xPanWorldBoundsIntoView: function(worldMin, worldMax, viewWidth) {
2392da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (this.xWorldToView(worldMin) < 0)
2402da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.xPanWorldPosToViewPos(worldMin, 'left', viewWidth);
2412da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      else if (this.xWorldToView(worldMax) > viewWidth)
2422da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        this.xPanWorldPosToViewPos(worldMax, 'right', viewWidth);
2432da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2442da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
24588448d9ae4dfff1805045790ef5f32495d62abccJeff Brown    xSetWorldBounds: function(worldMin, worldMax, viewWidth) {
24688448d9ae4dfff1805045790ef5f32495d62abccJeff Brown      var worldWidth = worldMax - worldMin;
24788448d9ae4dfff1805045790ef5f32495d62abccJeff Brown      var scaleX = viewWidth / worldWidth;
2482da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      var panX = -worldMin;
2492da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.setPanAndScale(panX, scaleX);
2502da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2512da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2522da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    get gridEnabled() {
2532da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return this.gridEnabled_;
2542da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2552da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2562da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    set gridEnabled(enabled) {
2572da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (this.gridEnabled_ == enabled)
2582da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        return;
25966a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
2602da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.gridEnabled_ = enabled && true;
2612da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.dispatchChangeEvent();
2622da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2632da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2642da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    get gridTimebase() {
2652da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return this.gridTimebase_;
2662da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2672da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2682da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    set gridTimebase(timebase) {
2692da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (this.gridTimebase_ == timebase)
2702da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        return;
2712da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.gridTimebase_ = timebase;
27266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      this.dispatchChangeEvent();
2732da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2742da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2752da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    get gridStep() {
2762da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return this.gridStep_;
2772da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2782da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2792da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    applyTransformToCanvas: function(ctx) {
2802da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      ctx.transform(this.scaleX_, 0, 0, 1, this.panX_ * this.scaleX_, 0);
2812da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2822da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2832da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    addMarker: function(positionWorld) {
28488448d9ae4dfff1805045790ef5f32495d62abccJeff Brown      var marker = new ViewportMarker(this, positionWorld);
2852da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.markers.push(marker);
2862da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.dispatchChangeEvent();
2872da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.dispatchMarkersChangeEvent_();
2882da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return marker;
2892da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
2902da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2912da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    removeMarker: function(marker) {
2922da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      for (var i = 0; i < this.markers.length; ++i) {
2932da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if (this.markers[i] === marker) {
2942da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          this.markers.splice(i, 1);
2952da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          this.dispatchChangeEvent();
2962da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          this.dispatchMarkersChangeEvent_();
2972da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          return true;
2982da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        }
2992da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
3002da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
3012da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
3022da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    findMarkerNear: function(positionWorld, nearnessInViewPixels) {
3032da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      // Converts pixels into distance in world.
3042da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      var nearnessThresholdWorld = this.xViewVectorToWorld(
3052da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          nearnessInViewPixels);
3062da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      for (var i = 0; i < this.markers.length; ++i) {
3072da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if (Math.abs(this.markers[i].positionWorld - positionWorld) <=
3082da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            nearnessThresholdWorld) {
3092da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          var marker = this.markers[i];
3102da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          return marker;
3112da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        }
3122da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
3132da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return undefined;
31466a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    },
31566a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
31666a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    drawGridLines: function(ctx, viewLWorld, viewRWorld) {
31766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      if (!this.gridEnabled)
31866a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        return;
31966a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
32066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      var x = this.gridTimebase;
32166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
32266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.beginPath();
32366a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      while (x < viewRWorld) {
32466a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        if (x >= viewLWorld) {
32566a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis          // Do conversion to viewspace here rather than on
32666a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis          // x to avoid precision issues.
32766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis          var vx = this.xWorldToView(x);
32866a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis          ctx.moveTo(vx, 0);
32966a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis          ctx.lineTo(vx, ctx.canvas.height);
33066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        }
33166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        x += this.gridStep;
33266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      }
33366a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.strokeStyle = 'rgba(255,0,0,0.25)';
33466a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.stroke();
33566a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    },
33666a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
33766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    drawMarkerArrows: function(ctx, viewLWorld, viewRWorld, drawHeight) {
33866a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      for (var i = 0; i < this.markers.length; ++i) {
33966a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        this.markers[i].drawTriangle_(ctx, viewLWorld, viewRWorld,
34066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis                                      ctx.canvas.height, drawHeight, this);
34166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      }
34266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    },
34366a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
34466a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    drawMarkerLines: function(ctx, viewLWorld, viewRWorld) {
34566a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      for (var i = 0; i < this.markers.length; ++i) {
34666a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        this.markers[i].drawLine(ctx, viewLWorld, viewRWorld,
34766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis            ctx.canvas.height, this);
34866a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      }
3492da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    }
3502da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  };
3512da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
3522da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  /**
3532da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   * Represents a marked position in the world, at a viewport level.
3542da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   * @constructor
3552da489cd246702bee5938545b18a6f710ed214bcJamie Gennis   */
35688448d9ae4dfff1805045790ef5f32495d62abccJeff Brown  function ViewportMarker(vp, positionWorld) {
3572da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.viewport_ = vp;
3582da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.positionWorld_ = positionWorld;
3592da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    this.selected_ = false;
3602da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  }
3612da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
36288448d9ae4dfff1805045790ef5f32495d62abccJeff Brown  ViewportMarker.prototype = {
3632da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    get positionWorld() {
3642da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return this.positionWorld_;
3652da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
3662da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
3672da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    set positionWorld(positionWorld) {
3682da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.positionWorld_ = positionWorld;
3692da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.viewport_.dispatchChangeEvent();
3702da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
3712da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
3722da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    set selected(selected) {
3732da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.selected_ = selected;
3742da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      this.viewport_.dispatchChangeEvent();
3752da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
3762da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
3772da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    get selected() {
3782da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return this.selected_;
3792da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
3802da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
3812da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    get color() {
3822da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (this.selected)
3832da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        return 'rgb(255,0,0)';
3842da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return 'rgb(0,0,0)';
3852da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
3862da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
3872da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    drawTriangle_: function(ctx, viewLWorld, viewRWorld,
3882da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                            canvasH, rulerHeight, vp) {
3892da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      ctx.beginPath();
39066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
3912da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      var ts = this.positionWorld_;
39266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      if (ts < viewLWorld || ts > viewRWorld)
39366a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        return;
39466a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
39566a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      var viewX = vp.xWorldToView(ts);
39666a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.moveTo(viewX, rulerHeight);
39766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.lineTo(viewX - 3, rulerHeight / 2);
39866a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.lineTo(viewX + 3, rulerHeight / 2);
39966a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.lineTo(viewX, rulerHeight);
40066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.closePath();
40166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.fillStyle = this.color;
40266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.fill();
40366a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
40466a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      if (rulerHeight === canvasH)
40566a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis        return;
40666a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
40766a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      // Draw line from bottom of triangle to the bottom of our canvas.
40866a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.beginPath();
40966a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.moveTo(viewX, rulerHeight);
41066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.lineTo(viewX, canvasH);
41166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.closePath();
41266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.strokeStyle = this.color;
41366a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      ctx.stroke();
4142da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    },
4152da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
4162da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    drawLine: function(ctx, viewLWorld, viewRWorld, canvasH, vp) {
4172da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      ctx.beginPath();
4182da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      var ts = this.positionWorld_;
4192da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if (ts >= viewLWorld && ts < viewRWorld) {
4202da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        var viewX = vp.xWorldToView(ts);
4212da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        ctx.moveTo(viewX, 0);
4222da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        ctx.lineTo(viewX, canvasH);
4232da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      }
4242da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      ctx.strokeStyle = this.color;
4252da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      ctx.stroke();
4262da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    }
4272da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  };
4282da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
4292da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  return {
4302da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    TimelineViewport: TimelineViewport,
43188448d9ae4dfff1805045790ef5f32495d62abccJeff Brown    ViewportMarker: ViewportMarker
4322da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  };
4332da489cd246702bee5938545b18a6f710ed214bcJamie Gennis});
434