Skip to content
Snippets Groups Projects
regionHierarchy.component.ts 7.49 KiB
Newer Older
import { EventEmitter, Component, ElementRef, ViewChild, HostListener, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, Input, Output, AfterViewInit } from "@angular/core";
import {  Subscription, Subject, fromEvent } from "rxjs";
Xiao Gui's avatar
Xiao Gui committed
import { buffer, debounceTime } from "rxjs/operators";
Xiao Gui's avatar
Xiao Gui committed
import { FilterNameBySearch } from "./filterNameBySearch.pipe";
import { generateLabelIndexId } from "src/services/stateStore.service";
Xiao Gui's avatar
Xiao Gui committed

const insertHighlight :(name:string, searchTerm:string) => string = (name:string, searchTerm:string = '') => {
  const regex = new RegExp(searchTerm, 'gi')
  return searchTerm === '' ?
    name :
    name.replace(regex, (s) => `<span class = "highlight">${s}</span>`)
}

const getDisplayTreeNode : (searchTerm:string, selectedRegions:any[]) => (item:any) => string = (searchTerm:string = '', selectedRegions:any[] = []) => ({ ngId, name, status, labelIndex }) =>  {
  return !!labelIndex
    && !!ngId
    && selectedRegions.findIndex(re =>
      generateLabelIndexId({ labelIndex: re.labelIndex, ngId: re.ngId }) === generateLabelIndexId({ ngId, labelIndex })
    ) >= 0
      ? `<span class="regionSelected">${insertHighlight(name, searchTerm)}</span>` + (status ? ` <span class="text-muted">(${insertHighlight(status, searchTerm)})</span>` : ``)
      : `<span class="regionNotSelected">${insertHighlight(name, searchTerm)}</span>` + (status ? ` <span class="text-muted">(${insertHighlight(status, searchTerm)})</span>` : ``)
const getFilterTreeBySearch = (pipe:FilterNameBySearch, searchTerm:string) => (node:any) => pipe.transform([node.name, node.status], searchTerm)
Xiao Gui's avatar
Xiao Gui committed
@Component({
  selector: 'region-hierarchy',
  templateUrl: './regionHierarchy.template.html',
  styleUrls: [
    './regionHierarchy.style.css'
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})

export class RegionHierarchy implements OnInit, AfterViewInit{
Xiao Gui's avatar
Xiao Gui committed

  @Input()
  public selectedRegions: any[] = []

Xiao Gui's avatar
Xiao Gui committed
  @Input()
  public selectedParcellation: any

Xiao Gui's avatar
Xiao Gui committed
  private _showRegionTree: boolean = false

  @Output()
Xiao Gui's avatar
Xiao Gui committed
  private showRegionFlagChanged: EventEmitter<boolean> = new EventEmitter()

  @Output()
  private singleClickRegion: EventEmitter<any> = new EventEmitter()

  @Output()
  private doubleClickRegion: EventEmitter<any> = new EventEmitter()
Xiao Gui's avatar
Xiao Gui committed

  @Output()
  private clearAllRegions: EventEmitter<null> = new EventEmitter()

Xiao Gui's avatar
Xiao Gui committed
  public searchTerm: string = ''
  private subscriptions: Subscription[] = []

  @ViewChild('searchTermInput', {read: ElementRef})
  private searchTermInput: ElementRef

  countItemsIntoTheTree: number
  windowHeight: number

Xiao Gui's avatar
Xiao Gui committed
  @HostListener('document:click', ['$event'])
  closeRegion(event: MouseEvent) {
Xiao Gui's avatar
Xiao Gui committed
    const contains = this.el.nativeElement.contains(event.target)
Xiao Gui's avatar
Xiao Gui committed
    this.showRegionTree = contains
    if (!this.showRegionTree)
      this.searchTerm = ''
  }

  @HostListener('window:resize', ['$event'])
  onResize(event) {
    this.windowHeight = event.target.innerHeight;
  }

Xiao Gui's avatar
Xiao Gui committed
  get regionsLabelIndexMap() {
Xiao Gui's avatar
Xiao Gui committed
    return null
Xiao Gui's avatar
Xiao Gui committed
  }

  constructor(
    private cdr:ChangeDetectorRef,
Xiao Gui's avatar
Xiao Gui committed
    private el:ElementRef
Xiao Gui's avatar
Xiao Gui committed
  ){
    this.windowHeight = window.innerHeight;
  }

  ngOnChanges(){
    this.aggregatedRegionTree = {
      name: this.selectedParcellation.name,
      children: this.selectedParcellation.regions
    }
    this.displayTreeNode = getDisplayTreeNode(this.searchTerm, this.selectedRegions)
    this.filterTreeBySearch = getFilterTreeBySearch(this.filterNameBySearchPipe, this.searchTerm)
Xiao Gui's avatar
Xiao Gui committed
  }

  clearRegions(event:MouseEvent){
    event.stopPropagation()
    this.clearAllRegions.emit()
  }

Xiao Gui's avatar
Xiao Gui committed
  get showRegionTree(){
    return this._showRegionTree
  }

  set showRegionTree(flag: boolean){
    this._showRegionTree = flag
    this.showRegionFlagChanged.emit(this._showRegionTree)
  }

Xiao Gui's avatar
Xiao Gui committed
  ngOnInit(){
Xiao Gui's avatar
Xiao Gui committed
    this.displayTreeNode = getDisplayTreeNode(this.searchTerm, this.selectedRegions)
    this.filterTreeBySearch = getFilterTreeBySearch(this.filterNameBySearchPipe, this.searchTerm)
Xiao Gui's avatar
Xiao Gui committed
    this.subscriptions.push(
      this.handleRegionTreeClickSubject.pipe(
        buffer(
          this.handleRegionTreeClickSubject.pipe(
            debounceTime(200)
          )
        )
      ).subscribe(arr => arr.length > 1 ? this.doubleClick(arr[0]) : this.singleClick(arr[0]))
    )
  }

  ngAfterViewInit(){
Xiao Gui's avatar
Xiao Gui committed
    /**
     * TODO
     * bandaid fix on
     * when region search loses focus, the searchTerm is cleared,
     * but hierarchy filter does not reset
     */
    this.subscriptions.push(
      fromEvent(this.searchTermInput.nativeElement, 'focus').pipe(
        
      ).subscribe(() => {
        this.displayTreeNode = getDisplayTreeNode(this.searchTerm, this.selectedRegions)
        this.filterTreeBySearch = getFilterTreeBySearch(this.filterNameBySearchPipe, this.searchTerm)
      })
    )
    this.subscriptions.push(
      fromEvent(this.searchTermInput.nativeElement, 'input').pipe(
        debounceTime(200)
      ).subscribe(ev => {
        this.changeSearchTerm(ev)
      })
    )

    setTimeout(() => {
      this.countItemsIntoTheTree = 1
      if (this.aggregatedRegionTree.children &&
          this.aggregatedRegionTree.children.length > 0) {
        this.countItems(this.aggregatedRegionTree)
      }
    })

Xiao Gui's avatar
Xiao Gui committed
  getInputPlaceholder(parcellation:any) {
    if (parcellation)
      return `Search region in ${parcellation.name}`
    else
      return `Start by selecting a template and a parcellation.`
  }

  escape(event:KeyboardEvent){
    this.showRegionTree = false
    this.searchTerm = '';
    (event.target as HTMLInputElement).blur()

  }

  countItems(objectToCount) {
    objectToCount.children.forEach(object => {
      this.countItemsIntoTheTree += 1
      if (object.children && object.children.length > 0) this.countItems(object)
    })
  }

  uncollapsedFlatTreeItems(event) {
    this.countItemsIntoTheTree = event
  }

  regionHierarchyHeight(){
    return({
      'height' : (this.countItemsIntoTheTree * 15 + 60).toString() + 'px',
      'max-height': (this.windowHeight - 100) + 'px'
    })
  }

Xiao Gui's avatar
Xiao Gui committed
  focusInput(event?:MouseEvent){
    if (event) {
      /**
       * need to stop propagation, or @closeRegion will be triggered
       */
      event.stopPropagation()
    }
    this.searchTermInput.nativeElement.focus()
    this.showRegionTree = true
  }

  /* NB need to bind two way data binding like this. Or else, on searchInput blur, the flat tree will be rebuilt,
    resulting in first click to be ignored */

  changeSearchTerm(event: any) {
    if (event.target.value === this.searchTerm)
      return
    this.searchTerm = event.target.value
    /**
     * TODO maybe introduce debounce
     */
    this.ngOnChanges()
Xiao Gui's avatar
Xiao Gui committed
    this.cdr.markForCheck()
  }

  private handleRegionTreeClickSubject: Subject<any> = new Subject()

  handleClickRegion(obj: any) {
Xiao Gui's avatar
Xiao Gui committed
    const {event} = obj
    /**
     * TODO figure out why @closeRegion gets triggered, but also, contains returns false
     */
    if (event)
      event.stopPropagation()
Xiao Gui's avatar
Xiao Gui committed
    this.handleRegionTreeClickSubject.next(obj)
  }

Xiao Gui's avatar
Xiao Gui committed
  /* single click selects/deselects region(s) */
  private singleClick(obj: any) {
Xiao Gui's avatar
Xiao Gui committed
    if (!obj)
      return
    const { inputItem : region } = obj
    if (!region)
      return
Xiao Gui's avatar
Xiao Gui committed
    this.singleClickRegion.emit(region)
Xiao Gui's avatar
Xiao Gui committed
  /* double click navigate to the interested area */
  private doubleClick(obj: any) {
    if (!obj)
      return
    const { inputItem : region } = obj
    if (!region)
      return
    this.doubleClickRegion.emit(region)
  public displayTreeNode: (item:any) => string
Xiao Gui's avatar
Xiao Gui committed

  private filterNameBySearchPipe = new FilterNameBySearch()
  public filterTreeBySearch: (node:any) => boolean 
Xiao Gui's avatar
Xiao Gui committed

  public aggregatedRegionTree: any