// Copyright (C) 2020 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use size 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 {TRACK_SHELL_WIDTH} from './css_constants'; import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel'; import {Flow, FlowPoint, globals} from './globals'; import {PanelVNode} from './panel'; import {findUiTrackId} from './scroll_helper'; import {SliceRect} from './track'; import {TrackGroupPanel} from './track_group_panel'; import {TrackPanel} from './track_panel'; const TRACK_GROUP_CONNECTION_OFFSET = 5; const TRIANGLE_SIZE = 5; const CIRCLE_RADIUS = 3; const BEZIER_OFFSET = 30; const CONNECTED_FLOW_HUE = 10; const SELECTED_FLOW_HUE = 230; const DEFAULT_FLOW_WIDTH = 2; const FOCUSED_FLOW_WIDTH = 3; const HIGHLIGHTED_FLOW_INTENSITY = 45; const FOCUSED_FLOW_INTENSITY = 55; const DEFAULT_FLOW_INTENSITY = 70; type LineDirection = 'LEFT'|'RIGHT'|'UP'|'DOWN'; type ConnectionType = 'TRACK'|'TRACK_GROUP'; interface TrackPanelInfo { panel: TrackPanel; yStart: number; } interface TrackGroupPanelInfo { panel: TrackGroupPanel; yStart: number; height: number; } function hasTrackId(obj: {}): obj is {trackId: number} { return (obj as {trackId?: number}).trackId !== undefined; } function hasManyTrackIds(obj: {}): obj is {trackIds: number[]} { return (obj as {trackIds?: number}).trackIds !== undefined; } function hasId(obj: {}): obj is {id: number} { return (obj as {id?: number}).id !== undefined; } function hasTrackGroupId(obj: {}): obj is {trackGroupId: string} { return (obj as {trackGroupId?: string}).trackGroupId !== undefined; } export class FlowEventsRendererArgs { trackIdToTrackPanel: Map; groupIdToTrackGroupPanel: Map; constructor(public canvasWidth: number, public canvasHeight: number) { this.trackIdToTrackPanel = new Map(); this.groupIdToTrackGroupPanel = new Map(); } registerPanel(panel: PanelVNode, yStart: number, height: number) { if (panel.state instanceof TrackPanel && hasId(panel.attrs)) { const config = globals.state.tracks[panel.attrs.id].config; if (hasTrackId(config)) { this.trackIdToTrackPanel.set( config.trackId, {panel: panel.state, yStart}); } if (hasManyTrackIds(config)) { for (const trackId of config.trackIds) { this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart}); } } } else if ( panel.state instanceof TrackGroupPanel && hasTrackGroupId(panel.attrs)) { this.groupIdToTrackGroupPanel.set( panel.attrs.trackGroupId, {panel: panel.state, yStart, height}); } } } export class FlowEventsRenderer { private getTrackGroupIdByTrackId(trackId: number): string|undefined { const uiTrackId = findUiTrackId(trackId); return uiTrackId ? globals.state.tracks[uiTrackId].trackGroup : undefined; } private getTrackGroupYCoordinate( args: FlowEventsRendererArgs, trackId: number): number|undefined { const trackGroupId = this.getTrackGroupIdByTrackId(trackId); if (!trackGroupId) { return undefined; } const trackGroupInfo = args.groupIdToTrackGroupPanel.get(trackGroupId); if (!trackGroupInfo) { return undefined; } return trackGroupInfo.yStart + trackGroupInfo.height - TRACK_GROUP_CONNECTION_OFFSET; } private getTrackYCoordinate(args: FlowEventsRendererArgs, trackId: number): number|undefined { return args.trackIdToTrackPanel.get(trackId) ?.yStart; } private getYConnection( args: FlowEventsRendererArgs, trackId: number, rect?: SliceRect): {y: number, connection: ConnectionType}|undefined { if (!rect) { const y = this.getTrackGroupYCoordinate(args, trackId); if (y === undefined) { return undefined; } return {y, connection: 'TRACK_GROUP'}; } const y = (this.getTrackYCoordinate(args, trackId) || 0) + rect.top + rect.height * 0.5; return { y: Math.min(Math.max(0, y), args.canvasHeight), connection: 'TRACK' }; } private getXCoordinate(ts: number): number { return globals.frontendLocalState.timeScale.timeToPx(ts); } private getSliceRect(args: FlowEventsRendererArgs, point: FlowPoint): SliceRect|undefined { const trackPanel = args.trackIdToTrackPanel.get(point.trackId) ?.panel; if (!trackPanel) { return undefined; } return trackPanel.getSliceRect( point.sliceStartTs, point.sliceEndTs, point.depth); } render(ctx: CanvasRenderingContext2D, args: FlowEventsRendererArgs) { ctx.save(); ctx.translate(TRACK_SHELL_WIDTH, 0); ctx.rect(0, 0, args.canvasWidth - TRACK_SHELL_WIDTH, args.canvasHeight); ctx.clip(); globals.connectedFlows.forEach(flow => { this.drawFlow(ctx, args, flow, CONNECTED_FLOW_HUE); }); globals.selectedFlows.forEach(flow => { const categories = getFlowCategories(flow); for (const cat of categories) { if (globals.visibleFlowCategories.get(cat) || globals.visibleFlowCategories.get(ALL_CATEGORIES)) { this.drawFlow(ctx, args, flow, SELECTED_FLOW_HUE); break; } } }); ctx.restore(); } private drawFlow( ctx: CanvasRenderingContext2D, args: FlowEventsRendererArgs, flow: Flow, hue: number) { const beginSliceRect = this.getSliceRect(args, flow.begin); const endSliceRect = this.getSliceRect(args, flow.end); const beginYConnection = this.getYConnection(args, flow.begin.trackId, beginSliceRect); const endYConnection = this.getYConnection(args, flow.end.trackId, endSliceRect); if (!beginYConnection || !endYConnection) { return; } let beginDir: LineDirection = 'LEFT'; let endDir: LineDirection = 'RIGHT'; if (beginYConnection.connection === 'TRACK_GROUP') { beginDir = beginYConnection.y > endYConnection.y ? 'DOWN' : 'UP'; } if (endYConnection.connection === 'TRACK_GROUP') { endDir = endYConnection.y > beginYConnection.y ? 'DOWN' : 'UP'; } const begin = { x: this.getXCoordinate(flow.begin.sliceEndTs), y: beginYConnection.y, dir: beginDir }; const end = { x: this.getXCoordinate(flow.end.sliceStartTs), y: endYConnection.y, dir: endDir }; const highlighted = flow.end.sliceId === globals.frontendLocalState.highlightedSliceId || flow.begin.sliceId === globals.frontendLocalState.highlightedSliceId; const focused = flow.id === globals.frontendLocalState.focusedFlowIdLeft || flow.id === globals.frontendLocalState.focusedFlowIdRight; let intensity = DEFAULT_FLOW_INTENSITY; let width = DEFAULT_FLOW_WIDTH; if (focused) { intensity = FOCUSED_FLOW_INTENSITY; width = FOCUSED_FLOW_WIDTH; } if (highlighted) { intensity = HIGHLIGHTED_FLOW_INTENSITY; } this.drawFlowArrow(ctx, begin, end, hue, intensity, width); } private getDeltaX(dir: LineDirection, offset: number): number { switch (dir) { case 'LEFT': return -offset; case 'RIGHT': return offset; case 'UP': return 0; case 'DOWN': return 0; default: return 0; } } private getDeltaY(dir: LineDirection, offset: number): number { switch (dir) { case 'LEFT': return 0; case 'RIGHT': return 0; case 'UP': return -offset; case 'DOWN': return offset; default: return 0; } } private drawFlowArrow( ctx: CanvasRenderingContext2D, begin: {x: number, y: number, dir: LineDirection}, end: {x: number, y: number, dir: LineDirection}, hue: number, intensity: number, width: number) { const END_OFFSET = (end.dir === 'RIGHT' || end.dir === 'LEFT' ? TRIANGLE_SIZE : 0); const color = `hsl(${hue}, 50%, ${intensity}%)`; // draw curved line from begin to end (bezier curve) ctx.strokeStyle = color; ctx.lineWidth = width; ctx.beginPath(); ctx.moveTo(begin.x, begin.y); ctx.bezierCurveTo( begin.x - this.getDeltaX(begin.dir, BEZIER_OFFSET), begin.y - this.getDeltaY(begin.dir, BEZIER_OFFSET), end.x - this.getDeltaX(end.dir, BEZIER_OFFSET + END_OFFSET), end.y - this.getDeltaY(end.dir, BEZIER_OFFSET + END_OFFSET), end.x - this.getDeltaX(end.dir, END_OFFSET), end.y - this.getDeltaY(end.dir, END_OFFSET)); ctx.stroke(); // TODO (andrewbb): probably we should add a parameter 'MarkerType' to be // able to choose what marker we want to draw _before_ the function call. // e.g. triangle, circle, square? if (begin.dir !== 'RIGHT' && begin.dir !== 'LEFT') { // draw a circle if we the line has a vertical connection ctx.fillStyle = color; ctx.beginPath(); ctx.arc(begin.x, begin.y, 3, 0, 2 * Math.PI); ctx.closePath(); ctx.fill(); } if (end.dir !== 'RIGHT' && end.dir !== 'LEFT') { // draw a circle if we the line has a vertical connection ctx.fillStyle = color; ctx.beginPath(); ctx.arc(end.x, end.y, CIRCLE_RADIUS, 0, 2 * Math.PI); ctx.closePath(); ctx.fill(); } else { const dx = this.getDeltaX(end.dir, TRIANGLE_SIZE); const dy = this.getDeltaY(end.dir, TRIANGLE_SIZE); // draw small triangle ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(end.x, end.y); ctx.lineTo(end.x - dx - dy, end.y + dx - dy); ctx.lineTo(end.x - dx + dy, end.y - dx - dy); ctx.closePath(); ctx.fill(); } } }