|  | // 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 {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<number, TrackPanelInfo>; | 
|  | groupIdToTrackGroupPanel: Map<string, TrackGroupPanelInfo>; | 
|  |  | 
|  | constructor(public canvasWidth: number, public canvasHeight: number) { | 
|  | this.trackIdToTrackPanel = new Map<number, TrackPanelInfo>(); | 
|  | this.groupIdToTrackGroupPanel = new Map<string, TrackGroupPanelInfo>(); | 
|  | } | 
|  |  | 
|  | 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 = globals.state.uiTrackIdByTraceTrackId[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.state.highlightedSliceId || | 
|  | flow.begin.sliceId === globals.state.highlightedSliceId; | 
|  | const focused = flow.id === globals.state.focusedFlowIdLeft || | 
|  | flow.id === globals.state.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 hasArrowHead = Math.abs(begin.x - end.x) > 3 * TRIANGLE_SIZE; | 
|  | const END_OFFSET = | 
|  | (((end.dir === 'RIGHT' || end.dir === 'LEFT') && hasArrowHead) ? | 
|  | 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 if (hasArrowHead) { | 
|  | this.drawArrowHead(end, ctx, color); | 
|  | } | 
|  | } | 
|  |  | 
|  | private drawArrowHead( | 
|  | end: {x: number; y: number; dir: LineDirection}, | 
|  | ctx: CanvasRenderingContext2D, color: string) { | 
|  | 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(); | 
|  | } | 
|  | } |