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
7base.requireStylesheet('tracing.tracks.ruler_track');
8
9base.require('tracing.constants');
10base.require('tracing.tracks.track');
11base.require('tracing.tracks.heading_track');
12base.require('ui');
13
14base.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