blob: 7b853e2d69c7b153f2284d2ce16d42de9ff6773d [file] [log] [blame]
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {assertTrue} from '../base/logging';
import {duration, Span, time, Time} from '../base/time';
import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
import {globals} from './globals';
import {TimeScale} from './time_scale';
const micros = 1000n;
const millis = 1000n * micros;
const seconds = 1000n * millis;
const minutes = 60n * seconds;
const hours = 60n * minutes;
const days = 24n * hours;
// These patterns cover the entire range of 0 - 2^63-1 nanoseconds
const patterns: [bigint, string][] = [
[1n, '|'],
[2n, '|:'],
[5n, '|....'],
[10n, '|....:....'],
[20n, '|.:.'],
[50n, '|....'],
[100n, '|....:....'],
[200n, '|.:.'],
[500n, '|....'],
[1n * micros, '|....:....'],
[2n * micros, '|.:.'],
[5n * micros, '|....'],
[10n * micros, '|....:....'],
[20n * micros, '|.:.'],
[50n * micros, '|....'],
[100n * micros, '|....:....'],
[200n * micros, '|.:.'],
[500n * micros, '|....'],
[1n * millis, '|....:....'],
[2n * millis, '|.:.'],
[5n * millis, '|....'],
[10n * millis, '|....:....'],
[20n * millis, '|.:.'],
[50n * millis, '|....'],
[100n * millis, '|....:....'],
[200n * millis, '|.:.'],
[500n * millis, '|....'],
[1n * seconds, '|....:....'],
[2n * seconds, '|.:.'],
[5n * seconds, '|....'],
[10n * seconds, '|....:....'],
[30n * seconds, '|.:.:.'],
[1n * minutes, '|.....'],
[2n * minutes, '|.:.'],
[5n * minutes, '|.....'],
[10n * minutes, '|....:....'],
[30n * minutes, '|.:.:.'],
[1n * hours, '|.....'],
[2n * hours, '|.:.'],
[6n * hours, '|.....'],
[12n * hours, '|.....:.....'],
[1n * days, '|.:.'],
[2n * days, '|.:.'],
[5n * days, '|....'],
[10n * days, '|....:....'],
[20n * days, '|.:.'],
[50n * days, '|....'],
[100n * days, '|....:....'],
[200n * days, '|.:.'],
[500n * days, '|....'],
[1000n * days, '|....:....'],
[2000n * days, '|.:.'],
[5000n * days, '|....'],
[10000n * days, '|....:....'],
[20000n * days, '|.:.'],
[50000n * days, '|....'],
[100000n * days, '|....:....'],
[200000n * days, '|.:.'],
];
// Returns the optimal step size and pattern of ticks within the step.
export function getPattern(minPatternSize: bigint): [duration, string] {
for (const [size, pattern] of patterns) {
if (size >= minPatternSize) {
return [size, pattern];
}
}
throw new Error('Pattern not defined for this minsize');
}
function tickPatternToArray(pattern: string): TickType[] {
const array = Array.from(pattern);
return array.map((char) => {
switch (char) {
case '|':
return TickType.MAJOR;
case ':':
return TickType.MEDIUM;
case '.':
return TickType.MINOR;
default:
// This is almost certainly a developer/fat-finger error
throw Error(`Invalid char "${char}" in pattern "${pattern}"`);
}
});
}
export enum TickType {
MAJOR,
MEDIUM,
MINOR,
}
export interface Tick {
type: TickType;
time: time;
}
export const MIN_PX_PER_STEP = 120;
export function getMaxMajorTicks(width: number) {
return Math.max(1, Math.floor(width / MIN_PX_PER_STEP));
}
// An iterable which generates a series of ticks for a given timescale.
export class TickGenerator implements Iterable<Tick> {
private _tickPattern: TickType[];
private _patternSize: duration;
private _timeSpan: Span<time, duration>;
private _offset: time;
constructor(
timeSpan: Span<time, duration>,
maxMajorTicks: number,
offset: time = Time.ZERO,
) {
assertTrue(timeSpan.duration > 0n, 'timeSpan.duration cannot be lte 0');
assertTrue(maxMajorTicks > 0, 'maxMajorTicks cannot be lte 0');
this._timeSpan = timeSpan.add(-offset);
this._offset = offset;
const minStepSize = BigInt(
Math.floor(Number(timeSpan.duration) / maxMajorTicks),
);
const [size, pattern] = getPattern(minStepSize);
this._patternSize = size;
this._tickPattern = tickPatternToArray(pattern);
}
// Returns an iterable, so this object can be iterated over directly using the
// `for x of y` notation. The use of a generator here is just to make things
// more elegant compared to creating an array of ticks and building an
// iterator for it.
*[Symbol.iterator](): Generator<Tick> {
const stepSize = this._patternSize / BigInt(this._tickPattern.length);
const start = Time.quantFloor(this._timeSpan.start, this._patternSize);
const end = this._timeSpan.end;
let patternIndex = 0;
for (
let time = start;
time < end;
time = Time.add(time, stepSize), patternIndex++
) {
if (time >= this._timeSpan.start) {
patternIndex = patternIndex % this._tickPattern.length;
const type = this._tickPattern[patternIndex];
yield {type, time: Time.add(time, this._offset)};
}
}
}
}
// Gets the timescale associated with the current visible window.
export function timeScaleForVisibleWindow(
startPx: number,
endPx: number,
): TimeScale {
return globals.timeline.getTimeScale(startPx, endPx);
}
export function drawGridLines(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
): void {
ctx.strokeStyle = TRACK_BORDER_COLOR;
ctx.lineWidth = 1;
const span = globals.timeline.visibleTimeSpan;
if (width > TRACK_SHELL_WIDTH && span.duration > 0n) {
const maxMajorTicks = getMaxMajorTicks(width - TRACK_SHELL_WIDTH);
const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width);
const offset = globals.timestampOffset();
for (const {type, time} of new TickGenerator(span, maxMajorTicks, offset)) {
const px = Math.floor(map.timeToPx(time));
if (type === TickType.MAJOR) {
ctx.beginPath();
ctx.moveTo(px + 0.5, 0);
ctx.lineTo(px + 0.5, height);
ctx.stroke();
}
}
}
}