Home | History | Annotate | Download | only in tracks
      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 base.requireStylesheet('tracing.tracks.ruler_track');
      8 
      9 base.require('tracing.constants');
     10 base.require('tracing.tracks.track');
     11 base.require('tracing.tracks.heading_track');
     12 base.require('ui');
     13 
     14 base.exportTo('tracing.tracks', function() {
     15 
     16   /**
     17    * A track that displays the ruler.
     18    * @constructor
     19    * @extends {HeadingTrack}
     20    */
     21 
     22   var RulerTrack = ui.define('ruler-track', tracing.tracks.HeadingTrack);
     23 
     24   var logOf10 = Math.log(10);
     25   function log10(x) {
     26     return Math.log(x) / logOf10;
     27   }
     28 
     29   RulerTrack.prototype = {
     30     __proto__: tracing.tracks.HeadingTrack.prototype,
     31 
     32     decorate: function(viewport) {
     33       tracing.tracks.HeadingTrack.prototype.decorate.call(this, viewport);
     34       this.classList.add('ruler-track');
     35       this.strings_secs_ = [];
     36       this.strings_msecs_ = [];
     37       this.addEventListener('mousedown', this.onMouseDown);
     38 
     39       this.viewportMarkersChange_ = this.viewportMarkersChange_.bind(this);
     40       viewport.addEventListener('markersChange', this.viewportMarkersChange_);
     41 
     42     },
     43 
     44     detach: function() {
     45       tracing.tracks.HeadingTrack.prototype.detach.call(this);
     46       this.viewport.removeEventListener('markersChange',
     47                                         this.viewportMarkersChange_);
     48     },
     49 
     50     viewportMarkersChange_: function() {
     51       if (this.viewport.markers.length < 2)
     52         this.classList.remove('ruler-track-with-distance-measurements');
     53       else
     54         this.classList.add('ruler-track-with-distance-measurements');
     55     },
     56 
     57     onMouseDown: function(e) {
     58       if (e.button !== 0)
     59         return;
     60       this.placeAndBeginDraggingMarker(e.clientX);
     61     },
     62 
     63     placeAndBeginDraggingMarker: function(clientX) {
     64       var pixelRatio = window.devicePixelRatio || 1;
     65 
     66       var viewX =
     67           (clientX - this.offsetLeft - tracing.constants.HEADING_WIDTH) *
     68               pixelRatio;
     69       var worldX = this.viewport.xViewToWorld(viewX);
     70       var marker = this.viewport.findMarkerNear(worldX, 6);
     71 
     72       var createdMarker = false;
     73       var movedMarker = false;
     74       if (!marker) {
     75         marker = this.viewport.addMarker(worldX);
     76         createdMarker = true;
     77       }
     78       marker.selected = true;
     79 
     80       var onMouseMove = function(e) {
     81         var viewX =
     82             (e.clientX - this.offsetLeft - tracing.constants.HEADING_WIDTH) *
     83                 pixelRatio;
     84         var worldX = this.viewport.xViewToWorld(viewX);
     85 
     86         marker.positionWorld = worldX;
     87         movedMarker = true;
     88       }.bind(this);
     89 
     90       var onMouseUp = function(e) {
     91         marker.selected = false;
     92         if (!movedMarker && !createdMarker)
     93           this.viewport.removeMarker(marker);
     94 
     95         document.removeEventListener('mouseup', onMouseUp);
     96         document.removeEventListener('mousemove', onMouseMove);
     97       }.bind(this);
     98 
     99       document.addEventListener('mouseup', onMouseUp);
    100       document.addEventListener('mousemove', onMouseMove);
    101     },
    102 
    103     drawLine_: function(ctx, x1, y1, x2, y2, color) {
    104       ctx.beginPath();
    105       ctx.moveTo(x1, y1);
    106       ctx.lineTo(x2, y2);
    107       ctx.closePath();
    108       ctx.strokeStyle = color;
    109       ctx.stroke();
    110     },
    111 
    112     drawArrow_: function(ctx, x1, y1, x2, y2, arrowWidth, color) {
    113       this.drawLine_(ctx, x1, y1, x2, y2, color);
    114 
    115       var dx = x2 - x1;
    116       var dy = y2 - y1;
    117       var len = Math.sqrt(dx * dx + dy * dy);
    118       var perc = (len - 10) / len;
    119       var bx = x1 + perc * dx;
    120       var by = y1 + perc * dy;
    121       var ux = dx / len;
    122       var uy = dy / len;
    123       var ax = uy * arrowWidth;
    124       var ay = -ux * arrowWidth;
    125 
    126       ctx.beginPath();
    127       ctx.fillStyle = color;
    128       ctx.moveTo(bx + ax, by + ay);
    129       ctx.lineTo(x2, y2);
    130       ctx.lineTo(bx - ax, by - ay);
    131       ctx.lineTo(bx + ax, by + ay);
    132       ctx.closePath();
    133       ctx.fill();
    134     },
    135 
    136     draw: function(type, viewLWorld, viewRWorld) {
    137       switch (type) {
    138         case tracing.tracks.DrawType.SLICE:
    139           this.drawSlices_(viewLWorld, viewRWorld);
    140           break;
    141       }
    142     },
    143 
    144     drawSlices_: function(viewLWorld, viewRWorld) {
    145       var ctx = this.context();
    146       var pixelRatio = window.devicePixelRatio || 1;
    147 
    148       var bounds = this.getBoundingClientRect();
    149       var width = bounds.width * pixelRatio;
    150       var height = bounds.height * pixelRatio;
    151 
    152       var measurements = this.classList.contains(
    153           'ruler-track-with-distance-measurements');
    154 
    155       var rulerHeight = measurements ? height / 2 : height;
    156 
    157       var vp = this.viewport;
    158       vp.drawMarkerArrows(ctx, viewLWorld, viewRWorld, rulerHeight);
    159 
    160       var idealMajorMarkDistancePix = 150 * pixelRatio;
    161       var idealMajorMarkDistanceWorld =
    162           vp.xViewVectorToWorld(idealMajorMarkDistancePix);
    163 
    164       var majorMarkDistanceWorld;
    165       var unit;
    166       var unitDivisor;
    167       var tickLabels;
    168 
    169       // The conservative guess is the nearest enclosing 0.1, 1, 10, 100, etc.
    170       var conservativeGuess =
    171           Math.pow(10, Math.ceil(log10(idealMajorMarkDistanceWorld)));
    172 
    173       // Once we have a conservative guess, consider things that evenly add up
    174       // to the conservative guess, e.g. 0.5, 0.2, 0.1 Pick the one that still
    175       // exceeds the ideal mark distance.
    176       var divisors = [10, 5, 2, 1];
    177       for (var i = 0; i < divisors.length; ++i) {
    178         var tightenedGuess = conservativeGuess / divisors[i];
    179         if (vp.xWorldVectorToView(tightenedGuess) < idealMajorMarkDistancePix)
    180           continue;
    181         majorMarkDistanceWorld = conservativeGuess / divisors[i - 1];
    182         break;
    183       }
    184 
    185       var tickLabels = undefined;
    186       if (majorMarkDistanceWorld < 100) {
    187         unit = 'ms';
    188         unitDivisor = 1;
    189         tickLabels = this.strings_msecs_;
    190       } else {
    191         unit = 's';
    192         unitDivisor = 1000;
    193         tickLabels = this.strings_secs_;
    194       }
    195 
    196       var numTicksPerMajor = 5;
    197       var minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor;
    198       var minorMarkDistancePx = vp.xWorldVectorToView(minorMarkDistanceWorld);
    199 
    200       var firstMajorMark =
    201           Math.floor(viewLWorld / majorMarkDistanceWorld) *
    202               majorMarkDistanceWorld;
    203 
    204       var minorTickH = Math.floor(height * 0.25);
    205 
    206       ctx.fillStyle = 'rgb(0, 0, 0)';
    207       ctx.strokeStyle = 'rgb(0, 0, 0)';
    208       ctx.textAlign = 'left';
    209       ctx.textBaseline = 'top';
    210 
    211       var pixelRatio = window.devicePixelRatio || 1;
    212       ctx.font = (9 * pixelRatio) + 'px sans-serif';
    213 
    214       // Each iteration of this loop draws one major mark
    215       // and numTicksPerMajor minor ticks.
    216       //
    217       // Rendering can't be done in world space because canvas transforms
    218       // affect line width. So, do the conversions manually.
    219       for (var curX = firstMajorMark;
    220            curX < viewRWorld;
    221            curX += majorMarkDistanceWorld) {
    222 
    223         var curXView = Math.floor(vp.xWorldToView(curX));
    224 
    225         var unitValue = curX / unitDivisor;
    226         var roundedUnitValue = Math.floor(unitValue * 100000) / 100000;
    227 
    228         if (!tickLabels[roundedUnitValue])
    229           tickLabels[roundedUnitValue] = roundedUnitValue + ' ' + unit;
    230         ctx.fillText(tickLabels[roundedUnitValue],
    231                      curXView + 2 * pixelRatio, 0);
    232         ctx.beginPath();
    233 
    234         // Major mark
    235         ctx.moveTo(curXView, 0);
    236         ctx.lineTo(curXView, rulerHeight);
    237 
    238         // Minor marks
    239         for (var i = 1; i < numTicksPerMajor; ++i) {
    240           var xView = Math.floor(curXView + minorMarkDistancePx * i);
    241           ctx.moveTo(xView, rulerHeight - minorTickH);
    242           ctx.lineTo(xView, rulerHeight);
    243         }
    244 
    245         ctx.stroke();
    246       }
    247       // Draw bottom bar.
    248       ctx.moveTo(0, rulerHeight);
    249       ctx.lineTo(width, rulerHeight);
    250       ctx.stroke();
    251 
    252       // Give distance between directly adjacent markers.
    253       if (measurements) {
    254         // Obtain a sorted array of markers
    255         var sortedMarkers = vp.markers.slice();
    256         sortedMarkers.sort(function(a, b) {
    257           return a.positionWorld_ - b.positionWorld_;
    258         });
    259 
    260         // Distance Variables.
    261         var displayDistance;
    262         var unitDivisor;
    263         var displayTextColor = 'rgb(0,0,0)';
    264         var measurementsPosY = rulerHeight + 2;
    265 
    266         // Arrow Variables.
    267         var arrowSpacing = 10;
    268         var arrowColor = 'rgb(128,121,121)';
    269         var arrowPosY = measurementsPosY + 4;
    270         var arrowWidthView = 3;
    271         var spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing);
    272 
    273         for (i = 0; i < sortedMarkers.length - 1; i++) {
    274           var rightMarker = sortedMarkers[i + 1];
    275           var leftMarker = sortedMarkers[i];
    276           var distanceBetweenMarkers =
    277               rightMarker.positionWorld - leftMarker.positionWorld;
    278           var distanceBetweenMarkersView =
    279               vp.xWorldVectorToView(distanceBetweenMarkers);
    280 
    281           var positionInMiddleOfMarkers = leftMarker.positionWorld +
    282                                               distanceBetweenMarkers / 2;
    283           var positionInMiddleOfMarkersView =
    284               vp.xWorldToView(positionInMiddleOfMarkers);
    285 
    286           // Determine units.
    287           if (distanceBetweenMarkers < 100) {
    288             unit = 'ms';
    289             unitDivisor = 1;
    290           } else {
    291             unit = 's';
    292             unitDivisor = 1000;
    293           }
    294           // Calculate display value to print.
    295           displayDistance = distanceBetweenMarkers / unitDivisor;
    296           var roundedDisplayDistance =
    297               Math.abs((Math.floor(displayDistance * 1000) / 1000));
    298           var textToDraw = roundedDisplayDistance + ' ' + unit;
    299           var textWidthView = ctx.measureText(textToDraw).width;
    300           var textWidthWorld = vp.xViewVectorToWorld(textWidthView);
    301           var spaceForArrowsAndTextView = textWidthView +
    302                                           spaceForArrowsView + arrowSpacing;
    303 
    304           // Set text positions.
    305           var textLeft = leftMarker.positionWorld +
    306               (distanceBetweenMarkers / 2) - (textWidthWorld / 2);
    307           var textRight = textLeft + textWidthWorld;
    308           var textPosY = measurementsPosY;
    309           var textLeftView = vp.xWorldToView(textLeft);
    310           var textRightView = vp.xWorldToView(textRight);
    311           var leftMarkerView = vp.xWorldToView(leftMarker.positionWorld);
    312           var rightMarkerView = vp.xWorldToView(rightMarker.positionWorld);
    313           var textDrawn = false;
    314 
    315           if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) {
    316             // Print the display distance text.
    317             ctx.fillStyle = displayTextColor;
    318             ctx.fillText(textToDraw, textLeftView, textPosY);
    319             textDrawn = true;
    320           }
    321 
    322           if (spaceForArrowsView <= distanceBetweenMarkersView) {
    323             var leftArrowStart;
    324             var rightArrowStart;
    325             if (textDrawn) {
    326               leftArrowStart = textLeftView - arrowSpacing;
    327               rightArrowStart = textRightView + arrowSpacing;
    328             } else {
    329               leftArrowStart = positionInMiddleOfMarkersView;
    330               rightArrowStart = positionInMiddleOfMarkersView;
    331             }
    332             // Draw left arrow.
    333             this.drawArrow_(ctx, leftArrowStart, arrowPosY,
    334                 leftMarkerView, arrowPosY, arrowWidthView, arrowColor);
    335             // Draw right arrow.
    336             this.drawArrow_(ctx, rightArrowStart, arrowPosY,
    337                 rightMarkerView, arrowPosY, arrowWidthView, arrowColor);
    338           }
    339         }
    340         // Draw bottom bar.
    341         ctx.moveTo(0, rulerHeight * 2);
    342         ctx.lineTo(width, rulerHeight * 2);
    343         ctx.stroke();
    344       }
    345     },
    346 
    347     /**
    348      * Adds items intersecting the given range to a selection.
    349      * @param {number} loVX Lower X bound of the interval to search, in
    350      *     viewspace.
    351      * @param {number} hiVX Upper X bound of the interval to search, in
    352      *     viewspace.
    353      * @param {number} loVY Lower Y bound of the interval to search, in
    354      *     viewspace.
    355      * @param {number} hiVY Upper Y bound of the interval to search, in
    356      *     viewspace.
    357      * @param {Selection} selection Selection to which to add hits.
    358      */
    359     addIntersectingItemsInRangeToSelection: function(
    360         loVX, hiVX, loY, hiY, selection) {
    361       // Does nothing. There's nothing interesting to pick on the ruler
    362       // track.
    363     },
    364 
    365     addAllObjectsMatchingFilterToSelection: function(filter, selection) {
    366     }
    367   };
    368 
    369   return {
    370     RulerTrack: RulerTrack
    371   };
    372 });
    373