Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 1 | <!DOCTYPE html> |
| 2 | <!-- |
| 3 | Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| 4 | Use of this source code is governed by a BSD-style license that can be |
| 5 | found in the LICENSE file. |
| 6 | --> |
| 7 | |
| 8 | <link rel="stylesheet" href="/core/tracks/ruler_track.css"> |
| 9 | |
| 10 | <link rel="import" href="/core/constants.html"> |
| 11 | <link rel="import" href="/core/tracks/track.html"> |
| 12 | <link rel="import" href="/core/tracks/heading_track.html"> |
| 13 | <link rel="import" href="/core/draw_helpers.html"> |
| 14 | <link rel="import" href="/base/ui.html"> |
| 15 | |
| 16 | <script> |
| 17 | 'use strict'; |
| 18 | |
| 19 | tv.exportTo('tv.c.tracks', function() { |
| 20 | /** |
| 21 | * A track that displays the ruler. |
| 22 | * @constructor |
| 23 | * @extends {HeadingTrack} |
| 24 | */ |
| 25 | var RulerTrack = tv.b.ui.define('ruler-track', tv.c.tracks.HeadingTrack); |
| 26 | |
| 27 | var logOf10 = Math.log(10); |
| 28 | function log10(x) { |
| 29 | return Math.log(x) / logOf10; |
| 30 | } |
| 31 | |
| 32 | RulerTrack.prototype = { |
| 33 | __proto__: tv.c.tracks.HeadingTrack.prototype, |
| 34 | |
| 35 | decorate: function(viewport) { |
| 36 | tv.c.tracks.HeadingTrack.prototype.decorate.call(this, viewport); |
| 37 | this.classList.add('ruler-track'); |
| 38 | this.strings_secs_ = []; |
| 39 | this.strings_msecs_ = []; |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame^] | 40 | this.strings_usecs_ = []; |
| 41 | this.strings_nsecs_ = []; |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 42 | |
| 43 | this.viewportChange_ = this.viewportChange_.bind(this); |
| 44 | viewport.addEventListener('change', this.viewportChange_); |
| 45 | |
| 46 | }, |
| 47 | |
| 48 | detach: function() { |
| 49 | tv.c.tracks.HeadingTrack.prototype.detach.call(this); |
| 50 | this.viewport.removeEventListener('change', |
| 51 | this.viewportChange_); |
| 52 | }, |
| 53 | |
| 54 | viewportChange_: function() { |
| 55 | if (this.viewport.interestRange.isEmpty) |
| 56 | this.classList.remove('tall-mode'); |
| 57 | else |
| 58 | this.classList.add('tall-mode'); |
| 59 | }, |
| 60 | |
| 61 | draw: function(type, viewLWorld, viewRWorld) { |
| 62 | switch (type) { |
| 63 | case tv.c.tracks.DrawType.GRID: |
| 64 | this.drawGrid_(viewLWorld, viewRWorld); |
| 65 | break; |
| 66 | case tv.c.tracks.DrawType.MARKERS: |
| 67 | if (!this.viewport.interestRange.isEmpty) |
| 68 | this.viewport.interestRange.draw(this.context(), |
| 69 | viewLWorld, viewRWorld); |
| 70 | break; |
| 71 | } |
| 72 | }, |
| 73 | |
| 74 | drawGrid_: function(viewLWorld, viewRWorld) { |
| 75 | var ctx = this.context(); |
| 76 | var pixelRatio = window.devicePixelRatio || 1; |
| 77 | |
| 78 | var canvasBounds = ctx.canvas.getBoundingClientRect(); |
| 79 | var trackBounds = this.getBoundingClientRect(); |
| 80 | var width = canvasBounds.width * pixelRatio; |
| 81 | var height = trackBounds.height * pixelRatio; |
| 82 | |
| 83 | var hasInterestRange = !this.viewport.interestRange.isEmpty; |
| 84 | |
| 85 | var rulerHeight = hasInterestRange ? (height * 2) / 5 : height; |
| 86 | |
| 87 | var vp = this.viewport; |
| 88 | var dt = vp.currentDisplayTransform; |
| 89 | |
| 90 | var idealMajorMarkDistancePix = 150 * pixelRatio; |
| 91 | var idealMajorMarkDistanceWorld = |
| 92 | dt.xViewVectorToWorld(idealMajorMarkDistancePix); |
| 93 | |
| 94 | var majorMarkDistanceWorld; |
| 95 | |
| 96 | // The conservative guess is the nearest enclosing 0.1, 1, 10, 100, etc. |
| 97 | var conservativeGuess = |
| 98 | Math.pow(10, Math.ceil(log10(idealMajorMarkDistanceWorld))); |
| 99 | |
| 100 | // Once we have a conservative guess, consider things that evenly add up |
| 101 | // to the conservative guess, e.g. 0.5, 0.2, 0.1 Pick the one that still |
| 102 | // exceeds the ideal mark distance. |
| 103 | var divisors = [10, 5, 2, 1]; |
| 104 | for (var i = 0; i < divisors.length; ++i) { |
| 105 | var tightenedGuess = conservativeGuess / divisors[i]; |
| 106 | if (dt.xWorldVectorToView(tightenedGuess) < idealMajorMarkDistancePix) |
| 107 | continue; |
| 108 | majorMarkDistanceWorld = conservativeGuess / divisors[i - 1]; |
| 109 | break; |
| 110 | } |
| 111 | |
| 112 | var unit; |
| 113 | var unitDivisor; |
| 114 | var tickLabels = undefined; |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame^] | 115 | if (majorMarkDistanceWorld < 0.0001) { |
| 116 | unit = "ns" |
| 117 | unitDivisor = 0.000001; |
| 118 | tickLabels = this.strings_nsecs_; |
| 119 | } else if (majorMarkDistanceWorld < 0.1) { |
| 120 | unit = "us"; |
| 121 | unitDivisor = 0.001; |
| 122 | tickLabels = this.strings_usecs_; |
| 123 | } else if (majorMarkDistanceWorld < 100) { |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 124 | unit = 'ms'; |
| 125 | unitDivisor = 1; |
| 126 | tickLabels = this.strings_msecs_; |
| 127 | } else { |
| 128 | unit = 's'; |
| 129 | unitDivisor = 1000; |
| 130 | tickLabels = this.strings_secs_; |
| 131 | } |
| 132 | |
| 133 | var numTicksPerMajor = 5; |
| 134 | var minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor; |
| 135 | var minorMarkDistancePx = dt.xWorldVectorToView(minorMarkDistanceWorld); |
| 136 | |
| 137 | var firstMajorMark = |
| 138 | Math.floor(viewLWorld / majorMarkDistanceWorld) * |
| 139 | majorMarkDistanceWorld; |
| 140 | |
| 141 | var minorTickH = Math.floor(rulerHeight * 0.25); |
| 142 | |
| 143 | ctx.save(); |
| 144 | |
| 145 | var pixelRatio = window.devicePixelRatio || 1; |
| 146 | ctx.lineWidth = Math.round(pixelRatio); |
| 147 | |
| 148 | // Apply subpixel translate to get crisp lines. |
| 149 | // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ |
| 150 | var crispLineCorrection = (ctx.lineWidth % 2) / 2; |
| 151 | ctx.translate(crispLineCorrection, -crispLineCorrection); |
| 152 | |
| 153 | ctx.fillStyle = 'rgb(0, 0, 0)'; |
| 154 | ctx.strokeStyle = 'rgb(0, 0, 0)'; |
| 155 | ctx.textAlign = 'left'; |
| 156 | ctx.textBaseline = 'top'; |
| 157 | |
| 158 | ctx.font = (9 * pixelRatio) + 'px sans-serif'; |
| 159 | |
| 160 | vp.majorMarkPositions = []; |
| 161 | |
| 162 | // Each iteration of this loop draws one major mark |
| 163 | // and numTicksPerMajor minor ticks. |
| 164 | // |
| 165 | // Rendering can't be done in world space because canvas transforms |
| 166 | // affect line width. So, do the conversions manually. |
| 167 | ctx.beginPath(); |
| 168 | for (var curX = firstMajorMark; |
| 169 | curX < viewRWorld; |
| 170 | curX += majorMarkDistanceWorld) { |
| 171 | |
| 172 | var curXView = Math.floor(dt.xWorldToView(curX)); |
| 173 | |
| 174 | var unitValue = curX / unitDivisor; |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame^] | 175 | var roundedUnitValue = Math.round(unitValue * 100000) / 100000; |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 176 | |
| 177 | if (!tickLabels[roundedUnitValue]) |
| 178 | tickLabels[roundedUnitValue] = roundedUnitValue + ' ' + unit; |
| 179 | ctx.fillText(tickLabels[roundedUnitValue], |
| 180 | curXView + (2 * pixelRatio), 0); |
| 181 | |
| 182 | vp.majorMarkPositions.push(curXView); |
| 183 | |
| 184 | // Major mark |
| 185 | tv.c.drawLine(ctx, curXView, 0, curXView, rulerHeight); |
| 186 | |
| 187 | // Minor marks |
| 188 | for (var i = 1; i < numTicksPerMajor; ++i) { |
| 189 | var xView = Math.floor(curXView + minorMarkDistancePx * i); |
| 190 | tv.c.drawLine(ctx, |
| 191 | xView, rulerHeight - minorTickH, |
| 192 | xView, rulerHeight); |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | // Draw bottom bar. |
| 197 | ctx.strokeStyle = 'rgb(0, 0, 0)'; |
| 198 | tv.c.drawLine(ctx, 0, height, width, height); |
| 199 | ctx.stroke(); |
| 200 | |
| 201 | // Give distance between directly adjacent markers. |
| 202 | if (!hasInterestRange) |
| 203 | return; |
| 204 | |
| 205 | // Draw middle bar. |
| 206 | tv.c.drawLine(ctx, 0, rulerHeight, width, rulerHeight); |
| 207 | ctx.stroke(); |
| 208 | |
| 209 | // Distance Variables. |
| 210 | var displayDistance; |
| 211 | var displayTextColor = 'rgb(0,0,0)'; |
| 212 | |
| 213 | // Arrow Variables. |
| 214 | var arrowSpacing = 10 * pixelRatio; |
| 215 | var arrowColor = 'rgb(128,121,121)'; |
| 216 | var arrowPosY = rulerHeight * 1.75; |
| 217 | var arrowWidthView = 3 * pixelRatio; |
| 218 | var arrowLengthView = 10 * pixelRatio; |
| 219 | var spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing); |
| 220 | |
| 221 | ctx.textBaseline = 'middle'; |
| 222 | ctx.font = (14 * pixelRatio) + 'px sans-serif'; |
| 223 | var textPosY = arrowPosY; |
| 224 | |
| 225 | var interestRange = vp.interestRange; |
| 226 | |
| 227 | // If the range is zero, draw it's min timestamp next to the line. |
| 228 | if (interestRange.range === 0) { |
| 229 | var markerWorld = interestRange.min; |
| 230 | var markerView = dt.xWorldToView(markerWorld); |
| 231 | var displayValue = markerWorld / unitDivisor; |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame^] | 232 | displayValue = Math.abs((Math.round(displayValue * 1000) / 1000)); |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 233 | |
| 234 | var textToDraw = displayValue + ' ' + unit; |
| 235 | var textLeftView = markerView + 4 * pixelRatio; |
| 236 | var textWidthView = ctx.measureText(textToDraw).width; |
| 237 | |
| 238 | // Put text to the left in case it gets cut off. |
| 239 | if (textLeftView + textWidthView > width) |
| 240 | textLeftView = markerView - 4 * pixelRatio - textWidthView; |
| 241 | |
| 242 | ctx.fillStyle = displayTextColor; |
| 243 | ctx.fillText(textToDraw, textLeftView, textPosY); |
| 244 | return; |
| 245 | } |
| 246 | |
| 247 | var leftMarker = interestRange.min; |
| 248 | var rightMarker = interestRange.max; |
| 249 | |
| 250 | var leftMarkerView = dt.xWorldToView(leftMarker); |
| 251 | var rightMarkerView = dt.xWorldToView(rightMarker); |
| 252 | |
| 253 | var distanceBetweenMarkers = interestRange.range; |
| 254 | var distanceBetweenMarkersView = |
| 255 | dt.xWorldVectorToView(distanceBetweenMarkers); |
| 256 | var positionInMiddleOfMarkersView = |
| 257 | leftMarkerView + (distanceBetweenMarkersView / 2); |
| 258 | |
| 259 | // Determine units. |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame^] | 260 | if (distanceBetweenMarkers < 0.0001) { |
| 261 | unit = "ns"; |
| 262 | unitDivisor = 0.000001; |
| 263 | } else if (distanceBetweenMarkers < 0.1) { |
| 264 | unit = "us"; |
| 265 | unitDivisor = 0.001; |
| 266 | } else if (distanceBetweenMarkers < 100) { |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 267 | unit = 'ms'; |
| 268 | unitDivisor = 1; |
| 269 | } else { |
| 270 | unit = 's'; |
| 271 | unitDivisor = 1000; |
| 272 | } |
| 273 | |
| 274 | // Calculate display value to print. |
| 275 | displayDistance = distanceBetweenMarkers / unitDivisor; |
| 276 | var roundedDisplayDistance = |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame^] | 277 | Math.abs((Math.round(displayDistance * 1000) / 1000)); |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 278 | var textToDraw = roundedDisplayDistance + ' ' + unit; |
| 279 | var textWidthView = ctx.measureText(textToDraw).width; |
| 280 | var spaceForArrowsAndTextView = |
| 281 | textWidthView + spaceForArrowsView + arrowSpacing; |
| 282 | |
| 283 | // Set text positions. |
| 284 | var textLeftView = positionInMiddleOfMarkersView - textWidthView / 2; |
| 285 | var textRightView = textLeftView + textWidthView; |
| 286 | |
| 287 | if (spaceForArrowsAndTextView > distanceBetweenMarkersView) { |
| 288 | // Print the display distance text right of the 2 markers. |
| 289 | textLeftView = rightMarkerView + 2 * arrowSpacing; |
| 290 | |
| 291 | // Put text to the left in case it gets cut off. |
| 292 | if (textLeftView + textWidthView > width) |
| 293 | textLeftView = leftMarkerView - 2 * arrowSpacing - textWidthView; |
| 294 | |
| 295 | ctx.fillStyle = displayTextColor; |
| 296 | ctx.fillText(textToDraw, textLeftView, textPosY); |
| 297 | |
| 298 | // Draw the arrows pointing from outside in and a line in between. |
| 299 | ctx.strokeStyle = arrowColor; |
| 300 | ctx.beginPath(); |
| 301 | tv.c.drawLine(ctx, leftMarkerView, arrowPosY, rightMarkerView, |
| 302 | arrowPosY); |
| 303 | ctx.stroke(); |
| 304 | |
| 305 | ctx.fillStyle = arrowColor; |
| 306 | tv.c.drawArrow(ctx, |
| 307 | leftMarkerView - 1.5 * arrowSpacing, arrowPosY, |
| 308 | leftMarkerView, arrowPosY, |
| 309 | arrowLengthView, arrowWidthView); |
| 310 | tv.c.drawArrow(ctx, |
| 311 | rightMarkerView + 1.5 * arrowSpacing, arrowPosY, |
| 312 | rightMarkerView, arrowPosY, |
| 313 | arrowLengthView, arrowWidthView); |
| 314 | |
| 315 | } else if (spaceForArrowsView <= distanceBetweenMarkersView) { |
| 316 | var leftArrowStart; |
| 317 | var rightArrowStart; |
| 318 | if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) { |
| 319 | // Print the display distance text. |
| 320 | ctx.fillStyle = displayTextColor; |
| 321 | ctx.fillText(textToDraw, textLeftView, textPosY); |
| 322 | |
| 323 | leftArrowStart = textLeftView - arrowSpacing; |
| 324 | rightArrowStart = textRightView + arrowSpacing; |
| 325 | } else { |
| 326 | leftArrowStart = positionInMiddleOfMarkersView; |
| 327 | rightArrowStart = positionInMiddleOfMarkersView; |
| 328 | } |
| 329 | |
| 330 | // Draw the arrows pointing inside out. |
| 331 | ctx.strokeStyle = arrowColor; |
| 332 | ctx.fillStyle = arrowColor; |
| 333 | tv.c.drawArrow(ctx, |
| 334 | leftArrowStart, arrowPosY, |
| 335 | leftMarkerView, arrowPosY, |
| 336 | arrowLengthView, arrowWidthView); |
| 337 | tv.c.drawArrow(ctx, |
| 338 | rightArrowStart, arrowPosY, |
| 339 | rightMarkerView, arrowPosY, |
| 340 | arrowLengthView, arrowWidthView); |
| 341 | } |
| 342 | |
| 343 | ctx.restore(); |
| 344 | }, |
| 345 | |
| 346 | /** |
| 347 | * Adds items intersecting the given range to a selection. |
| 348 | * @param {number} loVX Lower X bound of the interval to search, in |
| 349 | * viewspace. |
| 350 | * @param {number} hiVX Upper X bound of the interval to search, in |
| 351 | * viewspace. |
| 352 | * @param {number} loVY Lower Y bound of the interval to search, in |
| 353 | * viewspace. |
| 354 | * @param {number} hiVY Upper Y bound of the interval to search, in |
| 355 | * viewspace. |
| 356 | * @param {Selection} selection Selection to which to add results. |
| 357 | */ |
| 358 | addIntersectingItemsInRangeToSelection: function( |
| 359 | loVX, hiVX, loY, hiY, selection) { |
| 360 | // Does nothing. There's nothing interesting to pick on the ruler |
| 361 | // track. |
| 362 | }, |
| 363 | |
| 364 | addAllObjectsMatchingFilterToSelection: function(filter, selection) { |
| 365 | } |
| 366 | }; |
| 367 | |
| 368 | return { |
| 369 | RulerTrack: RulerTrack |
| 370 | }; |
| 371 | }); |
| 372 | </script> |