• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright 2020, The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17
18/**
19 * Represents a continuous section of the timeline that is rendered into the
20 * timeline svg.
21 */
22class Block {
23  /**
24   * Create a block.
25   * @param {number} startPos - The start position of the block as a percentage
26   * of the timeline width.
27   * @param {number} width - The width of the block as a percentage of the
28   * timeline width.
29   */
30  constructor(startPos, width) {
31    this.startPos = startPos;
32    this.width = width;
33  }
34}
35
36/**
37 * This Mixin should only be injected into components which have the following:
38 * - An element in the template referenced as 'timeline' (this.$refs.timeline).
39 */
40export default {
41  name: 'timeline',
42  props: {
43    /**
44     * A 'timeline' as an array of timestamps
45     */
46    'timeline': {
47      type: Array,
48    },
49    /**
50     * A scale factor is an array of two elements, the min and max timestamps of
51     * the timeline
52     */
53    'scale': {
54      type: Array,
55    },
56  },
57  data() {
58    return {
59      /**
60       * Is a number representing the percentage of the timeline a block should
61       * be at a minimum or what percentage of the timeline a single entry takes
62       * up when rendered.
63       */
64      pointWidth: 1,
65    };
66  },
67  computed: {
68    /**
69     * Converts the timeline (list of timestamps) to an array of blocks to be
70     * displayed. This is to have fewer elements in the rendered timeline.
71     * Instead of having one rect for each timestamp in the timeline we only
72     * have one for each continuous segment of the timeline. This is to improve
73     * both the Vue patching step's performance and the DOM rendering
74     * performance.
75     */
76    timelineBlocks() {
77      const blocks = [];
78
79      // The difference in time between two timestamps after which they are no
80      // longer rendered as a continuous segment/block.
81      const overlapDistanceInTs = (this.scale[1] - this.scale[0]) *
82        ((this.crop?.right ?? 1) - (this.crop?.left ?? 0)) *
83        1 / (100 - this.pointWidth);
84
85      let blockStartTs = this.timeline[0];
86      for (let i = 1; i < this.timeline.length; i++) {
87        const lastTs = this.timeline[i - 1];
88        const ts = this.timeline[i];
89        if (ts - lastTs > overlapDistanceInTs) {
90          const block = this.generateTimelineBlock(blockStartTs, lastTs);
91          blocks.push(block);
92          blockStartTs = ts;
93        }
94      }
95
96      const blockEndTs = this.timeline[this.timeline.length - 1];
97      const block = this.generateTimelineBlock(blockStartTs, blockEndTs);
98      blocks.push(block);
99
100      return Object.freeze(blocks);
101    },
102  },
103  methods: {
104    position(item) {
105      let pos;
106      pos = this.translate(item);
107      pos = this.applyCrop(pos);
108
109      return pos * (100 - this.pointWidth);
110    },
111
112    translate(cx) {
113      const scale = [...this.scale];
114      if (scale[0] >= scale[1]) {
115        return cx;
116      }
117
118      return (cx - scale[0]) / (scale[1] - scale[0]);
119    },
120
121    untranslate(pos) {
122      const scale = [...this.scale];
123      if (scale[0] >= scale[1]) {
124        return pos;
125      }
126
127      return pos * (scale[1] - scale[0]) + scale[0];
128    },
129
130    applyCrop(cx) {
131      if (!this.crop) {
132        return cx;
133      }
134
135      return (cx - this.crop.left) / (this.crop.right - this.crop.left);
136    },
137
138    unapplyCrop(pos) {
139      if (!this.crop) {
140        return pos;
141      }
142
143      return pos * (this.crop.right - this.crop.left) + this.crop.left;
144    },
145
146    /**
147     * Converts a position as a percentage of the timeline width to a timestamp.
148     * @param {number} position - target position as a percentage of the
149     *                            timeline's width.
150     * @return {number} The index of the closest timestamp in the timeline to
151     *                  the target position.
152     */
153    positionToTsIndex(position) {
154      let targetTimestamp = position / (100 - this.pointWidth);
155      targetTimestamp = this.unapplyCrop(targetTimestamp);
156      targetTimestamp = this.untranslate(targetTimestamp);
157
158      // The index of the timestamp in the timeline that is closest to the
159      // targetTimestamp.
160      const closestTsIndex = this.findClosestTimestampIndexTo(targetTimestamp);
161
162      return closestTsIndex;
163    },
164
165    indexOfClosestElementTo(target, array) {
166      let smallestDiff = Math.abs(target - array[0]);
167      let closestIndex = 0;
168      for (let i = 1; i < array.length; i++) {
169        const elem = array[i];
170        if (Math.abs(target - elem) < smallestDiff) {
171          closestIndex = i;
172          smallestDiff = Math.abs(target - elem);
173        }
174      }
175
176      return closestIndex;
177    },
178
179    findClosestTimestampIndexTo(ts) {
180      let left = 0;
181      let right = this.timeline.length - 1;
182      let mid = Math.floor((left + right) / 2);
183
184      while (left < right) {
185        if (ts < this.timeline[mid]) {
186          right = mid - 1;
187        } else if (ts > this.timeline[mid]) {
188          left = mid + 1;
189        } else {
190          return mid;
191        }
192        mid = Math.floor((left + right) / 2);
193      }
194
195      const candidateElements = this.timeline.slice(left - 1, right + 2);
196      const closestIndex =
197        this.indexOfClosestElementTo(ts, candidateElements) + (left - 1);
198      return closestIndex;
199    },
200
201    /**
202     * Transforms an absolute position in the timeline to a timestamp present in
203     * the timeline.
204     * @param {number} absolutePosition - Pixels from the left of the timeline.
205     * @return {number} The timestamp in the timeline that is closest to the
206     *                  target position.
207     */
208    absolutePositionAsTimestamp(absolutePosition) {
209      const timelineWidth = this.$refs.timeline.clientWidth;
210      const position = (absolutePosition / timelineWidth) * 100;
211
212      return this.timeline[this.positionToTsIndex(position)];
213    },
214
215    /**
216     * Handles the block click event.
217     * When a block in the timeline is clicked this function will determine
218     * the target timeline index and update the timeline to match this index.
219     * @param {MouseEvent} e - The mouse event of the click on a timeline block.
220     */
221    onBlockClick(e) {
222      const clickOffset = e.offsetX;
223      const timelineWidth = this.$refs.timeline.clientWidth;
224      const clickOffsetAsPercentage = (clickOffset / timelineWidth) * 100;
225
226      const clickedOnTsIndex =
227        this.positionToTsIndex(clickOffsetAsPercentage - this.pointWidth / 2);
228
229      if (this.disabled) {
230        return;
231      }
232      const timestamp = parseInt(this.timeline[clickedOnTsIndex]);
233      this.$store.dispatch('updateTimelineTime', timestamp);
234    },
235
236    /**
237     * Generate a block object that can be used by the timeline SVG to render
238     * a transformed block that starts at `startTs` and ends at `endTs`.
239     * @param {number} startTs - The timestamp at which the block starts.
240     * @param {number} endTs - The timestamp at which the block ends.
241     * @return {Block} A block object transformed to the timeline's crop and
242     *                 scale parameter.
243     */
244    generateTimelineBlock(startTs, endTs) {
245      const blockWidth = this.position(endTs) - this.position(startTs) +
246        this.pointWidth;
247      return Object.freeze(new Block(this.position(startTs), blockWidth));
248    },
249  },
250};
251