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="import" href="/tracing/base/range.html">
9<link rel="import" href="/tracing/model/event_set.html">
10<link rel="import" href="/tracing/model/slice.html">
11<link rel="import" href="/tracing/ui/base/ui.html">
12
13<script>
14'use strict';
15
16/**
17 * @fileoverview Provides the TimingTool class.
18 */
19tr.exportTo('tr.ui.b', function() {
20
21  /**
22   * Tool for taking time measurements in the TimelineTrackView using
23   * Viewportmarkers.
24   * @constructor
25   */
26  function TimingTool(viewport, targetElement) {
27    this.viewport_ = viewport;
28
29    // Prepare the event handlers to be added and removed repeatedly.
30    this.onMouseMove_ = this.onMouseMove_.bind(this);
31    this.onDblClick_ = this.onDblClick_.bind(this);
32    this.targetElement_ = targetElement;
33
34    // Valid only during mousedown.
35    this.isMovingLeftEdge_ = false;
36  };
37
38  TimingTool.prototype = {
39
40    onEnterTiming: function(e) {
41      this.targetElement_.addEventListener('mousemove', this.onMouseMove_);
42      this.targetElement_.addEventListener('dblclick', this.onDblClick_);
43    },
44
45    onBeginTiming: function(e) {
46      if (!this.isTouchPointInsideTrackBounds_(e.clientX, e.clientY))
47        return;
48
49      var pt = this.getSnappedToEventPosition_(e);
50      this.mouseDownAt_(pt.x, pt.y);
51
52      this.updateSnapIndicators_(pt);
53    },
54
55    updateSnapIndicators_: function(pt) {
56      if (!pt.snapped)
57        return;
58      var ir = this.viewport_.interestRange;
59      if (ir.min === pt.x)
60        ir.leftSnapIndicator = new tr.ui.SnapIndicator(pt.y, pt.height);
61      if (ir.max === pt.x)
62        ir.rightSnapIndicator = new tr.ui.SnapIndicator(pt.y, pt.height);
63    },
64
65    onUpdateTiming: function(e) {
66      var pt = this.getSnappedToEventPosition_(e);
67      this.mouseMoveAt_(pt.x, pt.y, true);
68      this.updateSnapIndicators_(pt);
69    },
70
71    onEndTiming: function(e) {
72      this.mouseUp_();
73    },
74
75    onExitTiming: function(e) {
76      this.targetElement_.removeEventListener('mousemove', this.onMouseMove_);
77      this.targetElement_.removeEventListener('dblclick', this.onDblClick_);
78    },
79
80    onMouseMove_: function(e) {
81      if (e.button)
82        return;
83      var worldX = this.getWorldXFromEvent_(e);
84      this.mouseMoveAt_(worldX, e.clientY, false);
85    },
86
87    onDblClick_: function(e) {
88      // TODO(nduca): Implement dobuleclicking.
89      console.error('not implemented');
90    },
91
92    ////////////////////////////////////////////////////////////////////////////
93
94    isTouchPointInsideTrackBounds_: function(clientX, clientY) {
95      if (!this.viewport_ ||
96          !this.viewport_.modelTrackContainer ||
97          !this.viewport_.modelTrackContainer.canvas)
98        return false;
99
100      var canvas = this.viewport_.modelTrackContainer.canvas;
101      var canvasRect = canvas.getBoundingClientRect();
102      if (clientX >= canvasRect.left && clientX <= canvasRect.right &&
103          clientY >= canvasRect.top && clientY <= canvasRect.bottom)
104        return true;
105
106      return false;
107    },
108
109    mouseDownAt_: function(worldX, y) {
110      var ir = this.viewport_.interestRange;
111      var dt = this.viewport_.currentDisplayTransform;
112
113      var pixelRatio = window.devicePixelRatio || 1;
114      var nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio);
115
116      if (ir.isEmpty) {
117        ir.setMinAndMax(worldX, worldX);
118        ir.rightSelected = true;
119        this.isMovingLeftEdge_ = false;
120        return;
121      }
122
123
124      // Left edge test.
125      if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) {
126        ir.leftSelected = true;
127        ir.min = worldX;
128        this.isMovingLeftEdge_ = true;
129        return;
130      }
131
132      // Right edge test.
133      if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) {
134        ir.rightSelected = true;
135        ir.max = worldX;
136        this.isMovingLeftEdge_ = false;
137        return;
138      }
139
140      ir.setMinAndMax(worldX, worldX);
141      ir.rightSelected = true;
142      this.isMovingLeftEdge_ = false;
143    },
144
145    mouseMoveAt_: function(worldX, y, mouseDown) {
146      var ir = this.viewport_.interestRange;
147
148      if (mouseDown) {
149        this.updateMovingEdge_(worldX);
150        return;
151      }
152
153      var ir = this.viewport_.interestRange;
154      var dt = this.viewport_.currentDisplayTransform;
155
156      var pixelRatio = window.devicePixelRatio || 1;
157      var nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio);
158
159      // Left edge test.
160      if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) {
161        ir.leftSelected = true;
162        ir.rightSelected = false;
163        return;
164      }
165
166      // Right edge test.
167      if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) {
168        ir.leftSelected = false;
169        ir.rightSelected = true;
170        return;
171      }
172
173      ir.leftSelected = false;
174      ir.rightSelected = false;
175      return;
176    },
177
178    updateMovingEdge_: function(newWorldX) {
179      var ir = this.viewport_.interestRange;
180      var a = ir.min;
181      var b = ir.max;
182      if (this.isMovingLeftEdge_)
183        a = newWorldX;
184      else
185        b = newWorldX;
186
187      if (a <= b)
188        ir.setMinAndMax(a, b);
189      else
190        ir.setMinAndMax(b, a);
191
192      if (ir.min == newWorldX) {
193        this.isMovingLeftEdge_ = true;
194        ir.leftSelected = true;
195        ir.rightSelected = false;
196      } else {
197        this.isMovingLeftEdge_ = false;
198        ir.leftSelected = false;
199        ir.rightSelected = true;
200      }
201    },
202
203    mouseUp_: function() {
204      var dt = this.viewport_.currentDisplayTransform;
205      var ir = this.viewport_.interestRange;
206
207      ir.leftSelected = false;
208      ir.rightSelected = false;
209
210      var pixelRatio = window.devicePixelRatio || 1;
211      var minWidthValue = dt.xViewVectorToWorld(2 * pixelRatio);
212      if (ir.range < minWidthValue)
213        ir.reset();
214    },
215
216    getWorldXFromEvent_: function(e) {
217      var pixelRatio = window.devicePixelRatio || 1;
218      var canvas = this.viewport_.modelTrackContainer.canvas;
219      var worldOffset = canvas.getBoundingClientRect().left;
220      var viewX = (e.clientX - worldOffset) * pixelRatio;
221      return this.viewport_.currentDisplayTransform.xViewToWorld(viewX);
222    },
223
224
225    /**
226     * Get the closest position of an event within a vertical range of the mouse
227     * position if possible, otherwise use the position of the mouse pointer.
228     * @param {MouseEvent} e Mouse event with the current mouse coordinates.
229     * @return {
230     *   {Number} x, The x coordinate in world space.
231     *   {Number} y, The y coordinate in world space.
232     *   {Number} height, The height of the event.
233     *   {boolean} snapped Whether the coordinates are from a snapped event or
234     *     the mouse position.
235     * }
236     */
237    getSnappedToEventPosition_: function(e) {
238      var pixelRatio = window.devicePixelRatio || 1;
239      var EVENT_SNAP_RANGE = 16 * pixelRatio;
240
241      var modelTrackContainer = this.viewport_.modelTrackContainer;
242      var modelTrackContainerRect = modelTrackContainer.getBoundingClientRect();
243
244      var viewport = this.viewport_;
245      var dt = viewport.currentDisplayTransform;
246      var worldMaxDist = dt.xViewVectorToWorld(EVENT_SNAP_RANGE);
247
248      var worldX = this.getWorldXFromEvent_(e);
249      var mouseY = e.clientY;
250
251      var selection = new tr.model.EventSet();
252
253      // Look at the track under mouse position first for better performance.
254      modelTrackContainer.addClosestEventToSelection(
255          worldX, worldMaxDist, mouseY, mouseY, selection);
256
257      // Look at all tracks visible on screen.
258      if (!selection.length) {
259        modelTrackContainer.addClosestEventToSelection(
260            worldX, worldMaxDist,
261            modelTrackContainerRect.top, modelTrackContainerRect.bottom,
262            selection);
263      }
264
265      var minDistX = worldMaxDist;
266      var minDistY = Infinity;
267      var pixWidth = dt.xViewVectorToWorld(1);
268
269      // Create result object with the mouse coordinates.
270      var result = {
271        x: worldX,
272        y: mouseY - modelTrackContainerRect.top,
273        height: 0,
274        snapped: false
275      };
276
277      var eventBounds = new tr.b.Range();
278      for (var i = 0; i < selection.length; i++) {
279        var event = selection[i];
280        var track = viewport.trackForEvent(event);
281        var trackRect = track.getBoundingClientRect();
282
283        eventBounds.reset();
284        event.addBoundsToRange(eventBounds);
285        var eventX;
286        if (Math.abs(eventBounds.min - worldX) <
287            Math.abs(eventBounds.max - worldX)) {
288          eventX = eventBounds.min;
289        } else {
290          eventX = eventBounds.max;
291        }
292
293        var distX = eventX - worldX;
294
295        var eventY = trackRect.top;
296        var eventHeight = trackRect.height;
297        var distY = Math.abs(eventY + eventHeight / 2 - mouseY);
298
299        // Prefer events with a closer y position if their x difference is below
300        // the width of a pixel.
301        if ((distX <= minDistX || Math.abs(distX - minDistX) < pixWidth) &&
302            distY < minDistY) {
303          minDistX = distX;
304          minDistY = distY;
305
306          // Retrieve the event position from the hit.
307          result.x = eventX;
308          result.y = eventY +
309              modelTrackContainer.scrollTop - modelTrackContainerRect.top;
310          result.height = eventHeight;
311          result.snapped = true;
312        }
313      }
314
315      return result;
316    }
317  };
318
319  return {
320    TimingTool: TimingTool
321  };
322});
323</script>
324