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