Javascript  |  359行  |  11.91 KB

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

base.requireStylesheet('tracks.ruler_track');

base.require('tracks.track');
base.require('tracks.canvas_based_track');
base.require('ui');

base.exportTo('tracing.tracks', function() {

  /**
   * A track that displays the ruler.
   * @constructor
   * @extends {CanvasBasedTrack}
   */

  var RulerTrack = tracing.ui.define(tracing.tracks.CanvasBasedTrack);

  var logOf10 = Math.log(10);
  function log10(x) {
    return Math.log(x) / logOf10;
  }

  RulerTrack.prototype = {

    __proto__: tracing.tracks.CanvasBasedTrack.prototype,

    decorate: function() {
      this.classList.add('ruler-track');
      this.strings_secs_ = [];
      this.strings_msecs_ = [];
      this.addEventListener('mousedown', this.onMouseDown);
    },

    onMouseDown: function(e) {
      if (e.button != 0)
        return;
      this.placeAndBeginDraggingMarker(e.clientX);
    },


    placeAndBeginDraggingMarker: function(clientX) {
      var pixelRatio = window.devicePixelRatio || 1;
      var viewX = (clientX - this.canvasContainer_.offsetLeft) * pixelRatio;
      var worldX = this.viewport_.xViewToWorld(viewX);
      var marker = this.viewport_.findMarkerNear(worldX, 6);
      var createdMarker = false;
      var movedMarker = false;
      if (!marker) {
        marker = this.viewport_.addMarker(worldX);
        createdMarker = true;
      }
      marker.selected = true;

      var that = this;
      var onMouseMove = function(e) {
        var viewX = (e.clientX - that.canvasContainer_.offsetLeft) * pixelRatio;
        var worldX = that.viewport_.xViewToWorld(viewX);
        marker.positionWorld = worldX;
        movedMarker = true;
      };

      var onMouseUp = function(e) {
        marker.selected = false;
        if (!movedMarker && !createdMarker)
          that.viewport_.removeMarker(marker);
        document.removeEventListener('mouseup', onMouseUp);
        document.removeEventListener('mousemove', onMouseMove);
      };

      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('mousemove', onMouseMove);
    },

    drawLine_: function(ctx, x1, y1, x2, y2, color) {
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.closePath();
      ctx.strokeStyle = color;
      ctx.stroke();
    },

    drawArrow_: function(ctx, x1, y1, x2, y2, arrowWidth, color) {

      this.drawLine_(ctx, x1, y1, x2, y2, color);

      var dx = x2 - x1;
      var dy = y2 - y1;
      var len = Math.sqrt(dx * dx + dy * dy);
      var perc = (len - 10) / len;
      var bx = x1 + perc * dx;
      var by = y1 + perc * dy;
      var ux = dx / len;
      var uy = dy / len;
      var ax = uy * arrowWidth;
      var ay = -ux * arrowWidth;

      ctx.beginPath();
      ctx.fillStyle = color;
      ctx.moveTo(bx + ax, by + ay);
      ctx.lineTo(x2, y2);
      ctx.lineTo(bx - ax, by - ay);
      ctx.lineTo(bx + ax, by + ay);
      ctx.closePath();
      ctx.fill();
    },

    redraw: function() {
      var ctx = this.ctx_;
      var canvasW = this.canvas_.width;
      var canvasH = this.canvas_.height;

      ctx.clearRect(0, 0, canvasW, canvasH);

      // Culling parametrs.
      var vp = this.viewport_;
      var pixWidth = vp.xViewVectorToWorld(1);
      var viewLWorld = vp.xViewToWorld(0);
      var viewRWorld = vp.xViewToWorld(canvasW);

      var measurements = this.classList.contains(
          'ruler-track-with-distance-measurements');

      var rulerHeight = measurements ? canvasH / 2 : canvasH;

      for (var i = 0; i < vp.markers.length; ++i) {
        vp.markers[i].drawTriangle_(ctx, viewLWorld, viewRWorld,
                                    canvasH, rulerHeight, vp);
      }

      var pixelRatio = window.devicePixelRatio || 1;
      var idealMajorMarkDistancePix = 150 * pixelRatio;
      var idealMajorMarkDistanceWorld =
          vp.xViewVectorToWorld(idealMajorMarkDistancePix);

      var majorMarkDistanceWorld;
      var unit;
      var unitDivisor;
      var tickLabels;

      // The conservative guess is the nearest enclosing 0.1, 1, 10, 100, etc.
      var conservativeGuess =
          Math.pow(10, Math.ceil(log10(idealMajorMarkDistanceWorld)));

      // Once we have a conservative guess, consider things that evenly add up
      // to the conservative guess, e.g. 0.5, 0.2, 0.1 Pick the one that still
      // exceeds the ideal mark distance.
      var divisors = [10, 5, 2, 1];
      for (var i = 0; i < divisors.length; ++i) {
        var tightenedGuess = conservativeGuess / divisors[i];
        if (vp.xWorldVectorToView(tightenedGuess) < idealMajorMarkDistancePix)
          continue;
        majorMarkDistanceWorld = conservativeGuess / divisors[i - 1];
        break;
      }
      var tickLabels = undefined;
      if (majorMarkDistanceWorld < 100) {
        unit = 'ms';
        unitDivisor = 1;
        tickLabels = this.strings_msecs_;
      } else {
        unit = 's';
        unitDivisor = 1000;
        tickLabels = this.strings_secs_;
      }

      var numTicksPerMajor = 5;
      var minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor;
      var minorMarkDistancePx = vp.xWorldVectorToView(minorMarkDistanceWorld);

      var firstMajorMark =
          Math.floor(viewLWorld / majorMarkDistanceWorld) *
              majorMarkDistanceWorld;

      var minorTickH = Math.floor(canvasH * 0.25);

      ctx.fillStyle = 'rgb(0, 0, 0)';
      ctx.strokeStyle = 'rgb(0, 0, 0)';
      ctx.textAlign = 'left';
      ctx.textBaseline = 'top';

      var pixelRatio = window.devicePixelRatio || 1;
      ctx.font = (9 * pixelRatio) + 'px sans-serif';

      // Each iteration of this loop draws one major mark
      // and numTicksPerMajor minor ticks.
      //
      // Rendering can't be done in world space because canvas transforms
      // affect line width. So, do the conversions manually.
      for (var curX = firstMajorMark;
           curX < viewRWorld;
           curX += majorMarkDistanceWorld) {

        var curXView = Math.floor(vp.xWorldToView(curX));

        var unitValue = curX / unitDivisor;
        var roundedUnitValue = Math.floor(unitValue * 100000) / 100000;

        if (!tickLabels[roundedUnitValue])
          tickLabels[roundedUnitValue] = roundedUnitValue + ' ' + unit;
        ctx.fillText(tickLabels[roundedUnitValue],
                     curXView + 2 * pixelRatio, 0);
        ctx.beginPath();

        // Major mark
        ctx.moveTo(curXView, 0);
        ctx.lineTo(curXView, rulerHeight);

        // Minor marks
        for (var i = 1; i < numTicksPerMajor; ++i) {
          var xView = Math.floor(curXView + minorMarkDistancePx * i);
          ctx.moveTo(xView, rulerHeight - minorTickH);
          ctx.lineTo(xView, rulerHeight);
        }

        ctx.stroke();
      }

      // Give distance between directly adjacent markers.
      if (measurements) {

        // Divide canvas horizontally between ruler and measurements.
        ctx.moveTo(0, rulerHeight);
        ctx.lineTo(canvasW, rulerHeight);
        ctx.stroke();

        // Obtain a sorted array of markers
        var sortedMarkers = vp.markers.slice();
        sortedMarkers.sort(function(a, b) {
          return a.positionWorld_ - b.positionWorld_;
        });

        // Distance Variables.
        var displayDistance;
        var unitDivisor;
        var displayTextColor = 'rgb(0,0,0)';
        var measurementsPosY = rulerHeight + 2;

        // Arrow Variables.
        var arrowSpacing = 10;
        var arrowColor = 'rgb(128,121,121)';
        var arrowPosY = measurementsPosY + 4;
        var arrowWidthView = 3;
        var spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing);

        for (i = 0; i < sortedMarkers.length - 1; i++) {
          var rightMarker = sortedMarkers[i + 1];
          var leftMarker = sortedMarkers[i];
          var distanceBetweenMarkers =
              rightMarker.positionWorld - leftMarker.positionWorld;
          var distanceBetweenMarkersView =
              vp.xWorldVectorToView(distanceBetweenMarkers);

          var positionInMiddleOfMarkers = leftMarker.positionWorld +
                                              distanceBetweenMarkers / 2;
          var positionInMiddleOfMarkersView =
              vp.xWorldToView(positionInMiddleOfMarkers);

          // Determine units.
          if (distanceBetweenMarkers < 100) {
            unit = 'ms';
            unitDivisor = 1;
          } else {
            unit = 's';
            unitDivisor = 1000;
          }
          // Calculate display value to print.
          displayDistance = distanceBetweenMarkers / unitDivisor;
          var roundedDisplayDistance =
              Math.abs((Math.floor(displayDistance * 1000) / 1000));
          var textToDraw = roundedDisplayDistance + ' ' + unit;
          var textWidthView = ctx.measureText(textToDraw).width;
          var textWidthWorld = vp.xViewVectorToWorld(textWidthView);
          var spaceForArrowsAndTextView = textWidthView +
                                          spaceForArrowsView + arrowSpacing;

          // Set text positions.
          var textLeft = leftMarker.positionWorld +
              (distanceBetweenMarkers / 2) - (textWidthWorld / 2);
          var textRight = textLeft + textWidthWorld;
          var textPosY = measurementsPosY;
          var textLeftView = vp.xWorldToView(textLeft);
          var textRightView = vp.xWorldToView(textRight);
          var leftMarkerView = vp.xWorldToView(leftMarker.positionWorld);
          var rightMarkerView = vp.xWorldToView(rightMarker.positionWorld);
          var textDrawn = false;

          if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) {
            // Print the display distance text.
            ctx.fillStyle = displayTextColor;
            ctx.fillText(textToDraw, textLeftView, textPosY);
            textDrawn = true;
          }

          if (spaceForArrowsView <= distanceBetweenMarkersView) {
            var leftArrowStart;
            var rightArrowStart;
            if (textDrawn) {
              leftArrowStart = textLeftView - arrowSpacing;
              rightArrowStart = textRightView + arrowSpacing;
            } else {
              leftArrowStart = positionInMiddleOfMarkersView;
              rightArrowStart = positionInMiddleOfMarkersView;
            }
            // Draw left arrow.
            this.drawArrow_(ctx, leftArrowStart, arrowPosY,
                leftMarkerView, arrowPosY, arrowWidthView, arrowColor);
            // Draw right arrow.
            this.drawArrow_(ctx, rightArrowStart, arrowPosY,
                rightMarkerView, arrowPosY, arrowWidthView, arrowColor);
          }
        }
      }
    },

    /**
     * Adds items intersecting a point to a selection.
     * @param {number} vX X location to search at, in viewspace.
     * @param {number} vY Y location to search at, in viewspace.
     * @param {Selection} selection Selection to which to add hits.
     * @return {boolean} true if a slice was found, otherwise false.
     */
    addIntersectingItemsToSelection: function(vX, vY, selection) {
      // Does nothing. There's nothing interesting to pick on the ruler
      // track.
    },

    /**
     * Adds items intersecting the given range to a selection.
     * @param {number} loVX Lower X bound of the interval to search, in
     *     viewspace.
     * @param {number} hiVX Upper X bound of the interval to search, in
     *     viewspace.
     * @param {number} loVY Lower Y bound of the interval to search, in
     *     viewspace.
     * @param {number} hiVY Upper Y bound of the interval to search, in
     *     viewspace.
     * @param {Selection} selection Selection to which to add hits.
     */
    addIntersectingItemsInRangeToSelection: function(
        loVX, hiVX, loY, hiY, selection) {
      // Does nothing. There's nothing interesting to pick on the ruler
      // track.
    },

    addAllObjectsMatchingFilterToSelection: function(filter, selection) {
    }
  };

  return {
    RulerTrack: RulerTrack
  };
});