Home | History | Annotate | Download | only in tracing
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /**
      8  * @fileoverview Code for the viewport.
      9  */
     10 base.require('base.events');
     11 
     12 base.exportTo('tracing', function() {
     13 
     14   /**
     15    * The TimelineViewport manages the transform used for navigating
     16    * within the timeline. It is a simple transform:
     17    *   x' = (x+pan) * scale
     18    *
     19    * The timeline code tries to avoid directly accessing this transform,
     20    * instead using this class to do conversion between world and viewspace,
     21    * as well as the math for centering the viewport in various interesting
     22    * ways.
     23    *
     24    * @constructor
     25    * @extends {base.EventTarget}
     26    */
     27   function TimelineViewport(parentEl) {
     28     this.parentEl_ = parentEl;
     29     this.modelTrackContainer_ = null;
     30     this.scaleX_ = 1;
     31     this.panX_ = 0;
     32     this.panY_ = 0;
     33     this.gridTimebase_ = 0;
     34     this.gridStep_ = 1000 / 60;
     35     this.gridEnabled_ = false;
     36     this.hasCalledSetupFunction_ = false;
     37 
     38     this.onResize_ = this.onResize_.bind(this);
     39     this.onModelTrackControllerScroll_ =
     40         this.onModelTrackControllerScroll_.bind(this);
     41 
     42     // The following code uses an interval to detect when the parent element
     43     // is attached to the document. That is a trigger to run the setup function
     44     // and install a resize listener.
     45     this.checkForAttachInterval_ = setInterval(
     46         this.checkForAttach_.bind(this), 250);
     47 
     48     this.markers = [];
     49   }
     50 
     51   TimelineViewport.prototype = {
     52     __proto__: base.EventTarget.prototype,
     53 
     54     /**
     55      * Allows initialization of the viewport when the viewport's parent element
     56      * has been attached to the document and given a size.
     57      * @param {Function} fn Function to call when the viewport can be safely
     58      * initialized.
     59      */
     60     setWhenPossible: function(fn) {
     61       this.pendingSetFunction_ = fn;
     62     },
     63 
     64     /**
     65      * @return {boolean} Whether the current timeline is attached to the
     66      * document.
     67      */
     68     get isAttachedToDocument_() {
     69       var cur = this.parentEl_;
     70       // Allow not providing a parent element, used by tests.
     71       if (cur === undefined)
     72         return;
     73       while (cur.parentNode)
     74         cur = cur.parentNode;
     75       return cur == this.parentEl_.ownerDocument;
     76     },
     77 
     78     onResize_: function() {
     79       this.dispatchChangeEvent();
     80     },
     81 
     82     /**
     83      * Checks whether the parentNode is attached to the document.
     84      * When it is, it installs the iframe-based resize detection hook
     85      * and then runs the pendingSetFunction_, if present.
     86      */
     87     checkForAttach_: function() {
     88       if (!this.isAttachedToDocument_ || this.clientWidth == 0)
     89         return;
     90 
     91       if (!this.iframe_) {
     92         this.iframe_ = document.createElement('iframe');
     93         this.iframe_.style.cssText =
     94             'position:absolute;width:100%;height:0;border:0;visibility:hidden;';
     95         this.parentEl_.appendChild(this.iframe_);
     96 
     97         this.iframe_.contentWindow.addEventListener('resize', this.onResize_);
     98       }
     99 
    100       var curSize = this.parentEl_.clientWidth + 'x' +
    101           this.parentEl_.clientHeight;
    102       if (this.pendingSetFunction_) {
    103         this.lastSize_ = curSize;
    104         try {
    105           this.pendingSetFunction_();
    106         } catch (ex) {
    107           console.log('While running setWhenPossible:',
    108               ex.message ? ex.message + '\n' + ex.stack : ex.stack);
    109         }
    110         this.pendingSetFunction_ = undefined;
    111       }
    112 
    113       window.clearInterval(this.checkForAttachInterval_);
    114       this.checkForAttachInterval_ = undefined;
    115     },
    116 
    117     /**
    118      * Fires the change event on this viewport. Used to notify listeners
    119      * to redraw when the underlying model has been mutated.
    120      */
    121     dispatchChangeEvent: function() {
    122       base.dispatchSimpleEvent(this, 'change');
    123     },
    124 
    125     dispatchMarkersChangeEvent_: function() {
    126       base.dispatchSimpleEvent(this, 'markersChange');
    127     },
    128 
    129     detach: function() {
    130       if (this.checkForAttachInterval_) {
    131         window.clearInterval(this.checkForAttachInterval_);
    132         this.checkForAttachInterval_ = undefined;
    133       }
    134       if (this.iframe_) {
    135         this.iframe_.removeEventListener('resize', this.onResize_);
    136         this.parentEl_.removeChild(this.iframe_);
    137       }
    138     },
    139 
    140     getStateInViewCoordinates: function() {
    141       return {
    142         panX: this.xWorldVectorToView(this.panX),
    143         panY: this.panY,
    144         scaleX: this.scaleX
    145       };
    146     },
    147 
    148     setStateInViewCoordinates: function(state) {
    149       this.panX = this.xViewVectorToWorld(state.panX);
    150       this.panY = state.panY;
    151     },
    152 
    153     onModelTrackControllerScroll_: function(e) {
    154       this.panY_ = this.modelTrackContainer_.scrollTop;
    155     },
    156 
    157     set modelTrackContainer(m) {
    158 
    159       if (this.modelTrackContainer_)
    160         this.modelTrackContainer_.removeEventListener('scroll',
    161             this.onModelTrackControllerScroll_);
    162 
    163       this.modelTrackContainer_ = m;
    164       this.modelTrackContainer_.addEventListener('scroll',
    165           this.onModelTrackControllerScroll_);
    166     },
    167 
    168     get scaleX() {
    169       return this.scaleX_;
    170     },
    171     set scaleX(s) {
    172       var changed = this.scaleX_ != s;
    173       if (changed) {
    174         this.scaleX_ = s;
    175         this.dispatchChangeEvent();
    176       }
    177     },
    178 
    179     get panX() {
    180       return this.panX_;
    181     },
    182     set panX(p) {
    183       var changed = this.panX_ != p;
    184       if (changed) {
    185         this.panX_ = p;
    186         this.dispatchChangeEvent();
    187       }
    188     },
    189 
    190     get panY() {
    191       return this.panY_;
    192     },
    193     set panY(p) {
    194       this.panY_ = p;
    195       this.modelTrackContainer_.scrollTop = p;
    196     },
    197 
    198     setPanAndScale: function(p, s) {
    199       var changed = this.scaleX_ != s || this.panX_ != p;
    200       if (changed) {
    201         this.scaleX_ = s;
    202         this.panX_ = p;
    203         this.dispatchChangeEvent();
    204       }
    205     },
    206 
    207     xWorldToView: function(x) {
    208       return (x + this.panX_) * this.scaleX_;
    209     },
    210 
    211     xWorldVectorToView: function(x) {
    212       return x * this.scaleX_;
    213     },
    214 
    215     xViewToWorld: function(x) {
    216       return (x / this.scaleX_) - this.panX_;
    217     },
    218 
    219     xViewVectorToWorld: function(x) {
    220       return x / this.scaleX_;
    221     },
    222 
    223     xPanWorldPosToViewPos: function(worldX, viewX, viewWidth) {
    224       if (typeof viewX == 'string') {
    225         if (viewX == 'left') {
    226           viewX = 0;
    227         } else if (viewX == 'center') {
    228           viewX = viewWidth / 2;
    229         } else if (viewX == 'right') {
    230           viewX = viewWidth - 1;
    231         } else {
    232           throw new Error('unrecognized string for viewPos. left|center|right');
    233         }
    234       }
    235       this.panX = (viewX / this.scaleX_) - worldX;
    236     },
    237 
    238     xPanWorldBoundsIntoView: function(worldMin, worldMax, viewWidth) {
    239       if (this.xWorldToView(worldMin) < 0)
    240         this.xPanWorldPosToViewPos(worldMin, 'left', viewWidth);
    241       else if (this.xWorldToView(worldMax) > viewWidth)
    242         this.xPanWorldPosToViewPos(worldMax, 'right', viewWidth);
    243     },
    244 
    245     xSetWorldBounds: function(worldMin, worldMax, viewWidth) {
    246       var worldWidth = worldMax - worldMin;
    247       var scaleX = viewWidth / worldWidth;
    248       var panX = -worldMin;
    249       this.setPanAndScale(panX, scaleX);
    250     },
    251 
    252     get gridEnabled() {
    253       return this.gridEnabled_;
    254     },
    255 
    256     set gridEnabled(enabled) {
    257       if (this.gridEnabled_ == enabled)
    258         return;
    259 
    260       this.gridEnabled_ = enabled && true;
    261       this.dispatchChangeEvent();
    262     },
    263 
    264     get gridTimebase() {
    265       return this.gridTimebase_;
    266     },
    267 
    268     set gridTimebase(timebase) {
    269       if (this.gridTimebase_ == timebase)
    270         return;
    271       this.gridTimebase_ = timebase;
    272       this.dispatchChangeEvent();
    273     },
    274 
    275     get gridStep() {
    276       return this.gridStep_;
    277     },
    278 
    279     applyTransformToCanvas: function(ctx) {
    280       ctx.transform(this.scaleX_, 0, 0, 1, this.panX_ * this.scaleX_, 0);
    281     },
    282 
    283     addMarker: function(positionWorld) {
    284       var marker = new ViewportMarker(this, positionWorld);
    285       this.markers.push(marker);
    286       this.dispatchChangeEvent();
    287       this.dispatchMarkersChangeEvent_();
    288       return marker;
    289     },
    290 
    291     removeMarker: function(marker) {
    292       for (var i = 0; i < this.markers.length; ++i) {
    293         if (this.markers[i] === marker) {
    294           this.markers.splice(i, 1);
    295           this.dispatchChangeEvent();
    296           this.dispatchMarkersChangeEvent_();
    297           return true;
    298         }
    299       }
    300     },
    301 
    302     findMarkerNear: function(positionWorld, nearnessInViewPixels) {
    303       // Converts pixels into distance in world.
    304       var nearnessThresholdWorld = this.xViewVectorToWorld(
    305           nearnessInViewPixels);
    306       for (var i = 0; i < this.markers.length; ++i) {
    307         if (Math.abs(this.markers[i].positionWorld - positionWorld) <=
    308             nearnessThresholdWorld) {
    309           var marker = this.markers[i];
    310           return marker;
    311         }
    312       }
    313       return undefined;
    314     },
    315 
    316     drawGridLines: function(ctx, viewLWorld, viewRWorld) {
    317       if (!this.gridEnabled)
    318         return;
    319 
    320       var x = this.gridTimebase;
    321 
    322       ctx.beginPath();
    323       while (x < viewRWorld) {
    324         if (x >= viewLWorld) {
    325           // Do conversion to viewspace here rather than on
    326           // x to avoid precision issues.
    327           var vx = this.xWorldToView(x);
    328           ctx.moveTo(vx, 0);
    329           ctx.lineTo(vx, ctx.canvas.height);
    330         }
    331         x += this.gridStep;
    332       }
    333       ctx.strokeStyle = 'rgba(255,0,0,0.25)';
    334       ctx.stroke();
    335     },
    336 
    337     drawMarkerArrows: function(ctx, viewLWorld, viewRWorld, drawHeight) {
    338       for (var i = 0; i < this.markers.length; ++i) {
    339         this.markers[i].drawTriangle_(ctx, viewLWorld, viewRWorld,
    340                                       ctx.canvas.height, drawHeight, this);
    341       }
    342     },
    343 
    344     drawMarkerLines: function(ctx, viewLWorld, viewRWorld) {
    345       for (var i = 0; i < this.markers.length; ++i) {
    346         this.markers[i].drawLine(ctx, viewLWorld, viewRWorld,
    347             ctx.canvas.height, this);
    348       }
    349     }
    350   };
    351 
    352   /**
    353    * Represents a marked position in the world, at a viewport level.
    354    * @constructor
    355    */
    356   function ViewportMarker(vp, positionWorld) {
    357     this.viewport_ = vp;
    358     this.positionWorld_ = positionWorld;
    359     this.selected_ = false;
    360   }
    361 
    362   ViewportMarker.prototype = {
    363     get positionWorld() {
    364       return this.positionWorld_;
    365     },
    366 
    367     set positionWorld(positionWorld) {
    368       this.positionWorld_ = positionWorld;
    369       this.viewport_.dispatchChangeEvent();
    370     },
    371 
    372     set selected(selected) {
    373       this.selected_ = selected;
    374       this.viewport_.dispatchChangeEvent();
    375     },
    376 
    377     get selected() {
    378       return this.selected_;
    379     },
    380 
    381     get color() {
    382       if (this.selected)
    383         return 'rgb(255,0,0)';
    384       return 'rgb(0,0,0)';
    385     },
    386 
    387     drawTriangle_: function(ctx, viewLWorld, viewRWorld,
    388                             canvasH, rulerHeight, vp) {
    389       ctx.beginPath();
    390 
    391       var ts = this.positionWorld_;
    392       if (ts < viewLWorld || ts > viewRWorld)
    393         return;
    394 
    395       var viewX = vp.xWorldToView(ts);
    396       ctx.moveTo(viewX, rulerHeight);
    397       ctx.lineTo(viewX - 3, rulerHeight / 2);
    398       ctx.lineTo(viewX + 3, rulerHeight / 2);
    399       ctx.lineTo(viewX, rulerHeight);
    400       ctx.closePath();
    401       ctx.fillStyle = this.color;
    402       ctx.fill();
    403 
    404       if (rulerHeight === canvasH)
    405         return;
    406 
    407       // Draw line from bottom of triangle to the bottom of our canvas.
    408       ctx.beginPath();
    409       ctx.moveTo(viewX, rulerHeight);
    410       ctx.lineTo(viewX, canvasH);
    411       ctx.closePath();
    412       ctx.strokeStyle = this.color;
    413       ctx.stroke();
    414     },
    415 
    416     drawLine: function(ctx, viewLWorld, viewRWorld, canvasH, vp) {
    417       ctx.beginPath();
    418       var ts = this.positionWorld_;
    419       if (ts >= viewLWorld && ts < viewRWorld) {
    420         var viewX = vp.xWorldToView(ts);
    421         ctx.moveTo(viewX, 0);
    422         ctx.lineTo(viewX, canvasH);
    423       }
    424       ctx.strokeStyle = this.color;
    425       ctx.stroke();
    426     }
    427   };
    428 
    429   return {
    430     TimelineViewport: TimelineViewport,
    431     ViewportMarker: ViewportMarker
    432   };
    433 });
    434