1// Copyright (C) 2020 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use size 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 {TRACK_SHELL_WIDTH} from './css_constants';
16import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel';
17import {Flow, FlowPoint, globals} from './globals';
18import {PanelVNode} from './panel';
19import {findUiTrackId} from './scroll_helper';
20import {SliceRect} from './track';
21import {TrackGroupPanel} from './track_group_panel';
22import {TrackPanel} from './track_panel';
23
24const TRACK_GROUP_CONNECTION_OFFSET = 5;
25const TRIANGLE_SIZE = 5;
26const CIRCLE_RADIUS = 3;
27const BEZIER_OFFSET = 30;
28
29const CONNECTED_FLOW_HUE = 10;
30const SELECTED_FLOW_HUE = 230;
31
32const DEFAULT_FLOW_WIDTH = 2;
33const FOCUSED_FLOW_WIDTH = 3;
34
35const HIGHLIGHTED_FLOW_INTENSITY = 45;
36const FOCUSED_FLOW_INTENSITY = 55;
37const DEFAULT_FLOW_INTENSITY = 70;
38
39type LineDirection = 'LEFT'|'RIGHT'|'UP'|'DOWN';
40type ConnectionType = 'TRACK'|'TRACK_GROUP';
41
42interface TrackPanelInfo {
43  panel: TrackPanel;
44  yStart: number;
45}
46
47interface TrackGroupPanelInfo {
48  panel: TrackGroupPanel;
49  yStart: number;
50  height: number;
51}
52
53function hasTrackId(obj: {}): obj is {trackId: number} {
54  return (obj as {trackId?: number}).trackId !== undefined;
55}
56
57function hasManyTrackIds(obj: {}): obj is {trackIds: number[]} {
58  return (obj as {trackIds?: number}).trackIds !== undefined;
59}
60
61function hasId(obj: {}): obj is {id: number} {
62  return (obj as {id?: number}).id !== undefined;
63}
64
65function hasTrackGroupId(obj: {}): obj is {trackGroupId: string} {
66  return (obj as {trackGroupId?: string}).trackGroupId !== undefined;
67}
68
69export class FlowEventsRendererArgs {
70  trackIdToTrackPanel: Map<number, TrackPanelInfo>;
71  groupIdToTrackGroupPanel: Map<string, TrackGroupPanelInfo>;
72
73  constructor(public canvasWidth: number, public canvasHeight: number) {
74    this.trackIdToTrackPanel = new Map<number, TrackPanelInfo>();
75    this.groupIdToTrackGroupPanel = new Map<string, TrackGroupPanelInfo>();
76  }
77
78  registerPanel(panel: PanelVNode, yStart: number, height: number) {
79    if (panel.state instanceof TrackPanel && hasId(panel.attrs)) {
80      const config = globals.state.tracks[panel.attrs.id].config;
81      if (hasTrackId(config)) {
82        this.trackIdToTrackPanel.set(
83            config.trackId, {panel: panel.state, yStart});
84      }
85      if (hasManyTrackIds(config)) {
86        for (const trackId of config.trackIds) {
87          this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart});
88        }
89      }
90    } else if (
91        panel.state instanceof TrackGroupPanel &&
92        hasTrackGroupId(panel.attrs)) {
93      this.groupIdToTrackGroupPanel.set(
94          panel.attrs.trackGroupId, {panel: panel.state, yStart, height});
95    }
96  }
97}
98
99export class FlowEventsRenderer {
100  private getTrackGroupIdByTrackId(trackId: number): string|undefined {
101    const uiTrackId = findUiTrackId(trackId);
102    return uiTrackId ? globals.state.tracks[uiTrackId].trackGroup : undefined;
103  }
104
105  private getTrackGroupYCoordinate(
106      args: FlowEventsRendererArgs, trackId: number): number|undefined {
107    const trackGroupId = this.getTrackGroupIdByTrackId(trackId);
108    if (!trackGroupId) {
109      return undefined;
110    }
111    const trackGroupInfo = args.groupIdToTrackGroupPanel.get(trackGroupId);
112    if (!trackGroupInfo) {
113      return undefined;
114    }
115    return trackGroupInfo.yStart + trackGroupInfo.height -
116        TRACK_GROUP_CONNECTION_OFFSET;
117  }
118
119  private getTrackYCoordinate(args: FlowEventsRendererArgs, trackId: number):
120      number|undefined {
121    return args.trackIdToTrackPanel.get(trackId) ?.yStart;
122  }
123
124  private getYConnection(
125      args: FlowEventsRendererArgs, trackId: number,
126      rect?: SliceRect): {y: number, connection: ConnectionType}|undefined {
127    if (!rect) {
128      const y = this.getTrackGroupYCoordinate(args, trackId);
129      if (y === undefined) {
130        return undefined;
131      }
132      return {y, connection: 'TRACK_GROUP'};
133    }
134    const y = (this.getTrackYCoordinate(args, trackId) || 0) + rect.top +
135        rect.height * 0.5;
136
137    return {
138      y: Math.min(Math.max(0, y), args.canvasHeight),
139      connection: 'TRACK'
140    };
141  }
142
143  private getXCoordinate(ts: number): number {
144    return globals.frontendLocalState.timeScale.timeToPx(ts);
145  }
146
147  private getSliceRect(args: FlowEventsRendererArgs, point: FlowPoint):
148      SliceRect|undefined {
149    const trackPanel = args.trackIdToTrackPanel.get(point.trackId) ?.panel;
150    if (!trackPanel) {
151      return undefined;
152    }
153    return trackPanel.getSliceRect(
154        point.sliceStartTs, point.sliceEndTs, point.depth);
155  }
156
157  render(ctx: CanvasRenderingContext2D, args: FlowEventsRendererArgs) {
158    ctx.save();
159    ctx.translate(TRACK_SHELL_WIDTH, 0);
160    ctx.rect(0, 0, args.canvasWidth - TRACK_SHELL_WIDTH, args.canvasHeight);
161    ctx.clip();
162
163    globals.connectedFlows.forEach(flow => {
164      this.drawFlow(ctx, args, flow, CONNECTED_FLOW_HUE);
165    });
166
167    globals.selectedFlows.forEach(flow => {
168      const categories = getFlowCategories(flow);
169      for (const cat of categories) {
170        if (globals.visibleFlowCategories.get(cat) ||
171            globals.visibleFlowCategories.get(ALL_CATEGORIES)) {
172          this.drawFlow(ctx, args, flow, SELECTED_FLOW_HUE);
173          break;
174        }
175      }
176    });
177
178    ctx.restore();
179  }
180
181  private drawFlow(
182      ctx: CanvasRenderingContext2D, args: FlowEventsRendererArgs, flow: Flow,
183      hue: number) {
184    const beginSliceRect = this.getSliceRect(args, flow.begin);
185    const endSliceRect = this.getSliceRect(args, flow.end);
186
187    const beginYConnection =
188        this.getYConnection(args, flow.begin.trackId, beginSliceRect);
189    const endYConnection =
190        this.getYConnection(args, flow.end.trackId, endSliceRect);
191
192    if (!beginYConnection || !endYConnection) {
193      return;
194    }
195
196    let beginDir: LineDirection = 'LEFT';
197    let endDir: LineDirection = 'RIGHT';
198    if (beginYConnection.connection === 'TRACK_GROUP') {
199      beginDir = beginYConnection.y > endYConnection.y ? 'DOWN' : 'UP';
200    }
201    if (endYConnection.connection === 'TRACK_GROUP') {
202      endDir = endYConnection.y > beginYConnection.y ? 'DOWN' : 'UP';
203    }
204
205    const begin = {
206      x: this.getXCoordinate(flow.begin.sliceEndTs),
207      y: beginYConnection.y,
208      dir: beginDir
209    };
210    const end = {
211      x: this.getXCoordinate(flow.end.sliceStartTs),
212      y: endYConnection.y,
213      dir: endDir
214    };
215    const highlighted =
216        flow.end.sliceId === globals.frontendLocalState.highlightedSliceId ||
217        flow.begin.sliceId === globals.frontendLocalState.highlightedSliceId;
218    const focused = flow.id === globals.frontendLocalState.focusedFlowIdLeft ||
219        flow.id === globals.frontendLocalState.focusedFlowIdRight;
220
221    let intensity = DEFAULT_FLOW_INTENSITY;
222    let width = DEFAULT_FLOW_WIDTH;
223    if (focused) {
224      intensity = FOCUSED_FLOW_INTENSITY;
225      width = FOCUSED_FLOW_WIDTH;
226    }
227    if (highlighted) {
228      intensity = HIGHLIGHTED_FLOW_INTENSITY;
229    }
230    this.drawFlowArrow(ctx, begin, end, hue, intensity, width);
231  }
232
233  private getDeltaX(dir: LineDirection, offset: number): number {
234    switch (dir) {
235      case 'LEFT':
236        return -offset;
237      case 'RIGHT':
238        return offset;
239      case 'UP':
240        return 0;
241      case 'DOWN':
242        return 0;
243      default:
244        return 0;
245    }
246  }
247
248  private getDeltaY(dir: LineDirection, offset: number): number {
249    switch (dir) {
250      case 'LEFT':
251        return 0;
252      case 'RIGHT':
253        return 0;
254      case 'UP':
255        return -offset;
256      case 'DOWN':
257        return offset;
258      default:
259        return 0;
260    }
261  }
262
263  private drawFlowArrow(
264      ctx: CanvasRenderingContext2D,
265      begin: {x: number, y: number, dir: LineDirection},
266      end: {x: number, y: number, dir: LineDirection}, hue: number,
267      intensity: number, width: number) {
268    const END_OFFSET =
269        (end.dir === 'RIGHT' || end.dir === 'LEFT' ? TRIANGLE_SIZE : 0);
270    const color = `hsl(${hue}, 50%, ${intensity}%)`;
271    // draw curved line from begin to end (bezier curve)
272    ctx.strokeStyle = color;
273    ctx.lineWidth = width;
274    ctx.beginPath();
275    ctx.moveTo(begin.x, begin.y);
276    ctx.bezierCurveTo(
277        begin.x - this.getDeltaX(begin.dir, BEZIER_OFFSET),
278        begin.y - this.getDeltaY(begin.dir, BEZIER_OFFSET),
279        end.x - this.getDeltaX(end.dir, BEZIER_OFFSET + END_OFFSET),
280        end.y - this.getDeltaY(end.dir, BEZIER_OFFSET + END_OFFSET),
281        end.x - this.getDeltaX(end.dir, END_OFFSET),
282        end.y - this.getDeltaY(end.dir, END_OFFSET));
283    ctx.stroke();
284
285    // TODO (andrewbb): probably we should add a parameter 'MarkerType' to be
286    // able to choose what marker we want to draw _before_ the function call.
287    // e.g. triangle, circle, square?
288    if (begin.dir !== 'RIGHT' && begin.dir !== 'LEFT') {
289      // draw a circle if we the line has a vertical connection
290      ctx.fillStyle = color;
291      ctx.beginPath();
292      ctx.arc(begin.x, begin.y, 3, 0, 2 * Math.PI);
293      ctx.closePath();
294      ctx.fill();
295    }
296
297
298    if (end.dir !== 'RIGHT' && end.dir !== 'LEFT') {
299      // draw a circle if we the line has a vertical connection
300      ctx.fillStyle = color;
301      ctx.beginPath();
302      ctx.arc(end.x, end.y, CIRCLE_RADIUS, 0, 2 * Math.PI);
303      ctx.closePath();
304      ctx.fill();
305    } else {
306      const dx = this.getDeltaX(end.dir, TRIANGLE_SIZE);
307      const dy = this.getDeltaY(end.dir, TRIANGLE_SIZE);
308      // draw small triangle
309      ctx.fillStyle = color;
310      ctx.beginPath();
311      ctx.moveTo(end.x, end.y);
312      ctx.lineTo(end.x - dx - dy, end.y + dx - dy);
313      ctx.lineTo(end.x - dx + dy, end.y - dx - dy);
314      ctx.closePath();
315      ctx.fill();
316    }
317  }
318}
319