| <!DOCTYPE html> |
| <!-- |
| Copyright (c) 2013 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. |
| --> |
| |
| <link rel="stylesheet" href="/core/tracks/ruler_track.css"> |
| |
| <link rel="import" href="/core/constants.html"> |
| <link rel="import" href="/core/tracks/track.html"> |
| <link rel="import" href="/core/tracks/heading_track.html"> |
| <link rel="import" href="/core/draw_helpers.html"> |
| <link rel="import" href="/base/ui.html"> |
| |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.c.tracks', function() { |
| /** |
| * A track that displays the ruler. |
| * @constructor |
| * @extends {HeadingTrack} |
| */ |
| var RulerTrack = tr.b.ui.define('ruler-track', tr.c.tracks.HeadingTrack); |
| |
| var logOf10 = Math.log(10); |
| function log10(x) { |
| return Math.log(x) / logOf10; |
| } |
| |
| RulerTrack.prototype = { |
| __proto__: tr.c.tracks.HeadingTrack.prototype, |
| |
| decorate: function(viewport) { |
| tr.c.tracks.HeadingTrack.prototype.decorate.call(this, viewport); |
| this.classList.add('ruler-track'); |
| this.strings_secs_ = []; |
| this.strings_msecs_ = []; |
| this.strings_usecs_ = []; |
| this.strings_nsecs_ = []; |
| |
| this.viewportChange_ = this.viewportChange_.bind(this); |
| viewport.addEventListener('change', this.viewportChange_); |
| |
| }, |
| |
| detach: function() { |
| tr.c.tracks.HeadingTrack.prototype.detach.call(this); |
| this.viewport.removeEventListener('change', |
| this.viewportChange_); |
| }, |
| |
| viewportChange_: function() { |
| if (this.viewport.interestRange.isEmpty) |
| this.classList.remove('tall-mode'); |
| else |
| this.classList.add('tall-mode'); |
| }, |
| |
| draw: function(type, viewLWorld, viewRWorld) { |
| switch (type) { |
| case tr.c.tracks.DrawType.GRID: |
| this.drawGrid_(viewLWorld, viewRWorld); |
| break; |
| case tr.c.tracks.DrawType.MARKERS: |
| if (!this.viewport.interestRange.isEmpty) |
| this.viewport.interestRange.draw(this.context(), |
| viewLWorld, viewRWorld); |
| break; |
| } |
| }, |
| |
| drawGrid_: function(viewLWorld, viewRWorld) { |
| var ctx = this.context(); |
| var pixelRatio = window.devicePixelRatio || 1; |
| |
| var canvasBounds = ctx.canvas.getBoundingClientRect(); |
| var trackBounds = this.getBoundingClientRect(); |
| var width = canvasBounds.width * pixelRatio; |
| var height = trackBounds.height * pixelRatio; |
| |
| var hasInterestRange = !this.viewport.interestRange.isEmpty; |
| |
| var rulerHeight = hasInterestRange ? (height * 2) / 5 : height; |
| |
| var vp = this.viewport; |
| var dt = vp.currentDisplayTransform; |
| |
| var idealMajorMarkDistancePix = 150 * pixelRatio; |
| var idealMajorMarkDistanceWorld = |
| dt.xViewVectorToWorld(idealMajorMarkDistancePix); |
| |
| var majorMarkDistanceWorld; |
| |
| // 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 (dt.xWorldVectorToView(tightenedGuess) < idealMajorMarkDistancePix) |
| continue; |
| majorMarkDistanceWorld = conservativeGuess / divisors[i - 1]; |
| break; |
| } |
| |
| var unit; |
| var unitDivisor; |
| var tickLabels = undefined; |
| if (majorMarkDistanceWorld < 0.0001) { |
| unit = 'ns'; |
| unitDivisor = 0.000001; |
| tickLabels = this.strings_nsecs_; |
| } else if (majorMarkDistanceWorld < 0.1) { |
| unit = 'us'; |
| unitDivisor = 0.001; |
| tickLabels = this.strings_usecs_; |
| } else 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 = dt.xWorldVectorToView(minorMarkDistanceWorld); |
| |
| var firstMajorMark = |
| Math.floor(viewLWorld / majorMarkDistanceWorld) * |
| majorMarkDistanceWorld; |
| |
| var minorTickH = Math.floor(rulerHeight * 0.25); |
| |
| ctx.save(); |
| |
| var pixelRatio = window.devicePixelRatio || 1; |
| ctx.lineWidth = Math.round(pixelRatio); |
| |
| // Apply subpixel translate to get crisp lines. |
| // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ |
| var crispLineCorrection = (ctx.lineWidth % 2) / 2; |
| ctx.translate(crispLineCorrection, -crispLineCorrection); |
| |
| ctx.fillStyle = 'rgb(0, 0, 0)'; |
| ctx.strokeStyle = 'rgb(0, 0, 0)'; |
| ctx.textAlign = 'left'; |
| ctx.textBaseline = 'top'; |
| |
| ctx.font = (9 * pixelRatio) + 'px sans-serif'; |
| |
| vp.majorMarkPositions = []; |
| |
| // 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. |
| ctx.beginPath(); |
| for (var curX = firstMajorMark; |
| curX < viewRWorld; |
| curX += majorMarkDistanceWorld) { |
| |
| var curXView = Math.floor(dt.xWorldToView(curX)); |
| |
| var unitValue = curX / unitDivisor; |
| var roundedUnitValue = Math.round(unitValue * 100000) / 100000; |
| |
| if (!tickLabels[roundedUnitValue]) |
| tickLabels[roundedUnitValue] = roundedUnitValue + ' ' + unit; |
| ctx.fillText(tickLabels[roundedUnitValue], |
| curXView + (2 * pixelRatio), 0); |
| |
| vp.majorMarkPositions.push(curXView); |
| |
| // Major mark |
| tr.c.drawLine(ctx, curXView, 0, curXView, rulerHeight); |
| |
| // Minor marks |
| for (var i = 1; i < numTicksPerMajor; ++i) { |
| var xView = Math.floor(curXView + minorMarkDistancePx * i); |
| tr.c.drawLine(ctx, |
| xView, rulerHeight - minorTickH, |
| xView, rulerHeight); |
| } |
| } |
| |
| // Draw bottom bar. |
| ctx.strokeStyle = 'rgb(0, 0, 0)'; |
| tr.c.drawLine(ctx, 0, height, width, height); |
| ctx.stroke(); |
| |
| // Give distance between directly adjacent markers. |
| if (!hasInterestRange) |
| return; |
| |
| // Draw middle bar. |
| tr.c.drawLine(ctx, 0, rulerHeight, width, rulerHeight); |
| ctx.stroke(); |
| |
| // Distance Variables. |
| var displayDistance; |
| var displayTextColor = 'rgb(0,0,0)'; |
| |
| // Arrow Variables. |
| var arrowSpacing = 10 * pixelRatio; |
| var arrowColor = 'rgb(128,121,121)'; |
| var arrowPosY = rulerHeight * 1.75; |
| var arrowWidthView = 3 * pixelRatio; |
| var arrowLengthView = 10 * pixelRatio; |
| var spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing); |
| |
| ctx.textBaseline = 'middle'; |
| ctx.font = (14 * pixelRatio) + 'px sans-serif'; |
| var textPosY = arrowPosY; |
| |
| var interestRange = vp.interestRange; |
| |
| // If the range is zero, draw it's min timestamp next to the line. |
| if (interestRange.range === 0) { |
| var markerWorld = interestRange.min; |
| var markerView = dt.xWorldToView(markerWorld); |
| var displayValue = markerWorld / unitDivisor; |
| displayValue = Math.abs((Math.round(displayValue * 1000) / 1000)); |
| |
| var textToDraw = displayValue + ' ' + unit; |
| var textLeftView = markerView + 4 * pixelRatio; |
| var textWidthView = ctx.measureText(textToDraw).width; |
| |
| // Put text to the left in case it gets cut off. |
| if (textLeftView + textWidthView > width) |
| textLeftView = markerView - 4 * pixelRatio - textWidthView; |
| |
| ctx.fillStyle = displayTextColor; |
| ctx.fillText(textToDraw, textLeftView, textPosY); |
| return; |
| } |
| |
| var leftMarker = interestRange.min; |
| var rightMarker = interestRange.max; |
| |
| var leftMarkerView = dt.xWorldToView(leftMarker); |
| var rightMarkerView = dt.xWorldToView(rightMarker); |
| |
| var distanceBetweenMarkers = interestRange.range; |
| var distanceBetweenMarkersView = |
| dt.xWorldVectorToView(distanceBetweenMarkers); |
| var positionInMiddleOfMarkersView = |
| leftMarkerView + (distanceBetweenMarkersView / 2); |
| |
| // Determine units. |
| if (distanceBetweenMarkers < 0.0001) { |
| unit = 'ns'; |
| unitDivisor = 0.000001; |
| } else if (distanceBetweenMarkers < 0.1) { |
| unit = 'us'; |
| unitDivisor = 0.001; |
| } else 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.round(displayDistance * 1000) / 1000)); |
| var textToDraw = roundedDisplayDistance + ' ' + unit; |
| var textWidthView = ctx.measureText(textToDraw).width; |
| var spaceForArrowsAndTextView = |
| textWidthView + spaceForArrowsView + arrowSpacing; |
| |
| // Set text positions. |
| var textLeftView = positionInMiddleOfMarkersView - textWidthView / 2; |
| var textRightView = textLeftView + textWidthView; |
| |
| if (spaceForArrowsAndTextView > distanceBetweenMarkersView) { |
| // Print the display distance text right of the 2 markers. |
| textLeftView = rightMarkerView + 2 * arrowSpacing; |
| |
| // Put text to the left in case it gets cut off. |
| if (textLeftView + textWidthView > width) |
| textLeftView = leftMarkerView - 2 * arrowSpacing - textWidthView; |
| |
| ctx.fillStyle = displayTextColor; |
| ctx.fillText(textToDraw, textLeftView, textPosY); |
| |
| // Draw the arrows pointing from outside in and a line in between. |
| ctx.strokeStyle = arrowColor; |
| ctx.beginPath(); |
| tr.c.drawLine(ctx, leftMarkerView, arrowPosY, rightMarkerView, |
| arrowPosY); |
| ctx.stroke(); |
| |
| ctx.fillStyle = arrowColor; |
| tr.c.drawArrow(ctx, |
| leftMarkerView - 1.5 * arrowSpacing, arrowPosY, |
| leftMarkerView, arrowPosY, |
| arrowLengthView, arrowWidthView); |
| tr.c.drawArrow(ctx, |
| rightMarkerView + 1.5 * arrowSpacing, arrowPosY, |
| rightMarkerView, arrowPosY, |
| arrowLengthView, arrowWidthView); |
| |
| } else if (spaceForArrowsView <= distanceBetweenMarkersView) { |
| var leftArrowStart; |
| var rightArrowStart; |
| if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) { |
| // Print the display distance text. |
| ctx.fillStyle = displayTextColor; |
| ctx.fillText(textToDraw, textLeftView, textPosY); |
| |
| leftArrowStart = textLeftView - arrowSpacing; |
| rightArrowStart = textRightView + arrowSpacing; |
| } else { |
| leftArrowStart = positionInMiddleOfMarkersView; |
| rightArrowStart = positionInMiddleOfMarkersView; |
| } |
| |
| // Draw the arrows pointing inside out. |
| ctx.strokeStyle = arrowColor; |
| ctx.fillStyle = arrowColor; |
| tr.c.drawArrow(ctx, |
| leftArrowStart, arrowPosY, |
| leftMarkerView, arrowPosY, |
| arrowLengthView, arrowWidthView); |
| tr.c.drawArrow(ctx, |
| rightArrowStart, arrowPosY, |
| rightMarkerView, arrowPosY, |
| arrowLengthView, arrowWidthView); |
| } |
| |
| ctx.restore(); |
| }, |
| |
| /** |
| * 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 results. |
| */ |
| addIntersectingEventsInRangeToSelection: function( |
| loVX, hiVX, loY, hiY, selection) { |
| // Does nothing. There's nothing interesting to pick on the ruler |
| // track. |
| }, |
| |
| addAllEventsMatchingFilterToSelection: function(filter, selection) { |
| } |
| }; |
| |
| return { |
| RulerTrack: RulerTrack |
| }; |
| }); |
| </script> |