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