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