// 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 }; });