• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2018 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {assertTrue} from '../base/logging';
16import {duration, Span, time, Time} from '../base/time';
17
18import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
19import {globals} from './globals';
20import {TimeScale} from './time_scale';
21
22const micros = 1000n;
23const millis = 1000n * micros;
24const seconds = 1000n * millis;
25const minutes = 60n * seconds;
26const hours = 60n * minutes;
27const days = 24n * hours;
28
29// These patterns cover the entire range of 0 - 2^63-1 nanoseconds
30const patterns: [bigint, string][] = [
31  [1n, '|'],
32  [2n, '|:'],
33  [5n, '|....'],
34  [10n, '|....:....'],
35  [20n, '|.:.'],
36  [50n, '|....'],
37  [100n, '|....:....'],
38  [200n, '|.:.'],
39  [500n, '|....'],
40  [1n * micros, '|....:....'],
41  [2n * micros, '|.:.'],
42  [5n * micros, '|....'],
43  [10n * micros, '|....:....'],
44  [20n * micros, '|.:.'],
45  [50n * micros, '|....'],
46  [100n * micros, '|....:....'],
47  [200n * micros, '|.:.'],
48  [500n * micros, '|....'],
49  [1n * millis, '|....:....'],
50  [2n * millis, '|.:.'],
51  [5n * millis, '|....'],
52  [10n * millis, '|....:....'],
53  [20n * millis, '|.:.'],
54  [50n * millis, '|....'],
55  [100n * millis, '|....:....'],
56  [200n * millis, '|.:.'],
57  [500n * millis, '|....'],
58  [1n * seconds, '|....:....'],
59  [2n * seconds, '|.:.'],
60  [5n * seconds, '|....'],
61  [10n * seconds, '|....:....'],
62  [30n * seconds, '|.:.:.'],
63  [1n * minutes, '|.....'],
64  [2n * minutes, '|.:.'],
65  [5n * minutes, '|.....'],
66  [10n * minutes, '|....:....'],
67  [30n * minutes, '|.:.:.'],
68  [1n * hours, '|.....'],
69  [2n * hours, '|.:.'],
70  [6n * hours, '|.....'],
71  [12n * hours, '|.....:.....'],
72  [1n * days, '|.:.'],
73  [2n * days, '|.:.'],
74  [5n * days, '|....'],
75  [10n * days, '|....:....'],
76  [20n * days, '|.:.'],
77  [50n * days, '|....'],
78  [100n * days, '|....:....'],
79  [200n * days, '|.:.'],
80  [500n * days, '|....'],
81  [1000n * days, '|....:....'],
82  [2000n * days, '|.:.'],
83  [5000n * days, '|....'],
84  [10000n * days, '|....:....'],
85  [20000n * days, '|.:.'],
86  [50000n * days, '|....'],
87  [100000n * days, '|....:....'],
88  [200000n * days, '|.:.'],
89];
90
91// Returns the optimal step size and pattern of ticks within the step.
92export function getPattern(minPatternSize: bigint): [duration, string] {
93  for (const [size, pattern] of patterns) {
94    if (size >= minPatternSize) {
95      return [size, pattern];
96    }
97  }
98
99  throw new Error('Pattern not defined for this minsize');
100}
101
102function tickPatternToArray(pattern: string): TickType[] {
103  const array = Array.from(pattern);
104  return array.map((char) => {
105    switch (char) {
106      case '|':
107        return TickType.MAJOR;
108      case ':':
109        return TickType.MEDIUM;
110      case '.':
111        return TickType.MINOR;
112      default:
113        // This is almost certainly a developer/fat-finger error
114        throw Error(`Invalid char "${char}" in pattern "${pattern}"`);
115    }
116  });
117}
118
119export enum TickType {
120  MAJOR,
121  MEDIUM,
122  MINOR,
123}
124
125export interface Tick {
126  type: TickType;
127  time: time;
128}
129
130export const MIN_PX_PER_STEP = 120;
131export function getMaxMajorTicks(width: number) {
132  return Math.max(1, Math.floor(width / MIN_PX_PER_STEP));
133}
134
135// An iterable which generates a series of ticks for a given timescale.
136export class TickGenerator implements Iterable<Tick> {
137  private _tickPattern: TickType[];
138  private _patternSize: duration;
139  private _timeSpan: Span<time, duration>;
140  private _offset: time;
141
142  constructor(
143    timeSpan: Span<time, duration>,
144    maxMajorTicks: number,
145    offset: time = Time.ZERO,
146  ) {
147    assertTrue(timeSpan.duration > 0n, 'timeSpan.duration cannot be lte 0');
148    assertTrue(maxMajorTicks > 0, 'maxMajorTicks cannot be lte 0');
149
150    this._timeSpan = timeSpan.add(-offset);
151    this._offset = offset;
152    const minStepSize = BigInt(
153      Math.floor(Number(timeSpan.duration) / maxMajorTicks),
154    );
155    const [size, pattern] = getPattern(minStepSize);
156    this._patternSize = size;
157    this._tickPattern = tickPatternToArray(pattern);
158  }
159
160  // Returns an iterable, so this object can be iterated over directly using the
161  // `for x of y` notation. The use of a generator here is just to make things
162  // more elegant compared to creating an array of ticks and building an
163  // iterator for it.
164  *[Symbol.iterator](): Generator<Tick> {
165    const stepSize = this._patternSize / BigInt(this._tickPattern.length);
166    const start = Time.quantFloor(this._timeSpan.start, this._patternSize);
167    const end = this._timeSpan.end;
168    let patternIndex = 0;
169
170    for (
171      let time = start;
172      time < end;
173      time = Time.add(time, stepSize), patternIndex++
174    ) {
175      if (time >= this._timeSpan.start) {
176        patternIndex = patternIndex % this._tickPattern.length;
177        const type = this._tickPattern[patternIndex];
178        yield {type, time: Time.add(time, this._offset)};
179      }
180    }
181  }
182}
183
184// Gets the timescale associated with the current visible window.
185export function timeScaleForVisibleWindow(
186  startPx: number,
187  endPx: number,
188): TimeScale {
189  return globals.timeline.getTimeScale(startPx, endPx);
190}
191
192export function drawGridLines(
193  ctx: CanvasRenderingContext2D,
194  width: number,
195  height: number,
196): void {
197  ctx.strokeStyle = TRACK_BORDER_COLOR;
198  ctx.lineWidth = 1;
199
200  const span = globals.timeline.visibleTimeSpan;
201  if (width > TRACK_SHELL_WIDTH && span.duration > 0n) {
202    const maxMajorTicks = getMaxMajorTicks(width - TRACK_SHELL_WIDTH);
203    const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width);
204    const offset = globals.timestampOffset();
205    for (const {type, time} of new TickGenerator(span, maxMajorTicks, offset)) {
206      const px = Math.floor(map.timeToPx(time));
207      if (type === TickType.MAJOR) {
208        ctx.beginPath();
209        ctx.moveTo(px + 0.5, 0);
210        ctx.lineTo(px + 0.5, height);
211        ctx.stroke();
212      }
213    }
214  }
215}
216