Skip to content
Snippets Groups Projects
feature-view.component.ts 9.01 KiB
Newer Older
import { ChangeDetectionStrategy, Component, Inject, Input, inject } from '@angular/core';
Xiao Gui's avatar
Xiao Gui committed
import { BehaviorSubject, Observable, combineLatest, concat, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
import { SAPI } from 'src/atlasComponents/sapi/sapi.service';
import { Feature, SimpleCompoundFeature, VoiFeature } from 'src/atlasComponents/sapi/sxplrTypes';
import { DARKTHEME } from 'src/util/injectionTokens';
Xiao Gui's avatar
Xiao Gui committed
import { isVoiData, notQuiteRight } from "../guards"
import { Action, Store, select } from '@ngrx/store';
import { atlasSelection, userInteraction } from 'src/state';
Xiao Gui's avatar
Xiao Gui committed
import { PathReturn } from 'src/atlasComponents/sapi/typeV3';
import { CFIndex } from '../compoundFeatureIndices';
import { ComponentStore } from '@ngrx/component-store';
import { DestroyDirective } from 'src/util/directives/destroy.directive';
import { FEATURE_CONCEPT_TOKEN, FeatureConcept } from '../util';

type FeatureCmpStore = {
  selectedCmpFeature: SimpleCompoundFeature|null
}
Xiao Gui's avatar
Xiao Gui committed

type PlotlyResponse = PathReturn<"/feature/{feature_id}/plotly">
function isSimpleCompoundFeature(feat: unknown): feat is SimpleCompoundFeature{
  return !!(feat?.['indices'])
@Component({
  selector: 'sxplr-feature-view',
  templateUrl: './feature-view.component.html',
  styleUrls: ['./feature-view.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    ComponentStore
  ],
  hostDirectives: [
    DestroyDirective
  ]
Xiao Gui's avatar
Xiao Gui committed
export class FeatureViewComponent {
  destroyed$ = inject(DestroyDirective).destroyed$

  busy$ = new BehaviorSubject<boolean>(false)

  #feature$ = new BehaviorSubject<Feature|SimpleCompoundFeature>(null)
  set feature(val: Feature|SimpleCompoundFeature) {
Xiao Gui's avatar
Xiao Gui committed
    this.#feature$.next(val)
  }

  #featureId = this.#feature$.pipe(
    map(f => f.id)
  )

Xiao Gui's avatar
Xiao Gui committed
  #featureDetail$ = this.#featureId.pipe(
    switchMap(fid => this.sapi.getV3FeatureDetailWithId(fid)),
Xiao Gui's avatar
Xiao Gui committed
  )
  
  #featureDesc$ = this.#feature$.pipe(
    switchMap(() => concat(
      of(null as string),
      this.#featureDetail$.pipe(
Xiao Gui's avatar
Xiao Gui committed
        map(v => v?.desc),
        catchError((err) => {
          let errortext = 'Error fetching feature instance'

          if (err.error instanceof Error) {
            errortext += `:\n\n${err.error.toString()}`
          } else {
            errortext += '!'
          }
          
          return of(errortext)
        }),
Xiao Gui's avatar
Xiao Gui committed
      )
    ))
  )
Xiao Gui's avatar
Xiao Gui committed
  #voi$: Observable<VoiFeature> = this.#feature$.pipe(
Xiao Gui's avatar
Xiao Gui committed
    switchMap(() => concat(
Xiao Gui's avatar
Xiao Gui committed
      of(null),
Xiao Gui's avatar
Xiao Gui committed
      this.#featureDetail$.pipe(
Xiao Gui's avatar
Xiao Gui committed
        catchError(() => of(null)),
Xiao Gui's avatar
Xiao Gui committed
        map(val => {
          if (isVoiData(val)) {
            return val
          }
Xiao Gui's avatar
Xiao Gui committed
          return null
Xiao Gui's avatar
Xiao Gui committed
        })
      )
    ))
  )

  #warnings$ = this.#feature$.pipe(
    switchMap(() => concat(
      of([] as string[]),
      this.#featureDetail$.pipe(
Xiao Gui's avatar
Xiao Gui committed
        catchError(() => of(null)),
        map(notQuiteRight),
Xiao Gui's avatar
Xiao Gui committed
      )
    ))
  )
  #isConnectivity$ = this.#feature$.pipe(
    map(v => v.category === "connectivity")
  )

  #selectedRegion$ = this.store.pipe(
    select(atlasSelection.selectors.selectedRegions)
  )

  #additionalParams$: Observable<Record<string, string>> = this.#isConnectivity$.pipe(
    withLatestFrom(this.#selectedRegion$),
    map(([ isConnnectivity, selectedRegions ]) => isConnnectivity
    ? {"regions": selectedRegions.map(r => r.name).join(" ")}
    : {} )
  )
  #plotlyInput$ = combineLatest([
    this.#featureId,
    this.darktheme$,
    this.#additionalParams$,
