• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2022 Huawei Device Co., Ltd.
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 */
15
16import { Graph } from './Graph';
17import { Rect } from './Rect';
18import { TimeRange } from './RangeRuler';
19import { Flag } from './Flag';
20import { ns2s, ns2x, randomRgbColor, TimerShaftElement } from '../TimerShaftElement';
21import { TraceRow } from '../base/TraceRow';
22import { SpApplication } from '../../../SpApplication';
23import { Utils } from '../base/Utils';
24
25export enum StType {
26  TEMP, //临时的
27  PERM, // 永久的
28}
29
30export class SlicesTime {
31  private _id: string;
32  startTime: number = 0;
33  endTime: number = 0;
34  startNS: number;
35  endNS: number;
36  color: string = '';
37  startX: number;
38  endX: number;
39  selected: boolean = true;
40  hidden: boolean = false;
41  text: string = '';
42  type: number = StType.PERM; // 默认类型为永久的
43  constructor(
44    startTime: number = 0,
45    endTime: number = 0,
46    startNS: number,
47    endNS: number,
48    startX: number,
49    endX: number,
50    color: string,
51    text: string,
52    selected: boolean = true
53  ) {
54    this._id = Utils.uuid();
55    this.startTime = startTime;
56    this.endTime = endTime;
57    this.startNS = startNS;
58    this.endNS = endNS;
59    this.color = color;
60    this.startX = startX;
61    this.endX = endX;
62    this.text = text;
63    this.selected = selected;
64  }
65
66  get id(): string {
67    return this._id;
68  }
69}
70const TRIWIDTH: number = 10; // 定义三角形的边长
71const TEXT_FONT: string = '12px Microsoft YaHei'; // 文本字体格式
72export class SportRuler extends Graph {
73  static isMouseInSportRuler = false;
74  public flagList: Array<Flag> = [];
75  public slicesTimeList: Array<SlicesTime> = [];
76  isRangeSelect: boolean = false; //region selection
77  private hoverFlag: Flag = new Flag(-1, 0, 0, 0, 0);
78  private lineColor: string | null = null;
79  private rulerW = 0;
80  private _range: TimeRange = {} as TimeRange;
81  private readonly notifyHandler:
82    | ((hoverFlag: Flag | undefined | null, selectFlag: Flag | undefined | null) => void)
83    | undefined;
84  private readonly flagClickHandler: ((flag: Flag | undefined | null) => void) | undefined;
85  private readonly rangeClickHandler: ((sliceTime: SlicesTime | undefined | null) => void) | undefined;
86  private invertedTriangleTime: number | null | undefined = null;
87  private slicesTime: {
88    startTime: number | null | undefined;
89    endTime: number | null | undefined;
90    color: string | null;
91  } | null = {
92    startTime: null,
93    endTime: null,
94    color: null,
95  };
96  private timerShaftEL: TimerShaftElement | undefined | null;
97  private timeArray: Array<number> = [];
98  private countArray: Array<number> = [];
99  private durArray: Array<number> = [];
100  constructor(
101    timerShaftEL: TimerShaftElement,
102    frame: Rect,
103    notifyHandler: (hoverFlag: Flag | undefined | null, selectFlag: Flag | undefined | null) => void,
104    flagClickHandler: (flag: Flag | undefined | null) => void,
105    rangeClickHandler: (sliceTime: SlicesTime | undefined | null) => void
106  ) {
107    super(timerShaftEL.canvas, timerShaftEL.ctx!, frame);
108    this.notifyHandler = notifyHandler;
109    this.flagClickHandler = flagClickHandler;
110    this.rangeClickHandler = rangeClickHandler;
111    this.timerShaftEL = timerShaftEL;
112  }
113
114  get range(): TimeRange {
115    return this._range;
116  }
117
118  set range(value: TimeRange) {
119    this._range = value;
120    this.draw();
121  }
122
123  set times(timeArray: Array<number>) {
124    this.timeArray = timeArray;
125  }
126
127  set counts(countArray: Array<number>) {
128    this.countArray = countArray;
129  }
130
131  set durations(durArray: Array<number>) {
132    this.durArray = durArray;
133  }
134
135  modifyFlagList(flag: Flag | null | undefined) {
136    if (flag) {
137      if (flag.hidden) {
138        let i = this.flagList.findIndex((it) => it.time == flag.time);
139        this.flagList.splice(i, 1);
140      } else {
141        let i = this.flagList.findIndex((it) => it.time == flag.time);
142        this.flagList[i] = flag;
143      }
144    } else {
145      this.flagList.forEach((it) => (it.selected = false));
146    }
147    this.draw();
148  }
149
150  modifySicesTimeList(slicestime: SlicesTime | null | undefined): void {
151    if (slicestime) {
152      let i = this.slicesTimeList.findIndex((it) => it.id == slicestime.id);
153      if (slicestime.hidden) {
154        this.slicesTimeList.splice(i, 1);
155        let selectionParam = this.timerShaftEL?.selectionMap.get(slicestime.id);
156        this.timerShaftEL?.selectionMap.delete(slicestime.id);
157        if (selectionParam) {
158          this.timerShaftEL?.selectionList.splice(this.timerShaftEL?.selectionList.indexOf(selectionParam), 1);
159        }
160      } else {
161        this.slicesTimeList[i] = slicestime;
162      }
163    } else {
164      this.slicesTimeList.forEach((it) => (it.selected = false));
165    }
166    this.draw();
167  }
168
169  draw(): void {
170    this.draBasicsRuler();
171    //绘制旗子
172    this.flagList.forEach((flagObj: Flag, b) => {
173      if (flagObj.time >= this.range.startNS && flagObj.time <= this.range.endNS) {
174        flagObj.x = Math.round(
175          (this.rulerW * (flagObj.time - this.range.startNS)) / (this.range.endNS - this.range.startNS)
176        );
177        this.drawFlag(flagObj.x, flagObj.color, flagObj.selected, flagObj.text, flagObj.type);
178      }
179    });
180    !this.hoverFlag.hidden && this.drawFlag(this.hoverFlag.x, this.hoverFlag.color, true, this.hoverFlag.text);
181    //If region selection is enabled, the serial number draws a line on the axis to show the length of the box selection
182    if (this.isRangeSelect) {
183      this.drawRangeSelect();
184    }
185    if (this.invertedTriangleTime != null && typeof this.invertedTriangleTime != undefined) {
186      this.drawInvertedTriangle(
187        this.invertedTriangleTime,
188        document.querySelector<SpApplication>('sp-application')!.dark ? '#FFFFFF' : '#000000'
189      );
190    }
191    this.slicesTimeList.forEach((slicesTime) => {
192      this.drawSlicesMarks(slicesTime);
193    });
194  }
195
196  draBasicsRuler(): void{
197    this.rulerW = this.canvas!.offsetWidth;
198    this.context2D.clearRect(this.frame.x, this.frame.y, this.frame.width, this.frame.height + 1);
199    this.context2D.beginPath();
200    this.lineColor = window.getComputedStyle(this.canvas!, null).getPropertyValue('color');
201    this.context2D.lineWidth = 1;
202    this.context2D.strokeStyle = this.lineColor; //"#dadada"
203    this.context2D.moveTo(this.frame.x, this.frame.y);
204    this.context2D.lineTo(this.frame.x + this.frame.width, this.frame.y);
205    this.context2D.stroke();
206    this.context2D.closePath();
207    this.context2D.beginPath();
208    this.context2D.strokeStyle = '#999999';
209    this.context2D.lineWidth = 3;
210    this.context2D.moveTo(this.frame.x, this.frame.y);
211    this.context2D.lineTo(this.frame.x, this.frame.y + this.frame.height);
212    this.context2D.stroke();
213    this.context2D.closePath();
214    this.context2D.beginPath();
215    this.context2D.strokeStyle = this.lineColor; //"#999999"
216    this.context2D.lineWidth = 1;
217    this.context2D.fillStyle = '#999999';
218    this.context2D.font = '8px sans-serif';
219    this.range.xs?.forEach((item, index) => {
220      this.context2D.moveTo(item, this.frame.y);
221      this.context2D.lineTo(item, this.frame.y + this.frame.height);
222      this.context2D.fillText(`${this.range.xsTxt[index]}`, item + 3, this.frame.y + 12);
223    });
224    this.context2D.stroke();
225    this.context2D.closePath();
226  }
227
228  private initRangeSelect() {
229    let range = TraceRow.rangeSelectObject;
230    this.context2D.beginPath();
231    if (document.querySelector<SpApplication>('sp-application')!.dark) {
232      this.context2D.strokeStyle = '#FFF';
233      this.context2D.fillStyle = '#FFF';
234    } else {
235      this.context2D.strokeStyle = '#000';
236      this.context2D.fillStyle = '#000';
237    }
238    let start_X = ns2x(range?.startNS || 0, this.range.startNS, this.range.endNS, this.range.totalNS, this.frame);
239    let endX = ns2x(range?.endNS || 0, this.range.startNS, this.range.endNS, this.range.totalNS, this.frame);
240    let lineWidth = endX - start_X;
241    let txt = ns2s((range?.endNS || 0) - (range?.startNS || 0));
242    this.context2D.moveTo(start_X, this.frame.y + 22);
243    this.context2D.lineTo(endX, this.frame.y + 22);
244    this.context2D.moveTo(start_X, this.frame.y + 22 - 5);
245    this.context2D.lineTo(start_X, this.frame.y + 22 + 5);
246    this.context2D.moveTo(endX, this.frame.y + 22 - 5);
247    this.context2D.lineTo(endX, this.frame.y + 22 + 5);
248    let textWidth = this.context2D.measureText(txt).width;
249    if (lineWidth > textWidth) {
250      this.context2D.fillText(`${txt}`, start_X + (lineWidth - textWidth) / 2, this.frame.y + 20);
251    } else {
252      if (endX + textWidth >= this.frame.width) {
253        this.context2D.fillText(`${txt}`, start_X - 5 - textWidth, this.frame.y + 20);
254      } else {
255        this.context2D.fillText(`${txt}`, endX + 5, this.frame.y + 20);
256      }
257    }
258  }
259
260  drawRangeSelect(): void{
261    this.initRangeSelect();
262    if (this.timeArray.length > 0 && TraceRow.rangeSelectObject) {
263      // 页面可视框选区域的宽度
264      let rangeSelectWidth = TraceRow.rangeSelectObject!.endX! - TraceRow.rangeSelectObject!.startX!;
265      // 每段宽度必须大于总数的宽度
266      // 10,2+8,2是线的宽度,8是留的空隙,不然很不好看
267      // 分section段
268      let section = Math.floor(
269        rangeSelectWidth / (this.context2D.measureText(String(this.timeArray.length)).width + 10)
270      );
271      // 最多画二十段
272      section < 20 ? (section = section) : (section = 20);
273      // 最少一段
274      section < 1 ? (section = 1) : (section = section);
275      // 框选泳道图并放大左右移动后,框选的部分区域会移出可视区域,
276      // TraceRow.rangeSelectObject的开始结束时间仍然是框选时的时间,要和this.range进行比较取可视框选范围的时间
277      let startNS;
278      let endNS;
279      TraceRow.rangeSelectObject!.startNS! > this.range.startNS
280        ? (startNS = TraceRow.rangeSelectObject!.startNS!)
281        : (startNS = this.range.startNS);
282      TraceRow.rangeSelectObject!.endNS! > this.range.endNS
283        ? (endNS = this.range.endNS)
284        : (endNS = TraceRow.rangeSelectObject!.endNS!);
285      // 每一格的时间
286      let sectionTime = (endNS - startNS) / section;
287      let countArr = new Uint32Array(section);
288      let count: number = 0; //某段时间的调用栈数量
289      const useIndex: number[] = [];
290      const isEbpf = this.durArray && this.durArray.length > 0;
291      for (let i = 1; i <= section; i++) {
292        count = 0;
293        for (let j = 0; j < this.timeArray.length; j++) {
294          if (isEbpf && useIndex.includes(j)) {
295            continue;
296          }
297          let inRange = this.freshInRange(j, startNS, sectionTime, i);
298          // 如果该时间小于第一个分割点的时间,计数加1,从而算出一段时间的时间数量
299          if (inRange) {
300            // nm统计模式则统计每个时间的count
301            if (this.countArray && this.countArray[j] > 0) {
302              count += this.countArray[j];
303            } else {
304              count++;
305            }
306            useIndex.push(j);
307            countArr[i - 1] = count;
308          } else {
309            // 如果遇到大于分割点的时间,就跳过该分割点,计算下一个分割点的时间点数量
310            continue;
311          }
312        }
313        this.drawRangeSelectFillText(rangeSelectWidth, section, i, countArr);
314      }
315    }
316    this.context2D.stroke();
317    this.context2D.closePath();
318  }
319
320  private drawRangeSelectFillText(rangeSelectWidth: number, section: number, i: number, countArr: Uint32Array){
321    let x = TraceRow.rangeSelectObject!.startX! + (rangeSelectWidth / section) * i;
322    if (i !== section) {
323      this.context2D.moveTo(x, this.frame.y + 22);
324      this.context2D.lineTo(x, this.frame.y + 22 + 5);
325    }
326    // 每一格的数量的数字宽度
327    let countTextWidth = this.context2D.measureText(String(countArr[i - 1])).width;
328    // 文本的开始位置 = 框选的开始位置 + 格数 + (一格的宽度 - 文本的宽度) / 2
329    let textY =
330      TraceRow.rangeSelectObject!.startX! +
331      (rangeSelectWidth / section) * (i - 1) +
332      (rangeSelectWidth / section - countTextWidth) / 2;
333    this.context2D.fillText(String(countArr[i - 1]), textY, this.frame.y + 22 + 12);
334  }
335
336  private freshInRange(j: number, startNS: number, sectionTime: number, i: number): boolean {
337    let inRange = false;
338    const itemTime = this.timeArray[j];
339    // ebpf需要考虑dur
340    if (this.durArray && this.durArray.length > 0) {
341      const dur = this.durArray[j];
342      if (itemTime === this.range.endNS) {
343        // 如果时间点刚好和时间轴结束时间一样会导致该时间点没有计数,所以此情况需要的判断条件要多个等号
344        inRange =
345          itemTime >= startNS + sectionTime * (i - 1) &&
346          itemTime <= startNS + sectionTime * i &&
347          itemTime >= this.range.startNS &&
348          itemTime <= this.range.endNS;
349      } else {
350        // 判断时间点是否在某时间段内时,一般情况下和左边界相同算在该时间段,和右边界相同算在下一段,
351        inRange =
352          itemTime + dur >= startNS + sectionTime * (i - 1) &&
353          itemTime < startNS + sectionTime * i &&
354          itemTime + dur >= this.range.startNS &&
355          itemTime < this.range.endNS;
356      }
357    } else {
358      if (itemTime === this.range.endNS) {
359        inRange =
360          itemTime >= startNS + sectionTime * (i - 1) &&
361          itemTime <= startNS + sectionTime * i &&
362          itemTime >= this.range.startNS &&
363          itemTime <= this.range.endNS;
364      } else {
365        inRange =
366          itemTime >= startNS + sectionTime * (i - 1) &&
367          itemTime < startNS + sectionTime * i &&
368          itemTime >= this.range.startNS &&
369          itemTime < this.range.endNS;
370      }
371    }
372    return inRange;
373  }
374
375  drawTriangle(time: number, type: string) {
376    if (time != null && typeof time != undefined) {
377      let i = this.flagList.findIndex((it) => it.time == time);
378      if (type == 'triangle') {
379        let triangle = this.flagList.findIndex((it) => it.type == type);
380        if (i !== -1) {
381          if (triangle !== -1) {
382            this.flagList[i].type == '' ? this.flagList.splice(triangle, 1) : '';
383          }
384        } else {
385          if (triangle == -1) {
386            this.flagList.forEach((it) => (it.selected = false));
387            this.flagList.push(new Flag(0, 125, 18, 18, time, randomRgbColor(), '', true, 'triangle'));
388          } else {
389            this.flagList.forEach((it) => (it.selected = false));
390            this.flagList[triangle].time = time;
391            this.flagList[triangle].selected = true;
392          }
393        }
394      } else if (type == 'square') {
395        if (i != -1) {
396          this.flagList[i].type = '';
397        } else {
398          let triangle = this.flagList.findIndex((it) => it.type == 'triangle');
399          if (triangle !== -1) {
400            this.flagList[triangle].type = '';
401            this.draw();
402            this.notifyHandler &&
403              this.notifyHandler(
404                !this.hoverFlag.hidden ? this.hoverFlag : null,
405                this.flagList.find((it) => it.selected) || null
406              );
407            return this.flagList[triangle].time;
408          }
409        }
410      } else if (type == 'inverted') {
411        this.invertedTriangleTime = time;
412      }
413      this.draw();
414      this.notifyHandler &&
415        this.notifyHandler(
416          !this.hoverFlag.hidden ? this.hoverFlag : null,
417          this.flagList.find((it) => it.selected) || null
418        );
419    }
420  }
421
422  removeTriangle(type: string) {
423    if (type == 'inverted') {
424      this.invertedTriangleTime = null;
425    }
426    this.draw();
427    this.notifyHandler &&
428      this.notifyHandler(
429        !this.hoverFlag.hidden ? this.hoverFlag : null,
430        this.flagList.find((it) => it.selected) || null
431      );
432  }
433
434  drawInvertedTriangle(time: number, color: string = '#000000') {
435    if (time != null && typeof time != undefined) {
436      let x = Math.round((this.rulerW * (time - this.range.startNS)) / (this.range.endNS - this.range.startNS));
437      this.context2D.beginPath();
438      this.context2D.fillStyle = color;
439      this.context2D.strokeStyle = color;
440      this.context2D.moveTo(x - 3, 141);
441      this.context2D.lineTo(x + 3, 141);
442      this.context2D.lineTo(x, 145);
443      this.context2D.fill();
444      this.context2D.closePath();
445      this.context2D.stroke();
446    }
447  }
448
449  setSlicesMark( startTime: number | null = null, endTime: number | null = null,
450    shiftKey: boolean | null = null
451  ): SlicesTime | null {
452    let findSlicesTime = this.slicesTimeList.find((it) => it.startTime === startTime && it.endTime === endTime);
453    if (findSlicesTime && this.slicesTimeList.length > 0) {
454      return null;
455    } else {
456      let newSlicestime: SlicesTime | null = null;
457      if (startTime != null && typeof startTime != undefined && endTime != null && typeof endTime != undefined) {
458        this.slicesTime = {
459          startTime: startTime <= endTime ? startTime : endTime,
460          endTime: startTime <= endTime ? endTime : startTime,
461          color: null,
462        };
463        let startX = Math.round(
464          (this.rulerW * (startTime - this.range.startNS)) / (this.range.endNS - this.range.startNS)
465        );
466        let endX = Math.round((this.rulerW * (endTime - this.range.startNS)) / (this.range.endNS - this.range.startNS));
467        this.slicesTime.color = randomRgbColor() || '#ff0000';
468        let text = '';
469        newSlicestime = new SlicesTime(
470          this.slicesTime.startTime || 0,
471          this.slicesTime.endTime || 0,
472          this.range.startNS,
473          this.range.endNS,
474          startX,
475          endX,
476          this.slicesTime.color,
477          text,
478          true
479        );
480        if (!shiftKey) {
481          this.clearTempSlicesTime(); // 清除临时对象
482          // 如果没有按下shift键,则把当前slicestime对象的类型设为临时类型。
483          newSlicestime.type = StType.TEMP;
484        }
485        this.slicesTimeList.forEach((slicestime) => (slicestime.selected = false));
486        newSlicestime.selected = true;
487        this.slicesTimeList.push(newSlicestime);
488      } else {
489        this.clearTempSlicesTime(); // 清除临时对象
490        this.slicesTime = { startTime: null, endTime: null, color: null };
491      }
492      this.range.slicesTime = this.slicesTime;
493      this.draw();
494      this.timerShaftEL?.render();
495      return newSlicestime;
496    }
497  }
498
499  // 清除临时对象
500  clearTempSlicesTime() {
501    // 清除以前放入的临时对象
502    this.slicesTimeList.forEach((slicestime, index) => {
503      slicestime.selected = false;
504      if (slicestime.type == StType.TEMP) {
505        this.slicesTimeList.splice(index, 1);
506        let selectionParam = this.timerShaftEL?.selectionMap.get(slicestime.id);
507        if (selectionParam && selectionParam != undefined) {
508          this.timerShaftEL?.selectionList.splice(this.timerShaftEL?.selectionList.indexOf(selectionParam), 1);
509          this.timerShaftEL?.selectionMap.delete(slicestime.id);
510        }
511      }
512    });
513  }
514
515  clearHoverFlag() {
516    this.hoverFlag.hidden = true;
517  }
518
519  showHoverFlag() {
520    this.hoverFlag.hidden = false;
521  }
522
523  private drawSlicesTimeText(slicesTime: SlicesTime, startX: number, endX: number): number[] {
524    this.context2D.beginPath();
525    this.context2D.strokeStyle = slicesTime.color;
526    this.context2D.fillStyle = slicesTime.color;
527    this.range.slicesTime.color = slicesTime.color; //紫色
528    this.context2D.moveTo(startX + TRIWIDTH, 132);
529    this.context2D.lineTo(startX, 142);
530    this.context2D.lineTo(startX, 132);
531    this.context2D.lineTo(startX + TRIWIDTH, 132);
532
533    this.context2D.lineTo(endX - TRIWIDTH, 132);
534    this.context2D.lineTo(endX, 132);
535    this.context2D.lineTo(endX, 142);
536    this.context2D.lineTo(endX - TRIWIDTH, 132);
537    this.context2D.closePath();
538    slicesTime.selected && this.context2D.fill();
539    this.context2D.stroke();
540    this.context2D.beginPath();
541    if (document.querySelector<SpApplication>('sp-application')!.dark) {
542      this.context2D.strokeStyle = '#FFF';
543      this.context2D.fillStyle = '#FFF';
544    } else {
545      this.context2D.strokeStyle = '#000';
546      this.context2D.fillStyle = '#000';
547    }
548    let lineWidth = endX - startX;
549    let txt = ns2s((slicesTime.endTime || 0) - (slicesTime.startTime || 0));
550    this.context2D.moveTo(startX, this.frame.y + 22);
551    this.context2D.lineTo(endX, this.frame.y + 22);
552    this.context2D.moveTo(startX, this.frame.y + 22 - 5);
553    this.context2D.lineTo(startX, this.frame.y + 22 + 5);
554    this.context2D.moveTo(endX, this.frame.y + 22 - 5);
555    this.context2D.lineTo(endX, this.frame.y + 22 + 5);
556    let txtWidth = this.context2D.measureText(txt).width;
557    this.context2D.fillStyle = '#FFF'; //为了解决文字重叠问题。在时间刻度的文字下面绘制一个小方块
558    this.context2D.fillRect(startX + (lineWidth - txtWidth) / 2, this.frame.y + 10, txtWidth + 2, 10);
559    this.context2D.fillStyle = 'black';
560    if (lineWidth > txtWidth) {
561      this.context2D.fillText(`${txt}`, startX + (lineWidth - txtWidth) / 2, this.frame.y + 20);
562    } else {
563      if (endX + txtWidth >= this.frame.width) {
564        this.context2D.fillText(`${txt}`, startX - 5 - txtWidth, this.frame.y + 20);
565      } else {
566        this.context2D.fillText(`${txt}`, endX + 5, this.frame.y + 20);
567      }
568    }
569    this.context2D.stroke();
570    this.context2D.closePath();
571    return [lineWidth, txtWidth];
572  }
573
574  drawSlicesMarks(slicesTime: SlicesTime) {
575    if (
576      slicesTime.startTime != null &&
577      typeof slicesTime.startTime != undefined &&
578      slicesTime.endTime != null &&
579      typeof slicesTime.endTime != undefined
580    ) {
581      let startX = Math.round(
582        (this.rulerW * (slicesTime.startTime - this.range.startNS)) / (this.range.endNS - this.range.startNS)
583      );
584      let endX = Math.round(
585        (this.rulerW * (slicesTime.endTime - this.range.startNS)) / (this.range.endNS - this.range.startNS)
586      );
587      // 放大、缩小、左右移动之后重置小三角的x轴坐标
588      slicesTime.startX = startX;
589      slicesTime.endX = endX;
590      let [lineWidth, txtWidth] = this.drawSlicesTimeText(slicesTime, startX, endX);
591      // 画框选的备注文字---begin-----------------
592      let text = slicesTime.text;
593      if (text) {
594        this.context2D.beginPath();
595        if (document.querySelector<SpApplication>('sp-application')!.dark) {
596          this.context2D.strokeStyle = '#FFF';
597          this.context2D.fillStyle = '#FFF';
598        } else {
599          this.context2D.strokeStyle = '#000';
600          this.context2D.fillStyle = '#000';
601        }
602
603        let textWidth = this.context2D.measureText(text).width;
604        if (textWidth > 0) {
605          this.context2D.fillStyle = 'black';
606          this.context2D.font = TEXT_FONT;
607          if (lineWidth > txtWidth) {
608            this.context2D.fillText(`${text}`, startX + (lineWidth - textWidth) / 2, this.frame.y + 43);
609          } else {
610            if (endX + textWidth >= this.frame.width) {
611              this.context2D.fillText(`${text}`, startX - 5 - textWidth, this.frame.y + 43);
612            } else {
613              this.context2D.fillText(`${text}`, endX + 5, this.frame.y + 43);
614            }
615          }
616        }
617        this.context2D.stroke();
618        this.context2D.closePath();
619      }
620      // 画框选的备注文字---end-----------------
621    }
622  }
623
624  //绘制旗子
625  drawFlag(x: number, color: string = '#999999', isFill: boolean = false, textStr: string = '', type: string = '') {
626    if (x < 0) return;
627    this.context2D.beginPath();
628    this.context2D.fillStyle = color;
629    this.context2D.strokeStyle = color;
630    this.context2D.moveTo(x, 125);
631    if (type == 'triangle') {
632      this.context2D.lineTo(x + 15, 131);
633    } else {
634      this.context2D.lineTo(x + 10, 125);
635      this.context2D.lineTo(x + 10, 127);
636      this.context2D.lineTo(x + 18, 127);
637      this.context2D.lineTo(x + 18, 137);
638      this.context2D.lineTo(x + 10, 137);
639      this.context2D.lineTo(x + 10, 135);
640    }
641    this.context2D.lineTo(x + 2, 135);
642    this.context2D.lineTo(x + 2, 142);
643    this.context2D.lineTo(x, 142);
644    this.context2D.closePath();
645    isFill && this.context2D.fill();
646    this.context2D.stroke();
647    if (textStr !== '') {
648      this.context2D.font = TEXT_FONT;
649      const { width } = this.context2D.measureText(textStr);
650      this.context2D.fillStyle = 'rgba(255, 255, 255, 0.8)'; //
651      this.context2D.fillRect(x + 21, 132, width + 4, 12);
652      this.context2D.fillStyle = 'black';
653      this.context2D.fillText(textStr, x + 23, 142);
654      this.context2D.stroke();
655    }
656  }
657
658  /**
659   * 查找鼠标所在位置是否存在"帽子"对象,为了操作方便,框选时把三角形的边长宽度左右各加一个像素。
660   * @param x 水平坐标值
661   * @returns
662   */
663  findSlicesTime(x: number, y: number): SlicesTime | null {
664    let slicestime = this.slicesTimeList.find((slicesTime) => {
665      return (
666        ((x >= slicesTime.startX - 1 && x <= slicesTime.startX + TRIWIDTH + 1) || // 选中了帽子的左边三角形区域
667          (x >= slicesTime.endX - TRIWIDTH - 1 && x <= slicesTime.endX + 1)) && // 选中了帽子的右边三角形区域
668        y >= 132 &&
669        y <= 142
670      );
671    });
672
673    if (!slicestime) {
674      return null;
675    }
676    return slicestime;
677  }
678
679  mouseUp(ev: MouseEvent) {
680    if (this.edgeDetection(ev)) {
681      let x = ev.offsetX - (this.canvas?.offsetLeft || 0); // 鼠标点击的x轴坐标
682      let y = ev.offsetY; // 鼠标点击的y轴坐标
683      let findSlicestime = this.findSlicesTime(x, y); // 查找帽子
684      if (findSlicestime) {
685        // 如果找到帽子,则选中帽子。
686        this.slicesTimeList.forEach((slicestime) => (slicestime.selected = false));
687        findSlicestime.selected = true;
688        this.rangeClickHandler && this.rangeClickHandler(findSlicestime);
689      } else {
690        // 如果没有找到帽子,则绘制旗子,此处避免旗子和帽子重叠。
691        // 查找旗子
692        let findFlag = this.flagList.find(
693          (it) => (x >= it.x && x <= it.x + 18 && it.type !== 'triangle') || (x === it.x && it.type === 'triangle')
694        );
695        this.flagList.forEach((it) => (it.selected = false));
696        if (findFlag) {
697          findFlag.selected = true;
698        } else {
699          let flagAtRulerTime = Math.round(((this.range.endNS - this.range.startNS) * x) / this.rulerW);
700          let flag = new Flag(x, 125, 18, 18, flagAtRulerTime + this.range.startNS, randomRgbColor(), '', true, '');
701          this.flagList.push(flag);
702        }
703        this.flagClickHandler && this.flagClickHandler(this.flagList.find((it) => it.selected)); // 绘制旗子
704      }
705    }
706  }
707
708  mouseMove(ev: MouseEvent) {
709    if (this.edgeDetection(ev)) {
710      let x = ev.offsetX - (this.canvas?.offsetLeft || 0);
711      let flg = this.flagList.find((it) => x >= it.x && x <= it.x + 18);
712      if (flg) {
713        this.hoverFlag.hidden = true;
714      } else {
715        this.hoverFlag.hidden = false;
716        this.hoverFlag.x = x;
717        this.hoverFlag.color = '#999999';
718      }
719    } else {
720      this.hoverFlag.hidden = true;
721    }
722    this.draw();
723    this.notifyHandler &&
724      this.notifyHandler(
725        !this.hoverFlag.hidden ? this.hoverFlag : null,
726        this.flagList.find((it) => it.selected) || null
727      );
728  }
729
730  mouseOut(ev: MouseEvent) {
731    if (!this.hoverFlag.hidden) {
732      this.hoverFlag.hidden = true;
733      this.notifyHandler &&
734        this.notifyHandler(
735          !this.hoverFlag.hidden ? this.hoverFlag : null,
736          this.flagList.find((it) => it.selected) || null
737        );
738    }
739    this.draw();
740  }
741
742  edgeDetection(ev: MouseEvent): boolean {
743    let x = ev.offsetX - (this.canvas?.offsetLeft || 0);
744    let y = ev.offsetY - (this.canvas?.offsetTop || 0);
745    SportRuler.isMouseInSportRuler =
746      x > 0 &&
747      x < this.canvas!.offsetWidth &&
748      ev.offsetY - this.frame.y > 20 &&
749      ev.offsetY - this.frame.y < this.frame.height;
750    return SportRuler.isMouseInSportRuler;
751  }
752}
753