diff --git a/Dockerfile b/Dockerfile index fbd5608c10bb90eb79f5b6f24781cf4e8907c2ef..b3f6d6ba67345ea1183ec8f9db906d7573e5b8c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,6 @@ FROM node:14 as builder ARG BACKEND_URL ENV BACKEND_URL=${BACKEND_URL} -ARG DATASET_PREVIEW_URL -ENV DATASET_PREVIEW_URL=${DATASET_PREVIEW_URL:-https://hbp-kg-dataset-previewer.apps.hbp.eu/v2} - ARG BS_REST_URL ENV BS_REST_URL=${BS_REST_URL:-https://siibra-api-stable.apps.hbp.eu/v1_0} diff --git a/build_env.md b/build_env.md index 1582bcdb0a487e3a91a88ecdcec45df445a687b9..1c29ea98614c3a40dbe91d7c620f9e6a5356b4e8 100644 --- a/build_env.md +++ b/build_env.md @@ -8,7 +8,6 @@ As interactive atlas viewer uses [webpack define plugin](https://webpack.js.org/ | `PRODUCTION` | if the build is for production, toggles optimisations such as minification | `undefined` | true | | `BACKEND_URL` | backend that the viewer calls to fetch available template spaces, parcellations, plugins, datasets | `null` | https://interactive-viewer.apps.hbp.eu/ | | `BS_REST_URL` | [siibra-api](https://github.com/FZJ-INM1-BDA/siibra-api) used to fetch different resources | https://siibra-api-stable.apps.hbp.eu/v1_0 | -| `DATASET_PREVIEW_URL` | dataset preview url used by component <https://github.com/fzj-inm1-bda/kg-dataset-previewer>. Useful for diagnosing issues with dataset previews.| https://hbp-kg-dataset-previewer.apps.hbp.eu/datasetPreview | http://localhost:1234/datasetPreview | | `MATOMO_URL` | base url for matomo analytics | `null` | https://example.com/matomo/ | | `MATOMO_ID` | application id for matomo analytics | `null` | 6 | | `STRICT_LOCAL` | hides **Explore** and **Download** buttons. Useful for offline demo's | `false` | `true` | diff --git a/common/constants.js b/common/constants.js index 1527bc74e86405a3f23353e182103e9a7da31882..0a930c3b857d9b4db64300653fb716b8e0eed533 100644 --- a/common/constants.js +++ b/common/constants.js @@ -27,6 +27,7 @@ NO_BULK_DOWNLOAD: `No datasets pinned`, // overlay/layout specific + TOGGLE_DELINEATION: `Toggle delineation [q]`, SELECT_ATLAS: 'Atlas', CONTEXT_MENU: `Viewer context menu`, TOGGLE_FRONTAL_OCTANT: `Toggle perspective view frontal octant`, @@ -84,6 +85,24 @@ } exports.CONST = { + KG_TOS: `The interactive viewer queries HBP Knowledge Graph Data Platform ("KG") for published datasets. + + +Access to the data and metadata provided through KG requires that you cite and acknowledge said data and metadata according to the Terms and Conditions of the Platform. + + +Citation requirements are outlined <https://www.humanbrainproject.eu/en/explore-the-brain/search-terms-of-use#citations> . + + +Acknowledgement requirements are outlined <https://www.humanbrainproject.eu/en/explore-the-brain/search-terms-of-use#acknowledgements> + + +These outlines are based on the authoritative Terms and Conditions are found <https://www.humanbrainproject.eu/en/explore-the-brain/search-terms-of-use> + + +If you do not accept the Terms & Conditions you are not permitted to access or use the KG to search for, to submit, to post, or to download any materials found there-in. +`, + LOADING_TXT: `Loading ...`, CANNOT_DECIPHER_HEMISPHERE: 'Cannot decipher region hemisphere.', diff --git a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts deleted file mode 100644 index d9ebdd9a94f5727043f4676bb3a433e1418a543b..0000000000000000000000000000000000000000 --- a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import {ConnectivityBrowserComponent} from "./connectivityBrowser.component"; -import {async, ComponentFixture, TestBed} from "@angular/core/testing"; -import {Action} from "@ngrx/store"; -import {HttpClientModule} from "@angular/common/http"; -import {CUSTOM_ELEMENTS_SCHEMA, Directive, Input} from "@angular/core"; -import {provideMockActions} from "@ngrx/effects/testing"; -import {MockStore, provideMockStore} from "@ngrx/store/testing"; -import {Observable, of} from "rxjs"; -import { viewerStateOverwrittenColorMapSelector } from "src/services/state/viewerState/selectors"; -import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState.store.helper"; -import {BS_ENDPOINT} from "src/util/constants"; - -/** - * injecting databrowser module is bad idea - * since it relies on its own selectors - * since the only reason why data browser is imported is to use show dataset dialogue - * just use a dummy directive - */ -const MOCK_BS_ENDPOINT = `http://localhost:1234` - -@Directive({ - selector: '[iav-dataset-show-dataset-dialog]' -}) - -class DummyDirective{ - @Input('iav-dataset-show-dataset-dialog-name') - name: string - @Input('iav-dataset-show-dataset-dialog-description') - description: string - @Input('iav-dataset-show-dataset-dialog-kgid') - kgId: string - @Input('iav-dataset-show-dataset-dialog-kgschema') - kgSchema: string -} - -describe('ConnectivityComponent', () => { - - let component: ConnectivityBrowserComponent; - let fixture: ComponentFixture<ConnectivityBrowserComponent>; - const actions$: Observable<Action> = of({type: 'TEST'}) - - let datasetList = [ - { - ['@id']: 'id1', - src_name: 'id1', - src_info: 'd1', - kgId: 'kgId1', - kgschema: 'kgschema1' - }, { - ['@id']: 'id2', - src_name: 'id2', - src_info: 'd2', - kgId: 'kgId2', - kgschema: 'kgschema2' - } - ] - - beforeEach(async (() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientModule, - ], - providers: [ - provideMockActions(() => actions$), - provideMockStore(), - { - provide: BS_ENDPOINT, - useValue: MOCK_BS_ENDPOINT - } - ], - declarations: [ - ConnectivityBrowserComponent, - DummyDirective, - ], - schemas: [ - CUSTOM_ELEMENTS_SCHEMA, - ], - }).compileComponents() - - })); - - beforeEach(() => { - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateOverwrittenColorMapSelector, null) - mockStore.overrideSelector(ngViewerSelectorClearViewEntries, []) - }) - - it('> component can be created', async () => { - fixture = TestBed.createComponent(ConnectivityBrowserComponent) - component = fixture.componentInstance - expect(component).toBeTruthy() - }) - - // ToDo create test for kgId and kgSchema after it will work while viewing dataset - it('> change dataset changes name and description', () => { - fixture = TestBed.createComponent(ConnectivityBrowserComponent) - component = fixture.componentInstance - - component.datasetList = datasetList - - component.changeDataset({value: 'id1'}) - - expect(component.selectedDataset).toEqual('id1') - expect(component.selectedDatasetDescription).toEqual('d1') - - component.changeDataset({value: 'id2'}) - - expect(component.selectedDataset).toEqual('id2') - expect(component.selectedDatasetDescription).toEqual('d2') - }) - -}); diff --git a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts deleted file mode 100644 index 0458757bbf0bfe396386003660dfe7de83cdc37d..0000000000000000000000000000000000000000 --- a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { - AfterViewInit, ChangeDetectorRef, - Component, - ElementRef, - EventEmitter, - OnDestroy, - Output, - ViewChild, - Input, - OnInit, Inject, -} from "@angular/core"; -import {select, Store} from "@ngrx/store"; -import {fromEvent, Observable, Subscription, Subject, combineLatest, of} from "rxjs"; -import {distinctUntilChanged, filter, map, switchMap, switchMapTo} from "rxjs/operators"; -import { ngViewerSelectorClearViewEntries, ngViewerActionClearView } from "src/services/state/ngViewerState.store.helper"; -import {HttpClient} from "@angular/common/http"; -import {BS_ENDPOINT} from "src/util/constants"; -import {getIdFromKgIdObj} from "common/util"; -import {OVERWRITE_SHOW_DATASET_DIALOG_TOKEN} from "src/util/interfaces"; -import { SAPI, SapiRegionModel } from "src/atlasComponents/sapi"; -import { actions } from "src/state/atlasSelection"; -import { atlasAppearance, atlasSelection } from "src/state"; - - -const CONNECTIVITY_NAME_PLATE = 'Connectivity' - -@Component({ - selector: 'connectivity-browser', - templateUrl: './connectivityBrowser.template.html', - providers: [ - { - provide: OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, - useValue: null - } - ] -}) -export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDestroy { - - private setColorMap$: Subject<boolean> = new Subject() - - /** - * accordion expansion should only toggle the clearviewqueue state - * which should be the single source of truth - * setcolormaps$ is set by the presence/absence of clearviewqueue[CONNECTIVITY_NAME_PLATE] - */ - private _isFirstUpdate = true - - public connectivityUrl: string - - private accordionIsExpanded = false - - @Input() - set accordionExpanded(flag: boolean) { - /** - * ignore first update - */ - if (this._isFirstUpdate) { - this._isFirstUpdate = false - return - } - this.accordionIsExpanded = flag - this.store$.dispatch( - ngViewerActionClearView({ - payload: { - [CONNECTIVITY_NAME_PLATE]: flag && !this.noDataReceived - } - }) - ) - this.store$.dispatch({ - type: 'SET_OVERWRITTEN_COLOR_MAP', - payload: flag? CONNECTIVITY_NAME_PLATE : false, - }) - - if (flag) { - this.addNewColorMap() - } else { - this.restoreDefaultColormap() - } - } - - @Output() - connectivityDataReceived = new EventEmitter<any>() - - @Output() - setOpenState: EventEmitter<boolean> = new EventEmitter() - - @Output() - connectivityLoadUrl: EventEmitter<string> = new EventEmitter() - - @Output() connectivityNumberReceived: EventEmitter<string> = new EventEmitter() - - @Input() - set region(val) { - const newRegionName = val && val.name - - if (!val) { - this.store$.dispatch({ - type: 'SET_OVERWRITTEN_COLOR_MAP', - payload: false, - }) - return - } - - if (newRegionName !== this.regionName && this.defaultColorMap) { - this.restoreDefaultColormap() - } - - if (val.status - && !val.name.includes('left hemisphere') - && !val.name.includes('right hemisphere')) { - this.regionHemisphere = val.status - } - - this.regionName = newRegionName - this.regionId = val.id? val.id.kg? getIdFromKgIdObj(val.id.kg) : val.id : null - this.atlasId = val.context.atlas['@id'] - this.parcellationId = val.context.parcellation['@id'] - - if(this.selectedDataset) { - this.setConnectivityUrl() - this.setProfileLoadUrl() - } - // TODO may not be necessary - this.changeDetectionRef.detectChanges() - } - public atlasId: any - public parcellationId: any - public regionId: string - public regionName: string - public regionHemisphere: string = null - public datasetList: any[] = [] - public selectedDataset: any - public selectedDatasetName: any - public selectedDatasetDescription: string = '' - public selectedDatasetKgId: string = '' - public selectedDatasetKgSchema: string = '' - public connectedAreas = [] - - // TODO this may be incompatible - private selectedParcellationFlatRegions$ = this.store$.pipe( - select(atlasSelection.selectors.selectedATP), - switchMap(({ atlas, template, parcellation }) => this.sapi.getParcRegions(atlas["@id"], parcellation["@id"], template["@id"])) - ) - public overwrittenColorMap$: Observable<any> - - private subscriptions: Subscription[] = [] - public expandMenuIndex = -1 - public allRegions = [] - public defaultColorMap: Map<string, Map<number, { red: number, green: number, blue: number }>> - - public noDataReceived = false - - @ViewChild('connectivityComponent', {read: ElementRef}) public connectivityComponentElement: ElementRef<any> - @ViewChild('fullConnectivityGrid') public fullConnectivityGridElement: ElementRef<any> - - constructor( - private store$: Store<any>, - private changeDetectionRef: ChangeDetectorRef, - private httpClient: HttpClient, - @Inject(BS_ENDPOINT) private siibraApiUrl: string, - private sapi: SAPI - ) { - - this.overwrittenColorMap$ = this.store$.pipe( - select(atlasAppearance.selectors.getOverwrittenColormap), - distinctUntilChanged() - ) - } - - public loadUrl: string - public fullConnectivityLoadUrl: string - - ngOnInit(): void { - this.setConnectivityUrl() - - this.httpClient.get<[]>(this.connectivityUrl).subscribe(res => { - this.datasetList = res - this.selectedDataset = this.datasetList[0]?.['@id'] - this.selectedDatasetName = this.datasetList[0]?.['src_name'] - this.selectedDatasetDescription = this.datasetList[0]?.['src_info'] - // this.selectedDatasetKgId = this.datasetList[0]?.kgId || null - // this.selectedDatasetKgSchema = this.datasetList[0]?.kgSchema || null - - this.changeDataset() - }) - } - - public ngAfterViewInit(): void { - this.subscriptions.push( - this.store$.pipe( - select(atlasAppearance.selectors.getOverwrittenColormap), - ).subscribe(value => { - if (this.accordionIsExpanded) { - this.setColorMap$.next(!!value) - } - }) - ) - - /** - * Listen to of clear view entries - * can come from within the component (when connectivity is not available for the dataset) - * --> do not collapse - * or outside (user clicks x in chip) - * --> collapse - */ - this.subscriptions.push( - this.store$.pipe( - select(ngViewerSelectorClearViewEntries), - map(arr => arr.filter(v => v === CONNECTIVITY_NAME_PLATE)), - filter(arr => arr.length ===0), - distinctUntilChanged() - ).subscribe(() => { - if (!this.noDataReceived) { - this.setOpenState.emit(false) - } - }) - ) - - - this.subscriptions.push(this.overwrittenColorMap$.subscribe(ocm => { - if (this.accordionIsExpanded && !ocm) { - this.setOpenState.emit(false) - } - })) - - this.subscriptions.push( - this.selectedParcellationFlatRegions$.subscribe(flattenedRegions => { - this.defaultColorMap = null - this.allRegions = flattenedRegions - }), - ) - - /** - * setting/restoring colormap - */ - this.subscriptions.push( - combineLatest( - this.setColorMap$.pipe( - distinctUntilChanged() - ), - fromEvent(this.connectivityComponentElement?.nativeElement, 'connectivityDataReceived').pipe( - map((e: CustomEvent) => { - if (e.detail !== 'No data') { - this.connectivityNumberReceived.emit(e.detail.length) - } - return e.detail - }) - ) - ).subscribe(([flag, connectedAreas]) => { - if (connectedAreas === 'No data') { - this.noDataReceived = true - return this.clearViewer() - } else { - this.store$.dispatch( - ngViewerActionClearView({ - payload: { - [CONNECTIVITY_NAME_PLATE]: true - } - }) - ) - this.noDataReceived = false - this.connectivityNumberReceived.emit(connectedAreas.length) - this.connectedAreas = connectedAreas - - if (flag) { - this.addNewColorMap() - this.store$.dispatch({ - type: 'SET_OVERWRITTEN_COLOR_MAP', - payload: 'connectivity', - }) - } else { - this.restoreDefaultColormap() - - this.store$.dispatch({type: 'SET_OVERWRITTEN_COLOR_MAP', payload: null}) - - } - } - }) - ) - - this.subscriptions.push( - fromEvent(this.connectivityComponentElement?.nativeElement, 'customToolEvent', {capture: true}) - .subscribe((e: CustomEvent) => { - if (e.detail.name === 'export csv') { - // ToDo Fix in future to use component - const a = document.querySelector('hbp-connectivity-matrix-row'); - (a as any).downloadCSV() - } - }), - fromEvent(this.connectivityComponentElement?.nativeElement, 'connectedRegionClicked', {capture: true}) - .subscribe((e: CustomEvent) => { - this.navigateToRegion(e.detail.name) - }), - ) - } - - public ngOnDestroy(): void { - this.connectivityNumberReceived.emit(null) - this.store$.dispatch( - ngViewerActionClearView({ - payload: { - [CONNECTIVITY_NAME_PLATE]: false - } - }) - ) - this.restoreDefaultColormap() - this.subscriptions.forEach(s => s.unsubscribe()) - } - - private setConnectivityUrl() { - this.connectivityUrl = `${this.siibraApiUrl}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcellationId)}/regions/${encodeURIComponent(this.regionName)}/features/ConnectivityProfile` - } - - private setProfileLoadUrl() { - const url = `${this.connectivityUrl}/${encodeURIComponent(this.selectedDataset)}` - this.connectivityLoadUrl.emit(url) - this.loadUrl = url - } - - clearViewer() { - this.store$.dispatch( - ngViewerActionClearView({ - payload: { - [CONNECTIVITY_NAME_PLATE]: false - } - }) - ) - this.connectedAreas = [] - this.connectivityNumberReceived.emit('0') - - return this.restoreDefaultColormap() - } - - // ToDo Affect on component - changeDataset(event = null) { - if (event) { - this.selectedDataset = event.value - const foundDataset = this.datasetList.find(d => d['@id'] === this.selectedDataset) - this.selectedDatasetName = foundDataset?.['src_name'] - this.selectedDatasetDescription = foundDataset?.['src_info'] - // this.selectedDatasetKgId = foundDataset?.kgId || null - // this.selectedDatasetKgSchema = foundDataset?.kgSchema || null - } - if (this.datasetList.length && this.selectedDataset) { - this.setProfileLoadUrl() - - this.fullConnectivityLoadUrl = `${this.siibraApiUrl}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcellationId)}/features/ConnectivityMatrix/${encodeURIComponent(this.selectedDatasetName)}` - } - } - - navigateToRegion(region: SapiRegionModel) { - this.store$.dispatch( - atlasSelection.actions.navigateToRegion({ - region - }) - ) - } - - selectRegion(region: SapiRegionModel) { - this.store$.dispatch( - actions.selectRegions({ - regions: [ region ] - }) - ) - } - - getRegionWithName(region) { - return this.allRegions.find(ar => { - if (this.regionHemisphere) { - let regionName = region - let regionStatus = null - if (regionName.includes('left hemisphere')) { - regionStatus = 'left hemisphere' - regionName = regionName.replace(' - left hemisphere', ''); - } else if (regionName.includes('right hemisphere')) { - regionStatus = 'right hemisphere' - regionName = regionName.replace(' - right hemisphere', ''); - } - return ar.name === regionName && ar.status === regionStatus - } - - return ar.name === region - }) - } - - public restoreDefaultColormap() { - if (!this.defaultColorMap) return - getWindow().interactiveViewer.viewerHandle.applyLayersColourMap(this.defaultColorMap) - } - - public addNewColorMap() { - if (!this.defaultColorMap) { - this.defaultColorMap = getWindow().interactiveViewer.viewerHandle.getLayersSegmentColourMap() - } - - const existingMap: Map<string, Map<number, { red: number, green: number, blue: number }>> = (getWindow().interactiveViewer.viewerHandle.getLayersSegmentColourMap()) - const colorMap = new Map(existingMap) - - this.allRegions.forEach(r => { - if (r.ngId) { - colorMap.get(r.ngId).set(r.labelIndex, {red: 255, green: 255, blue: 255}) - } - }) - - this.connectedAreas.forEach(area => { - const areaAsRegion = this.allRegions - .filter(r => { - - if (this.regionHemisphere) { - let regionName = area.name - let regionStatus = null - if (regionName.includes('left hemisphere')) { - regionStatus = 'left hemisphere' - regionName = regionName.replace(' - left hemisphere', ''); - } else if (regionName.includes('right hemisphere')) { - regionStatus = 'right hemisphere' - regionName = regionName.replace(' - right hemisphere', ''); - } - return r.name === regionName && r.status === regionStatus - } - - return r.name === area.name - }) - .map(r => r) - - if (areaAsRegion && areaAsRegion.length && areaAsRegion[0].ngId) { - colorMap.get(areaAsRegion[0].ngId).set(areaAsRegion[0].labelIndex, { - red: area.color.r, - green: area.color.g, - blue: area.color.b - }) - } - }) - getWindow().interactiveViewer.viewerHandle.applyLayersColourMap(colorMap) - } - - exportConnectivityProfile() { - const a = document.querySelector('hbp-connectivity-matrix-row'); - (a as any).downloadCSV() - } - - public exportFullConnectivity() { - this.fullConnectivityGridElement?.nativeElement['downloadCSV']() - } - -} - -function getWindow(): any { - return window -} diff --git a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html deleted file mode 100644 index 646d4b0abfeadec3cdc5c5c9675f2fb07580318e..0000000000000000000000000000000000000000 --- a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html +++ /dev/null @@ -1,88 +0,0 @@ -<div class="w-100 h-100 d-block d-flex flex-column sxplr-pb-2"> - <hbp-connectivity-matrix-row - (connectivityDataReceived)="connectivityDataReceived.emit($event)" - #connectivityComponent - *ngIf="regionName" - [region]="regionName + (regionHemisphere? ' - ' + regionHemisphere : '')" - theme="dark" - [loadurl]="loadUrl" - show-export="true" - show-source="false" - show-title="false" - show-toolbar="false" - show-description="false" - show-dataset-name="false" - custom-dataset-selector="true" - tools_showlog="true" - [tools_custom]='[{"name": "exportslot", "type": "slot"}]' - hide-export-view="true"> - - <div slot="dataset"> - <div *ngIf="datasetList.length && selectedDataset" class=" flex-grow-0 flex-shrink-0 d-flex flex-row flex-nowrap"> - <mat-form-field class="flex-grow-1 flex-shrink-1 w-0"> - <mat-label> - Dataset - </mat-label> - - <mat-select - panelClass="no-max-width" - [value]="selectedDataset" - (selectionChange)="changeDataset($event)"> - <mat-option - *ngFor="let dataset of datasetList" - [value]="dataset['@id']"> - {{ dataset['src_name'] }} - </mat-option> - </mat-select> - </mat-form-field> - <ng-container *ngIf="selectedDataset && (selectedDatasetDescription || selectedDatasetKgId)" > - TODO please reimplmenent button to explore KG dataset - <button class="flex-grow-0 flex-shrink-0" - mat-icon-button - (click)="exportFullConnectivity()" - matTooltip="Export full connectivity profile"> - <i class="fas fa-download"></i> - </button> - </ng-container> - </div> - <div> - Connectivity Profile - </div> - </div> - - <div slot="connectedRegionMenu"> - <div class="d-flex flex-column sxplr-p-0 m-0" *ngIf="expandMenuIndex >= 0"> - <mat-card-subtitle class="sxplr-pt-2 sxplr-pr-2 sxplr-pl-2 sxplr-pb-0"> - <div class="d-flex justify-content-between align-items-center"> - {{connectedAreas[expandMenuIndex].name}} - <small class="d-flex align-items-center"> - <button mat-icon-button matTooltip="Navigate" - (click)="navigateToRegion(connectedAreas[expandMenuIndex].name)"> - <i class="fas fa-map-marked-alt"></i> - </button> - <button mat-icon-button matTooltip="Explore" - (click)="selectRegion(getRegionWithName(connectedAreas[expandMenuIndex].name))"> - <i class="fas fa-search-location"></i> - </button> - </small> - </div> - </mat-card-subtitle> - <mat-divider></mat-divider> - </div> - </div> - <div slot="exportslot"> - <button mat-icon-button - [disabled]="noDataReceived" - (click)="exportConnectivityProfile()" - matTooltip="Export connectivity profile"> - <i class="fas fa-download mb-2"></i> - </button> - </div> - </hbp-connectivity-matrix-row> - <full-connectivity-grid #fullConnectivityGrid - [loadurl]="fullConnectivityLoadUrl" - [name]="selectedDataset" - [description]="selectedDatasetDescription" - only-export="true"> - </full-connectivity-grid> -</div> diff --git a/src/atlasComponents/connectivity/hasConnectivity.directive.ts b/src/atlasComponents/connectivity/hasConnectivity.directive.ts deleted file mode 100644 index 8879de8c093488b90acd5fb81d57820f341da36e..0000000000000000000000000000000000000000 --- a/src/atlasComponents/connectivity/hasConnectivity.directive.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {Directive, Inject, Input, OnDestroy, OnInit} from "@angular/core"; -import {of, Subscription} from "rxjs"; -import {switchMap} from "rxjs/operators"; -import {BS_ENDPOINT} from "src/util/constants"; -import {HttpClient} from "@angular/common/http"; - -@Directive({ - selector: '[has-connectivity]', - exportAs: 'hasConnectivityDirective' -}) - -export class HasConnectivity implements OnInit, OnDestroy { - - private subscriptions: Subscription[] = [] - - @Input() region: any - - public hasConnectivity = false - public connectivityNumber = 0 - - constructor(@Inject(BS_ENDPOINT) private siibraApiUrl: string, - private httpClient: HttpClient) {} - - ngOnInit() { - this.checkConnectivity(this.region[0]) - } - - checkConnectivity(region) { - if (!region.context) { - this.hasConnectivity = false - return - } - const {atlas, parcellation, template} = region.context - if (region.name) { - const connectivityUrl = `${this.siibraApiUrl}/atlases/${encodeURIComponent(atlas['@id'])}/parcellations/${encodeURIComponent(parcellation['@id'])}/regions/${encodeURIComponent(region.name)}/features/ConnectivityProfile` - - this.subscriptions.push( - this.httpClient.get<[]>(connectivityUrl).pipe(switchMap((res: any[]) => { - if (res && res.length) { - this.hasConnectivity = true - const url = `${connectivityUrl}/${encodeURIComponent(res[0]['@id'])}` - return this.httpClient.get(url) - } else { - this.hasConnectivity = false - this.connectivityNumber = 0 - } - return of(null) - })).subscribe(res => { - - if (res && res['__profile']) { - this.connectivityNumber = res['__profile'].filter(p => p > 0).length - } - }) - ) - } - } - - ngOnDestroy(){ - while (this.subscriptions.length > 0) this.subscriptions.pop().unsubscribe() - } - -} diff --git a/src/atlasComponents/connectivity/index.ts b/src/atlasComponents/connectivity/index.ts deleted file mode 100644 index c9a2e808fc6290283e77ea4bccff8f199eea3ca0..0000000000000000000000000000000000000000 --- a/src/atlasComponents/connectivity/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ConnectivityBrowserComponent } from "./connectivityBrowser/connectivityBrowser.component"; -export { AtlasCmptConnModule } from "./module"; \ No newline at end of file diff --git a/src/atlasComponents/connectivity/module.ts b/src/atlasComponents/connectivity/module.ts deleted file mode 100644 index c9231d4605589eeb8a7e5f2dd1f66a4a2b34089f..0000000000000000000000000000000000000000 --- a/src/atlasComponents/connectivity/module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; -import { AngularMaterialModule } from "src/sharedModules"; -import { ConnectivityBrowserComponent } from "./connectivityBrowser/connectivityBrowser.component"; -import {HasConnectivity} from "src/atlasComponents/connectivity/hasConnectivity.directive"; - -@NgModule({ - imports: [ - CommonModule, - AngularMaterialModule - ], - declarations: [ - ConnectivityBrowserComponent, - HasConnectivity - ], - exports: [ - ConnectivityBrowserComponent, - HasConnectivity - ], - schemas: [ - CUSTOM_ELEMENTS_SCHEMA, - ], -}) - -export class AtlasCmptConnModule{} diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts index b353aed0283c41b5f983adcb01fed1ab0a1c131b..d8b1d2306ae27b9215fd7ca9ffa0f43c9208e0a3 100644 --- a/src/atlasComponents/sapi/core/sapiRegion.ts +++ b/src/atlasComponents/sapi/core/sapiRegion.ts @@ -1,19 +1,33 @@ import { SAPI } from ".."; -import { SapiRegionalFeatureModel } from "../type"; +import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel } from "../type"; +import { strToRgb, hexToRgb } from 'common/util' export class SAPIRegion{ + + static GetDisplayColor(region: SapiRegionModel){ + if (!region) { + throw new Error(`region must be provided!`) + } + if (region.hasAnnotation?.displayColor) { + return hexToRgb(region.hasAnnotation.displayColor) + } + return strToRgb(JSON.stringify(region)) + } + + private prefix: string + constructor( private sapi: SAPI, public atlasId: string, public parcId: string, public id: string, ){ - + this.prefix = `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}` } getFeatures(spaceId: string): Promise<SapiRegionalFeatureModel[]> { return this.sapi.http.get<SapiRegionalFeatureModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}/features`, + `${this.prefix}/features`, { params: { space_id: spaceId @@ -24,7 +38,33 @@ export class SAPIRegion{ getFeatureInstance(instanceId: string, spaceId: string = null): Promise<SapiRegionalFeatureModel> { return this.sapi.http.get<SapiRegionalFeatureModel>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`, + `${this.prefix}/features/${encodeURIComponent(instanceId)}`, + { + params: { + space_id: spaceId + } + } + ).toPromise() + } + + getMapInfo(spaceId: string): Promise<SapiRegionMapInfoModel> { + return this.sapi.http.get<SapiRegionMapInfoModel>( + `${this.prefix}/regional_map/info`, + { + params: { + space_id: spaceId + } + } + ).toPromise() + } + + getMapUrl(spaceId: string): string { + return `${this.prefix}/regional_map/map?space_id=${encodeURI(spaceId)}` + } + + getDetail(spaceId: string): Promise<SapiRegionModel> { + return this.sapi.http.get<SapiRegionModel>( + `${this.prefix}/${this.id}`, { params: { space_id: spaceId diff --git a/src/atlasComponents/sapi/stories.base.ts b/src/atlasComponents/sapi/stories.base.ts index e3cd6f322117fed8ff26ec9c88227fee77b28ae7..86dddb2f0954bdc027087e35590a8c3452b9daaa 100644 --- a/src/atlasComponents/sapi/stories.base.ts +++ b/src/atlasComponents/sapi/stories.base.ts @@ -68,6 +68,9 @@ export async function getAtlas(id: string): Promise<SapiAtlasModel>{ export async function getParc(atlasId: string, id: string): Promise<SapiParcellationModel>{ return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId}/parcellations/${id}`)).json() } +export async function getParcRegions(atlasId: string, id: string, spaceId: string): Promise<SapiRegionModel[]>{ + return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json() +} export async function getSpace(atlasId: string, id: string): Promise<SapiSpaceModel> { return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId}/spaces/${id}`)).json() @@ -85,6 +88,10 @@ export async function getJba29(): Promise<SapiParcellationModel> { return await getParc(atlasId.human, parcId.human.jba29) } +export async function getJba29Regions(): Promise<SapiRegionModel[]> { + return await getParcRegions(atlasId.human, parcId.human.jba29, spaceId.human.mni152) +} + export async function getHoc1Left(spaceId=null): Promise<SapiRegionModel> { if (!spaceId) { return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20left`)).json() @@ -92,6 +99,13 @@ export async function getHoc1Left(spaceId=null): Promise<SapiRegionModel> { return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20left?space_id=${encodeURIComponent(spaceId)}`)).json() } +export async function get44Left(spaceId=null): Promise<SapiRegionModel> { + if (!spaceId) { + return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left`)).json() + } + return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left?space_id=${encodeURIComponent(spaceId)}`)).json() +} + export async function getHoc1Features(): Promise<SapiRegionalFeatureModel[]> { return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20left/features`)).json() } diff --git a/src/atlasComponents/sapi/type.ts b/src/atlasComponents/sapi/type.ts index 8e645b34e2133b1973b4cea33663614d13d9b7c5..9de24016f86a548458f9fccf0c088b0bf6b7b529 100644 --- a/src/atlasComponents/sapi/type.ts +++ b/src/atlasComponents/sapi/type.ts @@ -15,6 +15,7 @@ export type SapiAtlasModel = components["schemas"]["SapiAtlasModel"] export type SapiSpaceModel = components["schemas"]["SapiSpaceModel"] export type SapiParcellationModel = components["schemas"]["SapiParcellationModel"] export type SapiRegionModel = components["schemas"]["siibra__openminds__SANDS__v3__atlas__parcellationEntityVersion__Model"] +export type SapiRegionMapInfoModel = components["schemas"]["NiiMetadataModel"] export type SapiSpatialFeatureModel = components["schemas"]["VOIDataModel"] export type SapiVOIDataResponse = components["schemas"]["VOIDataModel"] diff --git a/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.component.ts b/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.component.ts index 0da41ae9de3a2d0a7274ab0849f604d26f261e9e..815a4d45ed29cad974c1465e8a8cca5a0af6bcee 100644 --- a/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.component.ts +++ b/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.component.ts @@ -1,8 +1,8 @@ -import { Component } from "@angular/core"; +import { Component, OnDestroy } from "@angular/core"; import { Store, select } from "@ngrx/store"; -import { Observable } from "rxjs"; +import { Observable, Subscription } from "rxjs"; import { ARIA_LABELS } from 'common/constants' -import { atlasSelection } from "src/state" +import { atlasSelection, generalActions } from "src/state" import { SAPI, SapiAtlasModel } from "src/atlasComponents/sapi"; @Component({ @@ -13,8 +13,10 @@ import { SAPI, SapiAtlasModel } from "src/atlasComponents/sapi"; ] }) -export class SapiViewsCoreAtlasAtlasDropdownSelector{ +export class SapiViewsCoreAtlasAtlasDropdownSelector implements OnDestroy{ + private subs: Subscription[] = [] + private fetchedAtlases: SapiAtlasModel[] = [] public fetchedAtlases$: Observable<SapiAtlasModel[]> = this.sapi.atlases$ public selectedAtlas$: Observable<SapiAtlasModel> = this.store$.pipe( select(atlasSelection.selectors.selectedAtlas) @@ -26,14 +28,29 @@ export class SapiViewsCoreAtlasAtlasDropdownSelector{ private store$: Store<any>, private sapi: SAPI, ){ + this.subs.push( + this.fetchedAtlases$.subscribe(val => this.fetchedAtlases = val) + ) + } + + ngOnDestroy(): void { + this.subs.pop().unsubscribe() } handleChangeAtlas({ value }) { - this.store$.dispatch( - atlasSelection.actions.selectATPById({ - atlasId: value - }) - ) + const found = this.fetchedAtlases.find(atlas => atlas["@id"] === value) + if (found) { + this.store$.dispatch( + atlasSelection.actions.selectAtlas({ + atlas: found + }) + ) + } else { + this.store$.dispatch( + generalActions.generalActionError({ + message: `Atlas with id ${value} not found.` + }) + ) + } } } - diff --git a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts index d3cbe274f4ba2194ebfacfc40837eed3c4eca20e..666ef5075c26f9d9fea5fa2f7ea6cde4560d9dac 100644 --- a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts +++ b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts @@ -62,10 +62,6 @@ export class SapiViewsCoreAtlasAtlasTmplParcSelector { public selectedTemplate$ = this.store$.pipe( select(atlasSelection.selectors.selectedTemplate), - withLatestFrom(this.availableTemplates$), - map(([selectedTmpl, fullInfoTemplates]) => { - return fullInfoTemplates.find(t => t['@id'] === selectedTmpl['@id']) - }) ) public selectedParcellation$ = this.store$.pipe( @@ -107,6 +103,14 @@ export class SapiViewsCoreAtlasAtlasTmplParcSelector { this.selectorExpanded = !this.selectorExpanded } + closeSelector(){ + this.selectorExpanded = false + } + + openSelector() { + this.selectorExpanded = true + } + selectTemplate(tmpl: SapiSpaceModel) { this.showOverlayIntent$.next(true) diff --git a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html index e5fba069a65e277955b1ac34bb736005b6aa9c96..68ce4f9a8540b0c1154408328260ed2e00a07b5a 100644 --- a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html +++ b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html @@ -1,11 +1,22 @@ +<ng-template #headerTmpl> + <ng-content select="[header]"></ng-content> +</ng-template> + <mat-card *ngIf="!dataset"> - Dataset not specified. + + <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template> + <span> + Dataset not specified. + </span> </mat-card> <mat-card *ngIf="dataset" - class="mat-elevation-z4 sxplr-z-index-4"> + class="mat-elevation-z4 sxplr-z-4"> <mat-card-title> - {{ dataset.metadata.fullName }} + <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template> + <span> + {{ dataset.metadata.fullName }} + </span> </mat-card-title> <mat-card-subtitle class="sxplr-d-inline-flex sxplr-align-items-stretch"> @@ -21,7 +32,7 @@ </mat-card-subtitle> </mat-card> -<mat-card *ngIf="dataset" class="sxplr-z-index-0"> +<mat-card *ngIf="dataset" class="sxplr-z-0"> <mat-card-content> <markdown-dom class="sxplr-muted" [markdown]="dataset?.metadata?.description"> </markdown-dom> diff --git a/src/atlasComponents/sapiViews/core/parcellation/filterUnsupportedParc.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/filterUnsupportedParc.pipe.ts index 5d2412cdd76b13e568166f7c68ae0ecfc73b8b1e..3a9d1a30ec5a9bc9ed281cca3e44051cd7b5b163 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/filterUnsupportedParc.pipe.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/filterUnsupportedParc.pipe.ts @@ -24,7 +24,13 @@ export class FilterUnsupportedParcPipe implements PipeTransform{ if (p instanceof GroupedParcellation) { return hideGroup.indexOf(p.name) < 0 } - return unsupportedIds.indexOf(p["@id"]) < 0 + if (unsupportedIds.includes(p["@id"])) { + return false + } + if (p.version) { + return !p.version.deprecated + } + return true }) } } diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..506acc493479285eb4271aee38f5cd1798174fda --- /dev/null +++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.ts @@ -0,0 +1,84 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; + +export function getTraverseFunctions(parcellations: SapiParcellationModel[]) { + + const getTraverse = (key: 'prev' | 'next') => (parc: SapiParcellationModel) => { + if (!parc.version) { + throw new Error(`parcellation ${parc.name} does not have version defined!`) + } + if (!parc.version[key]) { + return null + } + const found = parcellations.find(p => p["@id"] === parc.version[key]["@id"]) + if (!found) { + throw new Error(`parcellation ${parc.name} references ${parc.version[key]['@id']} as ${key} version, but it cannot be found.`) + } + return found + } + + const findNewer = getTraverse('next') + const findOlder = getTraverse('prev') + + const getFindMostFn = (findNewest) => { + const useFn = findNewest + ? findNewer + : findOlder + return () => { + let cursor = parcellations[0] + let returnParc: SapiParcellationModel + while (cursor) { + returnParc = cursor + cursor = useFn(cursor) + } + return returnParc + } + } + + return { + findNewer, + findOlder, + findNewest: getFindMostFn(true), + findOldest: getFindMostFn(false) + } + +} + + +@Pipe({ + name: 'orderParcellationByVersion', + pure: true +}) + +export class OrderParcellationByVersionPipe implements PipeTransform{ + public transform(parcellations: SapiParcellationModel[], newestFirst: boolean = true, index: number = 0) { + const { + findNewer, + findOlder + } = getTraverseFunctions(parcellations) + + const findMostFn = newestFirst ? findNewer : findOlder + const tranverseFn = newestFirst ? findOlder : findNewer + + const mostParc = (() => { + let cursor = parcellations[0] + let returnParc: SapiParcellationModel + while (cursor) { + returnParc = cursor + cursor = findMostFn(cursor) + } + return returnParc + })() + + let idx = 0 + let cursor = mostParc + while (idx < index) { + cursor = tranverseFn(cursor) + if (!cursor) { + throw new Error(`index out of bound`) + } + idx ++ + } + return cursor + } +} diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts index e28c7bec05cde38553e4bc7417dc7418155327bc..f56ae3fdb4de939dad8b5ac6285f10a81e7cda8b 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts @@ -2,6 +2,8 @@ import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from import { Observable } from "rxjs"; import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; import { ParcellationVisibilityService } from "../parcellationVis.service"; +import { ARIA_LABELS } from "common/constants" +import { getTraverseFunctions } from "../parcellationVersion.pipe"; @Component({ selector: `sxplr-sapiviews-core-parcellation-smartchip`, @@ -13,6 +15,8 @@ import { ParcellationVisibilityService } from "../parcellationVis.service"; export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges{ + public ARIA_LABELS = ARIA_LABELS + @Input('sxplr-sapiviews-core-parcellation-smartchip-parcellation') parcellation: SapiParcellationModel @@ -47,34 +51,13 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges } this.otherVersions = [] - const getTraverse = (key: 'prev' | 'next') => (parc: SapiParcellationModel) => { - if (!parc.version) { - throw new Error(`parcellation ${parc.name} does not have version defined!`) - } - if (!parc.version[key]) { - return null - } - const found = this.parcellations.find(p => p["@id"] === parc.version[key]["@id"]) - if (!found) { - throw new Error(`parcellation ${parc.name} references ${parc.version[key]['@id']} as ${key} version, but it cannot be found.`) - } - return found - } - const findNewer = getTraverse('next') - const findOlder = getTraverse('prev') + const { + findNewest, + findOlder + } = getTraverseFunctions(this.parcellations) - const newest = (() => { - let cursor = this.parcellation - let newest: SapiParcellationModel - while (cursor) { - newest = cursor - cursor = findNewer(cursor) - } - return newest - })() - - let cursor: SapiParcellationModel = newest + let cursor: SapiParcellationModel = findNewest() while (cursor) { this.otherVersions.push(cursor) cursor = findOlder(cursor) @@ -93,7 +76,6 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges selectParcellation(parc: SapiParcellationModel){ if (parc === this.parcellation) return - - console.log('select parcellation', parc) + this.onSelectParcellation.emit(parc) } } diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html index 0473218f9c475ea3b61c25bab951f88645e2f546..88cde95071e545574c543ef36c7cbfbe6223ebb1 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html +++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html @@ -1,12 +1,12 @@ <mat-menu #otherParcMenu="matMenu" [hasBackdrop]="false" - class="sxplr-bg-none sxplr-of-x-hidden sxplr-box-shadow-none overwrite-max-width-80vw"> + class="sxplr-bg-none sxplr-of-x-hidden sxplr-box-shadow-none sxplr-mxw-80vw"> <div (iav-outsideClick)="menuTrigger.closeMenu()"> <sxplr-sapiviews-core-parcellation-chip *ngFor="let parc of otherVersions" [sxplr-sapiviews-core-parcellation-chip-parcellation]="parc" [sxplr-sapiviews-core-parcellation-chip-color]="parcellation === parc ? 'primary' : 'default'" - (click)="selectParcellation(parc)"> + (sxplr-sapiviews-core-parcellation-chip-onclick)="selectParcellation(parc)"> </sxplr-sapiviews-core-parcellation-chip> </div> @@ -28,7 +28,10 @@ <div prefix class="sxplr-scale-70"> <button mat-mini-fab [color]="(parcellationVisibility$ | async) ? 'primary' : 'default'" + [matTooltip]="ARIA_LABELS.TOGGLE_DELINEATION" iav-stop="mousedown click" + [iav-key-listener]="[{'type': 'keydown', 'key': 'q', 'capture': true, 'target': 'document' }]" + (iav-key-event)="toggleParcellationVisibility()" (click)="toggleParcellationVisibility()"> <i class="fas" [ngClass]="(parcellationVisibility$ | async) ? 'fa-eye': 'fa-eye-slash'"> diff --git a/src/atlasComponents/sapiViews/core/region/module.ts b/src/atlasComponents/sapiViews/core/region/module.ts index 511a1fcb8c770c56dbd9e64de315c7c09b9d38c4..cdafe1bcd9779d40119f938b3c221c26865b24ae 100644 --- a/src/atlasComponents/sapiViews/core/region/module.ts +++ b/src/atlasComponents/sapiViews/core/region/module.ts @@ -1,8 +1,10 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; +import { SpinnerModule } from "src/components/spinner"; import { AngularMaterialModule } from "src/sharedModules"; import { SapiViewsFeaturesModule } from "../../features"; import { SapiViewsUtilModule } from "../../util/module"; +import { SapiViewsCoreRegionRegionChip } from "./region/chip/region.chip.component"; import { SapiViewsCoreRegionRegionListItem } from "./region/listItem/region.listItem.component"; import { SapiViewsCoreRegionRegionBase } from "./region/region.base.directive"; import { SapiViewsCoreRegionRegionalFeatureDirective } from "./region/region.features.directive"; @@ -13,17 +15,20 @@ import { SapiViewsCoreRegionRegionRich } from "./region/rich/region.rich.compone CommonModule, AngularMaterialModule, SapiViewsUtilModule, - SapiViewsFeaturesModule + SapiViewsFeaturesModule, + SpinnerModule, ], declarations: [ SapiViewsCoreRegionRegionListItem, SapiViewsCoreRegionRegionRich, + SapiViewsCoreRegionRegionChip, SapiViewsCoreRegionRegionBase, SapiViewsCoreRegionRegionalFeatureDirective, ], exports: [ SapiViewsCoreRegionRegionListItem, SapiViewsCoreRegionRegionRich, + SapiViewsCoreRegionRegionChip, SapiViewsCoreRegionRegionBase, SapiViewsCoreRegionRegionalFeatureDirective, ] diff --git a/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.component.ts b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1852f3e602a60d3739dc5bffa89a190b64fb33f7 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.component.ts @@ -0,0 +1,20 @@ +import { Component, EventEmitter, Output } from "@angular/core"; +import { SapiViewsCoreRegionRegionBase } from "../region.base.directive"; + +@Component({ + selector: `sxplr-sapiviews-core-region-region-chip`, + templateUrl: `./region.chip.template.html`, + styleUrls: [ + `./region.chip.style.css` + ] +}) + +export class SapiViewsCoreRegionRegionChip extends SapiViewsCoreRegionRegionBase { + shouldFetchDetail = true + @Output('sxplr-sapiviews-core-region-region-chip-clicked') + clickEmitter = new EventEmitter<MouseEvent>() + + onClick(event: MouseEvent){ + this.clickEmitter.emit(event) + } +} diff --git a/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.stories.ts b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..1fc06697565f11e974950f757052ead015238ed6 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.stories.ts @@ -0,0 +1,134 @@ +import { CommonModule } from "@angular/common" +import { HttpClientModule } from "@angular/common/http" +import { Meta, moduleMetadata, Story } from "@storybook/angular" +import { SAPI, SapiParcellationModel } from "src/atlasComponents/sapi" +import { atlasId, getAtlas, provideDarkTheme, getParc, getHumanAtlas, getJba29, getMni152, getHoc1Left, get44Left } from "src/atlasComponents/sapi/stories.base" +import { AngularMaterialModule } from "src/sharedModules" +import { SapiViewsCoreRegionModule } from "../../module" +import { SapiViewsCoreRegionRegionChip } from "./region.chip.component" + + +export default { + component: SapiViewsCoreRegionRegionChip, + decorators: [ + moduleMetadata({ + imports: [ + CommonModule, + HttpClientModule, + SapiViewsCoreRegionModule, + AngularMaterialModule, + ], + providers: [ + SAPI, + ...provideDarkTheme, + ], + declarations: [] + }) + ], +} as Meta + +const Template: Story<SapiViewsCoreRegionRegionChip> = (args: SapiViewsCoreRegionRegionChip, { loaded, parameters }) => { + const { + atlas, + parcellation, + template, + region, + } = loaded + const { + contentProjection + } = parameters + + return ({ + props: { + atlas, + parcellation, + template, + region, + }, + template: ` + <sxplr-sapiviews-core-region-region-chip> + ${contentProjection || ''} + </sxplr-sapiviews-core-region-region-chip> + ` + }) +} +Template.loaders = [] + +const getContentProjection = ({ prefix = null, suffix = null }) => { + let returnVal = `` + if (prefix) { + returnVal += `<div prefix>${prefix}</div>` + } + if (suffix) { + returnVal += `<div suffix>${suffix}</div>` + } + return returnVal +} + +export const Default = Template.bind({}) +Default.loaders = [ + async () => { + + const atlas = await getHumanAtlas() + const parcellation = await getJba29() + const template = await getMni152() + const region = await getHoc1Left() + + return { + atlas, + parcellation, + template, + region, + } + } +] + +export const Dark = Template.bind({}) +Dark.loaders = [ + async () => { + + const atlas = await getHumanAtlas() + const parcellation = await getJba29() + const template = await getMni152() + const region = await get44Left() + + return { + atlas, + parcellation, + template, + region, + } + } +] + +export const Prefix = Template.bind({}) +Prefix.loaders = [ + ...Default.loaders +] +Prefix.parameters = { + contentProjection: getContentProjection({ + prefix: `PREFIX`, + }) +} + +export const Suffix = Template.bind({}) +Suffix.loaders = [ + ...Default.loaders +] +Suffix.parameters = { + contentProjection: getContentProjection({ + suffix: `SUFFIX`, + }) +} + + +export const PrefixSuffix = Template.bind({}) +PrefixSuffix.loaders = [ + ...Default.loaders +] +PrefixSuffix.parameters = { + contentProjection: getContentProjection({ + prefix: `PREFIX`, + suffix: `SUFFIX`, + }) +} diff --git a/src/services/state/uiState/selectors.spec.ts b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.style.css similarity index 100% rename from src/services/state/uiState/selectors.spec.ts rename to src/atlasComponents/sapiViews/core/region/region/chip/region.chip.style.css diff --git a/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.template.html b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.template.html new file mode 100644 index 0000000000000000000000000000000000000000..03db372cf5c31f39529a87d07bf1efa8c4bdb9c6 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.template.html @@ -0,0 +1,35 @@ +<ng-template #prefixTmpl> + <ng-content select="[prefix]"></ng-content> +</ng-template> + +<ng-template #suffixTmpl> + <ng-content select="[suffix]"></ng-content> +</ng-template> + +<div *ngIf="!region"> + <div class="sxplr-d-inline-block"> + <ng-template [ngTemplateOutlet]="prefixTmpl"></ng-template> + </div> + <spinner-cmp class="sxplr-d-inline-block"></spinner-cmp> + <div class="sxplr-d-inline-block"> + <ng-template [ngTemplateOutlet]="suffixTmpl"></ng-template> + </div> +</div> + +<mat-chip-list *ngIf="region" + [ngClass]="{ + 'darktheme': regionDarkmode, + 'lighttheme': !regionDarkmode + }"> + <mat-chip + (click)="onClick($event)" + class="iv-custom-comp text" + [style.backgroundColor]="regionRgbString" + > + <ng-template [ngTemplateOutlet]="prefixTmpl"></ng-template> + <span class="mat-body"> + {{ region.name }} + </span> + <ng-template [ngTemplateOutlet]="suffixTmpl"></ng-template> + </mat-chip> +</mat-chip-list> diff --git a/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts b/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts index a84edee486bcbde88230a3629a5a0738ccc8db6b..4ff63ab42055f5ab35e85618db311102a9719ccd 100644 --- a/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts +++ b/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts @@ -1,12 +1,20 @@ -import { Directive, Input } from "@angular/core"; +import { Directive, EventEmitter, Input, OnDestroy, Output } from "@angular/core"; import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; -import { strToRgb, rgbToHsl, hexToRgb } from 'common/util' +import { rgbToHsl } from 'common/util' +import { SAPI } from "src/atlasComponents/sapi/sapi.service"; +import { Subject } from "rxjs"; +import { SAPIRegion } from "src/atlasComponents/sapi/core"; @Directive({ - selector: `sxplr-sapiviews-core-region` + selector: `[sxplr-sapiviews-core-region]`, + exportAs: "sapiViewsCoreRegion" }) export class SapiViewsCoreRegionRegionBase { + @Input('sxplr-sapiviews-core-region-detail-flag') + shouldFetchDetail = false + public fetchInProgress = false + @Input('sxplr-sapiviews-core-region-atlas') atlas: SapiAtlasModel @Input('sxplr-sapiviews-core-region-template') @@ -14,11 +22,36 @@ export class SapiViewsCoreRegionRegionBase { @Input('sxplr-sapiviews-core-region-parcellation') parcellation: SapiParcellationModel - private _region: SapiRegionModel + @Output('sxplr-sapiviews-core-region-navigate-to') + onNavigateTo = new EventEmitter<number[]>() + + protected region$ = new Subject<SapiRegionModel>() + private _region: SapiRegionModel @Input('sxplr-sapiviews-core-region-region') set region(val: SapiRegionModel) { - this._region = val - this.setupRegionDarkmode() + + this.region$.next(val) + + if (!this.shouldFetchDetail || !val) { + this._region = val + this.setupRegionDarkmode() + return + } + this.fetchInProgress = true + this._region = null + + this.fetchDetail(val) + .then(r => { + this._region = r + }) + .catch(e => { + console.warn(`populating detail failed.`, e) + this._region = val + }) + .finally(() => { + this.fetchInProgress = false + this.setupRegionDarkmode() + }) } get region(){ return this._region @@ -42,12 +75,7 @@ export class SapiViewsCoreRegionRegionBase { /** * color */ - let rgb = [255, 200, 200] - if (this.region.hasAnnotation?.displayColor) { - rgb = hexToRgb(this.region?.hasAnnotation?.displayColor) - } else { - rgb = strToRgb(JSON.stringify(this.region)) - } + let rgb = SAPIRegion.GetDisplayColor(this.region) this.regionRgbString = `rgb(${rgb.join(',')})` const [_h, _s, l] = rgbToHsl(...rgb) this.regionDarkmode = l < 0.4 @@ -67,6 +95,14 @@ export class SapiViewsCoreRegionRegionBase { } navigateTo(position: number[]) { - console.log('navigate to region', position) + this.onNavigateTo.emit(position.map(v => v*1e6)) + } + + protected async fetchDetail(region: SapiRegionModel) { + return await this.sapi.getRegion(this.atlas["@id"],this.parcellation["@id"], region.name).getDetail(this.template["@id"]) + } + + constructor(protected sapi: SAPI){ + } } diff --git a/src/atlasComponents/sapiViews/core/region/region/region.features.directive.ts b/src/atlasComponents/sapiViews/core/region/region/region.features.directive.ts index c2e1d640fc4069217f533ca859a534c530c30235..d542fc05dd988f4f599b292633b20445402f8dea 100644 --- a/src/atlasComponents/sapiViews/core/region/region/region.features.directive.ts +++ b/src/atlasComponents/sapiViews/core/region/region/region.features.directive.ts @@ -1,6 +1,6 @@ import { Directive, OnChanges, SimpleChanges } from "@angular/core"; -import { BehaviorSubject, Observable } from "rxjs"; -import { switchMap, filter, startWith, shareReplay } from "rxjs/operators"; +import { BehaviorSubject, merge, Observable } from "rxjs"; +import { switchMap, filter, startWith, shareReplay, mapTo, delay, tap } from "rxjs/operators"; import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; import { SapiViewsCoreRegionRegionBase } from "./region.base.directive"; @@ -23,18 +23,30 @@ export class SapiViewsCoreRegionRegionalFeatureDirective extends SapiViewsCoreRe this.ATPR$.next({ atlas, template, parcellation, region }) } - constructor(private sapi: SAPI){ - super() + constructor(sapi: SAPI){ + super(sapi) } - public listOfFeatures$: Observable<SapiRegionalFeatureModel[]> = this.ATPR$.pipe( + private features$: Observable<SapiRegionalFeatureModel[]> = this.ATPR$.pipe( filter(arg => { if (!arg) return false const { atlas, parcellation, region, template } = arg return !!atlas && !!parcellation && !!region && !!template }), switchMap(({ atlas, parcellation, region, template }) => this.sapi.getRegionFeatures(atlas["@id"], parcellation["@id"], template["@id"], region.name)), + ) + + public listOfFeatures$: Observable<SapiRegionalFeatureModel[]> = this.features$.pipe( startWith([]), shareReplay(1), ) + + public busy$: Observable<boolean> = merge( + this.ATPR$.pipe( + mapTo(true) + ), + this.features$.pipe( + mapTo(false) + ) + ) } diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts index 1d44db4e8578b0aa7d5b9eb663e1979b5f5caaf8..cf2e067d1d147fee14456c7d8b31a6e6a3e153a2 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts @@ -1,9 +1,11 @@ -import { Component, EventEmitter, Inject, Output } from "@angular/core"; -import { Observable, Subject } from "rxjs"; +import { Component, EventEmitter, Inject, OnDestroy, Output } from "@angular/core"; +import { Observable, Subject, Subscription } from "rxjs"; +import { filter } from "rxjs/operators" import { DARKTHEME } from "src/util/injectionTokens"; import { SapiViewsCoreRegionRegionBase } from "../region.base.directive"; import { ARIA_LABELS, CONST } from 'common/constants' import { SapiRegionalFeatureModel } from "src/atlasComponents/sapi"; +import { SAPI } from "src/atlasComponents/sapi/sapi.service"; @Component({ selector: 'sxplr-sapiviews-core-region-region-rich', @@ -13,23 +15,20 @@ import { SapiRegionalFeatureModel } from "src/atlasComponents/sapi"; ] }) -export class SapiViewsCoreRegionRegionRich extends SapiViewsCoreRegionRegionBase{ +export class SapiViewsCoreRegionRegionRich extends SapiViewsCoreRegionRegionBase { - get ARIA_LABELS() { - return ARIA_LABELS - } - - get CONST() { - return CONST - } + shouldFetchDetail = true + public ARIA_LABELS = ARIA_LABELS + public CONST = CONST @Output('sxplr-sapiviews-core-region-region-rich-feature-clicked') featureClicked = new EventEmitter<SapiRegionalFeatureModel>() constructor( - @Inject(DARKTHEME) public darktheme$: Observable<boolean> + sapi: SAPI, + @Inject(DARKTHEME) public darktheme$: Observable<boolean>, ){ - super() + super(sapi) } handleRegionalFeatureClicked(feat: SapiRegionalFeatureModel) { diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.stories.ts b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.stories.ts index 818f0b227362e5ce9ac3a6035a09bc4e25b2818e..551ed5a081fb5536fc91ff2e2d4c2196aed3740f 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.stories.ts +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.stories.ts @@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common" import { HttpClientModule } from "@angular/common/http" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI } from "src/atlasComponents/sapi" -import { getHoc1Left, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" +import { atlasId, getHoc1Left, getHumanAtlas, getJba29, getJba29Regions, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" import { SapiViewsCoreRegionModule } from "../../module" import { SapiViewsCoreRegionRegionRich } from "./region.rich.component" import { action } from '@storybook/addon-actions'; @@ -41,7 +41,6 @@ const Template: Story<SapiViewsCoreRegionRegionRich> = (args: SapiViewsCoreRegio const { contentProjection } = parameters return ({ props: { - ...args, atlas: human, template: mni152, parcellation: jba29, @@ -63,7 +62,6 @@ const loadRegions = async () => { const mni152 = await getMni152() const jba29 = await getJba29() const hoc1left = await getHoc1Left(mni152["@id"]) - return { human, mni152, @@ -101,3 +99,15 @@ HeaderContentProjection.loaders = [ HeaderContentProjection.parameters = { contentProjection: `<div header>HEADER CONTENT PROJECTED</div>` } + +export const InjectionSimpleRegion = Template.bind({}) +InjectionSimpleRegion.loaders = [ + ...HumanMni152Jba29Hoc1Left.loaders, + async () => { + const regions = await getJba29Regions() + const hoc1left = regions.find(r => /left/i.test(r.name) && /hoc1/i.test(r.name)) + return { + hoc1left + } + } +] diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html index 453707e72d044d826f925a950116752e73782a5e..23d94a6cf393179dd4c553d54d23dc90a4facf24 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html @@ -1,3 +1,15 @@ +<ng-template #headerTmpl> + <ng-content select="[header]"></ng-content> +</ng-template> + +<ng-template [ngIf]="!region"> + + <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template> + + <spinner-cmp *ngIf="fetchInProgress"></spinner-cmp> + <span *ngIf="!fetchInProgress"> Region must be provided! </span> +</ng-template> + <ng-template [ngIf]="region"> <mat-card class="mat-elevation-z4"> @@ -9,7 +21,7 @@ 'lighttheme': regionDarkmode === false }"> - <ng-content [select]="[header]"></ng-content> + <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template> <mat-card-title class="iv-custom-comp text"> {{ region.name }} @@ -61,6 +73,8 @@ class="feature-list-container" > + <spinner-cmp *ngIf="rfDir.busy$ | async"></spinner-cmp> + <sxplr-sapiviews-features-entry-list-item *ngFor="let feat of rfDir.listOfFeatures$ | async" [sxplr-sapiviews-features-entry-list-item-feature]="feat" @@ -134,9 +148,3 @@ </ng-template> </mat-expansion-panel> </ng-template> - -<!-- fall back if region is not provided --> - -<ng-template [ngIf]="!region"> - Region must be provided! -</ng-template> \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/entry/entry.component.ts b/src/atlasComponents/sapiViews/features/entry/entry.component.ts index cc5f8a396f61d4d2b63b8ad9c19afa197d15c332..79ee7640af146039fb2fc14d566c12daf7bef396 100644 --- a/src/atlasComponents/sapiViews/features/entry/entry.component.ts +++ b/src/atlasComponents/sapiViews/features/entry/entry.component.ts @@ -31,6 +31,6 @@ export class FeatureEntryCmp{ feature: SapiFeatureModel featureType = { - receptor: "siibra/receptor" + receptor: "siibra/features/receptor" } } diff --git a/src/atlasComponents/sapiViews/richDataset.stories.ts b/src/atlasComponents/sapiViews/richDataset.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..c49035dbe93ff1d54764413660bea9afd4bbef9e --- /dev/null +++ b/src/atlasComponents/sapiViews/richDataset.stories.ts @@ -0,0 +1,121 @@ +import { CommonModule } from "@angular/common" +import { HttpClientModule } from "@angular/common/http" +import { Component } from "@angular/core" +import { Meta, moduleMetadata, Story } from "@storybook/angular" +import { SAPI } from "src/atlasComponents/sapi" +import { getHoc1FeatureDetail, getHoc1Features, getHoc1Left, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" +import { AngularMaterialModule } from "src/sharedModules" +import { SapiAtlasModel, SapiFeatureModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "../sapi/type" +import { SapiViewsCoreDatasetModule } from "./core/datasets" +import { SapiViewsFeaturesModule } from "./features" + +@Component({ + selector: `rich-dataset-wrapper-cmp`, + template: ` + <div *ngIf="!dataset"> + Dataset must be provided + </div> + <mat-card *ngIf="dataset"> + <sxplr-sapiviews-core-datasets-dataset + [sxplr-sapiviews-core-datasets-dataset-input]="dataset"> + </sxplr-sapiviews-core-datasets-dataset> + + <sxplr-sapiviews-features-entry + [sxplr-sapiviews-features-entry-atlas]="atlas" + [sxplr-sapiviews-features-entry-space]="template" + [sxplr-sapiviews-features-entry-parcellation]="parcellation" + [sxplr-sapiviews-features-entry-region]="region" + [sxplr-sapiviews-features-entry-feature]="dataset"> + </sxplr-sapiviews-features-entry> + </mat-card> + + `, + styles: [ + `mat-card { max-width: 40rem; }` + ] +}) + +class RichDatasetWrapperCmp { + atlas: SapiAtlasModel + parcellation: SapiParcellationModel + template: SapiSpaceModel + region: SapiRegionModel + dataset: SapiFeatureModel +} + +export default { + component: RichDatasetWrapperCmp, + decorators: [ + moduleMetadata({ + imports: [ + AngularMaterialModule, + CommonModule, + HttpClientModule, + SapiViewsCoreDatasetModule, + SapiViewsFeaturesModule, + ], + providers: [ + SAPI, + ...provideDarkTheme, + ], + declarations: [] + }) + ], +} as Meta + +const Template: Story<RichDatasetWrapperCmp> = (args: RichDatasetWrapperCmp, { loaded }) => { + const { + + atlas, + parcellation, + template, + region, + feature + } = loaded + return ({ + props: { + atlas, + parcellation, + template, + region, + dataset: feature, + }, + }) +} + +const loadRegionMetadata = async () => { + + const atlas = await getHumanAtlas() + const parcellation = await getJba29() + const template = await getMni152() + const region = await getHoc1Left() + return { + atlas, + parcellation, + template, + region, + } +} + +const loadFeat = async () => { + const features = await getHoc1Features() + return { features } +} + +export const ReceptorDataset = Template.bind({}) +ReceptorDataset.args = { + +} +ReceptorDataset.loaders = [ + async () => { + return await loadRegionMetadata() + }, + async () => { + const { features } = await loadFeat() + const receptorfeat = features.find(f => f.type === "siibra/features/receptor") + const feature = await getHoc1FeatureDetail(receptorfeat["@id"]) + return { + feature + } + } +] diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index cca817c6b626f6524ed2bb1afc454cd9ac42c939..7cf6952ae0c87594b243aa328f424e8731a15f44 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -211,12 +211,3 @@ If you have any comments or need further support, please contact us at [${this.p @HostBinding('attr.version') public _version: string = environment.VERSION } - -export interface INgLayerInterface { - name: string - visible: boolean - source: string - type: string // image | segmentation | etc ... - transform?: [[number, number, number, number], [number, number, number, number], [number, number, number, number], [number, number, number, number]] | null - // colormap : string -} diff --git a/src/databrowser.fallback.ts b/src/databrowser.fallback.ts deleted file mode 100644 index 6d23e7fb4ec9d40e35c4b2c8cafed9578b68db64..0000000000000000000000000000000000000000 --- a/src/databrowser.fallback.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { InjectionToken } from "@angular/core" -import { Observable } from "rxjs" -import { IHasId } from "./util/interfaces" - -/** - * TODO gradually move to relevant. - */ - -export const kgTos = `The interactive viewer queries HBP Knowledge Graph Data Platform ("KG") for published datasets. - - -Access to the data and metadata provided through KG requires that you cite and acknowledge said data and metadata according to the Terms and Conditions of the Platform. - - -Citation requirements are outlined <https://www.humanbrainproject.eu/en/explore-the-brain/search-terms-of-use#citations> . - - -Acknowledgement requirements are outlined <https://www.humanbrainproject.eu/en/explore-the-brain/search-terms-of-use#acknowledgements> - - -These outlines are based on the authoritative Terms and Conditions are found <https://www.humanbrainproject.eu/en/explore-the-brain/search-terms-of-use> - - -If you do not accept the Terms & Conditions you are not permitted to access or use the KG to search for, to submit, to post, or to download any materials found there-in. -` - -export function getKgSchemaIdFromFullId(fullId: string): [string, string]{ - if (!fullId) { return [null, null] } - const match = /([\w\-.]*\/[\w\-.]*\/[\w\-.]*\/[\w\-.]*)\/([\w\-.]*)$/.exec(fullId) - if (!match) { return [null, null] } - return [match[1], match[2]] -} - - -export interface IKgReferenceSpace { - name: string -} - -export interface IKgPublication { - name: string - doi: string - cite: string -} - -export interface IKgParcellationRegion { - id?: string - name: string -} - -export interface IKgActivity { - methods: string[] - preparation: string[] - protocols: string[] -} - -export interface IKgDataEntry { - activity: IKgActivity[] - name: string - description: string - license: string[] - licenseInfo: string[] - parcellationRegion: IKgParcellationRegion[] - formats: string[] - custodians: string[] - contributors: string[] - referenceSpaces: IKgReferenceSpace[] - files: File[] - publications: IKgPublication[] - embargoStatus: IHasId[] - - methods: string[] - protocols: string[] - - preview?: boolean - - /** - * TODO typo, should be kgReferences - */ - kgReference: string[] - - id: string - fullId: string -} - -export type TypePreviewDispalyed = (file, dataset) => Observable<boolean> - - -export enum EnumPreviewFileTypes{ - NIFTI, - IMAGE, - CHART, - OTHER, - VOLUMES, -} - -export interface DatasetPreview { - datasetId: string - filename: string -} - -export function determinePreviewFileType(previewFile: any): EnumPreviewFileTypes { - if (!previewFile) throw new Error(`previewFile is required to determine the file type`) - const { mimetype, data } = previewFile - const chartType = data && data['chart.js'] && data['chart.js'].type - const registerdVolumes = data && data['iav-registered-volumes'] - if ( mimetype === 'application/nifti' ) { return EnumPreviewFileTypes.NIFTI } - if ( /^image/.test(mimetype)) { return EnumPreviewFileTypes.IMAGE } - if ( /application\/json/.test(mimetype) && (chartType === 'line' || chartType === 'radar')) { return EnumPreviewFileTypes.CHART } - if ( /application\/json/.test(mimetype) && !!registerdVolumes) { return EnumPreviewFileTypes.VOLUMES } - return EnumPreviewFileTypes.OTHER -} diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 7634811836054d36fb8d6c38e622993b4d2ca2df..764290062735cae7148fa2d534f69a11b422eb87 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -4,7 +4,6 @@ export const environment = { VERSION: 'unknown version', PRODUCTION: true, BACKEND_URL: null, - DATASET_PREVIEW_URL: 'https://hbp-kg-dataset-previewer.apps.hbp.eu/v2', BS_REST_URL: 'http://localhost:5000/v1_0', SPATIAL_TRANSFORM_BACKEND: 'https://hbp-spatial-backend.apps.hbp.eu', MATOMO_URL: null, diff --git a/src/environments/parseEnv.js b/src/environments/parseEnv.js index dbd985314dc0c472611870143293d584b95e1e11..b05909d596c85cc1875650602cafb8a5fa2fba3a 100644 --- a/src/environments/parseEnv.js +++ b/src/environments/parseEnv.js @@ -7,7 +7,6 @@ const main = async () => { const pathToEnvFile = path.join(__dirname, './environment.prod.ts') const { BACKEND_URL, - DATASET_PREVIEW_URL, STRICT_LOCAL, MATOMO_URL, MATOMO_ID, @@ -19,7 +18,6 @@ const main = async () => { console.log(`[parseEnv.js] parse envvar:`, { BACKEND_URL, - DATASET_PREVIEW_URL, STRICT_LOCAL, MATOMO_URL, MATOMO_ID, @@ -43,7 +41,6 @@ export const environment = { VERSION: ${version}, BS_REST_URL: ${JSON.stringify(BS_REST_URL)}, BACKEND_URL: ${JSON.stringify(BACKEND_URL)}, - DATASET_PREVIEW_URL: ${JSON.stringify(DATASET_PREVIEW_URL)}, STRICT_LOCAL: ${JSON.stringify(STRICT_LOCAL)}, MATOMO_URL: ${JSON.stringify(MATOMO_URL)}, MATOMO_ID: ${JSON.stringify(MATOMO_ID)}, diff --git a/src/extra_styles.css b/src/extra_styles.css index 3c9598b3f5e22dde46926c806307f648026e824d..4dc17c826004add9df28f132625f4fd7b9f39d51 100644 --- a/src/extra_styles.css +++ b/src/extra_styles.css @@ -718,10 +718,6 @@ kg-dataset-previewer > img margin: 15px; } -.m-1px -{ - margin:1px; -} .ml-15px-n { @@ -875,8 +871,3 @@ quick-tour-unit svg stroke-linecap: round; stroke-linejoin: round; } - -.overwrite-max-width-80vw -{ - max-width: 80vw!important; -} diff --git a/src/main.module.ts b/src/main.module.ts index 329b27c1fd72db2f7131029db082a3a4dc706cb9..de2ee0525e1e189c01f619314f01534c69f1d377 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -41,19 +41,22 @@ import { MessagingGlue } from './messagingGlue'; import { BS_ENDPOINT } from './util/constants'; import { QuickTourModule } from './ui/quickTour'; import { of } from 'rxjs'; -import { kgTos } from './databrowser.fallback' import { CANCELLABLE_DIALOG } from './util/interfaces'; import { environment } from 'src/environments/environment' import { NotSupportedCmp } from './notSupportedCmp/notSupported.component'; - import { atlasSelection, - RootEffecsModule, RootStoreModule, + getStoreEffects, } from "./state" import { DARKTHEME } from './util/injectionTokens'; import { map } from 'rxjs/operators'; +import { EffectsModule } from '@ngrx/effects'; +// TODO check if there is a more logical place import put layerctrl effects ts +import { LayerCtrlEffects } from './viewerModule/nehuba/layerCtrl.service/layerCtrl.effects'; +import { NehubaNavigationEffects } from './viewerModule/nehuba/navigation.service/navigation.effects'; +import { CONST } from "common/constants" @NgModule({ imports : [ @@ -78,7 +81,11 @@ import { map } from 'rxjs/operators'; AtlasViewerRouterModule, QuickTourModule, - RootEffecsModule, + EffectsModule.forRoot([ + ...getStoreEffects(), + LayerCtrlEffects, + NehubaNavigationEffects, + ]), RootStoreModule, HttpClientModule, ], @@ -143,7 +150,7 @@ import { map } from 'rxjs/operators'; }, { provide: TOS_OBS_INJECTION_TOKEN, - useValue: of(kgTos) + useValue: of(CONST.KG_TOS) }, { diff --git a/src/messagingGlue.ts b/src/messagingGlue.ts index 96bce9f87b0a723b592abc5b8f78a03ffde7ee9d..edb76b0f769ee11e5dcf04210e4ec992d442b9b5 100644 --- a/src/messagingGlue.ts +++ b/src/messagingGlue.ts @@ -1,9 +1,7 @@ import { Injectable, OnDestroy } from "@angular/core"; -import { select, Store } from "@ngrx/store"; +import { Store } from "@ngrx/store"; import { IMessagingActionTmpl, IWindowMessaging } from "./messaging/types"; -import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer } from "./services/state/ngViewerState/actions"; -import { generalActionError } from "./services/stateStore.helper"; -import { atlasSelection } from "src/state" +import { atlasAppearance, atlasSelection, generalActions } from "src/state" import { SAPI } from "./atlasComponents/sapi"; @Injectable() @@ -43,7 +41,7 @@ export class MessagingGlue implements IWindowMessaging, OnDestroy { const atlasId = this.tmplSpIdToAtlasId.get(payload['@id']) if (!atlasId) { return this.store.dispatch( - generalActionError({ + generalActions.generalActionError({ message: `atlas id with the corresponding templateId ${payload['@id']} not found.` }) ) @@ -68,29 +66,26 @@ export class MessagingGlue implements IWindowMessaging, OnDestroy { if (type === 'swc') { const { transform } = resourceParam const layer = { - name: swcLayerUuid, id: swcLayerUuid, source: `swc://${url}`, - mixability: 'mixable', - type: "segmentation", - "segments": [ + // type: "customlayer/nglayer", + segments: [ "1" ], - transform, + transform: transform, + clType: 'customlayer/nglayer' as 'customlayer/nglayer' } this.store.dispatch( - ngViewerActionAddNgLayer({ - layer + atlasAppearance.actions.addCustomLayer({ + customLayer: layer }) ) this.mapIdUnload.set(swcLayerUuid, () => { this.store.dispatch( - ngViewerActionRemoveNgLayer({ - layer: { - name: swcLayerUuid - } + atlasAppearance.actions.removeCustomLayer({ + id: swcLayerUuid }) ) unload() diff --git a/src/overwrite.scss b/src/overwrite.scss index 7db898b3e7adf09e11919aa4996a15240e136d9f..69b68e49481fdbd5989c5e0bd196066ee98b3793 100644 --- a/src/overwrite.scss +++ b/src/overwrite.scss @@ -47,9 +47,12 @@ $media-map: ( @for $i from 4 through 10 { - $maxheight: $i * 10; - .max-#{$maxheight}-vh { - max-height: $maxheight * 1vh; + $tensvar: $i * 10; + .#{$nsp}-mxh-#{$tensvar}vh { + max-height: $tensvar * 1vh; + } + .#{$nsp}-mxw-#{$tensvar}vw { + max-width: $tensvar * 1vw; } } @@ -95,42 +98,42 @@ $transform-origin-maps: ( } @for $zlvl from 0 through 10 { - .#{$nsp}-z-index-#{$zlvl} { + .#{$nsp}-z-#{$zlvl} { z-index: $zlvl; } } -@for $unit from 0 through 8 { - .#{$nsp}-pt-#{$unit} { - padding-top: $unit * 0.5rem; +@for $unit from 0 through 10 { + .#{$nsp}-pt-#{$unit}{ + padding-top: $unit * 0.5rem!important; } - .#{$nsp}-pb-#{$unit} { - padding-bottom: $unit * 0.5rem; + .#{$nsp}-pb-#{$unit}{ + padding-bottom: $unit * 0.5rem!important; } - .#{$nsp}-pl-#{$unit} { - padding-left: $unit * 0.5rem; + .#{$nsp}-pl-#{$unit}{ + padding-left: $unit * 0.5rem!important; } - .#{$nsp}-pr-#{$unit} { - padding-right: $unit * 0.5rem; + .#{$nsp}-pr-#{$unit}{ + padding-right: $unit * 0.5rem!important; } - .#{$nsp}-p-#{$unit} { - padding: $unit * 0.5rem; + .#{$nsp}-p-#{$unit}{ + padding: $unit * 0.5rem!important; } - .#{$nsp}-mt-#{$unit} { - margin-top: $unit * 0.5rem; + .#{$nsp}-mt-#{$unit}{ + margin-top: $unit * 0.5rem!important; } - .#{$nsp}-mb-#{$unit} { - margin-bottom: $unit * 0.5rem; + .#{$nsp}-mb-#{$unit}{ + margin-bottom: $unit * 0.5rem!important; } - .#{$nsp}-ml-#{$unit} { - margin-left: $unit * 0.5rem; + .#{$nsp}-ml-#{$unit}{ + margin-left: $unit * 0.5rem!important; } - .#{$nsp}-mr-#{$unit} { - margin-right: $unit * 0.5rem; + .#{$nsp}-mr-#{$unit}{ + margin-right: $unit * 0.5rem!important; } - .#{$nsp}-m-#{$unit} { - margin: $unit * 0.5rem; + .#{$nsp}-m-#{$unit}{ + margin: $unit * 0.5rem!important; } } @@ -200,7 +203,7 @@ $white-space-vars: nowrap; $pointer-events-vars: all, none; @each $pointer-events-var in $pointer-events-vars { - .#{$nsp}-pointer-events-#{$pointer-events-var} + .#{$nsp}-pe-#{$pointer-events-var} { pointer-events: $pointer-events-var!important; } @@ -217,3 +220,11 @@ $width-pc-vars: 100; { border-width: 1px; } + +// flex +$flex-wrap-vars: nowrap, wrap, wrap-reverse; +@each $flex-wrap-var in $flex-wrap-vars { + .#{$nsp}-flex-wrap-#{$flex-wrap-var} { + flex-wrap: $flex-wrap-var; + } +} diff --git a/src/plugin/atlasViewer.pluginService.service.spec.ts b/src/plugin/atlasViewer.pluginService.service.spec.ts index 41e342b8bf77d8941eb731554972b71217e75b73..56d8cfc106502e2ee6cb9c5ff93f6c73d5eab619 100644 --- a/src/plugin/atlasViewer.pluginService.service.spec.ts +++ b/src/plugin/atlasViewer.pluginService.service.spec.ts @@ -1,18 +1,16 @@ import { CommonModule } from "@angular/common" import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" -import { NgModule } from "@angular/core" -import { async, fakeAsync, TestBed, tick } from "@angular/core/testing" +import { fakeAsync, TestBed, tick } from "@angular/core/testing" import { MockStore, provideMockStore } from "@ngrx/store/testing" import { ComponentsModule } from "src/components" import { DialogService } from "src/services/dialogService.service" -import { selectorPluginCspPermission } from "src/services/state/userConfigState.helper" import { AngularMaterialModule } from "src/sharedModules" +import { userPreference } from "src/state" import { PureContantService } from "src/util" import { APPEND_SCRIPT_TOKEN, REMOVE_SCRIPT_TOKEN } from "src/util/constants" import { WidgetModule, WidgetServices } from "src/widget" import { PluginServices } from "./atlasViewer.pluginService.service" import { PluginModule } from "./plugin.module" -import { PluginUnit } from "./pluginUnit/pluginUnit.component" const MOCK_PLUGIN_MANIFEST = { name: 'fzj.xg.MOCK_PLUGIN_MANIFEST', @@ -33,8 +31,8 @@ describe('> atlasViewer.pluginService.service.ts', () => { let httpMock: HttpTestingController let mockStore: MockStore - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [ AngularMaterialModule, CommonModule, @@ -67,38 +65,37 @@ describe('> atlasViewer.pluginService.service.ts', () => { } } ] - }).compileComponents().then(() => { - - httpMock = TestBed.inject(HttpTestingController) - pluginService = TestBed.inject(PluginServices) - mockStore = TestBed.inject(MockStore) - pluginService.pluginViewContainerRef = { - createComponent: () => { - return { - onDestroy: () => {}, - instance: { - elementRef: { - nativeElement: { - append: () => {} - } + }).compileComponents() + + httpMock = TestBed.inject(HttpTestingController) + pluginService = TestBed.inject(PluginServices) + mockStore = TestBed.inject(MockStore) + pluginService.pluginViewContainerRef = { + createComponent: () => { + return { + onDestroy: () => {}, + instance: { + elementRef: { + nativeElement: { + append: () => {} } } } } - } as any + } + } as any - httpMock.expectOne('http://localhost:3000/plugins/manifests').flush('[]') + httpMock.expectOne('http://localhost:3000/plugins/manifests').flush('[]') - const widgetService = TestBed.inject(WidgetServices) - /** - * widget service floatingcontainer not inst in this circumstance - * TODO fix widget service tests importing widget service are not as flaky - */ - widgetService.addNewWidget = () => { - return {} as any - } - }) - })) + const widgetService = TestBed.inject(WidgetServices) + /** + * widget service floatingcontainer not inst in this circumstance + * TODO fix widget service tests importing widget service are not as flaky + */ + widgetService.addNewWidget = () => { + return {} as any + } + }) afterEach(() => { spyfn.appendSrc.calls.reset() @@ -127,7 +124,7 @@ describe('> atlasViewer.pluginService.service.ts', () => { describe('#launchPlugin', () => { beforeEach(() => { - mockStore.overrideSelector(selectorPluginCspPermission, { value: false }) + mockStore.overrideSelector(userPreference.selectors.userCsp, {}) }) describe('> basic fetching functionality', () => { @@ -200,7 +197,7 @@ describe('> atlasViewer.pluginService.service.ts', () => { describe('> if user permission has been given', () => { beforeEach(fakeAsync(() => { - mockStore.overrideSelector(selectorPluginCspPermission, { value: true }) + mockStore.overrideSelector(userPreference.selectors.userCsp, { 'fzj.xg.MOCK_PLUGIN_MANIFEST': {} }) userConfirmSpy.and.callFake(() => Promise.reject()) pluginService.launchPlugin({ ...cspManifest @@ -222,7 +219,7 @@ describe('> atlasViewer.pluginService.service.ts', () => { describe('> if user permission has not yet been given', () => { beforeEach(() => { - mockStore.overrideSelector(selectorPluginCspPermission, { value: false }) + mockStore.overrideSelector(userPreference.selectors.userCsp, {}) }) describe('> user permission', () => { beforeEach(fakeAsync(() => { diff --git a/src/plugin/atlasViewer.pluginService.service.ts b/src/plugin/atlasViewer.pluginService.service.ts index 3707c087d376b589ff673fe0482bf4a59b300046..a050e4312983d16e54aed95758ed118b3131177a 100644 --- a/src/plugin/atlasViewer.pluginService.service.ts +++ b/src/plugin/atlasViewer.pluginService.service.ts @@ -8,12 +8,12 @@ import { LoggingService } from 'src/logging'; import { WidgetUnit, WidgetServices } from "src/widget"; import { APPEND_SCRIPT_TOKEN, REMOVE_SCRIPT_TOKEN, getHttpHeader } from 'src/util/constants'; import { PluginFactoryDirective } from './pluginFactory.directive'; -import { selectorPluginCspPermission } from 'src/services/state/userConfigState.helper'; import { DialogService } from 'src/services/dialogService.service'; import { DomSanitizer } from '@angular/platform-browser'; import { MatSnackBar } from '@angular/material/snack-bar'; import { PureContantService } from 'src/util'; import { actions } from "src/state/plugins" +import { userPreference } from 'src/state'; const requiresReloadMd = `\n\n***\n\n**warning**: interactive atlas viewer **will** be reloaded in order for the change to take effect.` @@ -229,10 +229,11 @@ export class PluginServices { await new Promise((rs, rj) => { this.store.pipe( - select(selectorPluginCspPermission, { key: pluginKey }), + select(userPreference.selectors.userCsp), + map(dict => !!dict[pluginKey]), take(1), switchMap(userAgreed => { - if (userAgreed.value) return of(true) + if (userAgreed) return of(true) /** * check if csp exists diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts index 2837af008e31978422ba494aabdc8223f9d188ba..28a8c696bcf390011ca16244c2e6d30352a8da31 100644 --- a/src/routerModule/router.service.ts +++ b/src/routerModule/router.service.ts @@ -4,11 +4,11 @@ import { Inject } from "@angular/core"; import { NavigationEnd, Router } from '@angular/router' import { Store } from "@ngrx/store"; import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, switchMapTo, take, tap, withLatestFrom } from "rxjs/operators"; -import { generalApplyState } from "src/services/stateStore.helper"; import { PureContantService } from "src/util"; import { cvtStateToHashedRoutes, cvtFullRouteToState, encodeCustomState, decodeCustomState, verifyCustomState } from "./util"; import { BehaviorSubject, combineLatest, merge, NEVER, Observable, of } from 'rxjs' import { scan } from 'rxjs/operators' +import { generalActions } from "src/state" @Injectable({ providedIn: 'root' @@ -123,7 +123,7 @@ export class RouterService { if ( fullPath !== `/${routeFromState}`) { store$.dispatch( - generalApplyState({ + generalActions.generalApplyState({ state: stateFromRoute }) ) diff --git a/src/services/state/ngViewerState.store.helper.ts b/src/services/state/ngViewerState.store.helper.ts deleted file mode 100644 index 2898db9c78d9d39a26d23c8ba9c9e8a6bf3deef7..0000000000000000000000000000000000000000 --- a/src/services/state/ngViewerState.store.helper.ts +++ /dev/null @@ -1,20 +0,0 @@ -// TODO to be merged with ng viewer state after refactor -export { INgLayerInterface, PANELS } from './ngViewerState/constants' - -export { - ngViewerActionAddNgLayer, - ngViewerActionRemoveNgLayer, - - ngViewerActionToggleMax, - ngViewerActionClearView, - ngViewerActionSetPanelOrder, - ngViewerActionForceShowSegment, -} from './ngViewerState/actions' - -export { - ngViewerSelectorClearView, - ngViewerSelectorClearViewEntries, - ngViewerSelectorNehubaReady, - ngViewerSelectorPanelOrder, - ngViewerSelectorLayers, -} from './ngViewerState/selectors' diff --git a/src/services/state/ngViewerState/actions.ts b/src/services/state/ngViewerState/actions.ts deleted file mode 100644 index 858602e076a6c0d47a5ff339697971fa29c21924..0000000000000000000000000000000000000000 --- a/src/services/state/ngViewerState/actions.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { createAction, props, createReducer } from "@ngrx/store" -import { INgLayerInterface } from './constants' - -export const ngViewerActionAddNgLayer = createAction( - '[ngLayerAction] addNgLayer', - props<{ layer: INgLayerInterface|INgLayerInterface[] }>() -) - -export const ngViewerActionRemoveNgLayer = createAction( - '[ngLayerAction] removeNgLayer', - props<{ layer: Partial<INgLayerInterface>|Partial<INgLayerInterface>[] }>() -) - -export const ngViewerActionToggleMax = createAction( - `[ngViewerAction] toggleMax`, - props<{ payload: { index: number } }>() -) - -export const ngViewerActionSetPanelOrder = createAction( - `[ngViewerAction] setPanelOrder`, - props<{ payload: { panelOrder: string } }>() -) - -export const ngViewerActionSwitchPanelMode = createAction( - `[ngViewerAction] switchPanelMode`, - props<{ payload: { panelMode: string } }>() -) - -export const ngViewerActionForceShowSegment = createAction( - `[ngViewerAction] forceShowSegment`, - props<{ forceShowSegment: boolean }>() -) - -export const ngViewerActionNehubaReady = createAction( - `[ngViewerAction] nehubaReady`, - props<{ nehubaReady: boolean }>() -) - -/** - * Clear viewer view from additional layers such as PMap or connectivity - * To request view to be cleared, call - * this.store$.dispatch( - * ngViewerActionClearView({ - * payload: { - * ['my-unique-id']: true - * } - * }) - * ) - * - * When finished, call - * - * this.store$.dispatch( - * ngViewerActionClearView({ - * payload: { - * ['my-unique-id']: false - * } - * }) - * ) - */ -export const ngViewerActionClearView = createAction( - `[ngViewerAction] clearView`, - props<{ payload: { [key: string]: boolean }}>() -) - -export const ngViewerActionCycleViews = createAction( - `[ngViewerAction] cycleView` -) \ No newline at end of file diff --git a/src/services/state/ngViewerState/constants.ts b/src/services/state/ngViewerState/constants.ts deleted file mode 100644 index 947d86c3eea81125f5ad99427f4988328fea3d16..0000000000000000000000000000000000000000 --- a/src/services/state/ngViewerState/constants.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface INgLayerInterface { - name: string // displayName - source: string - mixability?: string //"base" | "mixable" | "nonmixable" - annotation?: string // - id?: string // unique identifier - visible?: boolean - shader?: string - transform?: any - opacity?: number -} - -export enum PANELS { - FOUR_PANEL = 'FOUR_PANEL', - V_ONE_THREE = 'V_ONE_THREE', - H_ONE_THREE = 'H_ONE_THREE', - SINGLE_PANEL = 'SINGLE_PANEL', -} diff --git a/src/services/state/ngViewerState/selectors.spec.ts b/src/services/state/ngViewerState/selectors.spec.ts deleted file mode 100644 index c44f676f86802ee790f4b7e291e7e79c50ccb849..0000000000000000000000000000000000000000 --- a/src/services/state/ngViewerState/selectors.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ngViewerSelectorClearViewEntries } from './selectors' - -let clearViewQueue = {} - -describe('> ngViewerState/selectors.ts', () => { - describe('> ngViewerSelectorClearViewEntries', () => { - beforeEach(() => { - clearViewQueue = {} - }) - describe('> when prop is not provided', () => { - it('> if clearViewQueue is empty (on startup)', () => { - const result = ngViewerSelectorClearViewEntries.projector(clearViewQueue) - expect(result).toEqual([]) - }) - it('> if clearViewQueue is non empty, but falsy, should return false', () => { - clearViewQueue['hello - world'] = null - clearViewQueue['oo bar'] = false - const result = ngViewerSelectorClearViewEntries.projector(clearViewQueue) - expect(result).toEqual([]) - }) - it('> if clearViewQueue is non empty and truthy, should return true', () => { - clearViewQueue['hello - world'] = 1 - const result = ngViewerSelectorClearViewEntries.projector(clearViewQueue) - expect(result).toEqual(['hello - world']) - }) - }) - }) -}) \ No newline at end of file diff --git a/src/services/state/ngViewerState/selectors.ts b/src/services/state/ngViewerState/selectors.ts deleted file mode 100644 index e5940a77d769ebf5f4a0a6b47fa310988c07ec11..0000000000000000000000000000000000000000 --- a/src/services/state/ngViewerState/selectors.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createSelector } from "@ngrx/store"; - -export const ngViewerSelectorClearViewEntries = createSelector( - (state: any) => state?.ngViewerState?.clearViewQueue, - (clearViewQueue = {}) => { - const returnKeys = [] - for (const key in clearViewQueue) { - if (!!clearViewQueue[key]) returnKeys.push(key) - } - return returnKeys - } -) - -export const ngViewerSelectorClearView = createSelector( - ngViewerSelectorClearViewEntries, - keys => keys.length > 0 -) - -export const ngViewerSelectorPanelOrder = createSelector( - state => state['ngViewerState'], - ngViewerState => ngViewerState.panelOrder -) - - -export const ngViewerSelectorNehubaReady = createSelector( - state => state['ngViewerState'], - ngViewerState => ngViewerState.nehubaReady -) - -export const ngViewerSelectorLayers = createSelector( - state => state['ngViewerState'], - ngViewerState => ngViewerState?.layers || [] -) \ No newline at end of file diff --git a/src/services/state/uiState.store.helper.ts b/src/services/state/uiState.store.helper.ts deleted file mode 100644 index b2201ef0b7ceb8419c52de5ca64a461e2dea0c1a..0000000000000000000000000000000000000000 --- a/src/services/state/uiState.store.helper.ts +++ /dev/null @@ -1,32 +0,0 @@ -// TODO merge with uiState.store.ts after refactor completes - -export { - uiActionSetPreviewingDatasetFiles, - uiActionShowSidePanelConnectivity, - uiStateCloseSidePanel, - uiStateCollapseSidePanel, - uiStateExpandSidePanel, - uiStateOpenSidePanel, - uiStateShowBottomSheet, - uiActionSnackbarMessage, - uiActionMouseoverLandmark, - uiActionMouseoverSegments, -} from './uiState/actions' - - -export enum EnumWidgetTypes{ - DATASET_PREVIEW, -} - -export interface IDatasetPreviewData{ - datasetId: string - filename: string - datasetSchema?: string -} - -export type TypeOpenedWidget = { - type: EnumWidgetTypes - data: IDatasetPreviewData -} - -export const SHOW_KG_TOS = `SHOW_KG_TOS` \ No newline at end of file diff --git a/src/services/state/uiState/actions.ts b/src/services/state/uiState/actions.ts deleted file mode 100644 index ae2fbc3bd9535428de9c3912a967ea71ee298c2c..0000000000000000000000000000000000000000 --- a/src/services/state/uiState/actions.ts +++ /dev/null @@ -1,49 +0,0 @@ - -import { createAction, props } from '@ngrx/store' -import { TemplateRef } from '@angular/core' -import { MatBottomSheetConfig } from '@angular/material/bottom-sheet' - -export const uiStateCloseSidePanel = createAction( - '[uiState] closeSidePanel' -) - -export const uiStateOpenSidePanel = createAction( - '[uiState] openSidePanel' -) - -export const uiStateCollapseSidePanel = createAction( - '[uiState] collapseSidePanelCurrentView' -) - -export const uiStateExpandSidePanel = createAction( - '[uiState] expandSidePanelCurrentView' -) - -export const uiStateShowBottomSheet = createAction( - '[uiState] showBottomSheet', - props<{ bottomSheetTemplate: TemplateRef<unknown>, config?: MatBottomSheetConfig }>() -) - -export const uiActionMouseoverLandmark = createAction( - `[uiState] mouseoverLandmark`, - props<{ landmark: string }>() -) - -export const uiActionMouseoverSegments = createAction( - `[uiState] mouseoverSegments`, - props<{ segments: any[] }>() -) - -export const uiActionSetPreviewingDatasetFiles = createAction( - `[uiState] setDatasetPreviews`, - props<{previewingDatasetFiles: {datasetId: string, filename: string}[]}>() -) - -export const uiActionShowSidePanelConnectivity = createAction( - `[uiState] showSidePanelConnectivity` -) - -export const uiActionSnackbarMessage = createAction( - `[uiState] snackbarMessage`, - props<{snackbarMessage: string}>() -) \ No newline at end of file diff --git a/src/services/state/uiState/common.ts b/src/services/state/uiState/common.ts deleted file mode 100644 index dd5da3140f280c9cef5c7cd29637c2577f47587f..0000000000000000000000000000000000000000 --- a/src/services/state/uiState/common.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface IUiState{ - - previewingDatasetFiles: {datasetId: string, filename: string}[] - - mouseOverSegments: Array<{ - layer: { - name: string - } - segment: any | null - }> - sidePanelIsOpen: boolean - sidePanelExploreCurrentViewIsOpen: boolean - mouseOverSegment: any | number - - mouseOverLandmark: string - mouseOverUserLandmark: any - - focusedSidePanel: string | null - - snackbarMessage: symbol - - agreedCookies: boolean - agreedKgTos: boolean -} diff --git a/src/services/state/userConfigState.helper.spec.ts b/src/services/state/userConfigState.helper.spec.ts deleted file mode 100644 index e007700e80d5b030b8d2d70cb41e71d64dfcd136..0000000000000000000000000000000000000000 --- a/src/services/state/userConfigState.helper.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { selectorPluginCspPermission } from "./userConfigState.helper" - -describe('> userConfigState.helper.ts', () => { - describe('> selectorPluginCspPermission', () => { - const expectedTrue = { - value: true - } - const expectedFalse = { - value: false - } - describe('> malformed init value', () => { - describe('> undefined userconfigstate', () => { - it('> return expected false val', () => { - const returnVal = selectorPluginCspPermission.projector(null, { key: 'foo-bar' }) - expect(returnVal).toEqual(expectedFalse) - }) - }) - describe('> undefined pluginCsp property', () => { - it('> return expected false val', () => { - const returnVal = selectorPluginCspPermission.projector({}, { key: 'foo-bar' }) - expect(returnVal).toEqual(expectedFalse) - }) - }) - }) - - describe('> well fored init valu', () => { - - describe('> undefined key', () => { - it('> return expected false val', () => { - const returnVal = selectorPluginCspPermission.projector({ - pluginCsp: {'yes-man': true} - }, { key: 'foo-bar' }) - expect(returnVal).toEqual(expectedFalse) - }) - }) - - describe('> truthly defined key', () => { - it('> return expected true val', () => { - const returnVal = selectorPluginCspPermission.projector({ pluginCsp: - { 'foo-bar': true } - }, { key: 'foo-bar' }) - expect(returnVal).toEqual(expectedTrue) - }) - }) - }) - }) -}) \ No newline at end of file diff --git a/src/services/state/userConfigState.helper.ts b/src/services/state/userConfigState.helper.ts deleted file mode 100644 index e71be024c1947ccf0fc3f3aca909cebbceeaf046..0000000000000000000000000000000000000000 --- a/src/services/state/userConfigState.helper.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createSelector } from "@ngrx/store" - -export const selectorPluginCspPermission = createSelector( - (state: any) => state.userConfigState, - (userConfigState: any, props: any = {}) => { - const { key } = props as { key: string } - return { - value: !!userConfigState?.pluginCsp?.[key] - } - } -) diff --git a/src/services/state/userConfigState.store.spec.ts b/src/services/state/userConfigState.store.spec.ts deleted file mode 100644 index 4f1b078d5298d3850bdc07148314234eee216889..0000000000000000000000000000000000000000 --- a/src/services/state/userConfigState.store.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" -import { fakeAsync, TestBed, tick } from "@angular/core/testing" -import { provideMockActions } from "@ngrx/effects/testing" -import { Action } from "@ngrx/store" -import { MockStore, provideMockStore } from "@ngrx/store/testing" -import { from, Observable } from "rxjs" -import { AngularMaterialModule } from "src/sharedModules" -import { PureContantService } from "src/util" -import { DialogService } from "../dialogService.service" -import { actionUpdatePluginCsp, UserConfigStateUseEffect } from "./userConfigState.store" -import { viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector } from "./viewerState/selectors" - -describe('> userConfigState.store.spec.ts', () => { - describe('> UserConfigStateUseEffect', () => { - let action$: Observable<Action> - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - AngularMaterialModule, - ], - providers: [ - provideMockActions(() => action$), - provideMockStore({ - initialState: { - viewerConfigState: { - gpuLimit: 1e9, - animation: true - } - } - }), - DialogService, - { - provide: PureContantService, - useValue: { - backendUrl: 'http://localhost:3000/' - } - } - ] - }) - - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, null) - mockStore.overrideSelector(viewerStateSelectedParcellationSelector, null) - mockStore.overrideSelector(viewerStateSelectedRegionsSelector, []) - }) - - it('> can be init', () => { - const useEffect = TestBed.inject(UserConfigStateUseEffect) - expect(useEffect).toBeTruthy() - }) - describe('> setInitPluginPermission$', () => { - let mockHttp: HttpTestingController - let useEffect: UserConfigStateUseEffect - const mockpluginPer = { - 'foo-bar': { - 'script-src': [ - '1', - '2', - ] - } - } - beforeEach(() => { - mockHttp = TestBed.inject(HttpTestingController) - useEffect = TestBed.inject(UserConfigStateUseEffect) - }) - afterEach(() => { - mockHttp.verify() - }) - it('> calls /GET user/pluginPermissions', fakeAsync(() => { - let val - useEffect.setInitPluginPermission$.subscribe(v => val = v) - tick(20) - const req = mockHttp.expectOne(`http://localhost:3000/user/pluginPermissions`) - req.flush(mockpluginPer) - expect(val).toEqual(actionUpdatePluginCsp({ payload: mockpluginPer })) - })) - - it('> if get fn fails', fakeAsync(() => { - let val - useEffect.setInitPluginPermission$.subscribe(v => val = v) - const req = mockHttp.expectOne(`http://localhost:3000/user/pluginPermissions`) - req.error(null, { status: 500, statusText: 'Internal Error' }) - expect(val).toEqual(actionUpdatePluginCsp({ payload: {} })) - })) - }) - }) -}) \ No newline at end of file diff --git a/src/services/state/viewerConfig.store.helper.ts b/src/services/state/viewerConfig.store.helper.ts deleted file mode 100644 index 7dc4a3c6c184e0f56ea034d9990c5dff58e52825..0000000000000000000000000000000000000000 --- a/src/services/state/viewerConfig.store.helper.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createSelector } from "@ngrx/store" - -export const VIEWER_CONFIG_FEATURE_KEY = 'viewerConfigState' -export interface IViewerConfigState { - gpuLimit: number - animation: boolean - useMobileUI: boolean -} - -// export const viewerConfigSelectorUseMobileUi = createSelector( -// state => state[VIEWER_CONFIG_FEATURE_KEY], -// viewerConfigState => viewerConfigState.useMobileUI -// ) diff --git a/src/services/state/viewerState/actions.ts b/src/services/state/viewerState/actions.ts deleted file mode 100644 index aa5b5aad286a38154a281d31ee58d489712f08d0..0000000000000000000000000000000000000000 --- a/src/services/state/viewerState/actions.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createAction, props } from "@ngrx/store" - -export const viewerStateNavigateToRegion = createAction( - `[viewerState] navigateToRegion`, - props<{ payload: { region: any } }>() -) - -export const viewerStateSelectTemplateWithId = createAction( - `[viewerState] selectTemplateWithId`, - props<{ payload: { ['@id']: string }, config?: { selectParcellation: { ['@id']: string } } }>() -) - -export const viewerStateAddUserLandmarks = createAction( - `[viewerState] addUserlandmark,`, - props<{ landmarks: any[] }>() -) - -export const viewerStateMouseOverCustomLandmark = createAction( - '[viewerState] mouseOverCustomLandmark', - props<{ payload: { userLandmark: any } }>() -) - -export const viewerStateMouseOverCustomLandmarkInPerspectiveView = createAction( - `[viewerState] mouseOverCustomLandmarkInPerspectiveView`, - props<{ payload: { label: string } }>() -) diff --git a/src/services/state/viewerState/constants.ts b/src/services/state/viewerState/constants.ts deleted file mode 100644 index dbfa8c7800f0cc602fe1b1f942b7088552fe20ce..0000000000000000000000000000000000000000 --- a/src/services/state/viewerState/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IRegion{ - name: string - [key: string]: string -} diff --git a/src/services/state/viewerState/selectors.spec.ts b/src/services/state/viewerState/selectors.spec.ts deleted file mode 100644 index e5916b75e671cdb281508c796106e9de484ff7f3..0000000000000000000000000000000000000000 --- a/src/services/state/viewerState/selectors.spec.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { - viewerStateGetOverlayingAdditionalParcellations, - viewerStateAtlasParcellationSelector, - viewerStateAtlasLatestParcellationSelector, - viewerStateParcVersionSelector, - selectorSelectedATP, - viewerStateGetSelectedAtlas, - viewerStateSelectedTemplateSelector, - viewerStateSelectedParcellationSelector, -} from './selectors' - - -const atlas1 = { - '@id': 'atlas-1', - name: 'atlas-1-name', - templateSpaces: [ - { - '@id': 'atlas-1-tmpl-1', - name: 'atlas-1-tmpl-1', - availableIn: [ - { - '@id': 'atlas-1-parc-1', - name: 'atlas-1-parc-1' - }, - { - '@id': 'atlas-1-parc-2', - name: 'atlas-1-parc-2' - } - ] - } - ], - parcellations: [ - { - '@id': 'atlas-1-parc-1', - name: 'atlas-1-parc-1', - baseLayer: true - }, - { - '@id': 'atlas-1-parc-2', - name: 'atlas-1-parc-2' - } - ] -} - -const tmpl1 = { - '@id': 'atlas-1-tmpl-1', - name: 'atlas-1-tmpl-1', - parcellations: [ - { - '@id': 'atlas-1-parc-1', - name: 'atlas-1-parc-1' - }, - { - '@id': 'atlas-1-parc-2', - name: 'atlas-1-parc-2' - } - ] -} - -const atlas2 = { - '@id': 'atlas-2', - name: 'atlas-2-name', - templateSpaces: [ - { - '@id': 'atlas-2-tmpl-1', - name: 'atlas-2-tmpl-1', - availableIn: [ - { - '@id': 'atlas-2-parc-1', - name: 'atlas-2-parc-1' - }, - { - '@id': 'atlas-2-parc-2', - name: 'atlas-2-parc-2' - } - ] - }, - { - '@id': 'atlas-2-tmpl-2', - name: 'atlas-2-tmpl-2', - availableIn: [ - { - '@id': 'atlas-2-parc-1', - name: 'atlas-2-parc-1' - }, - { - '@id': 'atlas-2-parc-2', - name: 'atlas-2-parc-2' - } - ] - } - ], - parcellations: [ - { - '@id': 'atlas-2-parc-1', - name: 'atlas-2-parc-1', - "@version": { - "@next": "atlas-2-parc-2", - "@this": "atlas-2-parc-1", - "name": "atlas-2-parc-1", - "@previous": null - } - }, - { - '@id': 'atlas-2-parc-2', - name: 'atlas-2-parc-2', - "@version": { - "@next": null, - "@this": "atlas-2-parc-2", - "name": "atlas-2-parc-2", - "@previous": "atlas-2-parc-1" - } - } - ] -} - -const tmpl2 = { - '@id': 'atlas-2-tmpl-1', - name: 'atlas-2-tmpl-1', - parcellations: [ - { - '@id': 'atlas-2-parc-1', - name: 'atlas-2-parc-1' - }, - { - '@id': 'atlas-2-parc-2', - name: 'atlas-2-parc-2' - } - ] -} - - -const tmpl2_2 = { - '@id': 'atlas-2-tmpl-2', - name: 'atlas-2-tmpl-2', - parcellations: [ - { - '@id': 'atlas-2-parc-1', - name: 'atlas-2-parc-1' - }, - { - '@id': 'atlas-2-parc-2', - name: 'atlas-2-parc-2' - } - ] -} - -const fetchedAtlases = [ - atlas1, - atlas2 -] - -const fetchedTemplates = [ - tmpl1, - tmpl2, - tmpl2_2 -] - -describe('viewerState/selector.ts', () => { - // describe('> viewerStateGetOverlayingAdditionalParcellations', () => { - // describe('> if atlas has no basic layer', () => { - // it('> should return empty array', () => { - - // const parcs = viewerStateGetOverlayingAdditionalParcellations.projector({ - // fetchedAtlases, - // selectedAtlasId: atlas2['@id'] - // }, { - // parcellationSelected: tmpl2.parcellations[0] - // }) - - // expect(parcs).toEqual([]) - // }) - // }) - - // describe('> if atlas has basic layer', () => { - // describe('> if non basiclayer is selected', () => { - // it('> should return non empty array', () => { - // const parc = atlas1.parcellations.find(p => !p['baseLayer']) - // const parcs = viewerStateGetOverlayingAdditionalParcellations.projector({ - // fetchedAtlases, - // selectedAtlasId: atlas1['@id'] - // }, { - // parcellationSelected: parc - // }) - // expect(parcs.length).toEqual(1) - // expect(parcs[0]['@id']).toEqual(parc['@id']) - // }) - // }) - - // describe('> if basic layer is selected', () => { - // it('> should return empty array', () => { - // const parc = atlas1.parcellations.find(p => !!p['baseLayer']) - // const parcs = viewerStateGetOverlayingAdditionalParcellations.projector({ - // fetchedAtlases, - // selectedAtlasId: atlas1['@id'] - // }, { - // parcellationSelected: parc - // }) - // expect(parcs.length).toEqual(0) - // }) - // }) - // }) - // }) - - // describe('> viewerStateAtlasParcellationSelector', () => { - // const check = (atlasJson, templates) => { - - // const parcs = viewerStateAtlasParcellationSelector.projector({ - // fetchedAtlases, - // selectedAtlasId: atlasJson['@id'] - // }, { - // fetchedTemplates - // }) - // const templateParcs = [] - // for (const tmpl of templates) { - // templateParcs.push(...tmpl.parcellations) - // } - // for (const parc of parcs) { - // const firstHalf = templateParcs.find(p => p['@id'] === parc['@id']) - // const secondHalf = atlasJson.parcellations.find(p => p['@id'] === parc['@id']) - // expect(firstHalf).toBeTruthy() - // expect(secondHalf).toBeTruthy() - // //TODO compare strict equality of firsthalf+secondhalf with parc - // } - // } - - // it('> should work', () => { - // check(atlas1, [tmpl1, tmpl2, tmpl2_2]) - // check(atlas2, [tmpl1, tmpl2, tmpl2_2]) - // }) - // }) - - // describe('> viewerStateAtlasLatestParcellationSelector', () => { - // it('> should only show 1 parc', () => { - // const parcs = viewerStateAtlasLatestParcellationSelector.projector(atlas2.parcellations) - // expect(parcs.length).toEqual(1) - // }) - // }) - - // describe('> viewerStateParcVersionSelector', () => { - // it('> should work', () => { - // const parcs = viewerStateParcVersionSelector.projector(atlas2.parcellations, { - // parcellationSelected: atlas2.parcellations[0] - // }) - // expect(parcs.length).toEqual(2) - - // expect(parcs[0]['@version']['@next']).toBeFalsy() - // expect(parcs[parcs.length-1]['@version']['@previous']).toBeFalsy() - // }) - // }) - - describe("> viewerStateGetSelectedAtlas", () => { - it("> projects properly", () => { - const atlas1 = { - "@id": "atlas1" - } - const atlas2 = { - "@id": "atlas2" - } - const atlas3 = { - "@id": "atlas3" - } - const allAtlases = [ atlas1, atlas2, atlas3 ] - const result = viewerStateGetSelectedAtlas.projector({ - fetchedAtlases: allAtlases, - overlayingAdditionalParcellations: [], - selectedAtlasId: atlas1["@id"] - }) - expect(result).toEqual(atlas1 as any) - }) - }) - - describe("> selectorSelectedATP", () => { - const mockAtlas = { - "@id": "mock atlas" - } as any - const mockTmpl = { - "@id": "mock Tmpl" - } as any - const mockParc = { - "@id": "mock Parc" - } as any - - it("> transforms the selectors properly", () => { - const result = selectorSelectedATP.projector(mockAtlas,mockTmpl,mockParc) - expect(result).toEqual({ atlas: mockAtlas, template: mockTmpl, parcellation: mockParc }) - }) - }) -}) \ No newline at end of file diff --git a/src/services/state/viewerState/selectors.ts b/src/services/state/viewerState/selectors.ts deleted file mode 100644 index c752e8aeae91104be3fd789cf7829894361ff9e3..0000000000000000000000000000000000000000 --- a/src/services/state/viewerState/selectors.ts +++ /dev/null @@ -1,188 +0,0 @@ -// import { createSelector } from "@ngrx/store" -// import { SapiAtlasModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi" -// import { SapiRegionModel } from "src/atlasComponents/sapi/type" -// import { viewerStateHelperStoreName, IViewerStateHelperStore } from "../viewerState.store.helper" -// import { IViewerState } from "./type" - -// import { -// selectors as atlasSelectionSelectors -// } from "src/state/atlasSelection" - -// const { -// selectedATP: selectorSelectedATP, -// selectedAtlas: viewerStateGetSelectedAtlas, -// selectedParcellation: viewerStateSelectedParcellationSelector, -// selectedTemplate: viewerStateSelectedTemplateSelector, -// } = atlasSelectionSelectors - - -// const viewerStateSelectedRegionsSelector = createSelector<any, any, SapiRegionModel[]>( -// state => state['viewerState'], -// viewerState => viewerState['regionsSelected'] -// ) - -// const viewerStateCustomLandmarkSelector = createSelector( -// state => state['viewerState'], -// viewerState => viewerState['userLandmarks'] -// ) - -// const flattenFetchedTemplatesIntoParcellationsReducer = (acc, curr) => { -// const parcelations = (curr['parcellations'] || []).map(p => { -// return { -// ...p, -// useTheme: curr['useTheme'] -// } -// }) - -// return acc.concat( parcelations ) -// } - -// const viewerStateFetchedTemplatesSelector = createSelector( -// state => state['viewerState'], -// viewerState => viewerState['fetchedTemplates'] -// ) - -// const viewerStateSelectorFeatureSelector = createSelector( -// (state: any) => state.viewerState as IViewerState, -// viewerState => viewerState.featureSelected -// ) - -// /** -// * viewerStateSelectedTemplateSelector may have it navigation mutated to allow for initiliasation of viewer at the correct navigation -// * in some circumstances, it may be required to get the original navigation object -// */ -// const viewerStateSelectedTemplatePureSelector = createSelector( -// viewerStateFetchedTemplatesSelector, -// viewerStateSelectedTemplateSelector, -// (fetchedTemplates, selectedTemplate) => { -// if (!selectedTemplate) return null -// return fetchedTemplates.find(t => t['@id'] === selectedTemplate['@id']) -// } -// ) - -// const viewerStateNavigationStateSelector = createSelector( -// state => state['viewerState'], -// viewerState => viewerState['navigation'] -// ) - -// const viewerStateOverwrittenColorMapSelector = createSelector( -// state => state['viewerState'], -// viewerState => viewerState['overwrittenColorMap'] -// ) - -// const viewerStateSelectorNavigation = createSelector( -// state => state['viewerState'], -// viewerState => viewerState['navigation'] -// ) - -// const viewerStateViewerModeSelector = createSelector( -// state => state['viewerState'], -// viewerState => viewerState['viewerMode'] -// ) - -// const viewerStateGetOverlayingAdditionalParcellations = createSelector( -// state => state[viewerStateHelperStoreName], -// state => state['viewerState'], -// (viewerHelperState, viewerState ) => { -// const { selectedAtlasId, fetchedAtlases } = viewerHelperState -// const { parcellationSelected } = viewerState -// const selectedAtlas = selectedAtlasId && fetchedAtlases.find(a => a['@id'] === selectedAtlasId) -// const hasBaseLayer = selectedAtlas?.parcellations.find(p => p.baseLayer) -// if (!hasBaseLayer) return [] -// const atlasLayer = selectedAtlas?.parcellations.find(p => p['@id'] === (parcellationSelected && parcellationSelected['@id'])) -// const isBaseLayer = atlasLayer && atlasLayer.baseLayer -// return (!!atlasLayer && !isBaseLayer) ? [{ -// ...(parcellationSelected || {} ), -// ...atlasLayer -// }] : [] -// } -// ) - -// const viewerStateFetchedAtlasesSelector = createSelector<any, any, SapiAtlasModel[]>( -// state => state[viewerStateHelperStoreName], -// helperState => helperState['fetchedAtlases'] -// ) - - -// const viewerStateAtlasParcellationSelector = createSelector( -// state => state[viewerStateHelperStoreName], -// state => state['viewerState'], -// (viewerHelperState, viewerState) => { -// const { selectedAtlasId, fetchedAtlases } = viewerHelperState -// const { fetchedTemplates } = viewerState - -// const allParcellations = fetchedTemplates.reduce(flattenFetchedTemplatesIntoParcellationsReducer, []) - -// const selectedAtlas = selectedAtlasId && fetchedAtlases.find(a => a['@id'] === selectedAtlasId) -// const atlasLayers = selectedAtlas?.parcellations -// .map(p => { -// const otherHalfOfParc = allParcellations.find(parc => parc['@id'] === p['@id']) || {} -// return { -// ...p, -// ...otherHalfOfParc, -// } -// }) -// return atlasLayers -// } -// ) - -// const viewerStateAtlasLatestParcellationSelector = createSelector( -// viewerStateAtlasParcellationSelector, -// parcs => (parcs && parcs.filter( p => !p['@version'] || !p['@version']['@next']) || []) -// ) - -// const viewerStateParcVersionSelector = createSelector( -// viewerStateAtlasParcellationSelector, -// state => state['viewerState'], -// (allAtlasParcellations, viewerState) => { -// if (!viewerState || !viewerState.parcellationSelected) return [] -// const returnParc = [] -// const foundParc = allAtlasParcellations.find(p => p['@id'] === viewerState.parcellationSelected['@id']) -// if (!foundParc) return [] -// returnParc.push(foundParc) -// const traverseParc = parc => { -// if (!parc) return [] -// if (!parc['@version']) return [] -// if (parc['@version']['@next']) { -// const nextParc = allAtlasParcellations.find(p => p['@id'] === parc['@version']['@next']) -// if (nextParc) { -// const nextParcAlreadyIncluded = returnParc.find(p => p['@id'] === nextParc['@id']) -// if (!nextParcAlreadyIncluded) { -// returnParc.unshift(nextParc) -// traverseParc(nextParc) -// } -// } -// } - -// if (parc['@version']['@previous']) { -// const previousParc = allAtlasParcellations.find(p => p['@id'] === parc['@version']['@previous']) -// if (previousParc) { -// const previousParcAlreadyIncluded = returnParc.find(p => p['@id'] === previousParc['@id']) -// if (!previousParcAlreadyIncluded) { -// returnParc.push(previousParc) -// traverseParc(previousParc) -// } -// } -// } -// } -// traverseParc(foundParc) -// return returnParc -// } -// ) - -// const viewerStateContextedSelectedRegionsSelector = createSelector( -// viewerStateSelectedRegionsSelector, -// viewerStateGetSelectedAtlas, -// viewerStateSelectedTemplatePureSelector, -// viewerStateSelectedParcellationSelector, -// (regions, atlas, template, parcellation) => regions.map(r => { -// return { -// ...r, -// context: { -// atlas, -// template, -// parcellation -// } -// } -// }) -// ) diff --git a/src/services/state/viewerState/type.ts b/src/services/state/viewerState/type.ts deleted file mode 100644 index 58bf80a3e7abd4dbea84b24c1e41e96b268677c3..0000000000000000000000000000000000000000 --- a/src/services/state/viewerState/type.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { IUserLandmark } from 'src/atlasViewer/atlasViewer.apiService.service'; -import { INgLayerInterface } from 'src/atlasViewer/atlasViewer.component'; - -export interface IViewerState { - fetchedTemplates: any[] - - templateSelected: any | null - parcellationSelected: any | null - regionsSelected: any[] - - viewerMode: string - - landmarksSelected: any[] - userLandmarks: IUserLandmark[] - - navigation: any | null - - loadedNgLayers: INgLayerInterface[] - connectivityRegion: string | null - overwrittenColorMap: string | null - - standaloneVolumes: any[] -} - - -export const defaultViewerState: IViewerState = { - - landmarksSelected : [], - fetchedTemplates : [], - loadedNgLayers: [], - regionsSelected: [], - viewerMode: null, - userLandmarks: [], - navigation: null, - parcellationSelected: null, - templateSelected: null, - connectivityRegion: '', - overwrittenColorMap: null, - standaloneVolumes: [] -} diff --git a/src/services/stateStore.helper.ts b/src/services/stateStore.helper.ts deleted file mode 100644 index 172c58258dec163d118529afe22aeb3ef4af1ce4..0000000000000000000000000000000000000000 --- a/src/services/stateStore.helper.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createAction, props } from "@ngrx/store"; - -export const GENERAL_ACTION_TYPES = { - APPLY_STATE: 'APPLY_STATE', -} - -export const generalApplyState = createAction( - GENERAL_ACTION_TYPES.APPLY_STATE, - props<{ state: any }>() -) - -export const generalActionError = createAction( - `[generalActionError]`, - props<{ message: string }>() -) diff --git a/src/services/stateStore.service.spec.ts b/src/services/stateStore.service.spec.ts deleted file mode 100644 index 1fb8e0058c9d33c10a4fa249d435e9f84c84f7ce..0000000000000000000000000000000000000000 --- a/src/services/stateStore.service.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { getMultiNgIdsRegionsLabelIndexMap } from './stateStore.service' - -const getRandomDummyData = () => Math.round(Math.random() * 1000).toString(16) - -const region1 = { - name: 'region 1', - labelIndex: 15, - ngId: 'left', - dummydata: getRandomDummyData() -} -const region2 = { - name: 'region 2', - labelIndex: 16, - ngId: 'right', - dummydata: getRandomDummyData() -} -const region3 = { - name: 'region 3', - labelIndex: 17, - ngId: 'right', - dummydata: getRandomDummyData() -} - -const dummyParcellationWithNgId = { - name: 'dummy parcellation name', - regions: [ - region1, - region2, - region3 - ] -} - -const dummyParcellationWithoutNgId = { - name: 'dummy parcellation name', - ngId: 'toplevel', - regions: [ - region1, - region2, - region3 - ].map(({ ngId, ...rest }) => { - return { - ...rest, - ...(ngId === 'left' ? { ngId } : {}) - } - }) -} - -describe('stateStore.service.ts', () => { - describe('getMultiNgIdsRegionsLabelIndexMap', () => { - describe('should not mutate original regions', () => { - - }) - - describe('should populate map properly', () => { - const map = getMultiNgIdsRegionsLabelIndexMap(dummyParcellationWithNgId) - it('populated map should have 2 top level', () => { - expect(map.size).toBe(2) - }) - - it('should container left and right top level', () => { - expect(map.get('left')).toBeTruthy() - expect(map.get('right')).toBeTruthy() - }) - - it('left top level should have 1 member', () => { - const leftMap = map.get('left') - expect(leftMap.size).toBe(1) - }) - - it('left top level should map 15 => region1', () => { - const leftMap = map.get('left') - expect(leftMap.get(15)).toEqual(region1) - }) - - it('right top level should have 2 member', () => { - const rightMap = map.get('right') - expect(rightMap.size).toBe(2) - }) - - it('right top level should map 16 => region2, 17 => region3', () => { - const rightMap = map.get('right') - expect(rightMap.get(16)).toEqual(region2) - expect(rightMap.get(17)).toEqual(region3) - }) - }) - - describe('should allow inheritance of ngId', () => { - - const map = getMultiNgIdsRegionsLabelIndexMap(dummyParcellationWithoutNgId) - it('populated map should have 2 top level', () => { - expect(map.size).toBe(2) - }) - - it('should container left and right top level', () => { - expect(map.get('left')).toBeTruthy() - expect(map.get('toplevel')).toBeTruthy() - }) - - it('left top level should have 1 member', () => { - const leftMap = map.get('left') - expect(leftMap.size).toBe(1) - }) - - it('left top level should map 15 => region1', () => { - const leftMap = map.get('left') - expect(leftMap.get(15)).toEqual(region1) - }) - - it('toplevel top level should have 2 member', () => { - const toplevelMap = map.get('toplevel') - expect(toplevelMap.size).toBe(2) - }) - - it('toplevel top level should map 16 => region2, 17 => region3', () => { - const toplevelMap = map.get('toplevel') - expect(toplevelMap.get(16).dummydata).toEqual(region2.dummydata) - expect(toplevelMap.get(17).dummydata).toEqual(region3.dummydata) - }) - }) - - describe('should allow inheritance of attr when specified', () => { - const attr = { - dummyattr: 'default dummy attr' - } - const map = getMultiNgIdsRegionsLabelIndexMap({ - ...dummyParcellationWithNgId, - dummyattr: 'p dummy attr' - }, attr) - it('every region should have dummyattr set properly', () => { - let regions = [] - for (const [ _key1, mMap ] of Array.from(map)) { - for (const [_key2, rs] of Array.from(mMap)) { - regions = [...regions, rs] - } - } - - for (const r of regions) { - expect(r.dummyattr).toEqual('p dummy attr') - } - }) - - }) - }) -}) \ No newline at end of file diff --git a/src/services/stateStore.service.ts b/src/services/stateStore.service.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/state/actions.ts b/src/state/actions.ts index 2465c644ab5cd842e2e9f9f9537b10c1a51f155b..2b29757e099d292ee870c3115a26e906a814b7d2 100644 --- a/src/state/actions.ts +++ b/src/state/actions.ts @@ -1,13 +1,16 @@ import { createAction, props } from "@ngrx/store"; -import { nameSpace } from "./const" +import { MainState, nameSpace } from "./const" -const generalActionError = createAction( +export const generalActionError = createAction( `${nameSpace} generalActionError`, props<{ message: string }>() ) -export const actions = { - generalActionError -} +export const generalApplyState = createAction( + `${nameSpace} generalApplyState`, + props<{ + state: MainState + }>() +) diff --git a/src/state/atlasAppearance/action.ts b/src/state/atlasAppearance/action.ts index 27461e622489e29cc7c4d85563a0a7331de092dc..6a4ce26e148d801c726ba23c3eee906e736aa510 100644 --- a/src/state/atlasAppearance/action.ts +++ b/src/state/atlasAppearance/action.ts @@ -1,12 +1,5 @@ import { createAction, props } from "@ngrx/store"; -import { nameSpace } from "./const" - -export const overwriteColorMap = createAction( - `${nameSpace} overwriteColorMap`, - props<{ - colormap: Record<string, number[]> - }>() -) +import { CustomLayer, nameSpace } from "./const" export const setOctantRemoval = createAction( `${nameSpace} setOctantRemoval`, @@ -20,4 +13,18 @@ export const setShowDelineation = createAction( props<{ flag: boolean }>() -) \ No newline at end of file +) + +export const addCustomLayer = createAction( + `${nameSpace} addCustomLayer`, + props<{ + customLayer: CustomLayer + }>() +) + +export const removeCustomLayer = createAction( + `${nameSpace} removeCustomLayer`, + props<{ + id: string + }>() +) diff --git a/src/state/atlasAppearance/const.ts b/src/state/atlasAppearance/const.ts index ee9d2e45f3c36217015b6c2a18e7e6cd4721ea9e..17ebe814184485cbdc24dd05553f68c8397c1cf1 100644 --- a/src/state/atlasAppearance/const.ts +++ b/src/state/atlasAppearance/const.ts @@ -1 +1,39 @@ -export const nameSpace = `[state.atlasAppearance]` \ No newline at end of file +import { SAPIRegion } from "src/atlasComponents/sapi/core" +export const nameSpace = `[state.atlasAppearance]` + +type CustomLayerBase = { + id: string +} + +export type ColorMapCustomLayer = { + clType: 'customlayer/colormap' | 'baselayer/colormap' + colormap: WeakMap<SAPIRegion, number[]> +} & CustomLayerBase + +export type NgLayerCustomLayer = { + clType: 'customlayer/nglayer' | 'baselayer/nglayer' + source: string + visible?: boolean + shader?: string + transform?: number[][] + opacity?: number + segments?: (number|string)[] + // type?: string + + // annotation?: string // TODO what is this used for? +} & CustomLayerBase + +/** + * custom layer is a catch all term that apply **any** special looks + * to an atlas. it could include: + * + * - different colormap + * - different volume (pmap) + * - different indicies + * + * It is up to the viewer on how to interprete these information. + * each instance **must** contain a clType and an id + * - clType facilitates viewer on how to interprete the custom layer + * - id allows custom layer to be removed, if necessary + */ +export type CustomLayer = ColorMapCustomLayer | NgLayerCustomLayer diff --git a/src/state/atlasAppearance/index.ts b/src/state/atlasAppearance/index.ts index bbbe62696bb68c5448b30b1aa3be6c2a55434658..6d95a7d9cbaf54679e0040637bda231b79003f1c 100644 --- a/src/state/atlasAppearance/index.ts +++ b/src/state/atlasAppearance/index.ts @@ -1,4 +1,4 @@ export * as actions from "./action" export * as selectors from "./selector" -export { nameSpace } from "./const" -export { reducer } from "./store" \ No newline at end of file +export { nameSpace, ColorMapCustomLayer, CustomLayer, NgLayerCustomLayer } from "./const" +export { reducer, AtlasAppearanceStore } from "./store" \ No newline at end of file diff --git a/src/state/atlasAppearance/selector.ts b/src/state/atlasAppearance/selector.ts index 842d2db0f527eba5538a5bf34a06ed2ed3b58d40..b7eac7bc1701c6e490a6a72ccd9cf8412d4e7aeb 100644 --- a/src/state/atlasAppearance/selector.ts +++ b/src/state/atlasAppearance/selector.ts @@ -4,11 +4,6 @@ import { AtlasAppearanceStore } from "./store" const selectStore = state => state[nameSpace] as AtlasAppearanceStore -export const getOverwrittenColormap = createSelector( - selectStore, - state => state.overwrittenColormap -) - export const octantRemoval = createSelector( selectStore, state => state.octantRemoval @@ -17,4 +12,9 @@ export const octantRemoval = createSelector( export const showDelineation = createSelector( selectStore, state => state.showDelineation -) \ No newline at end of file +) + +export const customLayers = createSelector( + selectStore, + state => state.customLayers +) diff --git a/src/state/atlasAppearance/store.ts b/src/state/atlasAppearance/store.ts index 476da05585d4fe905386bbacbadf8ba16c6e5ff2..648104a70910a795d74c2de55c4bd12b5abe9b6e 100644 --- a/src/state/atlasAppearance/store.ts +++ b/src/state/atlasAppearance/store.ts @@ -1,45 +1,62 @@ import { createReducer, on } from "@ngrx/store" import * as actions from "./action" +import { CustomLayer } from "./const" export type AtlasAppearanceStore = { - overwrittenColormap: Record<string, number[]> + octantRemoval: boolean showDelineation: boolean + customLayers: CustomLayer[] } const defaultState: AtlasAppearanceStore = { - overwrittenColormap: null, octantRemoval: true, showDelineation: true, + customLayers: [] } export const reducer = createReducer( defaultState, on( - actions.overwriteColorMap, - (state, { colormap }) => { + actions.setOctantRemoval, + (state, { flag }) => { return { ...state, - overwrittenColormap: colormap + octantRemoval: flag } } ), on( - actions.setOctantRemoval, + actions.setShowDelineation, (state, { flag }) => { return { ...state, - octantRemoval: flag + showDelineation: flag } } ), on( - actions.setShowDelineation, - (state, { flag }) => { + actions.addCustomLayer, + (state, { customLayer }) => { + const { customLayers } = state + return { ...state, - showDelineation: flag + customLayers: [ + customLayer, + ...customLayers.filter(l => l.id !== customLayer.id) + ] } } ), + on( + actions.removeCustomLayer, + (state, { id }) => { + const { customLayers } = state + return { + ...state, + customLayers: customLayers.filter(l => l.id !== id) + } + } + ) ) diff --git a/src/state/atlasSelection/actions.ts b/src/state/atlasSelection/actions.ts index 7d114fa5cd3a2acb7811f3d8d08c594e9c0d5164..d3f4f7735abe19dbc087da3c849a428b102dcfe0 100644 --- a/src/state/atlasSelection/actions.ts +++ b/src/state/atlasSelection/actions.ts @@ -1,6 +1,6 @@ import { createAction, props } from "@ngrx/store"; import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; -import { nameSpace, ViewerMode } from "./const" +import { BreadCrumb, nameSpace, ViewerMode } from "./const" export const selectAtlas = createAction( `${nameSpace} selectAtlas`, @@ -23,6 +23,13 @@ export const selectParcellation = createAction( }>() ) +export const setSelectedParcellationAllRegions = createAction( + `${nameSpace} setSelectedParcellationAllRegions`, + props<{ + regions: SapiRegionModel[] + }>() +) + export const selectRegions = createAction( `${nameSpace} selectRegions`, props<{ @@ -57,6 +64,20 @@ export const setViewerMode = createAction( }>() ) +export const showBreadCrumb = createAction( + `${nameSpace} showBreadCrumb`, + props<{ + breadcrumb: BreadCrumb + }>() +) + +export const dismissBreadCrumb = createAction( + `${nameSpace} dismissBreadCrumb`, + props<{ + id: string + }>() +) + export const clearSelectedRegions = createAction( `${nameSpace} clearSelectedRegions` ) @@ -78,6 +99,9 @@ export const clearStandAloneVolumes = createAction( `${nameSpace} clearStandAloneVolumes` ) +/** + * n.b. position in nm! + */ export const navigateTo = createAction( `${nameSpace} navigateTo`, props<{ @@ -124,4 +148,4 @@ export const viewSelRegionInNewSpace = createAction( region: SapiRegionModel template: SapiSpaceModel }>() -) \ No newline at end of file +) diff --git a/src/state/atlasSelection/const.ts b/src/state/atlasSelection/const.ts index 7e26794f3173ebf6b97ce8ab614c963e32685548..5ae1942f7be037d174612e9dbbb9b229065bbad9 100644 --- a/src/state/atlasSelection/const.ts +++ b/src/state/atlasSelection/const.ts @@ -1,2 +1,6 @@ export const nameSpace = `[state.atlasSelection]` -export type ViewerMode = 'annotating' | 'key frame' \ No newline at end of file +export type ViewerMode = 'annotating' | 'key frame' +export type BreadCrumb = { + id: string + name: string +} diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts index 1dbaf44a16567047161aa0687fb0bb08a93813e8..e3c8e15da99ed5552519337487a94c497895b2b4 100644 --- a/src/state/atlasSelection/effects.ts +++ b/src/state/atlasSelection/effects.ts @@ -1,12 +1,14 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; -import { forkJoin, merge, of } from "rxjs"; -import { filter, map, mapTo, switchMap, withLatestFrom } from "rxjs/operators"; +import { forkJoin, from, merge, of } from "rxjs"; +import { filter, map, mapTo, switchMap, switchMapTo, withLatestFrom } from "rxjs/operators"; import { SAPI } from "src/atlasComponents/sapi"; -import * as actions from "./actions" -import { actions as generalAction } from "../actions" +import * as mainActions from "../actions" import { select, Store } from "@ngrx/store"; -import { selectors } from '.' +import { selectors, actions } from '.' +import { fromRootStore } from "./util"; +import { ParcellationIsBaseLayer } from "src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe"; +import { OrderParcellationByVersionPipe } from "src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe"; @Injectable() export class Effect { @@ -47,6 +49,19 @@ export class Effect { }), )) + onATPSelectionGetAndSetAllRegions = createEffect(() => this.store.pipe( + select(selectors.selectedATP), + filter(({ atlas, template, parcellation }) => !!atlas && !!template && !!parcellation), + switchMap(({ atlas, template, parcellation }) => + this.sapiSvc.getParcRegions(atlas["@id"], parcellation["@id"], template["@id"]) + ), + map(regions => + actions.setSelectedParcellationAllRegions({ + regions + }) + ) + )) + onAtlasSelClearStandAloneVolumes = createEffect(() => this.action.pipe( ofType(actions.selectAtlas), mapTo(actions.setStandAloneVolumes({ @@ -63,11 +78,23 @@ export class Effect { onNonBaseLayerRemoval = createEffect(() => this.action.pipe( ofType(actions.clearNonBaseParcLayer), - mapTo(generalAction.generalActionError({ - message: `NYI` - })) + switchMapTo( + this.store.pipe( + fromRootStore.allAvailParcs(this.sapiSvc), + map(parcs => { + const baseLayers = parcs.filter(this.parcellationIsBaseLayerPipe.transform) + const newestLayer = this.orderParcellationByVersionPipe.transform(baseLayers) + return actions.selectParcellation({ + parcellation: newestLayer + }) + }) + ) + ) )) + private parcellationIsBaseLayerPipe = new ParcellationIsBaseLayer() + private orderParcellationByVersionPipe = new OrderParcellationByVersionPipe() + onClearStandAloneVolumes = createEffect(() => this.action.pipe( ofType(actions.clearStandAloneVolumes), mapTo(actions.setStandAloneVolumes({ @@ -82,22 +109,11 @@ export class Effect { */ onSelectATPById = createEffect(() => this.action.pipe( ofType(actions.selectATPById), - mapTo(generalAction.generalActionError({ - message: `NYI` - })) - )) - - /** - * consider what happens if it was nehuba viewer? - * what happens if it was three surfer viewer? - */ - onNavigateTo = createEffect(() => this.action.pipe( - ofType(actions.navigateTo), - mapTo(generalAction.generalActionError({ - message: `NYI` + mapTo(mainActions.generalActionError({ + message: `NYI, onSelectATPById` })) )) - + onClearViewerMode = createEffect(() => this.action.pipe( ofType(actions.clearViewerMode), mapTo(actions.setViewerMode({ viewerMode: null })) @@ -105,15 +121,15 @@ export class Effect { onToggleRegionSelectById = createEffect(() => this.action.pipe( ofType(actions.toggleRegionSelectById), - mapTo(generalAction.generalActionError({ - message: `NYI` + mapTo(mainActions.generalActionError({ + message: `NYI onToggleRegionSelectById` })) )) onNavigateToRegion = createEffect(() => this.action.pipe( ofType(actions.navigateToRegion), - mapTo(generalAction.generalActionError({ - message: `NYI` + mapTo(mainActions.generalActionError({ + message: `NYI onNavigateToRegion` })) )) @@ -128,9 +144,16 @@ export class Effect { ofType(actions.selectParcellation) ) ).pipe( - mapTo(actions.selectRegions({ - regions: [] - })) + switchMapTo( + of( + actions.selectRegions({ + regions: [] + }), + actions.setSelectedParcellationAllRegions({ + regions: [] + }) + ) + ) )) onRegionToggleSelect = createEffect(() => this.action.pipe( diff --git a/src/state/atlasSelection/selectors.ts b/src/state/atlasSelection/selectors.ts index a60c20225d382e20691a955a7f3010c4bb1a0dc0..7a6663b8b8489c6bb231db14fe50579fa3180b8d 100644 --- a/src/state/atlasSelection/selectors.ts +++ b/src/state/atlasSelection/selectors.ts @@ -21,6 +21,11 @@ export const selectedParcellation = createSelector( state => state.selectedParcellation ) +export const selectedParcAllRegions = createSelector( + selectStore, + state => state.selectedParcellationAllRegions +) + export const selectedRegions = createSelector( selectStore, state => state.selectedRegions @@ -47,3 +52,8 @@ export const viewerMode = createSelector( selectStore, state => state.viewerMode ) + +export const breadCrumbs = createSelector( + selectStore, + state => state.breadcrumbs +) \ No newline at end of file diff --git a/src/state/atlasSelection/store.ts b/src/state/atlasSelection/store.ts index f36738bf1febc95b28dc7b6b4dff47377298c28e..7332711b69be6094ca9708e0945f958627921b6f 100644 --- a/src/state/atlasSelection/store.ts +++ b/src/state/atlasSelection/store.ts @@ -1,12 +1,14 @@ import { createReducer, on } from "@ngrx/store"; import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; import * as actions from "./actions" -import { ViewerMode } from "./const" +import { ViewerMode, BreadCrumb } from "./const" export type AtlasSelectionState = { selectedAtlas: SapiAtlasModel selectedTemplate: SapiSpaceModel selectedParcellation: SapiParcellationModel + selectedParcellationAllRegions: SapiRegionModel[] + selectedRegions: SapiRegionModel[] standAloneVolumes: string[] @@ -23,16 +25,19 @@ export type AtlasSelectionState = { } viewerMode: ViewerMode + breadcrumbs: BreadCrumb[] } export const defaultState: AtlasSelectionState = { selectedAtlas: null, selectedParcellation: null, + selectedParcellationAllRegions: [], selectedRegions: [], selectedTemplate: null, standAloneVolumes: [], navigation: null, - viewerMode: null + viewerMode: null, + breadcrumbs: [] } const reducer = createReducer( @@ -64,6 +69,15 @@ const reducer = createReducer( } } ), + on( + actions.setSelectedParcellationAllRegions, + (state, { regions }) => { + return { + ...state, + selectedParcellationAllRegions: regions + } + } + ), on( actions.selectRegions, (state, { regions }) => { @@ -99,6 +113,27 @@ const reducer = createReducer( viewerMode } } + ), + on( + actions.showBreadCrumb, + (state, { breadcrumb }) => { + return { + ...state, + breadcrumbs: [ + ...state.breadcrumbs.filter(bc => bc.id !== breadcrumb.id), + breadcrumb + ] + } + } + ), + on( + actions.dismissBreadCrumb, + (state, { id }) => { + return { + ...state, + breadcrumbs: state.breadcrumbs.filter(bc => bc.id !== id) + } + } ) ) diff --git a/src/state/const.ts b/src/state/const.ts index 60f4c38b8b2f0d9f9dd4f8db61c0bc42e37a6ac4..29172b656f7af7c04a4857bcb4b7c9f416cc6f22 100644 --- a/src/state/const.ts +++ b/src/state/const.ts @@ -1 +1,13 @@ -export const nameSpace = `[state]` \ No newline at end of file +import { annotation, atlasAppearance, atlasSelection, plugins, userInteraction, userInterface, userPreference } from "." + +export const nameSpace = `[state]` + +export type MainState = { + [userPreference.nameSpace]: userPreference.UserPreference, + [atlasSelection.nameSpace]: atlasSelection.AtlasSelectionState, + [userInterface.nameSpace]: userInterface.UiStore, + [userInteraction.nameSpace]: userInteraction.UserInteraction, + [annotation.nameSpace]: annotation.AnnotationState, + [plugins.nameSpace]: plugins.PluginStore, + [atlasAppearance.nameSpace]: atlasAppearance.AtlasAppearanceStore +} diff --git a/src/state/index.ts b/src/state/index.ts index b8523aff300ecd5c0ff0987c4a99ae20a088e14f..18f568ae703038c5255550d238793bd7b267936a 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -9,7 +9,6 @@ import * as atlasAppearance from "./atlasAppearance" import * as plugins from "./plugins" import * as userInteraction from "./userInteraction" import * as userPreference from "./userPreference" -import { EffectsModule } from "@ngrx/effects" export { atlasSelection, @@ -21,7 +20,9 @@ export { userPreference, } -export function debug(reducer: ActionReducer<any>): ActionReducer<any> { +export * as generalActions from "./actions" + +function debug(reducer: ActionReducer<any>): ActionReducer<any> { return function(state, action) { console.log('state', state); console.log('action', action); @@ -44,8 +45,19 @@ export const RootStoreModule = StoreModule.forRoot({ ] }) -export const RootEffecsModule = EffectsModule.forRoot([ - plugins.Effects, - atlasSelection.Effect, - userInterface.Effects, -]) \ No newline at end of file +/** + * + * We have to use a function here. At import time, *.Effect(s) + * would not yet be defined. + * + * @returns Effects from state + */ +export function getStoreEffects() { + return [ + plugins.Effects, + atlasSelection.Effect, + userInterface.Effects, + ] +} + +export { MainState } from "./const" diff --git a/src/state/plugins/index.ts b/src/state/plugins/index.ts index bdca8a18cd994e4c61ac6588fe2052efd79e4634..11548c69ffd13af7819fb9c2a67c9fc189d725db 100644 --- a/src/state/plugins/index.ts +++ b/src/state/plugins/index.ts @@ -1,5 +1,5 @@ export * as selectors from "./selectors" export * as actions from "./actions" -export { reducer } from "./store" +export { reducer, PluginStore } from "./store" export { Effects } from "./effects" export { nameSpace, INIT_MANIFEST_SRC } from "./const" \ No newline at end of file diff --git a/src/state/userInteraction/index.ts b/src/state/userInteraction/index.ts index 2bad8f6b313c2a4a4f8c7afff8f5def60db6d43c..4903f7b16fe5d859f2a437fee17e2602de9081b9 100644 --- a/src/state/userInteraction/index.ts +++ b/src/state/userInteraction/index.ts @@ -2,4 +2,4 @@ export { Effect } from "./effects" export { nameSpace } from "./const" export * as actions from "./actions" export * as selectors from "./selectors" -export { reducer } from "./store" \ No newline at end of file +export { reducer, UserInteraction } from "./store" \ No newline at end of file diff --git a/src/state/userInterface/effects.ts b/src/state/userInterface/effects.ts index d1472dc5dd8fe1a61f8e9784e6f5aee5e11cf22a..aabfd36f14a0c0ff90b809d4f712fcca16798182 100644 --- a/src/state/userInterface/effects.ts +++ b/src/state/userInterface/effects.ts @@ -5,7 +5,7 @@ import { Actions, createEffect, ofType } from "@ngrx/effects"; import { select, Store } from "@ngrx/store"; import { of } from "rxjs"; import { filter, map, mapTo, pairwise, startWith, switchMap, tap, withLatestFrom } from "rxjs/operators"; -import { generalActionError } from "src/services/stateStore.helper"; +import { generalActionError } from "../actions"; import { userInterface } from ".."; import { selectors } from "../atlasSelection" import * as actions from "./actions" diff --git a/src/state/userInterface/index.ts b/src/state/userInterface/index.ts index cff6971a4bc22aaf6bee13218fcf516f136bf774..2329de3b38bacb0d8a648ee9fe13a911c8b92bb4 100644 --- a/src/state/userInterface/index.ts +++ b/src/state/userInterface/index.ts @@ -1,5 +1,5 @@ export * as actions from "./actions" export * as selectors from "./selectors" export { nameSpace, PanelMode } from "./const" -export { reducer } from "./store" +export { reducer, UiStore } from "./store" export { Effects } from "./effects" diff --git a/src/ui/layerbrowser/index.ts b/src/ui/layerbrowser/index.ts deleted file mode 100644 index e1c19e45596aa009b1c8a86a22b48628dceb991e..0000000000000000000000000000000000000000 --- a/src/ui/layerbrowser/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { LayerBrowserModule } from './layerBrowser.module' - - -export interface INgLayerInterface { - name: string - visible: boolean - source: string - type: string // image | segmentation | etc ... - transform?: [[number, number, number, number], [number, number, number, number], [number, number, number, number], [number, number, number, number]] | null - // colormap : string -} diff --git a/src/ui/layerbrowser/layerBrowser.module.ts b/src/ui/layerbrowser/layerBrowser.module.ts deleted file mode 100644 index a634859a0ce3ed07d2d57b2471cc481bbe89e27b..0000000000000000000000000000000000000000 --- a/src/ui/layerbrowser/layerBrowser.module.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - GetInitialLayerOpacityPipe, - LayerBrowser, - LockedLayerBtnClsPipe -} from './layerBrowserComponent/layerbrowser.component' -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { AngularMaterialModule } from 'src/sharedModules'; -import { LayerDetailComponent } from './layerDetail/layerDetail.component'; -import { FormsModule } from '@angular/forms'; -import { UtilModule } from 'src/util'; - -@NgModule({ - imports: [ - CommonModule, - FormsModule, - AngularMaterialModule, - UtilModule, - ], - declarations: [ - LayerBrowser, - LayerDetailComponent, - - GetInitialLayerOpacityPipe, - LockedLayerBtnClsPipe, - ], - exports: [ - GetInitialLayerOpacityPipe, - LayerBrowser, - LockedLayerBtnClsPipe - ] -}) - -export class LayerBrowserModule{} \ No newline at end of file diff --git a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts deleted file mode 100644 index 043e0005a7d84e5a99f7e3acf52a48410df73378..0000000000000000000000000000000000000000 --- a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, Pipe, PipeTransform } from "@angular/core"; -import { select, Store } from "@ngrx/store"; -import { combineLatest, Observable, Subscription } from "rxjs"; -import { debounceTime, distinctUntilChanged, map, shareReplay, startWith } from "rxjs/operators"; -import { MatSliderChange } from "@angular/material/slider"; - -import { getViewer } from "src/util/fn"; -import { PureContantService } from "src/util"; -import { ngViewerActionRemoveNgLayer, ngViewerActionForceShowSegment } from "src/services/state/ngViewerState/actions"; -import { getNgIds } from 'src/util/fn' -import { LoggingService } from "src/logging"; -import { ARIA_LABELS } from 'common/constants' - -import { INgLayerInterface } from "../index"; - -const SHOW_LAYER_NAMES = [ - 'PLI Fiber Orientation Red Channel', - 'PLI Fiber Orientation Green Channel', - 'PLI Fiber Orientation Blue Channel', - 'Blockface Image', - 'PLI Transmittance', - 'T2w MRI', - 'MRI Labels' -] - -@Component({ - selector : 'layer-browser', - templateUrl : './layerbrowser.template.html', - styleUrls : [ - './layerbrowser.style.css', - ], -}) - -export class LayerBrowser implements OnInit, OnDestroy { - - public TOGGLE_SHOW_LAYER_CONTROL_ARIA_LABEL = ARIA_LABELS.TOGGLE_SHOW_LAYER_CONTROL - - @Output() public nonBaseLayersChanged: EventEmitter<INgLayerInterface[]> = new EventEmitter() - - /** - * TODO make untangle nglayernames and its dependency on ng - */ - public loadedNgLayers$: Observable<INgLayerInterface[]> - public lockedLayers: string[] = [] - - public nonBaseNgLayers$: Observable<INgLayerInterface[]> - - public forceShowSegmentCurrentState: boolean | null = null - public forceShowSegment$: Observable<boolean|null> - - public ngLayers$: Observable<string[]> - public advancedMode: boolean = false - - private subscriptions: Subscription[] = [] - private disposeHandler: any - - @Input() - public showPlaceholder: boolean = true - - public darktheme$: Observable<boolean> - - private customNgLayers: string[] = ['spatial landmark layer'] - - constructor( - private store: Store<any>, - private pureConstantSvc: PureContantService, - private log: LoggingService, - ) { - this.ngLayers$ = store.pipe( - select('viewerState'), - select('templateSelected'), - map(templateSelected => { - if (!templateSelected) { return [] } - if (this.advancedMode) { return [] } - - const { ngId , otherNgIds = []} = templateSelected - - return [ - ngId, - ...this.customNgLayers, - ...otherNgIds, - ...templateSelected.parcellations.reduce((acc, curr) => { - return acc.concat([ - curr.ngId, - ...getNgIds(curr.regions), - ]) - }, []), - ] - }), - /** - * get unique array - */ - map(nonUniqueArray => Array.from(new Set(nonUniqueArray))), - /** - * remove falsy values - */ - map(arr => arr.filter(v => !!v)), - ) - - this.loadedNgLayers$ = this.store.pipe( - select('viewerState'), - select('loadedNgLayers'), - ) - - this.nonBaseNgLayers$ = combineLatest( - this.ngLayers$, - this.loadedNgLayers$, - ).pipe( - map(([baseNgLayerNames, loadedNgLayers]) => { - const baseNameSet = new Set(baseNgLayerNames) - return loadedNgLayers.filter(l => SHOW_LAYER_NAMES.includes(l.name)) - }), - distinctUntilChanged() - ) - - this.forceShowSegment$ = this.store.pipe( - select('ngViewerState'), - select('forceShowSegment'), - startWith(false) - ) - - this.darktheme$ = this.pureConstantSvc.darktheme$.pipe( - shareReplay(1), - ) - - } - - public ngOnInit() { - this.subscriptions.push( - this.nonBaseNgLayers$.pipe( - // on switching template, non base layer will fire - // debounce to ensure that the non base layer is indeed an extra layer - debounceTime(160), - ).subscribe(layers => this.nonBaseLayersChanged.emit(layers)), - ) - this.subscriptions.push( - this.forceShowSegment$.subscribe(state => this.forceShowSegmentCurrentState = state), - ) - - this.viewer = getViewer() - } - - public ngOnDestroy() { - this.subscriptions.forEach(s => s.unsubscribe()) - } - - public classVisible(layer: any): boolean { - return typeof layer.visible === 'undefined' - ? true - : layer.visible - } - - public checkLocked(ngLayer: INgLayerInterface): boolean { - if (!this.lockedLayers) { - /* locked layer undefined. always return false */ - return false - } else { - return this.lockedLayers.findIndex(l => l === ngLayer.name) >= 0 - } - } - - public viewer: any - - public toggleVisibility(layer: any) { - const layerName = layer.name - if (!layerName) { - this.log.error('layer name not defined', layer) - return - } - const ngLayer = this.viewer.layerManager.getLayerByName(layerName) - if (!ngLayer) { - this.log.error('ngLayer could not be found', layerName, this.viewer.layerManager.managedLayers) - } - ngLayer.setVisible(!ngLayer.visible) - } - - public toggleForceShowSegment(ngLayer: any) { - if (!ngLayer || ngLayer.type !== 'segmentation') { - /* toggle only on segmentation layer */ - return - } - - /** - * TODO perhaps useEffects ? - */ - this.store.dispatch( - ngViewerActionForceShowSegment({ - forceShowSegment : this.forceShowSegmentCurrentState === null - ? true - : this.forceShowSegmentCurrentState === true - ? false - : null, - }) - ) - } - - public removeLayer(layer: any) { - if (this.checkLocked(layer)) { - this.log.warn('this layer is locked and cannot be removed') - return - } - - this.store.dispatch( - ngViewerActionRemoveNgLayer({ - layer - }) - ) - } - - public changeOpacity(layerName: string, event: MatSliderChange){ - const { value } = event - const l = this.viewer.layerManager.getLayerByName(layerName) - if (!l) return - - if (typeof l.layer.opacity === 'object') { - l.layer.opacity.value = value - } else if (typeof l.layer.displayState === 'object') { - l.layer.displayState.selectedAlpha.value = value - } else { - this.log.warn({ - msg: `layer does not belong anywhere`, - layerName, - layer: l - }) - } - } - - /** - * TODO use observable and pipe to make this more perf - */ - public segmentationTooltip() { - return `toggle segments visibility: - ${this.forceShowSegmentCurrentState === true ? 'always show' : this.forceShowSegmentCurrentState === false ? 'always hide' : 'auto'}` - } - - get segmentationAdditionalClass() { - return this.forceShowSegmentCurrentState === null - ? 'blue' - : this.forceShowSegmentCurrentState === true - ? 'normal' - : this.forceShowSegmentCurrentState === false - ? 'muted' - : 'red' - } - - public matTooltipPosition: string = 'below' -} - -@Pipe({ - name: 'lockedLayerBtnClsPipe', -}) - -export class LockedLayerBtnClsPipe implements PipeTransform { - public transform(ngLayer: INgLayerInterface, lockedLayers?: string[]): boolean { - return (lockedLayers && new Set(lockedLayers).has(ngLayer.name)) || false - } -} - -@Pipe({ - name: 'getInitialLayerOpacityPipe' -}) - -export class GetInitialLayerOpacityPipe implements PipeTransform{ - public transform(viewer: any, layerName: string): number{ - if (!viewer) return 0 - const l = viewer.layerManager.getLayerByName(layerName) - if (!l || !l.layer) return 0 - if (typeof l.layer.opacity === 'object') return l.layer.opacity.value - else if (typeof l.layer.displayState === 'object') return l.layer.displayState.selectedAlpha.value - else return 0 - } -} diff --git a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.style.css b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.style.css deleted file mode 100644 index b38ff0ea019b33d7e8797092569c7cc865408ea2..0000000000000000000000000000000000000000 --- a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.style.css +++ /dev/null @@ -1,11 +0,0 @@ -:host -{ - padding: 0 0.2em; - display: flex; - flex-direction: column-reverse; -} - -.noLayerPlaceHolder -{ - padding: 0.5em 1em; -} diff --git a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.template.html b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.template.html deleted file mode 100644 index 43aa21f9f9784a690d238a2eec02b23be4994030..0000000000000000000000000000000000000000 --- a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.template.html +++ /dev/null @@ -1,106 +0,0 @@ -<!-- n.b. using mousedown for event trigger --> -<!-- Chrome & FF exhibit different behaviours when using click/mouseup as a event handler --> -<!-- in Chrome, it will complain that expression changed after change detection --> -<!-- in FF, the element changes, and focusout event is never fired properly --> - -<ng-container *ngIf="nonBaseNgLayers$ | async as nonBaseNgLayers; else noLayerPlaceHolder"> - <mat-accordion *ngIf="nonBaseNgLayers.length > 0; else noLayerPlaceHolder" - [multi]="true" - displayMode="flat"> - <mat-expansion-panel - [disabled]="true" - *ngFor="let ngLayer of nonBaseNgLayers" - class="layer-expansion-unit" - #expansionPanel> - <mat-expansion-panel-header> - <div class="align-items-center d-flex flex-nowrap sxplr-pr-4 w-100"> - <!-- toggle opacity --> - <div matTooltip="opacity"> - - <mat-slider - [disabled]="!ngLayer.visible" - min="0" - max="1" - (input)="changeOpacity(ngLayer.name, $event)" - [value]="viewer | getInitialLayerOpacityPipe: ngLayer.name" - step="0.01"> - - </mat-slider> - </div> - - <!-- toggle visibility --> - - <button - [matTooltipPosition]="matTooltipPosition" - [matTooltip]="(ngLayer | lockedLayerBtnClsPipe : lockedLayers) ? 'base layer cannot be hidden' : 'toggle visibility'" - (mousedown)="toggleVisibility(ngLayer)" - mat-icon-button - [disabled]="ngLayer | lockedLayerBtnClsPipe : lockedLayers" - [color]="ngLayer.visible ? 'primary' : null"> - <i [ngClass]="(ngLayer | lockedLayerBtnClsPipe : lockedLayers) ? 'fas fa-lock muted' : ngLayer.visible ? 'far fa-eye' : 'far fa-eye-slash'"> - </i> - </button> - - <!-- advanced mode only: toggle force show segmentation --> - <button - *ngIf="advancedMode" - [matTooltipPosition]="matTooltipPosition" - [matTooltip]="ngLayer.type === 'segmentation' ? segmentationTooltip() : 'only segmentation layer can hide/show segments'" - (mousedown)="toggleForceShowSegment(ngLayer)" - mat-icon-button> - <i - class="fas" - [ngClass]="ngLayer.type === 'segmentation' ? ('fa-th-large ' + segmentationAdditionalClass) : 'fa-lock muted' "> - - </i> - </button> - - <!-- remove layer --> - <button - color="warn" - mat-icon-button - (mousedown)="removeLayer(ngLayer)" - [disabled]="ngLayer | lockedLayerBtnClsPipe : lockedLayers" - [matTooltip]="(ngLayer | lockedLayerBtnClsPipe : lockedLayers) ? 'base layers cannot be removed' : 'remove layer'"> - <i [class]="(ngLayer | lockedLayerBtnClsPipe : lockedLayers) ? 'fas fa-lock muted' : 'fas fa-trash'"> - </i> - </button> - - <!-- layer description --> - <mat-label - [matTooltipPosition]="matTooltipPosition" - [matTooltip]="ngLayer.name | getFilenamePipe " - [class]="((darktheme$ | async) ? 'text-light' : 'text-dark') + ' text-truncate flex-grow-1 flex-shrink-1'"> - {{ ngLayer.name | getFilenamePipe }} - </mat-label> - - <button mat-icon-button - [attr.aria-label]="TOGGLE_SHOW_LAYER_CONTROL_ARIA_LABEL" - (click)="expansionPanel.toggle()"> - <ng-container *ngIf="expansionPanel.expanded; else btnIconAlt"> - <i class="fas fa-chevron-up"></i> - </ng-container> - - <ng-template #btnIconAlt> - <i class="fas fa-chevron-down"></i> - </ng-template> - </button> - - </div> - </mat-expansion-panel-header> - - <ng-template matExpansionPanelContent> - <layer-detail-cmp [layerName]="ngLayer.name"> - </layer-detail-cmp> - </ng-template> - - </mat-expansion-panel> - </mat-accordion> -</ng-container> - -<!-- fall back when no layers are showing --> -<ng-template #noLayerPlaceHolder> - <small *ngIf="showPlaceholder" class="noLayerPlaceHolder text-muted"> - No additional layers added. - </small> -</ng-template> diff --git a/src/ui/layerbrowser/layerDetail/layerDetail.component.spec.ts b/src/ui/layerbrowser/layerDetail/layerDetail.component.spec.ts deleted file mode 100644 index 575f538b1b828342c9f290f37aab9bf2c2209556..0000000000000000000000000000000000000000 --- a/src/ui/layerbrowser/layerDetail/layerDetail.component.spec.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { LayerDetailComponent, VIEWER_INJECTION_TOKEN } from './layerDetail.component' -import { async, TestBed } from '@angular/core/testing' -import { NgLayersService } from '../ngLayerService.service' -import { By } from '@angular/platform-browser' -import * as CONSTANT from 'src/util/constants' -import { AngularMaterialModule } from 'src/sharedModules' -import { CommonModule } from '@angular/common' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' - -const getSpies = (service: NgLayersService) => { - const lowThMapGetSpy = spyOn(service.lowThresholdMap, 'get').and.callThrough() - const highThMapGetSpy = spyOn(service.highThresholdMap, 'get').and.callThrough() - const brightnessMapGetSpy = spyOn(service.brightnessMap, 'get').and.callThrough() - const contractMapGetSpy = spyOn(service.contrastMap, 'get').and.callThrough() - const removeBgMapGetSpy = spyOn(service.removeBgMap, 'get').and.callThrough() - - const lowThMapSetSpy = spyOn(service.lowThresholdMap, 'set').and.callThrough() - const highThMapSetSpy = spyOn(service.highThresholdMap, 'set').and.callThrough() - const brightnessMapSetSpy = spyOn(service.brightnessMap, 'set').and.callThrough() - const contrastMapSetSpy = spyOn(service.contrastMap, 'set').and.callThrough() - const removeBgMapSetSpy = spyOn(service.removeBgMap, 'set').and.callThrough() - - return { - lowThMapGetSpy, - highThMapGetSpy, - brightnessMapGetSpy, - contractMapGetSpy, - removeBgMapGetSpy, - lowThMapSetSpy, - highThMapSetSpy, - brightnessMapSetSpy, - contrastMapSetSpy, - removeBgMapSetSpy, - } -} - -const getCtrl = () => { - const lowThSlider = By.css('mat-slider[aria-label="Set lower threshold"]') - const highThSlider = By.css('mat-slider[aria-label="Set higher threshold"]') - const brightnessSlider = By.css('mat-slider[aria-label="Set brightness"]') - const contrastSlider = By.css('mat-slider[aria-label="Set contrast"]') - const removeBgSlideToggle = By.css('mat-slide-toggle[aria-label="Remove background"]') - return { - lowThSlider, - highThSlider, - brightnessSlider, - contrastSlider, - removeBgSlideToggle, - } -} - -const getSliderChangeTest = ctrlName => describe(`testing: ${ctrlName}`, () => { - - it('on change, calls window', () => { - const service = TestBed.inject(NgLayersService) - const spies = getSpies(service) - - const fixture = TestBed.createComponent(LayerDetailComponent) - const layerName = `hello-kitty` - fixture.componentInstance.layerName = layerName - const triggerChSpy = spyOn(fixture.componentInstance, 'triggerChange') - const ctrls = getCtrl() - - const sLower = fixture.debugElement.query( ctrls[`${ctrlName}Slider`] ) - sLower.componentInstance.input.emit({ value: 0.5 }) - expect(spies[`${ctrlName}MapSetSpy`]).toHaveBeenCalledWith(layerName, 0.5) - expect(triggerChSpy).toHaveBeenCalled() - }) -}) - -const fragmentMainSpy = { - value: `test value`, - restoreState: () => {} -} - -const defaultViewer = { - layerManager: { - getLayerByName: jasmine.createSpy('getLayerByName').and.returnValue({layer: {fragmentMain: fragmentMainSpy}}) - } -} - -describe('> layerDetail.component.ts', () => { - describe('> LayerDetailComponent', () => { - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ - LayerDetailComponent - ], - imports: [ - AngularMaterialModule, - CommonModule, - FormsModule, - ReactiveFormsModule, - ], - providers: [ - NgLayersService, - { - provide: VIEWER_INJECTION_TOKEN, - useValue: defaultViewer - } - ] - }).compileComponents() - })) - - describe('> basic funcitonalities', () => { - - it('> it should be created', () => { - const fixture = TestBed.createComponent(LayerDetailComponent) - const element = fixture.debugElement.componentInstance - expect(element).toBeTruthy() - }) - - it('> on bind input, if input is truthy, calls get on layerService maps', () => { - TestBed.overrideProvider(VIEWER_INJECTION_TOKEN, { - useValue: {} - }) - const service = TestBed.inject(NgLayersService) - const { - brightnessMapGetSpy, - contractMapGetSpy, - highThMapGetSpy, - lowThMapGetSpy, - removeBgMapGetSpy - } = getSpies(service) - - const layerName = `hello-kitty` - const fixture = TestBed.createComponent(LayerDetailComponent) - fixture.componentInstance.layerName = layerName - fixture.componentInstance.ngOnChanges() - fixture.detectChanges() - expect(brightnessMapGetSpy).toHaveBeenCalledWith(layerName) - expect(contractMapGetSpy).toHaveBeenCalledWith(layerName) - expect(highThMapGetSpy).toHaveBeenCalledWith(layerName) - expect(lowThMapGetSpy).toHaveBeenCalledWith(layerName) - expect(removeBgMapGetSpy).toHaveBeenCalledWith(layerName) - }) - - it('> on bind input, if input is falsy, does not call layerService map get', () => { - const service = TestBed.inject(NgLayersService) - const { - brightnessMapGetSpy, - contractMapGetSpy, - highThMapGetSpy, - lowThMapGetSpy, - removeBgMapGetSpy - } = getSpies(service) - - const layerName = null - const fixture = TestBed.createComponent(LayerDetailComponent) - fixture.componentInstance.layerName = layerName - fixture.componentInstance.ngOnChanges() - fixture.detectChanges() - expect(brightnessMapGetSpy).not.toHaveBeenCalled() - expect(contractMapGetSpy).not.toHaveBeenCalled() - expect(highThMapGetSpy).not.toHaveBeenCalled() - expect(lowThMapGetSpy).not.toHaveBeenCalled() - expect(removeBgMapGetSpy).not.toHaveBeenCalled() - }) - - }) - - const testingSlidersCtrl = [ - 'lowTh', - 'highTh', - 'brightness', - 'contrast', - ] - - for (const sliderCtrl of testingSlidersCtrl ) { - getSliderChangeTest(sliderCtrl) - } - - describe('testing: removeBG toggle', () => { - it('on change, calls window', () => { - - const service = TestBed.inject(NgLayersService) - const { removeBgMapSetSpy } = getSpies(service) - - const fixture = TestBed.createComponent(LayerDetailComponent) - const triggerChSpy = spyOn(fixture.componentInstance, 'triggerChange') - const layerName = `hello-kitty` - fixture.componentInstance.layerName = layerName - - const { removeBgSlideToggle } = getCtrl() - const bgToggle = fixture.debugElement.query( removeBgSlideToggle ) - bgToggle.componentInstance.change.emit({ checked: true }) - expect(removeBgMapSetSpy).toHaveBeenCalledWith('hello-kitty', true) - expect(triggerChSpy).toHaveBeenCalled() - - removeBgMapSetSpy.calls.reset() - triggerChSpy.calls.reset() - expect(removeBgMapSetSpy).not.toHaveBeenCalled() - expect(triggerChSpy).not.toHaveBeenCalled() - - bgToggle.componentInstance.change.emit({ checked: false }) - - expect(removeBgMapSetSpy).toHaveBeenCalledWith('hello-kitty', false) - expect(triggerChSpy).toHaveBeenCalled() - }) - }) - - describe('triggerChange', () => { - it('should throw if viewer is not defined', () => { - TestBed.overrideProvider(VIEWER_INJECTION_TOKEN, { - useValue: null - }) - const fixutre = TestBed.createComponent(LayerDetailComponent) - expect(function(){ - fixutre.componentInstance.triggerChange() - }).toThrowError('viewer is not defined') - }) - - it('should throw if layer is not found', () => { - const fakeGetLayerByName = jasmine.createSpy().and.returnValue(undefined) - const fakeNgInstance = { - layerManager: { - getLayerByName: fakeGetLayerByName - } - } - - TestBed.overrideProvider(VIEWER_INJECTION_TOKEN, { - useValue: fakeNgInstance - }) - - const fixutre = TestBed.createComponent(LayerDetailComponent) - const layerName = `test-kitty` - - fixutre.componentInstance.layerName = layerName - - expect(function(){ - fixutre.componentInstance.triggerChange() - }).toThrowError(`layer with name: ${layerName}, not found.`) - }) - - it('should throw if layer.layer.fragmentMain is undefined', () => { - const layerName = `test-kitty` - - const fakeLayer = { - hello: 'world' - } - const fakeGetLayerByName = jasmine.createSpy().and.returnValue(fakeLayer) - const fakeNgInstance = { - layerManager: { - getLayerByName: fakeGetLayerByName - } - } - - TestBed.overrideProvider(VIEWER_INJECTION_TOKEN, { - useValue: fakeNgInstance - }) - - const fixutre = TestBed.createComponent(LayerDetailComponent) - - fixutre.componentInstance.layerName = layerName - - expect(function(){ - fixutre.componentInstance.triggerChange() - }).toThrowError(`layer.fragmentMain is not defined... is this an image layer?`) - }) - - it('should call getShader and restoreState if all goes right', () => { - - const replacementShader = `blabla ahder` - const getShaderSpy = jasmine.createSpy('getShader').and.returnValue(replacementShader) - spyOnProperty(CONSTANT, 'getShader').and.returnValue(getShaderSpy) - - const layerName = `test-kitty` - - const fakeRestoreState = jasmine.createSpy('fakeGetLayerByName') - const fakeLayer = { - layer: { - fragmentMain: { - restoreState: fakeRestoreState - } - } - } - const fakeGetLayerByName = jasmine.createSpy('fakeGetLayerByName').and.returnValue(fakeLayer) - const fakeNgInstance = { - layerManager: { - getLayerByName: fakeGetLayerByName - } - } - TestBed.overrideProvider(VIEWER_INJECTION_TOKEN, { - useValue: fakeNgInstance - }) - - const fixutre = TestBed.createComponent(LayerDetailComponent) - fixutre.componentInstance.layerName = layerName - fixutre.detectChanges() - - fixutre.componentInstance.triggerChange() - - expect(fakeGetLayerByName).toHaveBeenCalledWith(layerName) - expect(getShaderSpy).toHaveBeenCalled() - expect(fakeRestoreState).toHaveBeenCalledWith(replacementShader) - }) - }) - }) -}) \ No newline at end of file diff --git a/src/ui/layerbrowser/layerDetail/layerDetail.component.ts b/src/ui/layerbrowser/layerDetail/layerDetail.component.ts deleted file mode 100644 index 414bab57e2f2ef11e8232a7405e8b206d5707cda..0000000000000000000000000000000000000000 --- a/src/ui/layerbrowser/layerDetail/layerDetail.component.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Component, Input, OnChanges, ChangeDetectionStrategy, Optional, Inject } from "@angular/core"; -import { NgLayersService } from "../ngLayerService.service"; -import { MatSliderChange } from "@angular/material/slider"; -import { MatSlideToggleChange } from "@angular/material/slide-toggle"; -import { getShader } from "src/util/constants"; - -export const VIEWER_INJECTION_TOKEN = `VIEWER_INJECTION_TOKEN` - -@Component({ - selector: 'layer-detail-cmp', - templateUrl: './layerDetail.template.html', - changeDetection: ChangeDetectionStrategy.OnPush -}) - -export class LayerDetailComponent implements OnChanges{ - @Input() - layerName: string - - private colormap = null - - constructor( - private layersService: NgLayersService, - @Optional() @Inject(VIEWER_INJECTION_TOKEN) private injectedViewer - ){ - - } - - ngOnChanges(){ - if (!this.layerName) return - - this.lowThreshold = this.layersService.lowThresholdMap.get(this.layerName) || this.lowThreshold - this.highThreshold = this.layersService.highThresholdMap.get(this.layerName) || this.highThreshold - this.brightness = this.layersService.brightnessMap.get(this.layerName) || this.brightness - this.contrast = this.layersService.contrastMap.get(this.layerName) || this.contrast - this.removeBg = this.layersService.removeBgMap.get(this.layerName) || this.removeBg - this.colormap = this.layersService.colorMapMap.get(this.layerName) || this.colormap - } - - public lowThreshold: number = 0 - public highThreshold: number = 1 - public brightness: number = 0 - public contrast: number = 0 - public removeBg: boolean = false - - handleChange(mode: 'low' | 'high' | 'brightness' | 'contrast', event: MatSliderChange){ - switch(mode) { - case 'low': - this.layersService.lowThresholdMap.set(this.layerName, event.value) - this.lowThreshold = event.value - break; - case 'high': - this.layersService.highThresholdMap.set(this.layerName, event.value) - this.highThreshold = event.value - break; - case 'brightness': - this.layersService.brightnessMap.set(this.layerName, event.value) - this.brightness = event.value - break; - case 'contrast': - this.layersService.contrastMap.set(this.layerName, event.value) - this.contrast = event.value - break; - default: return - } - this.triggerChange() - } - - handleToggleBg(event: MatSlideToggleChange){ - this.layersService.removeBgMap.set(this.layerName, event.checked) - this.removeBg = event.checked - this.triggerChange() - } - - triggerChange(){ - const { lowThreshold, highThreshold, brightness, contrast, removeBg, colormap } = this - const shader = getShader({ - lowThreshold, - highThreshold, - colormap, - brightness, - contrast, - removeBg - }) - this.fragmentMain.restoreState(shader) - } - - private get viewer(){ - return this.injectedViewer || (window as any).viewer - } - - private get fragmentMain(){ - - if (!this.viewer) throw new Error(`viewer is not defined`) - const layer = this.viewer.layerManager.getLayerByName(this.layerName) - if (!layer) throw new Error(`layer with name: ${this.layerName}, not found.`) - if (! (layer.layer?.fragmentMain?.restoreState) ) throw new Error(`layer.fragmentMain is not defined... is this an image layer?`) - - return layer.layer.fragmentMain - } -} diff --git a/src/ui/layerbrowser/layerDetail/layerDetail.template.html b/src/ui/layerbrowser/layerDetail/layerDetail.template.html deleted file mode 100644 index 376b875c3ffd924a5a69166d2555104cae0c0390..0000000000000000000000000000000000000000 --- a/src/ui/layerbrowser/layerDetail/layerDetail.template.html +++ /dev/null @@ -1,79 +0,0 @@ -<div class="d-flex flex-column"> - <div> - <mat-label> - Low threshold - </mat-label> - <mat-slider - aria-label="Set lower threshold" - (input)="handleChange('low', $event)" - [value]="lowThreshold" - min="0" - max="1" - step="0.001"> - </mat-slider> - <mat-label> - {{ lowThreshold }} - </mat-label> - </div> - - <div> - <mat-label> - High threshold - </mat-label> - <mat-slider - aria-label="Set higher threshold" - (input)="handleChange('high', $event)" - [value]="highThreshold" - min="0" - max="1" - step="0.001"> - </mat-slider> - <mat-label> - {{ highThreshold }} - </mat-label> - </div> - <div> - <mat-label> - Brightness - </mat-label> - <mat-slider - aria-label="Set brightness" - (input)="handleChange('brightness', $event)" - [value]="brightness" - min="-1" - max="1" - step="0.01"> - </mat-slider> - <mat-label> - {{ brightness }} - </mat-label> - </div> - <div> - <mat-label> - Contrast - </mat-label> - <mat-slider - aria-label="Set contrast" - (input)="handleChange('contrast', $event)" - [value]="contrast" - min="-1" - max="1" - step="0.01"> - </mat-slider> - <mat-label> - {{ contrast }} - </mat-label> - </div> - - <div> - <mat-label> - Remove background - </mat-label> - <mat-slide-toggle - aria-label="Remove background" - (change)="handleToggleBg($event)" - [(ngModel)]="removeBg"> - - </mat-slide-toggle> - </div> -</div> \ No newline at end of file diff --git a/src/ui/layerbrowser/ngLayerService.service.ts b/src/ui/layerbrowser/ngLayerService.service.ts deleted file mode 100644 index 35bd949ba6885423862692dacdac94e35dc96a7b..0000000000000000000000000000000000000000 --- a/src/ui/layerbrowser/ngLayerService.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Injectable } from "@angular/core"; -import { EnumColorMapName } from "src/util/colorMaps"; - -@Injectable({ - providedIn: 'root' -}) - -export class NgLayersService{ - public lowThresholdMap: Map<string, number> = new Map() - public highThresholdMap: Map<string, number> = new Map() - public brightnessMap: Map<string, number> = new Map() - public contrastMap: Map<string, number> = new Map() - public removeBgMap: Map<string, boolean> = new Map() - public colorMapMap: Map<string, EnumColorMapName> = new Map() -} diff --git a/src/ui/nehubaContainer/pipes/mobileControlNubStyle.pipe.ts b/src/ui/nehubaContainer/pipes/mobileControlNubStyle.pipe.ts deleted file mode 100644 index 1f70f6b658cb27001bab3415c77aa0ec90d5be70..0000000000000000000000000000000000000000 --- a/src/ui/nehubaContainer/pipes/mobileControlNubStyle.pipe.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { PANELS } from 'src/services/state/ngViewerState.store.helper' - - -@Pipe({ - name: 'mobileControlNubStylePipe', -}) - -export class MobileControlNubStylePipe implements PipeTransform { - public transform(panelMode: string): any { - switch (panelMode) { - case PANELS.SINGLE_PANEL: - return { - top: '80%', - left: '95%', - } - case PANELS.V_ONE_THREE: - case PANELS.H_ONE_THREE: - return { - top: '66.66%', - left: '66.66%', - } - case PANELS.FOUR_PANEL: - default: - return { - top: '50%', - left: '50%', - } - } - } -} diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 3e389e9585292b377ea1d1b16f87c43dad703425..1115cb7c6769c3831fe56eb6ecfbf7c514c585a5 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -12,7 +12,6 @@ import { DownloadDirective } from "../util/directives/download.directive"; import { LogoContainer } from "./logoContainer/logoContainer.component"; import { MobileOverlay } from "./nehubaContainer/mobileOverlay/mobileOverlay.component"; -import { MobileControlNubStylePipe } from "./nehubaContainer/pipes/mobileControlNubStyle.pipe"; import { HumanReadableFileSizePipe } from "src/util/pipes/humanReadableFileSize.pipe"; @@ -56,7 +55,6 @@ import { DialogInfoModule } from "./dialogInfo" ActionDialog, /* pipes */ - MobileControlNubStylePipe, HumanReadableFileSizePipe, ReorderPanelIndexPipe, diff --git a/src/util/constants.ts b/src/util/constants.ts index fadf5811251a20f5a4002fc37342e73d6f4ed536..5c4e7185743a1dca35e0523f076385b797fc21b5 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -14,7 +14,6 @@ export const LOCAL_STORAGE_CONST = { export const COOKIE_VERSION = '0.3.0' export const KG_TOS_VERSION = '0.3.0' -export const DS_PREVIEW_URL = environment.DATASET_PREVIEW_URL export const BACKENDURL = (() => { const { BACKEND_URL } = environment if (!BACKEND_URL) return `` @@ -83,7 +82,6 @@ export const getShader = ({ removeBg = false } = {}): string => { const { header, main, premain } = mapKeyColorMap.get(colormap) || (() => { - console.warn(`colormap ${colormap} not found. Using default colormap instead`) return mapKeyColorMap.get(EnumColorMapName.GREYSCALE) })() diff --git a/src/util/generator.ts b/src/util/generator.ts index 9c9d14a9064fb6d113d7c648ae59c69575314c69..fcf39012ca428114ebe93974a5aa1951db8bef15 100644 --- a/src/util/generator.ts +++ b/src/util/generator.ts @@ -1,7 +1,7 @@ export function* timedValues(ms: number = 500, mode: string = 'linear') { const startTime = Date.now() - const getValue = (fraction) => { + const getValue = (fraction: number) => { switch (mode) { case 'linear': default: diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 1e12da494fea67d1ed6a68c526bfaca2269ec458..7516440907c6505b9479c826fde3452768bc0288 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -18,12 +18,10 @@ import { INJ_ANNOT_TARGET } from "src/atlasComponents/userAnnotations/tools/type import { NEHUBA_INSTANCE_INJTKN } from "./nehuba/util"; import { map } from "rxjs/operators"; import { TContextArg } from "./viewer.interface"; -import { ViewerStateBreadCrumbModule } from "./viewerStateBreadCrumb/module"; import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, AtlasViewerAPIServices, setViewerHandleFactory } from "src/atlasViewer/atlasViewer.apiService.service"; import { ILoadMesh, LOAD_MESH_TOKEN } from "src/messaging/types"; import { KeyFrameModule } from "src/keyframesModule/module"; import { ViewerInternalStateSvc } from "./viewerInternalState.service"; -import { LayerBrowserModule } from "src/ui/layerbrowser"; import { SAPIModule } from 'src/atlasComponents/sapi'; import { NehubaVCtxToBbox } from "./pipes/nehubaVCtxToBbox.pipe"; import { SapiViewsModule, SapiViewsUtilModule } from "src/atlasComponents/sapiViews"; @@ -43,9 +41,7 @@ import { SapiViewsModule, SapiViewsUtilModule } from "src/atlasComponents/sapiVi UserAnnotationsModule, QuickTourModule, ContextMenuModule, - ViewerStateBreadCrumbModule, KeyFrameModule, - LayerBrowserModule, SAPIModule, SapiViewsModule, SapiViewsUtilModule diff --git a/src/viewerModule/nehuba/constants.ts b/src/viewerModule/nehuba/constants.ts index bbd36606525015fcd2f8e6315a061ea2a7f56a36..6537a7aef4ea762d6d38c85ac335011551b5767e 100644 --- a/src/viewerModule/nehuba/constants.ts +++ b/src/viewerModule/nehuba/constants.ts @@ -4,17 +4,6 @@ import { Observable } from 'rxjs' export { getNgIds } from 'src/util/fn' export const NEHUBA_VIEWER_FEATURE_KEY = 'ngViewerFeature' -export interface INgLayerInterface { - name: string // displayName - source: string - mixability: string // base | mixable | nonmixable - annotation?: string // - id?: string // unique identifier - visible?: boolean - shader?: string - transform?: any -} - export interface IRegion { [key: string]: any ngId: string diff --git a/src/viewerModule/nehuba/layerCtrl.service/index.ts b/src/viewerModule/nehuba/layerCtrl.service/index.ts index 05cbb34fe0a5803c935749c2c45e6345bb467746..09065a46f416dd4373e98395f4cf985715718cce 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/index.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/index.ts @@ -7,5 +7,5 @@ export { SET_COLORMAP_OBS, SET_LAYER_VISIBILITY, getRgb, - INgLayerInterface, + } from './layerCtrl.util' diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..967472606b37e3b06775396db0ab353d63f90687 --- /dev/null +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts @@ -0,0 +1,66 @@ +import { Injectable } from "@angular/core"; +import { createEffect } from "@ngrx/effects"; +import { select, Store } from "@ngrx/store"; +import { of } from "rxjs"; +import { mapTo, switchMap, withLatestFrom, filter, catchError } from "rxjs/operators"; +import { SAPI } from "src/atlasComponents/sapi"; +import { atlasAppearance, atlasSelection } from "src/state"; +import { EnumColorMapName } from "src/util/colorMaps"; +import { getShader } from "src/util/constants"; +import { NehubaLayerControlService } from "./layerCtrl.service"; + +@Injectable() +export class LayerCtrlEffects { + onRegionSelectClearPmapLayer = createEffect(() => this.store.pipe( + select(atlasSelection.selectors.selectedRegions), + withLatestFrom(this.store.pipe( + select(atlasSelection.selectors.selectedATP) + )), + mapTo( + atlasAppearance.actions.removeCustomLayer({ + id: NehubaLayerControlService.PMAP_LAYER_NAME + }) + ) + )) + + onRegionSelectShowNewPmapLayer = createEffect(() => this.store.pipe( + select(atlasSelection.selectors.selectedRegions), + filter(regions => regions.length > 0), + withLatestFrom( + this.store.pipe( + select(atlasSelection.selectors.selectedATP) + ) + ), + switchMap(([ regions, { atlas, parcellation, template } ]) => { + const sapiRegion = this.sapi.getRegion(atlas["@id"], parcellation["@id"], regions[0].name) + return sapiRegion.getMapInfo(template["@id"]) + .then(val => + atlasAppearance.actions.addCustomLayer({ + customLayer: { + clType: "customlayer/nglayer", + id: NehubaLayerControlService.PMAP_LAYER_NAME, + source: `nifti://${sapiRegion.getMapUrl(template["@id"])}`, + shader: getShader({ + colormap: EnumColorMapName.VIRIDIS, + highThreshold: val.max, + lowThreshold: val.min, + removeBg: true, + }) + } + }) + ) + }), + catchError((err, obs) => of( + atlasAppearance.actions.removeCustomLayer({ + id: NehubaLayerControlService.PMAP_LAYER_NAME + }) + )) + )) + + constructor( + private store: Store<any>, + private sapi: SAPI, + ){ + + } +} \ No newline at end of file diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts index 5ef1deb33b6fad50fa407239e64a83ad3b154793..8f6e2d64648bda49141346dbfbcb62bfeb72eaab 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts @@ -1,11 +1,7 @@ -import { fakeAsync, TestBed, tick } from "@angular/core/testing" +import { fakeAsync, TestBed } from "@angular/core/testing" import { MockStore, provideMockStore } from "@ngrx/store/testing" import { NehubaLayerControlService } from "./layerCtrl.service" import * as layerCtrlUtil from '../constants' -import { hot } from "jasmine-marbles" -import { IColorMap } from "./layerCtrl.util" -import { debounceTime } from "rxjs/operators" -import { ngViewerSelectorClearView, ngViewerSelectorLayers } from "src/services/state/ngViewerState.store.helper" import { atlasSelection } from "src/state" diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index 8cb8fed1ea05c9334b107200ceba86828ae9d4de..5af33b5694d9c63e4d325f2ab5a00fe7a7e45193 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -1,14 +1,12 @@ import { Injectable, OnDestroy } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { BehaviorSubject, combineLatest, from, merge, Observable, Subject, Subscription } from "rxjs"; +import { BehaviorSubject, combineLatest, merge, Observable, Subject, Subscription } from "rxjs"; import { debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, withLatestFrom } from "rxjs/operators"; -import { IColorMap, INgLayerCtrl, INgLayerInterface, TNgLayerCtrl } from "./layerCtrl.util"; +import { IColorMap, INgLayerCtrl, TNgLayerCtrl } from "./layerCtrl.util"; import { IAuxMesh } from '../store' import { IVolumeTypeDetail } from "src/util/siibraApiConstants/types"; -import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerSelectorClearView, ngViewerSelectorLayers } from "src/services/state/ngViewerState.store.helper"; -import { hexToRgb } from 'common/util' import { SAPI, SapiParcellationModel } from "src/atlasComponents/sapi"; -import { SAPISpace } from "src/atlasComponents/sapi/core"; +import { SAPISpace, SAPIRegion } from "src/atlasComponents/sapi/core"; import { getParcNgId, fromRootStore as nehubaConfigSvcFromRootStore } from "../config.service" import { getRegionLabelIndex } from "../config.service/util"; import { annotation, atlasAppearance, atlasSelection } from "src/state"; @@ -65,7 +63,8 @@ export class NehubaLayerControlService implements OnDestroy{ public selectedATPR$ = this.selectedATP$.pipe( switchMap(({ atlas, template, parcellation }) => - from(this.sapiSvc.getParcRegions(atlas["@id"], parcellation["@id"], template["@id"])).pipe( + this.store$.pipe( + select(atlasSelection.selectors.selectedParcAllRegions), map(regions => ({ atlas, template, parcellation, regions })), @@ -85,7 +84,7 @@ export class NehubaLayerControlService implements OnDestroy{ if (!r.hasAnnotation.visualizedIn) continue const ngId = getParcNgId(atlas, template, parcellation, r) - const [ red, green, blue ] = hexToRgb(r.hasAnnotation.displayColor) || Object.keys(BACKUP_COLOR).map(key => BACKUP_COLOR[key]) + const [ red, green, blue ] = SAPIRegion.GetDisplayColor(r) const labelIndex = getRegionLabelIndex(atlas, template, parcellation, r) if (!labelIndex) continue @@ -181,99 +180,21 @@ export class NehubaLayerControlService implements OnDestroy{ payload: layerObj }) }), - this.store$.pipe( - select(atlasSelection.selectors.selectedRegions) - ).subscribe(() => { - /** - * TODO - * below is the original code, but can be refactored. - * essentially, new workflow will be like the following: - * - get SapiRegion - * - getRegionalMap info (min/max) - * - dispatch new layer call - */ - - // if (roi$) { - - // this.sub.push( - // roi$.pipe( - // switchMap(roi => { - // if (!roi || !roi.hasRegionalMap) { - // // clear pmap - // return of(null) - // } - - // const { links } = roi - // const { regional_map: regionalMapUrl, regional_map_info: regionalMapInfoUrl } = links - // return from(fetch(regionalMapInfoUrl).then(res => res.json())).pipe( - // map(regionalMapInfo => { - // return { - // roi, - // regionalMapUrl, - // regionalMapInfo - // } - // }) - // ) - // }) - // ).subscribe(processedRoi => { - // if (!processedRoi) { - // this.store$.dispatch( - // ngViewerActionRemoveNgLayer({ - // layer: { - // name: NehubaLayerControlService.PMAP_LAYER_NAME - // } - // }) - // ) - // return - // } - // const { - // roi, - // regionalMapUrl, - // regionalMapInfo - // } = processedRoi - // const { min, max, colormap = EnumColorMapName.VIRIDIS } = regionalMapInfo || {} as any - - // const shaderObj = { - // ...PMAP_DEFAULT_CONFIG, - // ...{ colormap }, - // ...( typeof min !== 'undefined' ? { lowThreshold: min } : {} ), - // ...( max ? { highThreshold: max } : { highThreshold: 1 } ) - // } - - // const layer = { - // name: NehubaLayerControlService.PMAP_LAYER_NAME, - // source : `nifti://${regionalMapUrl}`, - // mixability : 'nonmixable', - // shader : getShader(shaderObj), - // } - - // this.store$.dispatch( - // ngViewerActionAddNgLayer({ layer }) - // ) - - // // this.layersService.highThresholdMap.set(layerName, highThreshold) - // // this.layersService.lowThresholdMap.set(layerName, lowThreshold) - // // this.layersService.colorMapMap.set(layerName, cmap) - // // this.layersService.removeBgMap.set(layerName, removeBg) - // }) - // ) - // } - - }) ) this.sub.push( - this.ngLayers$.subscribe(({ ngLayers }) => { - this.ngLayersRegister.layers = ngLayers + this.ngLayers$.subscribe(({ customLayers }) => { + this.ngLayersRegister = customLayers }) ) this.sub.push( this.store$.pipe( - select(ngViewerSelectorClearView), + select(atlasAppearance.selectors.customLayers), + map(cl => cl.filter(l => l.clType === "customlayer/colormap").length > 0), distinctUntilChanged() ).subscribe(flag => { - const pmapLayer = this.ngLayersRegister.layers.find(l => l.name === NehubaLayerControlService.PMAP_LAYER_NAME) + const pmapLayer = this.ngLayersRegister.find(l => l.id === NehubaLayerControlService.PMAP_LAYER_NAME) if (!pmapLayer) return const payload = { type: 'update', @@ -369,21 +290,13 @@ export class NehubaLayerControlService implements OnDestroy{ * if layer contains non mixable layer */ this.store$.pipe( - select(ngViewerSelectorLayers), - map(layers => layers.findIndex(l => l.mixability === 'nonmixable') >= 0), + select(atlasAppearance.selectors.customLayers), + map(layers => layers.filter(l => l.clType === "customlayer/nglayer").length > 0), ), - /** - * clearviewqueue, indicating something is controlling colour map - * show all seg - */ - this.store$.pipe( - select(ngViewerSelectorClearView), - distinctUntilChanged() - ) ]).pipe( withLatestFrom(this.selectedATP$), - map(([[ regions, nonmixableLayerExists, clearViewFlag ], { atlas, parcellation, template }]) => { - if (nonmixableLayerExists && !clearViewFlag) { + map(([[ regions, nonmixableLayerExists ], { atlas, parcellation, template }]) => { + if (nonmixableLayerExists) { return null } @@ -395,7 +308,7 @@ export class NehubaLayerControlService implements OnDestroy{ return serializeSegment(ngId, label) }) ) - if (selectedRegionIndexSet.size > 0 && !clearViewFlag) { + if (selectedRegionIndexSet.size > 0) { return [...selectedRegionIndexSet] } else { return [] @@ -407,38 +320,33 @@ export class NehubaLayerControlService implements OnDestroy{ * ngLayers controller */ - private ngLayersRegister: {layers: INgLayerInterface[]} = { - layers: [] - } + private ngLayersRegister: atlasAppearance.NgLayerCustomLayer[] = [] public removeNgLayers(layerNames: string[]) { - this.ngLayersRegister.layers - .filter(layer => layerNames?.findIndex(l => l === layer.name) >= 0) - .map(l => l.name) + this.ngLayersRegister + .filter(layer => layerNames?.findIndex(l => l === layer.id) >= 0) + .map(l => l.id) .forEach(layerName => { - this.store$.dispatch(ngViewerActionRemoveNgLayer({ - layer: { - name: layerName - } - })) + this.store$.dispatch( + atlasAppearance.actions.removeCustomLayer({ + id: layerName + }) + ) }) } - public addNgLayer(layers: INgLayerInterface[]){ - this.store$.dispatch(ngViewerActionAddNgLayer({ - layer: layers - })) - } + private ngLayers$ = this.store$.pipe( - select(ngViewerSelectorLayers), - map((ngLayers: INgLayerInterface[]) => { - const newLayers = ngLayers.filter(l => { - const registeredLayerNames = this.ngLayersRegister.layers.map(l => l.name) - return !registeredLayerNames.includes(l.name) + select(atlasAppearance.selectors.customLayers), + map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]), + map(customLayers => { + const newLayers = customLayers.filter(l => { + const registeredLayerNames = this.ngLayersRegister.map(l => l.id) + return !registeredLayerNames.includes(l.id) }) - const removeLayers = this.ngLayersRegister.layers.filter(l => { - const stateLayerNames = ngLayers.map(l => l.name) - return !stateLayerNames.includes(l.name) + const removeLayers = this.ngLayersRegister.filter(l => { + const stateLayerNames = customLayers.map(l => l.id) + return !stateLayerNames.includes(l.id) }) - return { newLayers, removeLayers, ngLayers } + return { newLayers, removeLayers, customLayers } }), shareReplay(1) ) @@ -450,11 +358,9 @@ export class NehubaLayerControlService implements OnDestroy{ map(newLayers => { const newLayersObj: any = {} - newLayers.forEach(({ name, source, ...rest }) => newLayersObj[name] = { + newLayers.forEach(({ id, source, ...rest }) => newLayersObj[id] = { ...rest, source, - // source: getProxyUrl(source), - // ...getProxyOther({source}) }) return { @@ -467,7 +373,7 @@ export class NehubaLayerControlService implements OnDestroy{ map(({ removeLayers }) => removeLayers), filter(layers => layers.length > 0), map(removeLayers => { - const removeLayerNames = removeLayers.map(v => v.name) + const removeLayerNames = removeLayers.map(v => v.id) return { type: 'remove', payload: { names: removeLayerNames } diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts index 0b76bb2700889156b617dc49d3c091a474dce4df..d5a743d67ee5dfe7ab13521b7ae0383d2e9a7524 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts @@ -1,6 +1,7 @@ import { InjectionToken } from '@angular/core' import { strToRgb } from 'common/util' import { Observable } from 'rxjs' +import { atlasAppearance } from 'src/state' export interface IColorMap { [key: string]: { @@ -42,10 +43,10 @@ export interface INgLayerCtrl { names: string[] } add: { - [key: string]: INgLayerInterface + [key: string]: atlasAppearance.NgLayerCustomLayer } update: { - [key: string]: Partial<INgLayerInterface> + [key: string]: Partial<atlasAppearance.NgLayerCustomLayer> } setLayerTransparency: { [key: string]: number @@ -61,14 +62,3 @@ export const SET_COLORMAP_OBS = new InjectionToken<Observable<IColorMap>>('SET_C export const SET_LAYER_VISIBILITY = new InjectionToken<Observable<string[]>>('SET_LAYER_VISIBILITY') export const SET_SEGMENT_VISIBILITY = new InjectionToken<Observable<string[]>>('SET_SEGMENT_VISIBILITY') export const NG_LAYER_CONTROL = new InjectionToken<TNgLayerCtrl<keyof INgLayerCtrl>>('NG_LAYER_CONTROL') - -export interface INgLayerInterface { - name: string // displayName - source: string - mixability: string // base | mixable | nonmixable - annotation?: string // - id?: string // unique identifier - visible?: boolean - shader?: string - transform?: any -} diff --git a/src/viewerModule/nehuba/maximisePanelButton/maximisePanelButton.component.ts b/src/viewerModule/nehuba/maximisePanelButton/maximisePanelButton.component.ts index 16cecc54bea2b3191ecd6b3b4b227fdcd7144778..2838f216817ce253062339deb59b635826ab607b 100644 --- a/src/viewerModule/nehuba/maximisePanelButton/maximisePanelButton.component.ts +++ b/src/viewerModule/nehuba/maximisePanelButton/maximisePanelButton.component.ts @@ -1,8 +1,6 @@ import { Component, Input } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { Observable } from "rxjs"; import { distinctUntilChanged, map } from "rxjs/operators"; -import { PANELS } from 'src/services/state/ngViewerState.store.helper' import { ARIA_LABELS } from 'common/constants' import { userInterface } from "src/state" diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.ts index 552af17c944f40a27e8b6ec1427e7b29ad6f88ac..db85e917b8a64feeb28ec279d6c0d61f10243877 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.ts @@ -35,8 +35,7 @@ export class NehubaMeshService implements OnDestroy { } private allRegions$ = this.store$.pipe( - select(atlasSelection.selectors.selectedATP), - switchMap(({ atlas, template, parcellation }) => this.sapiSvc.getParcRegions(atlas["@id"], parcellation["@id"], template["@id"])) + select(atlasSelection.selectors.selectedParcAllRegions), ) private selectedRegions$ = this.store$.pipe( diff --git a/src/viewerModule/nehuba/navigation.service/navigation.effects.ts b/src/viewerModule/nehuba/navigation.service/navigation.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..65db6b37735fdadb2d10f68cfd3aed28702c7969 --- /dev/null +++ b/src/viewerModule/nehuba/navigation.service/navigation.effects.ts @@ -0,0 +1,102 @@ +import { Inject, Injectable, OnDestroy } from "@angular/core"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { select, Store } from "@ngrx/store"; +import { Observable, Subscription } from "rxjs"; +import { filter, tap, withLatestFrom } from "rxjs/operators"; +import { atlasSelection, MainState, userPreference } from "src/state" +import { timedValues } from "src/util/generator"; +import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; +import { NEHUBA_INSTANCE_INJTKN } from "../util"; +import { navAdd, navMul } from "./navigation.util"; + +@Injectable() +export class NehubaNavigationEffects implements OnDestroy{ + + private subscription: Subscription[] = [] + private nehubaInst: NehubaViewerUnit + private rafRef: number + + /** + * This is an implementation which reconciles local state with the global navigation state. + * - it should **never** set global state. + * - there exist two (potential) source of navigation state change + * - directly set via global state (with statuscard, url, etc) + * - via viewer (mostly through user interaction) + * if the latter were emitted, it is the local's responsibility to check the (debounced) diff between local/global state, + * and update global state accordingly. + * - This effect updates the internal navigation state. It should leave reporting any diff to the local viewer's native implementation. + */ + onNavigateTo = createEffect(() => this.action.pipe( + ofType(atlasSelection.actions.navigateTo), + filter(() => !!this.nehubaInst), + withLatestFrom( + this.store.pipe( + select(userPreference.selectors.useAnimation) + ), + this.store.pipe( + select(atlasSelection.selectors.navigation) + ) + ), + tap(([{ navigation, animation, physical }, globalAnimationFlag, currentNavigation]) => { + if (!animation || !globalAnimationFlag) { + this.nehubaInst.setNavigationState({ + ...navigation, + positionReal: physical + }) + return + } + + const gen = timedValues() + const src = currentNavigation + + const dest = { + ...src, + ...navigation + } + + const delta = navAdd(dest, navMul(src, -1)) + + const animate = () => { + + /** + * if nehubaInst becomes nullish whilst animation is running + */ + if (!this.nehubaInst) { + this.rafRef = null + return + } + + const next = gen.next() + const d = next.value + + const n = navAdd(src, navMul(delta, d)) + this.nehubaInst.setNavigationState({ + ...n, + positionReal: true + }) + + if ( !next.done ) { + this.rafRef = requestAnimationFrame(() => animate()) + } else { + this.rafRef = null + } + } + this.rafRef = requestAnimationFrame(() => animate()) + + }) + ), { dispatch: false }) + + constructor( + private action: Actions, + private store: Store<MainState>, + @Inject(NEHUBA_INSTANCE_INJTKN) nehubaInst$: Observable<NehubaViewerUnit>, + ){ + this.subscription.push( + nehubaInst$.subscribe(val => this.nehubaInst = val), + ) + } + + ngOnDestroy(){ + while(this.subscription.length > 0) this.subscription.pop().unsubscribe() + } +} \ No newline at end of file diff --git a/src/viewerModule/nehuba/navigation.service/navigation.service.ts b/src/viewerModule/nehuba/navigation.service/navigation.service.ts index 17dd917898fe30761c9ab3f6e8c2ac8bb43b7d92..59ef98c02b7a82fa3850040170ae23608d1ea3e6 100644 --- a/src/viewerModule/nehuba/navigation.service/navigation.service.ts +++ b/src/viewerModule/nehuba/navigation.service/navigation.service.ts @@ -101,7 +101,7 @@ export class NehubaNavigationService implements OnDestroy{ if (!navEql) { this.store$.dispatch( - actions.navigateTo({ + actions.setNavigation({ navigation: roundedNav }) ) diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 9627bd89b60ebeb6efb6ddcc576bcc665f474c41..625be54ba3d759ae203c3928a465e79c0f841909 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -85,6 +85,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { private subscriptions: Subscription[] = [] + private _nehubaReady = false @Output() public nehubaReady: EventEmitter<null> = new EventEmitter() @Output() public layersChanged: EventEmitter<null> = new EventEmitter() private layersChangedHandler: any @@ -181,8 +182,19 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { this.patchNG() this.loadNehuba() - this.layersChangedHandler = this.nehubaViewer.ngviewer.layerManager.layersChanged.add(() => this.layersChanged.emit(null)) - this.nehubaViewer.ngviewer.registerDisposer(this.layersChangedHandler) + const viewer = this.nehubaViewer.ngviewer + this.layersChangedHandler = viewer.layerManager.layersChanged.add(() => { + this.layersChanged.emit(null) + const readiedLayerNames: string[] = viewer.layerManager.managedLayers.filter(l => l.layer).map(l => l.name) + for (const layerName in this.ngIdSegmentsMap) { + if (!readiedLayerNames.includes(layerName)) { + return + } + this._nehubaReady = true + this.nehubaReady.emit(null) + } + }) + viewer.registerDisposer(this.layersChangedHandler) }) .then(() => { // all mutation to this.nehubaViewer should await createNehubaPromise @@ -255,7 +267,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { this.ondestroySubscriptions.push( this.setColormap$.pipe( switchMap(switchMapWaitFor({ - condition: () => !!(this.nehubaViewer?.ngviewer) + condition: () => this._nehubaReady })), debounceTime(160), ).subscribe(v => { @@ -277,7 +289,9 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { if (this.layerVis$) { this.ondestroySubscriptions.push( this.layerVis$.pipe( - switchMap(switchMapWaitFor({ condition: () => !!(this.nehubaViewer?.ngviewer) })), + switchMap(switchMapWaitFor({ + condition: () => this._nehubaReady + })), distinctUntilChanged(arrayOrderedEql), debounceTime(160), ).subscribe((layerNames: string[]) => { @@ -307,12 +321,11 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { this.segVis$.pipe( switchMap( switchMapWaitFor({ - condition: () => this.nehubaViewer?.ngviewer, + condition: () => this._nehubaReady, leading: true, }) ) ).subscribe(val => { - console.log(val) // null === hide all seg if (val === null) { this.hideAllSeg() @@ -335,7 +348,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { this.ondestroySubscriptions.push( this.layerCtrl$.pipe( bufferUntil(({ - condition: () => !!this.nehubaViewer?.ngviewer + condition: () => this._nehubaReady })) ).subscribe(messages => { for (const message of messages) { @@ -430,6 +443,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { public loadNehuba() { this.nehubaViewer = this.exportNehuba.createNehubaViewer(this.config, (err) => { /* print in debug mode */ + debugger this.log.error(err) }) @@ -443,7 +457,6 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { /* creation of the layout is done on next frame, hence the settimeout */ setTimeout(() => { getViewer().display.panels.forEach(patchSliceViewPanel) - this.nehubaReady.emit(null) }) this.newViewerInit() @@ -691,7 +704,6 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { public showAllSeg() { if (!this.nehubaViewer) { return } for (const ngId in this.ngIdSegmentsMap) { - console.log(ngId) for (const idx of this.ngIdSegmentsMap[ngId]) { this.nehubaViewer.showSegment(idx, { name: ngId, @@ -964,7 +976,6 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { this._s$.forEach(_s$ => { if (_s$) { _s$.unsubscribe() } }) - } private setColorMap(map: Map<string, Map<number, {red: number, green: number, blue: number}>>) { diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 66d4e77a72026a7e9eb033aa37662087f07733e6..4b941c8a3593f20bf03ba4c8dd74ba5537332dfa 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -1,12 +1,9 @@ import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnDestroy, Optional, Output, TemplateRef, ViewChild } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { asyncScheduler, BehaviorSubject, combineLatest, fromEvent, merge, Observable, of, Subject, Subscription } from "rxjs"; -import { ngViewerActionCycleViews, ngViewerActionToggleMax } from "src/services/state/ngViewerState/actions"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { debounceTime, distinctUntilChanged, filter, map, mapTo, scan, shareReplay, startWith, switchMap, switchMapTo, take, tap, throttleTime } from "rxjs/operators"; -import { viewerStateAddUserLandmarks, viewerStateMouseOverCustomLandmark } from "src/services/state/viewerState/actions"; import { ARIA_LABELS, IDS, QUICKTOUR_DESC } from 'common/constants' -import { PANELS } from "src/services/state/ngViewerState/constants"; import { LoggingService } from "src/logging"; import { EnumViewerEvt, IViewer, TViewerEvent } from "../../viewer.interface"; import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; @@ -27,10 +24,8 @@ import { MatDialog } from "@angular/material/dialog"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; import { NehubaConfig, getNehubaConfig, fromRootStore, NgLayerSpec, NgPrecompMeshSpec, NgSegLayerSpec, getParcNgId, getRegionLabelIndex } from "../config.service"; -import { generalActionError } from "src/services/stateStore.helper"; import { SET_MESHES_TO_LOAD } from "../constants"; -import { actions } from "src/state/atlasSelection"; -import { annotation, atlasSelection, userInteraction, userInterface } from "src/state"; +import { annotation, atlasAppearance, atlasSelection, userInteraction, userInterface, generalActions } from "src/state"; export const INVALID_FILE_INPUT = `Exactly one (1) nifti file is required!` @@ -79,7 +74,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni public ARIA_LABELS = ARIA_LABELS public IDS = IDS - private currentPanelMode: PANELS + private currentPanelMode: userInterface.PanelMode @ViewChild(NehubaViewerContainerDirective, { static: true }) public nehubaContainerDirective: NehubaViewerContainerDirective @@ -266,13 +261,21 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni /* on selecting of new template, remove additional nglayers */ if (this.nehubaConfig) { - const baseLayerNames = Object.keys(this.nehubaConfig.dataset.initialNgState.layers) - this.layerCtrlService.removeNgLayers(baseLayerNames) + + const initialSpec = this.nehubaConfig.dataset.initialNgState + const { layers } = initialSpec + + for (const layerName in layers) { + this.store$.dispatch( + atlasAppearance.actions.removeCustomLayer({ + id: layerName + }) + ) + } } } private async loadTmpl(atlas: SapiAtlasModel, _template: SapiSpaceModel, parcellation: SapiParcellationModel, ngLayers: Record<string, NgLayerSpec | NgPrecompMeshSpec | NgSegLayerSpec>) { - if (!_template) return /** * recalcuate zoom @@ -285,9 +288,11 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni /** * selected parc does not have space as a valid output */ - this.store$.dispatch(generalActionError({ - message: `space ${_template.fullName} is not defined in parcellation ${parcellation.brainAtlasVersions[0].fullName}` - })) + this.store$.dispatch( + generalActions.generalActionError({ + message: `space ${_template.fullName} is not defined in parcellation ${parcellation.brainAtlasVersions[0].fullName}` + }) + ) template = await this.sapiSvc.getSpaceDetail(this.selectedAtlas["@id"], parcellation.brainAtlasVersions[0].coordinateSpace["@id"] as string) } const config = getNehubaConfig(template) @@ -303,7 +308,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni this.nehubaConfig = config - this.nehubaContainerDirective.createNehubaInstance(config) + await this.nehubaContainerDirective.createNehubaInstance(config) this.viewerUnit = this.nehubaContainerDirective.nehubaViewerInstance this.sliceRenderEvent$.pipe( takeOnePipe() @@ -321,26 +326,25 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni await this.loadParc(atlas, parcellation, template, ngLayers) const initialSpec = config.dataset.initialNgState - const {layers} = initialSpec - - const dispatchLayers = Object.keys(layers).map((key, idx) => { - const layer = { - name : key, - source : layers[key].source, - mixability : idx === 0 - ? 'base' - : 'mixable', - visible : typeof layers[key].visible === 'undefined' - ? true - : layers[key].visible, - transform : typeof layers[key].transform === 'undefined' - ? null - : layers[key].transform, + const { layers } = initialSpec + + for (const layerName in layers) { + const l = layers[layerName] + const customLayer: atlasAppearance.NgLayerCustomLayer = { + id: layerName, + source: l.source, + visible: l.visible, + transform: l.transform, + clType: 'baselayer/nglayer' } - return layer - }) - this.layerCtrlService.addNgLayer(dispatchLayers) + this.store$.dispatch( + atlasAppearance.actions.addCustomLayer({ + customLayer + }) + ) + } + this.newViewer$.next(true) } @@ -400,7 +404,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni switchMap(this.waitForNehuba.bind(this)) ).subscribe(([mode, panelOrder]) => { - this.currentPanelMode = mode + this.currentPanelMode = mode as userInterface.PanelMode const viewPanels = panelOrder.split('').map(v => Number(v)).map(idx => this.viewPanels[idx]) as [HTMLElement, HTMLElement, HTMLElement, HTMLElement] @@ -412,26 +416,26 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni return } - switch (mode) { - case PANELS.H_ONE_THREE: { + switch (this.currentPanelMode) { + case "H_ONE_THREE": { const element = this.removeExistingPanels() const newEl = getHorizontalOneThree(viewPanels) element.appendChild(newEl) break; } - case PANELS.V_ONE_THREE: { + case "V_ONE_THREE": { const element = this.removeExistingPanels() const newEl = getVerticalOneThree(viewPanels) element.appendChild(newEl) break; } - case PANELS.FOUR_PANEL: { + case "FOUR_PANEL": { const element = this.removeExistingPanels() const newEl = getFourPanel(viewPanels) element.appendChild(newEl) break; } - case PANELS.SINGLE_PANEL: { + case "SINGLE_PANEL": { const element = this.removeExistingPanels() const newEl = getSinglePanel(viewPanels) element.appendChild(newEl) @@ -586,7 +590,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni }), moveToNavigationLoc : (coord, _realSpace?) => { this.store$.dispatch( - actions.navigateTo({ + atlasSelection.actions.navigateTo({ navigation: { position: coord }, @@ -618,10 +622,10 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni if (!landmarks.every(l => l.position.constructor === Array) || !landmarks.every(l => l.position.every(v => !isNaN(v))) || !landmarks.every(l => l.position.length == 3)) { throw new Error('position needs to be a length 3 tuple of numbers ') } - - this.store$.dispatch(viewerStateAddUserLandmarks({ - landmarks - })) + /** + * add implementation to user landmarks + */ + console.warn(`adding landmark not yet implemented`) }, remove3DLandmarks : landmarkIds => { this.store$.dispatch( @@ -719,9 +723,9 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni } handleCycleViewEvent(){ - if (this.currentPanelMode !== PANELS.SINGLE_PANEL) return + if (this.currentPanelMode !== "SINGLE_PANEL") return this.store$.dispatch( - ngViewerActionCycleViews() + userInterface.actions.cyclePanelMode() ) } @@ -740,7 +744,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni const trueOnhoverSegments = this.onhoverSegments && this.onhoverSegments.filter(v => typeof v === 'object') if (!trueOnhoverSegments || (trueOnhoverSegments.length === 0)) return true this.store$.dispatch( - actions.selectRegions({ + atlasSelection.actions.selectRegions({ regions: trueOnhoverSegments.slice(0, 1) }) ) @@ -752,9 +756,11 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni }) public toggleMaximiseMinimise(index: number) { - this.store$.dispatch(ngViewerActionToggleMax({ - payload: { index } - })) + this.store$.dispatch( + userInterface.actions.toggleMaximiseView({ + targetIndex: index + }) + ) } public zoomNgView(panelIndex: number, factor: number) { @@ -789,7 +795,12 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni private dismissAllAddedLayers(){ while (this.droppedLayerNames.length) { const { resourceUrl, layerName } = this.droppedLayerNames.pop() - this.layerCtrlService.removeNgLayers([ layerName ]) + this.store$.dispatch( + atlasAppearance.actions.removeCustomLayer({ + id: layerName + }) + ) + URL.revokeObjectURL(resourceUrl) } } @@ -835,17 +846,21 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni layerName: randomUuid, resourceUrl: url }) - this.layerCtrlService.addNgLayer([{ - name: randomUuid, - mixability: 'mixable', - source: `nifti://${url}`, - shader: getShader({ - colormap: EnumColorMapName.MAGMA, - lowThreshold: meta.min || 0, - highThreshold: meta.max || 1 - }) - }]) + this.store$.dispatch( + atlasAppearance.actions.addCustomLayer({ + customLayer: { + id: randomUuid, + source: `nifti://${url}`, + shader: getShader({ + colormap: EnumColorMapName.MAGMA, + lowThreshold: meta.min || 0, + highThreshold: meta.max || 1 + }), + clType: 'customlayer/nglayer' + } + }) + ) this.dialog.open( this.layerCtrlTmpl, { @@ -900,19 +915,13 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni } public handleMouseEnterCustomLandmark(lm) { - this.store$.dispatch( - viewerStateMouseOverCustomLandmark({ - payload: { userLandmark: lm } - }) - ) + console.log('handle enter custom landmark') + } public handleMouseLeaveCustomLandmark(_lm) { - this.store$.dispatch( - viewerStateMouseOverCustomLandmark({ - payload: { userLandmark: null } - }) - ) + console.log("handle leave custom landmark") + } public quickTourOverwritingPos = { diff --git a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.spec.ts b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.spec.ts index baab9f9d27b4d65801c5944541e54c899ecf69b0..49dbb2e31e9a663981ed69f059c2a2085d5d6ebb 100644 --- a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.spec.ts @@ -5,8 +5,6 @@ import { MockStore, provideMockStore } from "@ngrx/store/testing" import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component" import { NehubaViewerContainerDirective } from "./nehubaViewerInterface.directive" import { Subject } from "rxjs" -import { ngViewerActionNehubaReady } from "src/services/state/ngViewerState/actions" -import { viewerStateMouseOverCustomLandmarkInPerspectiveView } from "src/services/state/viewerState/actions" import { userPreference, atlasSelection, atlasAppearance } from "src/state" describe('> nehubaViewerInterface.directive.ts', () => { @@ -130,16 +128,7 @@ describe('> nehubaViewerInterface.directive.ts', () => { describe('> on clear called', () => { it('> dispatches nehubaReady: false action', () => { - const mockStore = TestBed.inject(MockStore) - const mockStoreDispatchSpy = spyOn(mockStore, 'dispatch') - directiveInstance.clear() - expect( - mockStoreDispatchSpy - ).toHaveBeenCalledWith( - ngViewerActionNehubaReady({ - nehubaReady: false - }) - ) + }) it('> iavNehubaViewerContainerViewerLoading emits false', () => { @@ -206,82 +195,25 @@ describe('> nehubaViewerInterface.directive.ts', () => { }) it('> single null emits null', fakeAsync(() => { - directiveInstance.createNehubaInstance(template, lifecycle) - spyNehubaViewerInstance.mouseoverUserlandmarkEmitter.next(null) - - tick(200) - expect(dispatchSpy).toHaveBeenCalledWith( - viewerStateMouseOverCustomLandmarkInPerspectiveView({ - payload: { label: null } - }) - ) })) it('> single value emits value', fakeAsync(() => { - directiveInstance.createNehubaInstance(template, lifecycle) - spyNehubaViewerInstance.mouseoverUserlandmarkEmitter.next("24") - - tick(200) - expect(dispatchSpy).toHaveBeenCalledWith( - viewerStateMouseOverCustomLandmarkInPerspectiveView({ - payload: { label: "24" } - }) - ) })) describe('> double value in 140ms emits last value', () => { it('> null - 24 emits 24', fakeAsync(() => { - directiveInstance.createNehubaInstance(template, lifecycle) - spyNehubaViewerInstance.mouseoverUserlandmarkEmitter.next(null) - spyNehubaViewerInstance.mouseoverUserlandmarkEmitter.next("24") - - tick(200) - expect(dispatchSpy).toHaveBeenCalledWith( - viewerStateMouseOverCustomLandmarkInPerspectiveView({ - payload: { label: "24" } - }) - ) })) it('> 24 - null emits null', fakeAsync(() => { - directiveInstance.createNehubaInstance(template, lifecycle) - spyNehubaViewerInstance.mouseoverUserlandmarkEmitter.next("24") - spyNehubaViewerInstance.mouseoverUserlandmarkEmitter.next(null) - - tick(200) - expect(dispatchSpy).toHaveBeenCalledWith( - viewerStateMouseOverCustomLandmarkInPerspectiveView({ - payload: { label: null } - }) - ) + })) }) it('> single value outside 140 ms emits separately', fakeAsync(() => { - directiveInstance.createNehubaInstance(template, lifecycle) - spyNehubaViewerInstance.mouseoverUserlandmarkEmitter.next(null) - tick(200) - spyNehubaViewerInstance.mouseoverUserlandmarkEmitter.next("24") - - tick(200) - expect( - dispatchSpy.calls.allArgs() - ).toEqual([ - [ - viewerStateMouseOverCustomLandmarkInPerspectiveView({ - payload: { label: null } - }) - ], - [ - viewerStateMouseOverCustomLandmarkInPerspectiveView({ - payload: { label: "24" } - }) - ] - ]) })) }) }) diff --git a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts index e476b805551731e9f691a7b28937b4de1203c53a..04c087b6ea103f661035837108e876a9aae51334 100644 --- a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts +++ b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts @@ -4,9 +4,7 @@ import { Store, select } from "@ngrx/store"; import { Subscription, Observable, fromEvent, asyncScheduler, combineLatest } from "rxjs"; import { distinctUntilChanged, filter, debounceTime, scan, map, throttleTime, switchMapTo } from "rxjs/operators"; import { serializeSegment, takeOnePipe } from "../util"; -import { ngViewerActionNehubaReady } from "src/services/state/ngViewerState/actions"; import { LoggingService } from "src/logging"; -import { uiActionMouseoverLandmark } from "src/services/state/uiState/actions"; import { arrayOfPrimitiveEqual } from 'src/util/fn' import { INavObj, NehubaNavigationService } from "../navigation.service"; import { NehubaConfig, defaultNehubaConfig } from "../config.service"; @@ -204,7 +202,7 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ function onInit() { this.overrideShowLayers = forceShowLayerNames } - this.createNehubaInstance(copiedNehubaConfig, { onInit }) + await this.createNehubaInstance(copiedNehubaConfig, { onInit }) }), this.gpuLimit$.pipe( @@ -235,8 +233,11 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ this.nehubaViewerInstance.toggleOctantRemoval(flag) } - createNehubaInstance(nehubaConfig: NehubaConfig, lifeCycle: INehubaLifecycleHook = {}){ + async createNehubaInstance(nehubaConfig: NehubaConfig, lifeCycle: INehubaLifecycleHook = {}){ this.clear() + + await new Promise((rs, rj) => setTimeout(rs, 0)) + this.iavNehubaViewerContainerViewerLoading.emit(true) this.cr = this.el.createComponent(this.nehubaViewerFactory) @@ -273,11 +274,6 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ /** * TODO when user selects new template, window.viewer */ - this.store$.dispatch( - ngViewerActionNehubaReady({ - nehubaReady: true, - }) - ) }), this.nehubaViewerInstance.mouseoverSegmentEmitter.pipe( @@ -288,11 +284,7 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ this.nehubaViewerInstance.mouseoverLandmarkEmitter.pipe( distinctUntilChanged() ).subscribe(label => { - this.store$.dispatch( - uiActionMouseoverLandmark({ - landmark: label - }) - ) + console.warn(`mouseover landmark`, label) }), this.nehubaViewerInstance.mouseoverUserlandmarkEmitter.pipe( @@ -327,12 +319,6 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ this.nehubaViewerSubscriptions.pop().unsubscribe() } - this.store$.dispatch( - ngViewerActionNehubaReady({ - nehubaReady: false, - }) - ) - this.iavNehubaViewerContainerViewerLoading.emit(false) if(this.cr) this.cr.destroy() this.el.clear() diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts index 2b2db1dc27a6a2e7add85ee09f106ffdc85ff2cb..bc85f84690053f7b765fe7f317552086d39e448f 100644 --- a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts +++ b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts @@ -1,8 +1,8 @@ -import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges, OnDestroy, SimpleChanges } from "@angular/core"; +import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges, OnDestroy } from "@angular/core"; import { Store } from "@ngrx/store"; import { isMat4 } from "common/util" import { Observable } from "rxjs"; -import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer } from "src/services/state/ngViewerState.store.helper"; +import { atlasAppearance } from "src/state"; import { NehubaViewerUnit } from ".."; import { NEHUBA_INSTANCE_INJTKN } from "../util"; @@ -120,20 +120,21 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{ this.removeLayer = null } this.store.dispatch( - ngViewerActionAddNgLayer({ - layer: [{ - name: name, + atlasAppearance.actions.addCustomLayer({ + customLayer: { + id: name, shader: this.shader, transform: this.transform, + clType: 'customlayer/nglayer', source: `precomputed://${this.source}`, - opacity: this.opacity - }] + opacity: this.opacity, + } }) ) this.removeLayer = () => { this.store.dispatch( - ngViewerActionRemoveNgLayer({ - layer: [{ name }] + atlasAppearance.actions.removeCustomLayer({ + id: name }) ) } diff --git a/src/viewerModule/nehuba/store/actions.ts b/src/viewerModule/nehuba/store/actions.ts index 03e4821f5b8b26ec5e29464dfc07f15c0f7377b3..c8392e03b7fa423286c416c9c67fc83ad2cb4dfd 100644 --- a/src/viewerModule/nehuba/store/actions.ts +++ b/src/viewerModule/nehuba/store/actions.ts @@ -3,13 +3,6 @@ import { createAction, props } from "@ngrx/store"; import { NEHUBA_VIEWER_FEATURE_KEY } from "../constants"; import { IAuxMesh } from "./type"; -export const actionAddNgLayer = createAction( - `[${NEHUBA_VIEWER_FEATURE_KEY}] [addNgLayer]`, - props<{ - layers: any[] - }>() -) - export const actionSetAuxMesh = createAction( `[${NEHUBA_VIEWER_FEATURE_KEY}] [setAuxMesh]`, props<{ @@ -30,7 +23,3 @@ export const actionSetAuxMeshes = createAction( payload: IAuxMesh[] }>() ) - -export const actionClearAuxMeshes = createAction( - `[${NEHUBA_VIEWER_FEATURE_KEY}] [clearAuxMeshes]` -) diff --git a/src/viewerModule/nehuba/store/index.ts b/src/viewerModule/nehuba/store/index.ts index bfaa5f424408a852f371a4abddc152bae202d81d..70ad438c394fc79d2a59ba9b21497adedc825548 100644 --- a/src/viewerModule/nehuba/store/index.ts +++ b/src/viewerModule/nehuba/store/index.ts @@ -1,8 +1,6 @@ export { - actionAddNgLayer, actionRemoveAuxMesh, actionSetAuxMesh, - actionClearAuxMeshes, actionSetAuxMeshes, } from './actions' export { @@ -14,7 +12,6 @@ export { export { IAuxMesh, INehubaFeature, - INgLayerInterface } from './type' export { fromRootStore } from './util' \ No newline at end of file diff --git a/src/viewerModule/nehuba/store/type.ts b/src/viewerModule/nehuba/store/type.ts index 4b1d4d3d69ab657aa67edbd18512397cda07d495..d45347a166b1630d9ecc47cebbfaa48a3a95985b 100644 --- a/src/viewerModule/nehuba/store/type.ts +++ b/src/viewerModule/nehuba/store/type.ts @@ -1,3 +1,5 @@ +import { atlasAppearance } from "src/state" + export interface IAuxMesh { ['@id']: string name: string @@ -8,19 +10,9 @@ export interface IAuxMesh { visible: boolean } -export interface INgLayerInterface { - name: string // displayName - source: string - mixability: string // base | mixable | nonmixable - annotation?: string // - id?: string // unique identifier - visible?: boolean - shader?: string - transform?: any -} export interface INehubaFeature { - layers: INgLayerInterface[] + layers: atlasAppearance.NgLayerCustomLayer[] panelMode: string panelOrder: string octantRemoval: boolean diff --git a/src/viewerModule/nehuba/util.ts b/src/viewerModule/nehuba/util.ts index fcec9aa50df3f4a6147b1fb124a343bd52226b92..049f647b533d02527a978bf2d48eeff02806c56a 100644 --- a/src/viewerModule/nehuba/util.ts +++ b/src/viewerModule/nehuba/util.ts @@ -1,11 +1,11 @@ import { InjectionToken } from '@angular/core' import { Observable, pipe } from 'rxjs' import { filter, scan, take } from 'rxjs/operators' -import { PANELS } from 'src/services/state/ngViewerState.store.helper' import { getExportNehuba, getViewer } from 'src/util/fn' import { NehubaViewerUnit } from './nehubaViewer/nehubaViewer.component' import { NgConfigViewerState } from "./config.service" import { RecursivePartial } from './config.service/type' +import { userInterface } from 'src/state' const flexContCmnCls = ['w-100', 'h-100', 'd-flex', 'justify-content-center', 'align-items-stretch'] @@ -39,30 +39,37 @@ const left = true const right = true const bottom = true -const mapModeIdxClass = new Map() +const mapModeIdxClass = new Map< + userInterface.PanelMode, Map<number,{ + top?: boolean + bottom?: boolean + left?: boolean + right?: boolean + }> +>() -mapModeIdxClass.set(PANELS.FOUR_PANEL, new Map([ +mapModeIdxClass.set("FOUR_PANEL", new Map([ [0, { top, left }], [1, { top, right }], [2, { bottom, left }], [3, { right, bottom }], ])) -mapModeIdxClass.set(PANELS.SINGLE_PANEL, new Map([ +mapModeIdxClass.set("SINGLE_PANEL", new Map([ [0, { top, left, right, bottom }], [1, {}], [2, {}], [3, {}], ])) -mapModeIdxClass.set(PANELS.H_ONE_THREE, new Map([ +mapModeIdxClass.set("H_ONE_THREE", new Map([ [0, { top, left, bottom }], [1, { top, right }], [2, { right }], [3, { bottom, right }], ])) -mapModeIdxClass.set(PANELS.V_ONE_THREE, new Map([ +mapModeIdxClass.set("V_ONE_THREE", new Map([ [0, { top, left, right }], [1, { bottom, left }], [2, { bottom }], @@ -89,7 +96,7 @@ export const panelTouchSide = (panel: HTMLElement, { top: touchTop, left: touchL return panel } -export const addTouchSideClasses = (panel: HTMLElement, actualOrderIndex: number, panelMode: string) => { +export const addTouchSideClasses = (panel: HTMLElement, actualOrderIndex: number, panelMode: userInterface.PanelMode) => { if (actualOrderIndex < 0) { return panel } @@ -105,7 +112,7 @@ export const addTouchSideClasses = (panel: HTMLElement, actualOrderIndex: number export const getHorizontalOneThree = (panels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement]) => { washPanels(panels) - panels.forEach((panel, idx) => addTouchSideClasses(panel, idx, PANELS.H_ONE_THREE)) + panels.forEach((panel, idx) => addTouchSideClasses(panel, idx, "H_ONE_THREE")) const majorContainer = makeCol(panels[0]) const minorContainer = makeCol(panels[1], panels[2], panels[3]) @@ -119,7 +126,7 @@ export const getHorizontalOneThree = (panels: [HTMLElement, HTMLElement, HTMLEle export const getVerticalOneThree = (panels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement]) => { washPanels(panels) - panels.forEach((panel, idx) => addTouchSideClasses(panel, idx, PANELS.V_ONE_THREE)) + panels.forEach((panel, idx) => addTouchSideClasses(panel, idx, "V_ONE_THREE")) const majorContainer = makeRow(panels[0]) const minorContainer = makeRow(panels[1], panels[2], panels[3]) @@ -133,7 +140,7 @@ export const getVerticalOneThree = (panels: [HTMLElement, HTMLElement, HTMLEleme export const getFourPanel = (panels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement]) => { washPanels(panels) - panels.forEach((panel, idx) => addTouchSideClasses(panel, idx, PANELS.FOUR_PANEL)) + panels.forEach((panel, idx) => addTouchSideClasses(panel, idx, "FOUR_PANEL")) const majorContainer = makeRow(panels[0], panels[1]) const minorContainer = makeRow(panels[2], panels[3]) @@ -147,7 +154,7 @@ export const getFourPanel = (panels: [HTMLElement, HTMLElement, HTMLElement, HTM export const getSinglePanel = (panels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement]) => { washPanels(panels) - panels.forEach((panel, idx) => addTouchSideClasses(panel, idx, PANELS.SINGLE_PANEL)) + panels.forEach((panel, idx) => addTouchSideClasses(panel, idx, "SINGLE_PANEL")) const majorContainer = makeRow(panels[0]) const minorContainer = makeRow(panels[1], panels[2], panels[3]) diff --git a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts index b9476bf4f2c384cc7925f8334bb50dbc3c4e8989..b9c8b3737adda7bdb3f0aee902f9cb52caad9391 100644 --- a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts +++ b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts @@ -12,7 +12,7 @@ import { ViewerCtrlCmp } from "./viewerCtrlCmp.component" import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' import { HarnessLoader } from "@angular/cdk/testing" import { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing' -import { atlasAppearance } from "src/state" +import { atlasAppearance, atlasSelection } from "src/state" describe('> viewerCtrlCmp.component.ts', () => { @@ -80,9 +80,8 @@ describe('> viewerCtrlCmp.component.ts', () => { }) beforeEach(() => { mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateSelectedTemplatePureSelector, {}) + mockStore.overrideSelector(atlasSelection.selectors.selectedTemplate, {} as any) mockStore.overrideSelector(atlasAppearance.selectors.octantRemoval, true) - mockStore.overrideSelector(viewerStateCustomLandmarkSelector, []) mockStore.overrideSelector(selectorAuxMeshes, []) }) @@ -236,120 +235,5 @@ describe('> viewerCtrlCmp.component.ts', () => { ) }) }) - - describe('> flagDelin', () => { - let toggleParcVsblSpy: jasmine.Spy - beforeEach(() => { - fixture = TestBed.createComponent(ViewerCtrlCmp) - toggleParcVsblSpy = spyOn(fixture.componentInstance as any, 'toggleParcVsbl') - fixture.detectChanges() - }) - it('> calls toggleParcVsbl', () => { - toggleParcVsblSpy.and.callFake(() => {}) - fixture.componentInstance.flagDelin = false - expect(toggleParcVsblSpy).toHaveBeenCalled() - }) - }) - describe('> toggleParcVsbl', () => { - let getViewerConfigSpy: jasmine.Spy - let getLayerByNameSpy: jasmine.Spy - beforeEach(() => { - const pureCstSvc = TestBed.inject(PureContantService) - getLayerByNameSpy = mockNehubaViewer.nehubaViewer.ngviewer.layerManager.getLayerByName - getViewerConfigSpy = pureCstSvc.getViewerConfig as jasmine.Spy - fixture = TestBed.createComponent(ViewerCtrlCmp) - fixture.detectChanges() - }) - - it('> calls pureSvc.getViewerConfig', async () => { - getViewerConfigSpy.and.returnValue({}) - await fixture.componentInstance['toggleParcVsbl']() - expect(getViewerConfigSpy).toHaveBeenCalled() - }) - - describe('> if _flagDelin is true', () => { - beforeEach(() => { - fixture.componentInstance['_flagDelin'] = true - fixture.componentInstance['hiddenLayerNames'] = [ - 'foo', - 'bar', - 'baz' - ] - }) - it('> go through all hideen layer names and set them to true', async () => { - const setVisibleSpy = jasmine.createSpy('setVisible') - getLayerByNameSpy.and.returnValue({ - setVisible: setVisibleSpy - }) - await fixture.componentInstance['toggleParcVsbl']() - expect(getLayerByNameSpy).toHaveBeenCalledTimes(3) - for (const arg of ['foo', 'bar', 'baz']) { - expect(getLayerByNameSpy).toHaveBeenCalledWith(arg) - } - expect(setVisibleSpy).toHaveBeenCalledTimes(3) - expect(setVisibleSpy).toHaveBeenCalledWith(true) - expect(setVisibleSpy).not.toHaveBeenCalledWith(false) - }) - it('> hiddenLayerNames resets', async () => { - await fixture.componentInstance['toggleParcVsbl']() - expect(fixture.componentInstance['hiddenLayerNames']).toEqual([]) - }) - }) - - describe('> if _flagDelin is false', () => { - let managedLayerSpyProp: jasmine.Spy - let setVisibleSpy: jasmine.Spy - beforeEach(() => { - fixture.componentInstance['_flagDelin'] = false - setVisibleSpy = jasmine.createSpy('setVisible') - getLayerByNameSpy.and.returnValue({ - setVisible: setVisibleSpy - }) - getViewerConfigSpy.and.resolveTo({ - 'foo': {}, - 'bar': {}, - 'baz': {} - }) - managedLayerSpyProp = spyOnProperty(mockNehubaViewer.nehubaViewer.ngviewer.layerManager, 'managedLayers') - managedLayerSpyProp.and.returnValue([{ - visible: true, - name: 'foo' - }, { - visible: false, - name: 'bar' - }, { - visible: true, - name: 'baz' - }]) - }) - - afterEach(() => { - managedLayerSpyProp.calls.reset() - }) - - it('> calls schedulRedraw', async () => { - await fixture.componentInstance['toggleParcVsbl']() - await new Promise(rs => requestAnimationFrame(rs)) - expect(mockNehubaViewer.nehubaViewer.ngviewer.display.scheduleRedraw).toHaveBeenCalled() - }) - - it('> only calls setVisible false on visible layers', async () => { - await fixture.componentInstance['toggleParcVsbl']() - expect(getLayerByNameSpy).toHaveBeenCalledTimes(2) - - for (const arg of ['foo', 'baz']) { - expect(getLayerByNameSpy).toHaveBeenCalledWith(arg) - } - expect(setVisibleSpy).toHaveBeenCalledTimes(2) - expect(setVisibleSpy).toHaveBeenCalledWith(false) - expect(setVisibleSpy).not.toHaveBeenCalledWith(true) - }) - - it('> sets hiddenLayerNames correctly', async () => { - await fixture.componentInstance['toggleParcVsbl']() - expect(fixture.componentInstance['hiddenLayerNames']).toEqual(['foo', 'baz']) - }) - }) - }) }) }) diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 14ae775e5ea9c5584b3de8c149bc32f1de3122b5..4c7fdce6fc2cdfd80a7302c76e13aca4f898f69a 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -287,7 +287,6 @@ export class ViewerCmp implements OnDestroy { if (context.viewerType === 'threeSurfer') { hoveredRegions = (context as TContextArg<'threeSurfer'>).payload._mouseoverRegion } - console.log('hoveredRegions', hoveredRegions) if (hoveredRegions.length > 0) { append({ @@ -313,6 +312,12 @@ export class ViewerCmp implements OnDestroy { while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()() } + public clearRoi(){ + this.store$.dispatch( + actions.clearSelectedRegions() + ) + } + public selectRoi(roi: SapiRegionModel) { this.store$.dispatch( actions.selectRegions({ @@ -389,9 +394,25 @@ export class ViewerCmp implements OnDestroy { } onDismissNonbaseLayer(){ - + this.store$.dispatch( + atlasSelection.actions.clearNonBaseParcLayer() + ) } - onSelectParcellation(parc: SapiParcellationModel){ - + onSelectParcellation(parcellation: SapiParcellationModel){ + this.store$.dispatch( + atlasSelection.actions.selectParcellation({ + parcellation + }) + ) + } + navigateTo(position: number[]) { + this.store$.dispatch( + atlasSelection.actions.navigateTo({ + navigation: { + position + }, + animation: true, + }) + ) } } diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css index 3e85ab331101bd7a6e2215d37e9e710031bc0cd3..e30172096bd62fc52a359af3c20131c6c517d261 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.style.css +++ b/src/viewerModule/viewerCmp/viewerCmp.style.css @@ -3,11 +3,6 @@ position: relative; } -.explore-btn -{ - margin-top: 4.5rem; -} - .region-text-search-autocomplete-position { z-index: 10; @@ -59,3 +54,13 @@ mat-drawer { padding-top: 5rem; } + +sxplr-sapiviews-core-region-region-chip +{ + margin-left:-5rem; +} + +sxplr-sapiviews-core-region-region-chip [prefix] +{ + padding-left:5rem; +} \ No newline at end of file diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 103d28ebc59212e559b68ce3c5788c55184e90f6..d1531cab26aae829c616c2f10c9458c3c2046c4d 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -204,19 +204,26 @@ <!-- such a gross implementation --> <!-- TODO fix this --> - <div class="mt-1-n w-100 sxplr-pl-2 sxplr-pr-2 m-1px"> + <div class="sxplr-mt-1-n w-100 sxplr-pl-1 sxplr-pr-1" + sxplr-sapiviews-core-region + [sxplr-sapiviews-core-region-atlas]="selectedAtlas$ | async" + [sxplr-sapiviews-core-region-template]="templateSelected$ | async" + [sxplr-sapiviews-core-region-parcellation]="parcellationSelected$ | async" + [sxplr-sapiviews-core-region-region]="(selectedRegions$ | async)[0]" + [sxplr-sapiviews-core-region-detail-flag]="true" + #sapiRegion="sapiViewsCoreRegion"> <!-- TODO use sapiViews/core/region/base and fix the rest --> <button mat-raised-button *ngIf="!(onlyShowMiniTray$ | async)" [attr.aria-label]="ARIA_LABELS.EXPAND" (click)="showFullSidenav()" - class="explore-btn pe-all w-100" + class="sxplr-mt-9 sxplr-pe-all w-100" [ngClass]="{ - 'darktheme': true, - 'lighttheme': false + 'darktheme': sapiRegion.regionDarkmode, + 'lighttheme': !sapiRegion.regionDarkmode }" - [style.backgroundColor]="'accent'"> + [style.backgroundColor]="sapiRegion.regionRgbString"> <span class="text iv-custom-comp"> Explore </span> @@ -355,26 +362,55 @@ <ng-template #bottomLeftTmpl let-showFullSideNav="showFullSideNav"> <!-- atlas selector --> - <sxplr-sapiviews-core-atlas-tmplparcselector *ngIf="viewerLoaded && !(isStandaloneVolumes$ | async)"> + <sxplr-sapiviews-core-atlas-tmplparcselector + *ngIf="viewerLoaded && !(isStandaloneVolumes$ | async)" + [iav-key-listener]="[{'type': 'keydown', 'key': 'Escape'}]" + (iav-key-event)="tmplParcSelector.closeSelector()" + (iav-outsideClick)="tmplParcSelector.closeSelector()" + #tmplParcSelector="sxplrSapiViewsCoreAtlasTmplparcselector"> </sxplr-sapiviews-core-atlas-tmplparcselector> - <!-- selected parcellation chip --> - <sxplr-sapiviews-core-parcellation-smartchip - class="sxplr-pointer-events-all" - [sxplr-sapiviews-core-parcellation-smartchip-parcellation]="parcellationSelected$ | async" - [sxplr-sapiviews-core-parcellation-smartchip-all-parcellations]="allAvailableParcellations$ | async" - (sxplr-sapiviews-core-parcellation-smartchip-dismiss-nonbase-layer)="onDismissNonbaseLayer()" - (sxplr-sapiviews-core-parcellation-smartchip-select-parcellation)="onSelectParcellation($event)" - > - </sxplr-sapiviews-core-parcellation-smartchip> - - <!-- chips --> - <!-- <div *ngIf="parcellationSelected$ | async" - class="d-inline-block flex-grow-1 flex-shrink-1 pe-none overflow-x-auto overflow-y-hidden"> + <!-- scroll container --> + <div class="sxplr-d-inline-flex + sxplr-flex-wrap-nowrap + sxplr-mxw-80vw + sxplr-pe-all + sxplr-of-x-auto + sxplr-of-y-hidden"> + + <!-- selected parcellation chip --> + <sxplr-sapiviews-core-parcellation-smartchip + class="sxplr-z-2 sxplr-mr-1" + [sxplr-sapiviews-core-parcellation-smartchip-parcellation]="parcellationSelected$ | async" + [sxplr-sapiviews-core-parcellation-smartchip-all-parcellations]="allAvailableParcellations$ | async" + (sxplr-sapiviews-core-parcellation-smartchip-dismiss-nonbase-layer)="onDismissNonbaseLayer()" + (sxplr-sapiviews-core-parcellation-smartchip-select-parcellation)="onSelectParcellation($event)" + > + </sxplr-sapiviews-core-parcellation-smartchip> + + <!-- selected region chip --> + <sxplr-sapiviews-core-region-region-chip + *ngFor="let region of selectedRegions$ | async" + class="sxplr-pe-all sxplr-z-1 sxplr-mr-1" + (sxplr-sapiviews-core-region-region-chip-clicked)="showFullSideNav()" + [sxplr-sapiviews-core-region-atlas]="selectedAtlas$ | async" + [sxplr-sapiviews-core-region-template]="templateSelected$ | async" + [sxplr-sapiviews-core-region-parcellation]="parcellationSelected$ | async" + [sxplr-sapiviews-core-region-region]="region"> + + <div prefix> + </div> - <viewer-state-breadcrumb class="d-inline-block pe-all" (on-item-click)="showFullSideNav()"> - </viewer-state-breadcrumb> - </div> --> + <div suffix class="sxplr-scale-70"> + <button mat-mini-fab + color="primary" + iav-stop="mousedown click" + (click)="clearRoi()"> + <i class="fas fa-times"></i> + </button> + </div> + </sxplr-sapiviews-core-region-region-chip> + </div> </ng-template> @@ -472,18 +508,27 @@ <!-- if region selected > 0 --> <ng-template [ngIf]="regionSelected?.length > 0" [ngIfElse]="tabTmpl_nothingSelected"> + <div sxplr-sapiviews-core-region + [sxplr-sapiviews-core-region-detail-flag]="true" + [sxplr-sapiviews-core-region-atlas]="selectedAtlas$ | async" + [sxplr-sapiviews-core-region-template]="templateSelected$ | async" + [sxplr-sapiviews-core-region-parcellation]="parcellationSelected$ | async" + [sxplr-sapiviews-core-region-region]="regionSelected[0]" + #tabTmpl_iavRegion="sapiViewsCoreRegion"> + + </div> <!-- TODO fix with sapiView/core/region directive --> - <!-- <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { + <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { matColor: 'accent', - customColor: tabTmpl_iavRegion.rgbString, - customColorDarkmode: tabTmpl_iavRegion.rgbDarkmode, + customColor: tabTmpl_iavRegion.regionRgbString, + customColorDarkmode: tabTmpl_iavRegion.regionDarkmode, fontIcon: 'fa-brain', - tooltip: 'Explore ' + tabTmpl_iavRegion.region.name, + tooltip: 'Explore ' + regionSelected[0].name, click: click }"> - </ng-container> --> + </ng-container> </ng-template> <!-- nothing is selected --> @@ -562,6 +607,7 @@ [sxplr-sapiviews-core-region-parcellation]="parcellationSelected$ | async" [sxplr-sapiviews-core-region-region]="selectedRegions[0]" (sxplr-sapiviews-core-region-region-rich-feature-clicked)="showDataset($event)" + (sxplr-sapiviews-core-region-navigate-to)="navigateTo($event)" > <div class="sapi-container" header></div> </sxplr-sapiviews-core-region-region-rich> @@ -798,20 +844,24 @@ let-backCb="backCb" let-feature="feature"> - <!-- back btn --> - <button mat-button - *ngIf="backCb" - (click)="backCb()" - [attr.aria-label]="ARIA_LABELS.CLOSE" - class="position-absolute z-index-10 m-2"> - <i class="fas fa-chevron-left"></i> - <span class="ml-1"> - Back - </span> - </button> - - <sxplr-sapiviews-core-datasets-dataset class="sxplr-mt-8 sxplr-z-index-2 mat-elevation-z2" + <sxplr-sapiviews-core-datasets-dataset class="sxplr-z-2 mat-elevation-z2" [sxplr-sapiviews-core-datasets-dataset-input]="feature"> + + <div header> + <!-- back btn --> + <button mat-button + *ngIf="backCb" + (click)="backCb()" + [attr.aria-label]="ARIA_LABELS.CLOSE" + class="sxplr-mb-2" + > + <i class="fas fa-chevron-left"></i> + <span class="ml-1"> + Back + </span> + </button> + </div> + </sxplr-sapiviews-core-datasets-dataset> <sxplr-sapiviews-features-entry diff --git a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.component.ts b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.component.ts deleted file mode 100644 index a2b0b594b3ab0e2f002e2ab140aefe54f289d3da..0000000000000000000000000000000000000000 --- a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.component.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Component, EventEmitter, Output } from "@angular/core"; -import { IQuickTourData } from "src/ui/quickTour"; -import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' -import { select, Store } from "@ngrx/store"; -import { distinctUntilChanged, map } from "rxjs/operators"; -import { ngViewerActionClearView, ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState.store.helper"; -import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN } from "src/util/interfaces"; -import { atlasSelection } from "src/state" -import { NEVER, of } from "rxjs"; -import { SapiParcellationModel } from "src/atlasComponents/sapi"; - -@Component({ - selector: 'viewer-state-breadcrumb', - templateUrl: './breadcrumb.template.html', - styleUrls: [ - './breadcrumb.style.css' - ], - providers: [ - { - provide: OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, - useValue: null - } - ] -}) - -export class ViewerStateBreadCrumb { - - public CONST = CONST - public ARIA_LABELS = ARIA_LABELS - - @Output('on-item-click') - onChipClick = new EventEmitter() - - public quickTourChips: IQuickTourData = { - order: 5, - description: QUICKTOUR_DESC.CHIPS, - } - - public clearViewKeys$ = this.store$.pipe( - select(ngViewerSelectorClearViewEntries) - ) - - // TODO what is this observable anyway? - public selectedAdditionalLayers$ = of([]) - - public parcellationSelected$ = this.store$.pipe( - select(atlasSelection.selectors.selectedParcellation), - distinctUntilChanged(), - - ) - - public selectedRegions$ = this.store$.pipe( - select(atlasSelection.selectors.selectedRegions), - distinctUntilChanged(), - ) - - // TODO add version info in siibra-api/siibra-python - public selectedLayerVersions$ = NEVER - - constructor(private store$: Store<any>){ - } - - handleChipClick(){ - this.onChipClick.emit(null) - } - - public clearSelectedRegions(){ - this.store$.dispatch( - atlasSelection.actions.clearSelectedRegions() - ) - } - - public unsetClearViewByKey(key: string){ - this.store$.dispatch( - ngViewerActionClearView({ payload: { - [key]: false - }}) - ) - } - - public clearAdditionalLayer(layer: { ['@id']: string }){ - this.store$.dispatch( - atlasSelection.actions.clearNonBaseParcLayer() - ) - } - - public selectParcellationWithId(parcId: string) { - this.store$.dispatch( - atlasSelection.actions.selectATPById({ - parcellationId: parcId - }) - ) - } - - public bindFns(fns){ - return () => { - for (const [ fn, ...arg] of fns) { - fn(...arg) - } - } - } - -} diff --git a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.style.css b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.style.css deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.template.html b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.template.html deleted file mode 100644 index d1338f81bbd2ab900e4a6d66f1c6d2e9d74c4a09..0000000000000000000000000000000000000000 --- a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.template.html +++ /dev/null @@ -1,187 +0,0 @@ -<mat-chip-list - quick-tour - [quick-tour-description]="quickTourChips.description" - [quick-tour-order]="quickTourChips.order"> - - <!-- additional layer --> - - <ng-container> - <ng-container *ngTemplateOutlet="currParcellationTmpl; context: { - addParc: (selectedAdditionalLayers$ | async), - parc: (parcellationSelected$ | async) - }"> - </ng-container> - </ng-container> - - <!-- any selected region(s) --> - <ng-container> - <ng-container *ngTemplateOutlet="selectedRegionTmpl"> - </ng-container> - </ng-container> -</mat-chip-list> - - -<!-- parcellation chip / region chip --> -<ng-template #currParcellationTmpl let-parc="parc" let-addParc="addParc"> - <div [matMenuTriggerFor]="layerVersionMenu" - [matMenuTriggerData]="{ layerVersionMenuTrigger: layerVersionMenuTrigger }" - #layerVersionMenuTrigger="matMenuTrigger"> - - <ng-template [ngIf]="addParc && addParc.length > 0" [ngIfElse]="defaultParcTmpl"> - <ng-container *ngFor="let p of addParc"> - <ng-container *ngTemplateOutlet="chipTmpl; context: { - parcel: p, - selected: true, - dismissable: true, - ariaLabel: ARIA_LABELS.PARC_VER_SELECT, - onclick: layerVersionMenuTrigger.toggleMenu.bind(layerVersionMenuTrigger) - }"> - </ng-container> - </ng-container> - </ng-template> - <ng-template #defaultParcTmpl> - <ng-template [ngIf]="parc"> - - <ng-container *ngTemplateOutlet="chipTmpl; context: { - parcel: parc, - selected: false, - dismissable: false, - ariaLabel: ARIA_LABELS.PARC_VER_SELECT, - onclick: layerVersionMenuTrigger.toggleMenu.bind(layerVersionMenuTrigger) - }"> - </ng-container> - </ng-template> - </ng-template> - </div> -</ng-template> - - -<ng-template #selectedRegionTmpl> - - <!-- regions chip --> - <ng-template [ngIf]="selectedRegions$ | async" let-selectedRegions="ngIf"> - <!-- if regions.length > 1 --> - <!-- use group chip --> - <ng-template [ngIf]="selectedRegions.length > 1" [ngIfElse]="singleRegionChipTmpl"> - <mat-chip - color="primary" - selected - (click)="handleChipClick()" - class="pe-all position-relative z-index-1 ml-8-n"> - <span class="iv-custom-comp text text-truncate d-inline sxplr-pl-4"> - {{ CONST.MULTI_REGION_SELECTION }} - </span> - <mat-icon - (click)="clearSelectedRegions()" - fontSet="fas" - iav-stop="click" - fontIcon="fa-times"> - </mat-icon> - </mat-chip> - </ng-template> - - <!-- if reginos.lengt === 1 --> - <!-- use single region chip --> - <ng-template #singleRegionChipTmpl> - <ng-container *ngFor="let r of selectedRegions"> - - <!-- region chip for discrete map --> - - <!-- TODO use actual region chip --> - <mat-chip>REGION PLACE HOLDER</mat-chip> - - <!-- chips for previewing origin datasets/continous map --> - <ng-container *ngFor="let originDataset of (r.originDatasets || []); let index = index"> - - <mat-chip *ngFor="let key of clearViewKeys$ | async" - (click)="handleChipClick()" - class="pe-all position-relative ml-8-n"> - <span class="sxplr-pl-4"> - {{ key }} - </span> - <mat-icon (click)="unsetClearViewByKey(key)" - fontSet="fas" - iav-stop="click" - fontIcon="fa-times"> - - </mat-icon> - </mat-chip> - </ng-container> - - </ng-container> - </ng-template> - </ng-template> - -</ng-template> - -<!-- layer version selector --> -<mat-menu #layerVersionMenu - class="bg-none box-shadow-none" - [aria-label]="ARIA_LABELS.PARC_VER_CONTAINER" - [hasBackdrop]="false"> - <ng-template matMenuContent let-layerVersionMenuTrigger="layerVersionMenuTrigger"> - <div (iav-outsideClick)="layerVersionMenuTrigger.closeMenu()"> - <ng-container *ngFor="let parcVer of selectedLayerVersions$ | async"> - <ng-container *ngIf="parcellationSelected$ | async as selectedParcellation"> - - <ng-container *ngTemplateOutlet="chipTmpl; context: { - parcel: parcVer, - selected: selectedParcellation['@id'] === parcVer['@id'], - dismissable: false, - class: 'w-100', - ariaLabel: parcVer.displayName || parcVer.name, - onclick: bindFns([ - [ selectParcellationWithId.bind(this), parcVer['@id'] ], - [ layerVersionMenuTrigger.closeMenu.bind(layerVersionMenuTrigger) ] - ]) - }"> - </ng-container> - </ng-container> - <div class="mt-1"></div> - </ng-container> - </div> - </ng-template> -</mat-menu> - - -<ng-template #chipTmpl - let-parcel="parcel" - let-selected="selected" - let-dismissable="dismissable" - let-chipClass="class" - let-ariaLabel="ariaLabel" - let-onclick="onclick"> - <mat-chip class="pe-all position-relative z-index-2 d-inline-flex justify-content-between" - [ngClass]="chipClass" - [attr.aria-label]="ariaLabel" - (click)="onclick && onclick()" - [selected]="selected"> - - <span class="ws-no-wrap"> - {{ parcel?.groupName ? (parcel?.groupName + ' - ') : '' }}{{ parcel && (parcel.displayName || parcel.name) }} - </span> - - <!-- info icon --> - <ng-container *ngFor="let originDatainfo of (parcel | originDatainfoPipe)"> - - <mat-icon - fontSet="fas" - fontIcon="fa-info-circle" - iav-stop="click" - iav-dataset-show-dataset-dialog - [iav-dataset-show-dataset-dialog-name]="originDatainfo.metadata.fullName" - [iav-dataset-show-dataset-dialog-description]="originDatainfo.metadata.description" - [iav-dataset-show-dataset-dialog-urls]="originDatainfo.urls"> - </mat-icon> - - </ng-container> - - <!-- dismiss icon --> - <mat-icon - *ngIf="dismissable" - (click)="clearAdditionalLayer(parcel); $event.stopPropagation()" - fontSet="fas" - fontIcon="fa-times"> - </mat-icon> - </mat-chip> -</ng-template> diff --git a/src/viewerModule/viewerStateBreadCrumb/module.ts b/src/viewerModule/viewerStateBreadCrumb/module.ts deleted file mode 100644 index c331dbbaed2dce17f35c48ebdc9aa4f662ca8a6d..0000000000000000000000000000000000000000 --- a/src/viewerModule/viewerStateBreadCrumb/module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; -import { QuickTourModule } from "src/ui/quickTour"; -import { AngularMaterialModule } from "src/sharedModules"; -import { UtilModule } from "src/util"; -import { ViewerStateBreadCrumb } from "./breadcrumb/breadcrumb.component"; -import { OriginalDatainfoPipe } from "./pipes/originDataInfo.pipe" -import { DialogInfoModule } from "src/ui/dialogInfo"; - -@NgModule({ - imports: [ - CommonModule, - AngularMaterialModule, - QuickTourModule, - UtilModule, - DialogInfoModule, - ], - declarations: [ - ViewerStateBreadCrumb, - OriginalDatainfoPipe, - ], - exports: [ - ViewerStateBreadCrumb, - ], - providers:[ - ] -}) - -export class ViewerStateBreadCrumbModule{} \ No newline at end of file diff --git a/src/viewerModule/viewerStateBreadCrumb/pipes/originDataInfo.pipe.ts b/src/viewerModule/viewerStateBreadCrumb/pipes/originDataInfo.pipe.ts deleted file mode 100644 index 3c8676554dd7c1b766878d579b42b9a2bdbfc2ce..0000000000000000000000000000000000000000 --- a/src/viewerModule/viewerStateBreadCrumb/pipes/originDataInfo.pipe.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core" -import { - SapiDatasetModel, - SapiParcellationModel, - SapiRegionModel, -} from "src/atlasComponents/sapi" - -@Pipe({ - name: 'originDatainfoPipe' -}) - -export class OriginalDatainfoPipe implements PipeTransform{ - public transform(obj: SapiParcellationModel | SapiRegionModel): SapiDatasetModel[]{ - - if (obj["@type"] === "minds/core/parcellationatlas/v1.0.0") { - const ds = (obj as SapiParcellationModel).datasets[0] - return (obj as SapiParcellationModel).datasets - } - if (obj["@type"] === "https://openminds.ebrains.eu/sands/ParcellationEntityVersion") { - (obj as SapiRegionModel) - return [] - } - } -}