Xiao Gui's avatar
Xiao Gui committed
  ]).pipe(
    debounceTime(16),
    map(([ id, darktheme, additionalParams ]) => ({ id, darktheme, additionalParams })),
    distinctUntilChanged((o, n) => o.id === n.id && o.darktheme === n.darktheme),
    shareReplay(1),
  )
Xiao Gui's avatar
Xiao Gui committed

  #loadingDetail$ = this.#feature$.pipe(
    switchMap(() => concat(
      of(true),
      this.#featureDetail$.pipe(
Xiao Gui's avatar
Xiao Gui committed
        catchError(() => of(null)),
Xiao Gui's avatar
Xiao Gui committed
        map(() => false)
      )
    ))
  )
Xiao Gui's avatar
Xiao Gui committed
  #loadingPlotly$ = this.#plotlyInput$.pipe(
    switchMap(() => concat(
      of(true),
Xiao Gui's avatar
Xiao Gui committed
      this.#plotly$.pipe(
        map(() => false)
      )
    )),
  )

Xiao Gui's avatar
Xiao Gui committed
  #plotly$: Observable<PlotlyResponse> = this.#plotlyInput$.pipe(
    switchMap(({ id, darktheme, additionalParams }) => {
      if (!id) {
        return of(null)
      }
      return concat(
        of(null),
        this.sapi.getFeaturePlot(
          id,
          {
            template: darktheme ? 'plotly_dark' : 'plotly_white',
            ...additionalParams
          }
        ).pipe(
          catchError(() => of(null))
        )
Xiao Gui's avatar
Xiao Gui committed
    shareReplay(1),
  )
Xiao Gui's avatar
Xiao Gui committed
  
  #detailLinks = this.#feature$.pipe(
    switchMap(() => concat(
      of([] as string[]),
      this.#featureDetail$.pipe(
Xiao Gui's avatar
Xiao Gui committed
        catchError(() => of(null as null)),
        map(val => (val?.link || []).map(l => l.href))
Xiao Gui's avatar
Xiao Gui committed
      )
    ))
  )
  #compoundFeatEmts$ = this.#feature$.pipe(
    map(f => {
      if (isSimpleCompoundFeature(f)) {
        return f.indices
      }
      return null
    })
  )

  additionalLinks$ = this.#detailLinks.pipe(
    distinctUntilChanged((o, n) => o.length == n.length),
Xiao Gui's avatar
Xiao Gui committed
    withLatestFrom(this.#feature$),
    map(([links, feature]) => {
      const set = new Set((feature.link || []).map(v => v.href))
      return links.filter(l => !set.has(l))
    })
  )

Xiao Gui's avatar
Xiao Gui committed
  downloadLink$ = this.sapi.sapiEndpoint$.pipe(
Xiao Gui's avatar
Xiao Gui committed
    switchMap(endpoint => this.#featureId.pipe(
      map(featureId => `${endpoint}/feature/${featureId}/download`),
      shareReplay(1)
    ))
