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