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 {TimeSpan} from '../common/time';
16
17import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
18import {TimeScale} from './time_scale';
19
20export const DESIRED_PX_PER_STEP = 80;
21
22/**
23 * Returns the step size of a grid line in seconds.
24 * The returned step size has two properties:
25 * (1) It is 1, 2, or 5, multiplied by some integer power of 10.
26 * (2) The number steps in |range| produced by |stepSize| is as close as
27 *     possible to |desiredSteps|.
28 */
29export function getGridStepSize(range: number, desiredSteps: number): number {
30  // First, get the largest possible power of 10 that is smaller than the
31  // desired step size, and set it to the current step size.
32  // For example, if the range is 2345ms and the desired steps is 10, then the
33  // desired step size is 234.5 and the step size will be set to 100.
34  const desiredStepSize = range / desiredSteps;
35  const zeros = Math.floor(Math.log10(desiredStepSize));
36  const initialStepSize = Math.pow(10, zeros);
37
38  // This function first calculates how many steps within the range a certain
39  // stepSize will produce, and returns the difference between that and
40  // desiredSteps.
41  const distToDesired = (evaluatedStepSize: number) =>
42      Math.abs(range / evaluatedStepSize - desiredSteps);
43
44  // We know that |initialStepSize| is a power of 10, and
45  // initialStepSize <= desiredStepSize <= 10 * initialStepSize. There are four
46  // possible candidates for final step size: 1, 2, 5 or 10 * initialStepSize.
47  // We pick the candidate that minimizes distToDesired(stepSize).
48  const stepSizeMultipliers = [2, 5, 10];
49
50  let minimalDistance = distToDesired(initialStepSize);
51  let minimizingStepSize = initialStepSize;
52
53  for (const multiplier of stepSizeMultipliers) {
54    const newStepSize = multiplier * initialStepSize;
55    const newDistance = distToDesired(newStepSize);
56    if (newDistance < minimalDistance) {
57      minimalDistance = newDistance;
58      minimizingStepSize = newStepSize;
59    }
60  }
61  return minimizingStepSize;
62}
63
64/**
65 * Generator that returns that (given a width im px, span, and scale) returns
66 * pairs of [xInPx, timestampInS] pairs describing where gridlines should be
67 * drawn.
68 */
69export function gridlines(width: number, span: TimeSpan, timescale: TimeScale):
70    Array<[number, number]> {
71  const desiredSteps = width / DESIRED_PX_PER_STEP;
72  const step = getGridStepSize(span.duration, desiredSteps);
73  const actualSteps = Math.floor(span.duration / step);
74  const start = Math.round(span.start / step) * step;
75  const lines: Array<[number, number]> = [];
76  let previousTimestamp = Number.NEGATIVE_INFINITY;
77  // Iterating over the number of steps instead of
78  // for (let s = start; s < span.end; s += step) because if start is very large
79  // number and step very small, s will never reach end.
80  for (let i = 0; i <= actualSteps; i++) {
81    let xPos = TRACK_SHELL_WIDTH;
82    const timestamp = start + i * step;
83    xPos += Math.floor(timescale.timeToPx(timestamp));
84    if (xPos < TRACK_SHELL_WIDTH) continue;
85    if (xPos > width) break;
86    if (Math.abs(timestamp - previousTimestamp) > Number.EPSILON) {
87      previousTimestamp = timestamp;
88      lines.push([xPos, timestamp]);
89    }
90  }
91  return lines;
92}
93
94export function drawGridLines(
95    ctx: CanvasRenderingContext2D,
96    x: TimeScale,
97    timeSpan: TimeSpan,
98    width: number,
99    height: number): void {
100  ctx.strokeStyle = TRACK_BORDER_COLOR;
101  ctx.lineWidth = 1;
102
103  for (const xAndTime of gridlines(width, timeSpan, x)) {
104    ctx.beginPath();
105    ctx.moveTo(xAndTime[0] + 0.5, 0);
106    ctx.lineTo(xAndTime[0] + 0.5, height);
107    ctx.stroke();
108  }
109}
110