Xiao Gui's avatar
Xiao Gui committed
  )
  // intents$ = this.#isConnectivity$.pipe(
  //   withLatestFrom(this.#featureId, this.#selectedRegion$),
  //   switchMap(([flag, fid, selectedRegion]) => {
  //     if (!flag) {
  //       return EMPTY
  //     }
  //     return this.sapi.getFeatureIntents(fid, {
  //       region: selectedRegion.map(r => r.name).join(" ")
  //     }).pipe(
  //       switchMap(val => 
  //         this.sapi.iteratePages(
  //           val,
  //           page => this.sapi.getFeatureIntents(fid, {
  //             region: selectedRegion.map(r => r.name).join(" "),
  //             page: page.toString()
  //           }
  //         )
  //       ))
  //     )
  //   })
  // )
  constructor(
    private sapi: SAPI,
    private store: Store,
    private readonly cmpStore: ComponentStore<FeatureCmpStore>,
    @Inject(DARKTHEME) public darktheme$: Observable<boolean>,
    @Inject(FEATURE_CONCEPT_TOKEN) private featConcept: FeatureConcept,
    this.cmpStore.setState({ selectedCmpFeature: null })

    this.#feature$.pipe(
      takeUntil(this.destroyed$),
      filter(isSimpleCompoundFeature),
    ).subscribe(selectedCmpFeature => {
      this.cmpStore.patchState({ selectedCmpFeature })
    })
  navigateToRegionByName(regionName: string){
    this.store.dispatch(
      atlasSelection.actions.navigateToRegion({
        region: {
          name: regionName
        }
      })
    )
  }

  onAction(action: Action){
    this.store.dispatch(action)
  }

  #etheralView$ = combineLatest([
    this.cmpStore.state$,
    this.#feature$,
    this.featConcept.concept$
  ]).pipe(
    map(([ { selectedCmpFeature }, feature, selectedConcept ]) => {
      const { id: selectedConceptFeatId, concept } = selectedConcept
      const prevCmpFeat: SimpleCompoundFeature = selectedCmpFeature?.indices.some(idx => idx.id === feature?.id) && selectedCmpFeature || null
      return {
        prevCmpFeat,
        concept: selectedConceptFeatId === feature.id && concept || null
      }
    })
  )
Xiao Gui's avatar
Xiao Gui committed
  
Xiao Gui's avatar
Xiao Gui committed
  #specialView$ = combineLatest([
    concat(
      of(null as VoiFeature),
      this.#voi$
    ),
    concat(
      of(null as PlotlyResponse),
      this.#plotly$,
    ),
    this.#compoundFeatEmts$,
    this.store.pipe(
      select(atlasSelection.selectors.selectedTemplate)
Xiao Gui's avatar
Xiao Gui committed
  ]).pipe(
    map(([ voi, plotly, cmpFeatElmts, selectedTemplate ]) => {
Xiao Gui's avatar
Xiao Gui committed
      return {
        voi, plotly, cmpFeatElmts, selectedTemplate, 
Xiao Gui's avatar
Xiao Gui committed
  #baseView$ = combineLatest([
Xiao Gui's avatar
Xiao Gui committed
    this.#feature$,
    combineLatest([
      this.#loadingDetail$,
      this.#loadingPlotly$,
      this.busy$,
Xiao Gui's avatar
Xiao Gui committed
    ]).pipe(
      map(flags => flags.some(f => f))
    ),
    this.#warnings$,
    this.additionalLinks$,
    this.downloadLink$,
    this.#featureDesc$
  ]).pipe(
    map(([ feature, busy, warnings, additionalLinks, downloadLink, desc ]) => {
      return {
Xiao Gui's avatar
Xiao Gui committed
        featureId: feature.id,
Xiao Gui's avatar
Xiao Gui committed
        name: feature.name,
        links: feature.link,
        category: feature.category === 'Unknown category'
        ? `Other feature`
        : `${feature.category} feature`,
        busy,
        warnings,
        additionalLinks,
        downloadLink,
        desc
      }
    })
  )

  view$ = combineLatest([
Xiao Gui's avatar
Xiao Gui committed
    this.#baseView$,
    this.#specialView$,
    this.#etheralView$
Xiao Gui's avatar
Xiao Gui committed
  ]).pipe(
    map(([obj1, obj2, obj3]) => {
Xiao Gui's avatar
Xiao Gui committed
      return {
        ...obj1,
        ...obj2,
Xiao Gui's avatar
Xiao Gui committed
      }
    })
  )
  showSubfeature(item: CFIndex|Feature){
    this.store.dispatch(
      userInteraction.actions.showFeature({
        feature: item
      })
    )
  }
  
  clearSelectedFeature(): void{
    this.store.dispatch(
      userInteraction.actions.clearShownFeature()
    )