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