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