• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {
18  Component,
19  ElementRef,
20  EventEmitter,
21  HostListener,
22  Inject,
23  Input,
24  OnDestroy,
25  OnInit,
26  Output,
27  SimpleChange,
28  SimpleChanges,
29} from '@angular/core';
30import {MatButtonToggleChange} from '@angular/material/button-toggle';
31import {CanColor} from '@angular/material/core';
32import {MatIconRegistry} from '@angular/material/icon';
33import {MatSelectChange} from '@angular/material/select';
34import {DomSanitizer} from '@angular/platform-browser';
35import {assertDefined} from 'common/assert_utils';
36import {Distance} from 'common/geometry/distance';
37import {PersistentStore} from 'common/store/persistent_store';
38import {getRootUrl} from 'common/url_utils';
39import {Analytics} from 'logging/analytics';
40import {TRACE_INFO} from 'trace/trace_info';
41import {TraceType} from 'trace/trace_type';
42import {DisplayIdentifier} from 'viewers/common/display_identifier';
43import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
44import {UserOptions} from 'viewers/common/user_options';
45import {RectDblClickDetail, ViewerEvents} from 'viewers/common/viewer_events';
46import {RectSpec, TraceRectType} from 'viewers/components/rects/rect_spec';
47import {UiRect} from 'viewers/components/rects/ui_rect';
48import {iconDividerStyle} from 'viewers/components/styles/icon_divider.styles';
49import {multlineTooltip} from 'viewers/components/styles/tooltip.styles';
50import {viewerCardInnerStyle} from 'viewers/components/styles/viewer_card.styles';
51import {Canvas} from './canvas';
52import {Mapper3D} from './mapper3d';
53import {ShadingMode} from './shading_mode';
54
55@Component({
56  selector: 'rects-view',
57  template: `
58    <div class="view-header">
59      <div class="title-section">
60        <collapsible-section-title
61          [title]="title"
62          (collapseButtonClicked)="collapseButtonClicked.emit()"></collapsible-section-title>
63        <div class="right-btn-container">
64          <button
65            color="accent"
66            class="shading-mode"
67            (mouseenter)="onInteractionStart([shadingModeButton])"
68            (mouseleave)="onInteractionEnd([shadingModeButton])"
69            mat-icon-button
70            [matTooltip]="getShadingMode()"
71            [disabled]="shadingModes.length < 2"
72            (click)="onShadingModeButtonClicked()" #shadingModeButton>
73            <mat-icon *ngIf="largeRectsMapper3d.isWireFrame()" class="material-symbols-outlined" aria-hidden="true"> deployed_code </mat-icon>
74            <mat-icon *ngIf="largeRectsMapper3d.isShadedByGradient()" svgIcon="cube_partial_shade"></mat-icon>
75            <mat-icon *ngIf="largeRectsMapper3d.isShadedByOpacity()" svgIcon="cube_full_shade"></mat-icon>
76          </button>
77
78          <div class="icon-divider"></div>
79
80          <div class="slider-container">
81            <mat-icon
82              color="accent"
83              matTooltip="Rotation"
84              class="slider-icon"
85              (mouseenter)="onInteractionStart([rotationSlider, rotationSliderIcon])"
86              (mouseleave)="onInteractionEnd([rotationSlider, rotationSliderIcon])" #rotationSliderIcon> rotate_90_degrees_ccw </mat-icon>
87            <mat-slider
88              class="slider-rotation"
89              step="0.02"
90              min="0"
91              max="1"
92              aria-label="units"
93              [value]="largeRectsMapper3d.getCameraRotationFactor()"
94              (input)="onRotationSliderChange($event.value)"
95              (focus)="$event.target.blur()"
96              color="accent"
97              (mousedown)="onInteractionStart([rotationSlider, rotationSliderIcon])"
98              (mouseup)="onInteractionEnd([rotationSlider, rotationSliderIcon])" #rotationSlider></mat-slider>
99            <mat-icon
100              color="accent"
101              matTooltip="Spacing"
102              class="slider-icon material-symbols-outlined"
103              (mouseenter)="onInteractionStart([spacingSlider, spacingSliderIcon])"
104              (mouseleave)="onInteractionEnd([spacingSlider, spacingSliderIcon])" #spacingSliderIcon> format_letter_spacing </mat-icon>
105            <mat-slider
106              class="slider-spacing"
107              step="0.02"
108              min="0.02"
109              max="1"
110              aria-label="units"
111              [value]="getZSpacingFactor()"
112              (input)="onSeparationSliderChange($event.value)"
113              (focus)="$event.target.blur()"
114              color="accent"
115              (mousedown)="onInteractionStart([spacingSlider, spacingSliderIcon])"
116              (mouseup)="onInteractionEnd([spacingSlider, spacingSliderIcon])" #spacingSlider></mat-slider>
117          </div>
118
119          <div class="icon-divider"></div>
120
121          <button
122            color="accent"
123            (mouseenter)="onInteractionStart([zoomInButton])"
124            (mouseleave)="onInteractionEnd([zoomInButton])"
125            mat-icon-button
126            class="zoom-in-button"
127            (click)="onZoomInClick()" #zoomInButton>
128            <mat-icon aria-hidden="true"> zoom_in </mat-icon>
129          </button>
130          <button
131            color="accent"
132            (mouseenter)="onInteractionStart([zoomOutButton])"
133            (mouseleave)="onInteractionEnd([zoomOutButton])"
134            mat-icon-button
135            class="zoom-out-button"
136            (click)="onZoomOutClick()" #zoomOutButton>
137            <mat-icon aria-hidden="true"> zoom_out </mat-icon>
138          </button>
139
140          <div class="icon-divider"></div>
141
142          <button
143            color="accent"
144            (mouseenter)="onInteractionStart([resetZoomButton])"
145            (mouseleave)="onInteractionEnd([resetZoomButton])"
146            mat-icon-button
147            matTooltip="Restore camera settings"
148            class="reset-button"
149            (click)="resetCamera()" #resetZoomButton>
150            <mat-icon aria-hidden="true"> restore </mat-icon>
151          </button>
152        </div>
153      </div>
154      <div class="filter-controls view-controls">
155        <user-options
156          class="block-filter-controls"
157          [userOptions]="userOptions"
158          [eventType]="ViewerEvents.RectsUserOptionsChange"
159          [traceType]="dependencies[0]"
160          [logCallback]="Analytics.Navigation.logRectSettingsChanged">
161        </user-options>
162
163        <div class="displays-section">
164          <mat-button-toggle-group
165            *ngIf="allRectSpecs"
166            [value]="rectSpec"
167            (change)="onRectTypeButtonClicked($event)"
168            appearance="rect-type-toggle"
169            class="rect-type-toggle">
170            <mat-button-toggle *ngFor="let spec of allRectSpecs" [value]="spec">
171              <mat-icon
172                [color]="spec === rectSpec ? 'primary' : 'accent'"
173                [matTooltip]="'Show ' + spec.type"
174                class="rect-type-icon material-symbols-outlined">{{spec.icon}}</mat-icon>
175            </mat-button-toggle>
176          </mat-button-toggle-group>
177          <span class="mat-body-1">{{groupLabel}}:</span>
178          <mat-form-field appearance="none" class="displays-select">
179            <mat-select
180              #displaySelect
181              disableOptionCentering
182              (selectionChange)="onDisplaySelectChange($event)"
183              [value]="currentDisplays"
184              [disabled]="internalDisplays.length === 1"
185              multiple>
186              <mat-select-trigger>
187                <span>
188                  {{ getSelectTriggerValue() }}
189                </span>
190              </mat-select-trigger>
191              <mat-option
192                *ngFor="let display of internalDisplays"
193                [value]="display"
194                [matTooltip]="'Display Id: ' + display.displayId"
195                matTooltipPosition="right">
196                <div class="option-label">
197                  <button
198                    mat-flat-button
199                    class="option-only-button"
200                    (click)="onOnlyButtonClick($event, display)"> Only </button>
201                  <span class="option-label-text"> {{ display.name }} </span>
202                </div>
203              </mat-option>
204            </mat-select>
205          </mat-form-field>
206        </div>
207      </div>
208    </div>
209    <mat-divider></mat-divider>
210    <span
211      *ngIf="showRectSpecWarning()"
212      class="mat-body-1 warning">
213      <mat-icon class="warning-icon"> warning </mat-icon>
214      <span class="warning-message">
215        Showing {{rectSpec.type}} - change rect type via toggle above
216      </span>
217    </span>
218    <span class="mat-body-1 placeholder-text" *ngIf="rects.length===0"> No rects found. </span>
219    <span class="mat-body-1 placeholder-text" *ngIf="currentDisplays.length===0"> No displays selected. </span>
220    <div class="rects-content">
221      <div class="canvas-container">
222        <canvas
223          class="large-rects-canvas"
224          (click)="onRectClick($event)"
225          (dblclick)="onRectDblClick($event)"
226          oncontextmenu="return false"></canvas>
227        <div class="large-rects-labels"></div>
228        <canvas
229          class="mini-rects-canvas"
230          (dblclick)="onMiniRectDblClick($event)"
231          oncontextmenu="return false"></canvas>
232      </div>
233    </div>
234    <span class="mat-body-1 rect-legend" *ngIf="rectSpec">
235      <span class="shading-opts" [class.force-show-all]="legendExpanded" #shadingOpts>
236        <ng-container *ngFor="let opt of rectSpec.legend">
237          <span
238            *ngIf="!largeRectsMapper3d.isWireFrame() || opt.showInWireFrameMode"
239            class="shading-opt">
240            <mat-icon
241              *ngIf="opt.fill === undefined"
242              [style.border-color]="opt.border"
243              class="square">question_mark</mat-icon>
244            <div
245              *ngIf="opt.fill !== undefined"
246              [style.background-color]="opt.fill"
247              [style.border-color]="opt.border"
248              class="square"></div>
249            <span class="mat-body-1 shading-opt-desc">{{opt.desc}}</span>
250          </span>
251        </ng-container>
252      </span>
253      <button
254        *ngIf="showExpandButton(shadingOpts)"
255        mat-icon-button
256        class="rect-legend-expand-button"
257        (click)="legendExpanded = !legendExpanded">
258        <mat-icon class="material-symbols-outlined">{{legendExpanded ? 'expand_circle_down' : 'more_horiz'}}</mat-icon>
259      </button>
260    </span>
261  `,
262  styles: [
263    `
264      .view-header {
265        display: flex;
266        flex-direction: column;
267      }
268      .right-btn-container {
269        display: flex;
270        align-items: center;
271        padding: 2px 0px;
272      }
273      .right-btn-container .mat-slider-horizontal {
274        min-width: 64px !important;
275      }
276      .icon-divider {
277        height: 50%;
278      }
279      .slider-container {
280        padding: 0 5px;
281        display: flex;
282        align-items: center;
283      }
284      .slider-icon {
285        min-width: 18px;
286        width: 18px;
287        height: 18px;
288        line-height: 18px;
289        font-size: 18px;
290      }
291      .filter-controls {
292        justify-content: space-between;
293      }
294      .block-filter-controls {
295        display: flex;
296        flex-direction: row;
297        align-items: baseline;
298      }
299      .displays-section {
300        display: flex;
301        flex-direction: row;
302        align-items: center;
303        width: fit-content;
304        flex-wrap: nowrap;
305      }
306      .displays-select {
307        font-size: 14px;
308        background-color: var(--disabled-color);
309        border-radius: 4px;
310        height: 24px;
311        margin-left: 5px;
312      }
313      .rect-type-toggle {
314        margin: 0 4px;
315      }
316      .rects-content {
317        height: 100%;
318        display: flex;
319        flex-direction: column;
320        padding: 0px 12px;
321      }
322      .canvas-container {
323        height: 100%;
324        width: 100%;
325        position: relative;
326      }
327      .large-rects-canvas {
328        position: absolute;
329        top: 0;
330        left: 0;
331        width: 100%;
332        height: 100%;
333        cursor: pointer;
334      }
335      .large-rects-labels {
336        position: absolute;
337        top: 0;
338        left: 0;
339        width: 100%;
340        height: 100%;
341        pointer-events: none;
342      }
343      .mini-rects-canvas {
344        cursor: pointer;
345        width: 30%;
346        height: 30%;
347        top: 16px;
348        display: block;
349        position: absolute;
350        z-index: 1000;
351      }
352      .option-label {
353        display: flex;
354        align-items: center;
355        justify-content: space-between;
356      }
357      .option-only-button {
358        padding: 0 10px;
359        border-radius: 10px;
360        background-color: var(--disabled-color) !important;
361        color: var(--default-text-color);
362        min-width: fit-content;
363        height: 18px;
364        align-items: center;
365        display: flex;
366      }
367      .option-label-text {
368        overflow: hidden;
369        text-overflow: ellipsis;
370      }
371      .rect-legend {
372        display: flex;
373        justify-content: space-between;
374        background-color: var(--card-title-background-color);
375      }
376      .shading-opts {
377        display: flex;
378        flex-wrap: wrap;
379        padding: 0 4px;
380      }
381      .shading-opts:not(.force-show-all) {
382        max-height: 24px;
383        overflow-y: hidden;
384      }
385      .shading-opt {
386        display: flex;
387        align-items: center;
388        padding: 2px;
389      }
390      .square {
391        width: 12px;
392        height: 12px;
393        line-height: 12px;
394        font-size: 12px;
395        border-style: solid;
396        border-width: 1.5px;
397      }
398      .shading-opt-desc {
399        padding-inline-start: 2px;
400      }
401      .rect-legend-expand-button {
402        height: 24px;
403        width: 24px;
404        line-height: 24px;
405        font-size: 24px;
406      }
407    `,
408    multlineTooltip,
409    iconDividerStyle,
410    viewerCardInnerStyle,
411  ],
412})
413export class RectsComponent implements OnInit, OnDestroy {
414  Analytics = Analytics;
415  ViewerEvents = ViewerEvents;
416
417  @Input() title = 'title';
418  @Input() zoomFactor = 1;
419  @Input() store?: PersistentStore;
420  @Input() rects: UiRect[] = [];
421  @Input() miniRects: UiRect[] | undefined;
422  @Input() displays: DisplayIdentifier[] = [];
423  @Input() highlightedItem = '';
424  @Input() groupLabel = 'Displays';
425  @Input() isStackBased = false;
426  @Input() shadingModes: ShadingMode[] = [ShadingMode.GRADIENT];
427  @Input() rectSpec: RectSpec | undefined;
428  @Input() allRectSpecs: RectSpec[] | undefined;
429  @Input() userOptions: UserOptions = {};
430  @Input() dependencies: TraceType[] = [];
431  @Input() pinnedItems: UiHierarchyTreeNode[] = [];
432  @Input() isDarkMode = false;
433
434  @Output() collapseButtonClicked = new EventEmitter();
435
436  legendExpanded = false;
437  private internalRects: UiRect[] = [];
438  private internalMiniRects?: UiRect[];
439  private storeKeyZSpacingFactor = '';
440  private storeKeyShadingMode = '';
441  private storeKeySelectedDisplays = '';
442  private internalDisplays: DisplayIdentifier[] = [];
443  private internalHighlightedItem = '';
444  private currentDisplays: DisplayIdentifier[] = [];
445  private largeRectsMapper3d = new Mapper3D();
446  private miniRectsMapper3d = new Mapper3D();
447  private largeRectsCanvas?: Canvas;
448  private miniRectsCanvas?: Canvas;
449  private resizeObserver = new ResizeObserver((entries) => {
450    this.updateLargeRectsPosition();
451  });
452  private largeRectsCanvasElement?: HTMLCanvasElement;
453  private miniRectsCanvasElement?: HTMLCanvasElement;
454  private largeRectsLabelsElement?: HTMLElement;
455  private mouseMoveListener = (event: MouseEvent) => this.onMouseMove(event);
456  private mouseUpListener = (event: MouseEvent) => this.onMouseUp(event);
457  private panning = false;
458  private defaultRectType: TraceRectType | undefined;
459
460  private static readonly ZOOM_SCROLL_RATIO = 0.3;
461
462  constructor(
463    @Inject(ElementRef) private elementRef: ElementRef<HTMLElement>,
464    @Inject(MatIconRegistry) private matIconRegistry: MatIconRegistry,
465    @Inject(DomSanitizer) private domSanitizer: DomSanitizer,
466  ) {
467    this.matIconRegistry.addSvgIcon(
468      'cube_full_shade',
469      this.domSanitizer.bypassSecurityTrustResourceUrl(
470        getRootUrl() + 'cube_full_shade.svg',
471      ),
472    );
473    this.matIconRegistry.addSvgIcon(
474      'cube_partial_shade',
475      this.domSanitizer.bypassSecurityTrustResourceUrl(
476        getRootUrl() + 'cube_partial_shade.svg',
477      ),
478    );
479  }
480
481  ngOnInit() {
482    this.largeRectsMapper3d.setAllowedShadingModes(this.shadingModes);
483
484    const canvasContainer = assertDefined(
485      this.elementRef.nativeElement.querySelector<HTMLElement>(
486        '.canvas-container',
487      ),
488    );
489    this.resizeObserver.observe(canvasContainer);
490
491    this.largeRectsCanvasElement = canvasContainer.querySelector(
492      '.large-rects-canvas',
493    )! as HTMLCanvasElement;
494    this.largeRectsLabelsElement = assertDefined(
495      canvasContainer.querySelector('.large-rects-labels'),
496    ) as HTMLElement;
497    this.largeRectsCanvas = new Canvas(
498      this.largeRectsCanvasElement,
499      this.largeRectsLabelsElement,
500      () => this.isDarkMode,
501    );
502    this.largeRectsCanvasElement.addEventListener('mousedown', (event) =>
503      this.onCanvasMouseDown(event),
504    );
505
506    this.largeRectsMapper3d.increaseZoomFactor(this.zoomFactor - 1);
507
508    if (this.store) {
509      this.updateControlsFromStore();
510    }
511
512    this.redrawLargeRectsAndLabels();
513
514    this.miniRectsCanvasElement = canvasContainer.querySelector(
515      '.mini-rects-canvas',
516    )! as HTMLCanvasElement;
517    this.miniRectsCanvas = new Canvas(
518      this.miniRectsCanvasElement,
519      undefined,
520      () => this.isDarkMode,
521    );
522    this.miniRectsMapper3d.setShadingMode(ShadingMode.GRADIENT);
523    this.miniRectsMapper3d.resetToOrthogonalState();
524    if (this.miniRects && this.miniRects.length > 0) {
525      this.internalMiniRects = this.miniRects;
526      this.drawMiniRects();
527    }
528    this.defaultRectType = this.rectSpec?.type;
529  }
530
531  ngOnChanges(simpleChanges: SimpleChanges) {
532    this.handleLargeRectChanges(simpleChanges);
533    if (
534      simpleChanges['miniRects'] ||
535      (this.miniRects && simpleChanges['isDarkMode'])
536    ) {
537      this.internalMiniRects = this.miniRects;
538      this.drawMiniRects();
539    }
540  }
541
542  private handleLargeRectChanges(simpleChanges: SimpleChanges) {
543    let displayChange = false;
544    if (simpleChanges['displays']) {
545      const curr: DisplayIdentifier[] = simpleChanges['displays'].currentValue;
546      const prev: DisplayIdentifier[] =
547        simpleChanges['displays'].previousValue ?? [];
548      displayChange =
549        curr.length !== prev.length ||
550        (curr.length > 0 &&
551          !curr.every((d, index) => d.displayId === prev[index].displayId));
552    }
553
554    let redrawRects = false;
555    let recolorRects = false;
556    let recolorLabels = false;
557    if (simpleChanges['pinnedItems']) {
558      this.largeRectsMapper3d.setPinnedItems(this.pinnedItems);
559      recolorRects = true;
560    }
561    if (simpleChanges['highlightedItem']) {
562      this.internalHighlightedItem =
563        simpleChanges['highlightedItem'].currentValue;
564      this.largeRectsMapper3d.setHighlightedRectId(
565        this.internalHighlightedItem,
566      );
567      recolorRects = true;
568      recolorLabels = true;
569    }
570    if (simpleChanges['isDarkMode']) {
571      recolorRects = true;
572      recolorLabels = true;
573    }
574    if (simpleChanges['rects']) {
575      this.internalRects = simpleChanges['rects'].currentValue;
576      redrawRects = true;
577    }
578
579    if (displayChange) {
580      this.onDisplaysChange(simpleChanges['displays']);
581    } else if (redrawRects) {
582      this.redrawLargeRectsAndLabels();
583    } else if (recolorRects && recolorLabels) {
584      this.updateLargeRectsAndLabelsColors();
585    } else if (recolorRects) {
586      this.updateLargeRectsColors();
587    }
588  }
589
590  ngOnDestroy() {
591    this.resizeObserver?.disconnect();
592  }
593
594  onDisplaysChange(change: SimpleChange) {
595    const displays = change.currentValue;
596    this.internalDisplays = displays;
597    const activeDisplay = this.getActiveDisplay(this.internalDisplays);
598
599    if (displays.length === 0) {
600      this.updateCurrentDisplays([], false);
601      return;
602    }
603
604    if (change.firstChange) {
605      this.updateCurrentDisplays([activeDisplay], false);
606      return;
607    }
608
609    const curr = this.internalDisplays.filter((display) =>
610      this.currentDisplays.some((curr) => curr.displayId === display.displayId),
611    );
612    if (curr.length > 0) {
613      this.updateCurrentDisplays(curr);
614      return;
615    }
616
617    const currGroupIds = this.largeRectsMapper3d.getCurrentGroupIds();
618    const displaysWithCurrentGroupId = this.internalDisplays.filter((display) =>
619      currGroupIds.some((curr) => curr === display.groupId),
620    );
621    if (displaysWithCurrentGroupId.length === 0) {
622      this.updateCurrentDisplays([activeDisplay]);
623      return;
624    }
625
626    this.updateCurrentDisplays([
627      this.getActiveDisplay(displaysWithCurrentGroupId),
628    ]);
629    return;
630  }
631
632  updateControlsFromStore() {
633    this.storeKeyZSpacingFactor = `rectsView.${this.title}.zSpacingFactor`;
634    this.storeKeyShadingMode = `rectsView.${this.title}.shadingMode`;
635    this.storeKeySelectedDisplays = `rectsView.${this.title}.selectedDisplayId`;
636
637    const storedZSpacingFactor = assertDefined(this.store).get(
638      this.storeKeyZSpacingFactor,
639    );
640    if (storedZSpacingFactor !== undefined) {
641      this.largeRectsMapper3d.setZSpacingFactor(Number(storedZSpacingFactor));
642    }
643
644    const storedShadingMode = assertDefined(this.store).get(
645      this.storeKeyShadingMode,
646    );
647    if (
648      storedShadingMode !== undefined &&
649      this.shadingModes.includes(storedShadingMode as ShadingMode)
650    ) {
651      this.largeRectsMapper3d.setShadingMode(storedShadingMode as ShadingMode);
652    }
653
654    const storedSelectedDisplays = assertDefined(this.store).get(
655      this.storeKeySelectedDisplays,
656    );
657    if (storedSelectedDisplays !== undefined) {
658      const storedIds: Array<number | string> = JSON.parse(
659        storedSelectedDisplays,
660      );
661      const displays = this.internalDisplays.filter((display) => {
662        return storedIds.some((id) => display.displayId === id);
663      });
664      if (displays.length > 0) {
665        this.currentDisplays = displays;
666        this.largeRectsMapper3d.setCurrentGroupIds(
667          displays.map((d) => d.groupId),
668        );
669      }
670    }
671  }
672
673  onSeparationSliderChange(factor: number) {
674    Analytics.Navigation.logRectSettingsChanged(
675      'z spacing',
676      factor,
677      TRACE_INFO[this.dependencies[0]].name,
678    );
679    this.store?.add(this.storeKeyZSpacingFactor, `${factor}`);
680    this.largeRectsMapper3d.setZSpacingFactor(factor);
681    this.redrawLargeRectsAndLabels();
682  }
683
684  onRotationSliderChange(factor: number) {
685    this.largeRectsMapper3d.setCameraRotationFactor(factor);
686    this.updateLargeRectsPositionAndLabels();
687  }
688
689  resetCamera() {
690    Analytics.Navigation.logZoom('reset', 'rects');
691    this.largeRectsMapper3d.resetCamera();
692    this.redrawLargeRectsAndLabels(true);
693  }
694
695  @HostListener('wheel', ['$event'])
696  onScroll(event: WheelEvent) {
697    if ((event.target as HTMLElement).className === 'large-rects-canvas') {
698      if (event.deltaY > 0) {
699        Analytics.Navigation.logZoom('scroll', 'rects', 'out');
700        this.doZoomOut(RectsComponent.ZOOM_SCROLL_RATIO);
701      } else {
702        Analytics.Navigation.logZoom('scroll', 'rects', 'in');
703        this.doZoomIn(RectsComponent.ZOOM_SCROLL_RATIO);
704      }
705    }
706  }
707
708  onCanvasMouseDown(event: MouseEvent) {
709    document.addEventListener('mousemove', this.mouseMoveListener);
710    document.addEventListener('mouseup', this.mouseUpListener);
711  }
712
713  onMouseMove(event: MouseEvent) {
714    this.panning = true;
715    const distance = new Distance(event.movementX, event.movementY);
716    this.largeRectsMapper3d.addPanScreenDistance(distance);
717    this.updateLargeRectsPosition();
718  }
719
720  onMouseUp(event: MouseEvent) {
721    document.removeEventListener('mousemove', this.mouseMoveListener);
722    document.removeEventListener('mouseup', this.mouseUpListener);
723  }
724
725  onZoomInClick() {
726    Analytics.Navigation.logZoom('button', 'rects', 'in');
727    this.doZoomIn();
728  }
729
730  onZoomOutClick() {
731    Analytics.Navigation.logZoom('button', 'rects', 'out');
732    this.doZoomOut();
733  }
734
735  onDisplaySelectChange(event: MatSelectChange) {
736    const selectedDisplays: DisplayIdentifier[] = event.value;
737    this.updateCurrentDisplays(selectedDisplays);
738  }
739
740  getSelectTriggerValue(): string {
741    return this.currentDisplays.map((d) => d.name).join(', ');
742  }
743
744  onOnlyButtonClick(event: MouseEvent, selected: DisplayIdentifier) {
745    event.preventDefault();
746    event.stopPropagation();
747    this.updateCurrentDisplays([selected]);
748  }
749
750  onRectClick(event: MouseEvent) {
751    if (this.panning) {
752      this.panning = false;
753      return;
754    }
755    event.preventDefault();
756
757    const id = this.findClickedRectId(event);
758    if (id !== undefined) {
759      this.notifyHighlightedItem(id);
760    }
761  }
762
763  onRectDblClick(event: MouseEvent) {
764    event.preventDefault();
765
766    const clickedRectId = this.findClickedRectId(event);
767    if (clickedRectId === undefined) {
768      return;
769    }
770
771    this.elementRef.nativeElement.dispatchEvent(
772      new CustomEvent(ViewerEvents.RectsDblClick, {
773        bubbles: true,
774        detail: new RectDblClickDetail(clickedRectId),
775      }),
776    );
777  }
778
779  onMiniRectDblClick(event: MouseEvent) {
780    event.preventDefault();
781
782    this.elementRef.nativeElement.dispatchEvent(
783      new CustomEvent(ViewerEvents.MiniRectsDblClick, {bubbles: true}),
784    );
785  }
786
787  getZSpacingFactor(): number {
788    return this.largeRectsMapper3d.getZSpacingFactor();
789  }
790
791  getShadingMode(): ShadingMode {
792    return this.largeRectsMapper3d.getShadingMode();
793  }
794
795  onShadingModeButtonClicked() {
796    this.largeRectsMapper3d.updateShadingMode();
797    const newMode = this.largeRectsMapper3d.getShadingMode();
798    Analytics.Navigation.logRectSettingsChanged(
799      'shading mode',
800      newMode,
801      TRACE_INFO[this.dependencies[0]].name,
802    );
803    this.store?.add(this.storeKeyShadingMode, newMode);
804    this.updateLargeRectsColors();
805  }
806
807  onInteractionStart(components: CanColor[]) {
808    components.forEach((c) => (c.color = 'primary'));
809  }
810
811  onInteractionEnd(components: CanColor[]) {
812    components.forEach((c) => (c.color = 'accent'));
813  }
814
815  onRectTypeButtonClicked(event: MatButtonToggleChange) {
816    const spec: RectSpec = event.value;
817    this.elementRef.nativeElement.dispatchEvent(
818      new CustomEvent(ViewerEvents.RectTypeButtonClick, {
819        bubbles: true,
820        detail: {type: spec.type},
821      }),
822    );
823  }
824
825  showRectSpecWarning(): boolean {
826    return (
827      this.defaultRectType !== undefined &&
828      this.defaultRectType !== this.rectSpec?.type
829    );
830  }
831
832  showExpandButton(options: HTMLElement): boolean {
833    return (
834      options.scrollHeight > options.clientHeight ||
835      (this.legendExpanded && options.scrollHeight > 24)
836    );
837  }
838
839  private getActiveDisplay(displays: DisplayIdentifier[]): DisplayIdentifier {
840    const displaysWithRects = displays.filter((display) =>
841      this.internalRects.some(
842        (rect) => !rect.isDisplay && rect.groupId === display.groupId,
843      ),
844    );
845    return (
846      displaysWithRects.find((display) => display.isActive) ??
847      displaysWithRects.at(0) ?? // fallback if no active displays
848      displays[0]
849    );
850  }
851
852  private updateCurrentDisplays(
853    displays: DisplayIdentifier[],
854    storeChange = true,
855  ) {
856    if (storeChange) {
857      this.store?.add(
858        this.storeKeySelectedDisplays,
859        JSON.stringify(displays.map((d) => d.displayId)),
860      );
861    }
862    this.currentDisplays = displays;
863    this.largeRectsMapper3d.setCurrentGroupIds(displays.map((d) => d.groupId));
864    this.redrawLargeRectsAndLabels(true);
865  }
866
867  private findClickedRectId(event: MouseEvent): string | undefined {
868    const canvas = event.target as Element;
869    const canvasOffset = canvas.getBoundingClientRect();
870
871    const x =
872      ((event.clientX - canvasOffset.left) / canvas.clientWidth) * 2 - 1;
873    const y =
874      -((event.clientY - canvasOffset.top) / canvas.clientHeight) * 2 + 1;
875    const z = 0;
876
877    return this.largeRectsCanvas?.getClickedRectId(x, y, z);
878  }
879
880  private doZoomIn(ratio = 1) {
881    this.largeRectsMapper3d.increaseZoomFactor(ratio);
882    this.updateLargeRectsPositionAndLabels();
883  }
884
885  private doZoomOut(ratio = 1) {
886    this.largeRectsMapper3d.decreaseZoomFactor(ratio);
887    this.updateLargeRectsPositionAndLabels();
888  }
889
890  private redrawLargeRectsAndLabels(updateBoundingBox = false) {
891    this.largeRectsMapper3d.setRects(this.internalRects);
892    const scene = this.largeRectsMapper3d.computeScene(updateBoundingBox);
893    this.largeRectsCanvas?.updateViewPosition(
894      scene.camera,
895      scene.boundingBox,
896      scene.zDepth,
897    );
898    this.largeRectsCanvas?.updateRects(scene.rects);
899    this.largeRectsCanvas?.updateLabels(scene.labels);
900    this.largeRectsCanvas?.renderView();
901  }
902
903  private updateLargeRectsPosition() {
904    const scene = this.largeRectsMapper3d.computeScene(false);
905    this.largeRectsCanvas?.updateViewPosition(
906      scene.camera,
907      scene.boundingBox,
908      scene.zDepth,
909    );
910    this.largeRectsCanvas?.renderView();
911  }
912
913  private updateLargeRectsPositionAndLabels() {
914    const scene = this.largeRectsMapper3d.computeScene(false);
915    this.largeRectsCanvas?.updateViewPosition(
916      scene.camera,
917      scene.boundingBox,
918      scene.zDepth,
919    );
920    this.largeRectsCanvas?.updateLabels(scene.labels);
921    this.largeRectsCanvas?.renderView();
922  }
923
924  private updateLargeRectsColors() {
925    const scene = this.largeRectsMapper3d.computeScene(false);
926    this.largeRectsCanvas?.updateRects(scene.rects);
927    this.largeRectsCanvas?.renderView();
928  }
929
930  private updateLargeRectsAndLabelsColors() {
931    const scene = this.largeRectsMapper3d.computeScene(false);
932    this.largeRectsCanvas?.updateRects(scene.rects);
933    this.largeRectsCanvas?.updateLabels(scene.labels);
934    this.largeRectsCanvas?.renderView();
935  }
936
937  private drawMiniRects() {
938    if (this.internalMiniRects && this.miniRectsCanvas) {
939      this.miniRectsMapper3d.setShadingMode(ShadingMode.GRADIENT);
940      this.miniRectsMapper3d.setCurrentGroupIds([
941        this.internalMiniRects[0]?.groupId,
942      ]);
943      this.miniRectsMapper3d.resetToOrthogonalState();
944      this.miniRectsMapper3d.setRects(this.internalMiniRects);
945
946      const scene = this.miniRectsMapper3d.computeScene(true);
947      this.miniRectsCanvas.updateViewPosition(
948        scene.camera,
949        scene.boundingBox,
950        scene.zDepth,
951      );
952      this.miniRectsCanvas.updateRects(scene.rects);
953      this.miniRectsCanvas.updateLabels(scene.labels);
954      this.miniRectsCanvas.renderView();
955
956      // Canvas internally sets these values to 100%. They need to be reset afterwards
957      if (this.miniRectsCanvasElement) {
958        this.miniRectsCanvasElement.style.width = '30%';
959        this.miniRectsCanvasElement.style.height = '30%';
960      }
961    }
962  }
963
964  private notifyHighlightedItem(id: string) {
965    const event: CustomEvent = new CustomEvent(
966      ViewerEvents.HighlightedIdChange,
967      {
968        bubbles: true,
969        detail: {id},
970      },
971    );
972    this.elementRef.nativeElement.dispatchEvent(event);
973  }
974}
975