diff --git a/common/util.js b/common/util.js index c24a9fd0bc4fe3d9cffb0bba000243a93ccc560b..b69099b6a020a213f76f9498fbf7d56bdb0363dc 100644 --- a/common/util.js +++ b/common/util.js @@ -151,6 +151,8 @@ exports.flattenRegions = flattenRegions + exports.hexToRgb = hex => hex && hex.match(/[a-fA-F0-9]{2}/g).map(v => parseInt(v, 16)) + exports.rgbToHex = rgb => rgb && `#${rgb.map(color => color.toString(16).padStart(2, '0')).join("")}` exports.getRandomHex = (digit = 1024 * 1024 * 1024 * 1024) => Math.round(Math.random() * digit).toString(16) /** @@ -193,37 +195,6 @@ ) } - exports.serialiseParcellationRegion = ({ ngId, labelIndex }) => { - if (!ngId) { - throw new Error(`#serialiseParcellationRegion error: ngId must be defined`) - } - - if (!labelIndex) { - throw new Error(`#serialiseParcellationRegion error labelIndex must be defined`) - } - - return `${ngId}#${labelIndex}` - } - - const deserialiseParcRegionId = labelIndexId => { - const _ = labelIndexId && labelIndexId.split && labelIndexId.split('#') || [] - const ngId = _.length > 1 - ? _[0] - : null - const labelIndex = _.length > 1 - ? Number(_[1]) - : _.length === 0 - ? null - : Number(_[0]) - return { labelIndex, ngId } - } - - exports.deserialiseParcRegionId = deserialiseParcRegionId - - exports.deserialiseParcellationRegion = ({ region, labelIndexId, inheritedNgId = 'root' }) => { - const { labelIndex, ngId } = deserialiseParcRegionId(labelIndexId) - } - const getPad = ({ length, pad }) => { if (pad.length !== 1) throw new Error(`pad needs to be precisely 1 character`) return input => { diff --git a/docs/releases/v2.7.0.md b/docs/releases/v2.7.0.md new file mode 100644 index 0000000000000000000000000000000000000000..427b35f4b925138ef419e63c6a5acebfd24be727 --- /dev/null +++ b/docs/releases/v2.7.0.md @@ -0,0 +1,5 @@ +# v2.7.0 + +## New feature + +- (experimental) Add first implementation of fetching VOI from siibra-api diff --git a/mkdocs.yml b/mkdocs.yml index d56285fcb2696c7884fb90844282f9ba87e94d2b..8fe33d7163f9db4af08adf389f2ffbf4b58c179a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ pages: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.7.0: 'releases/v2.7.0.md' - v2.6.5: 'releases/v2.6.5.md' - v2.6.4: 'releases/v2.6.4.md' - v2.6.3: 'releases/v2.6.3.md' diff --git a/package.json b/package.json index 4ef7b728e472e70f049ec71e0eb77e7893154581..1d6270f7cd6e00c655df94f57cd001413c0c8b37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interactive-viewer", - "version": "2.6.5", + "version": "2.7.0", "description": "HBP interactive atlas viewer. Integrating KG query, dataset previews & more. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", "scripts": { "build-aot": "ng build && node ./third_party/matomo/processMatomo.js", @@ -14,7 +14,8 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", - "test-ci": "ng test --progress false --watch false --browsers=ChromeHeadless" + "test-ci": "ng test --progress false --watch false --browsers=ChromeHeadless", + "sapi-schema": "npx openapi-typescript@5.1.1 http://localhost:5000/v1_0/openapi.json --output ./src/atlasComponents/sapi/schema.ts" }, "keywords": [], "author": "FZJ-INM1-BDA <inm1-bda@fz-juelich.de>", diff --git a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts index 6a7449787611327577ac114c5060b7abffb6a9d8..d9ebdd9a94f5727043f4676bb3a433e1418a543b 100644 --- a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts +++ b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts @@ -6,7 +6,7 @@ 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 { viewerStateAllRegionsFlattenedRegionSelector, viewerStateOverwrittenColorMapSelector } from "src/services/state/viewerState/selectors"; +import { viewerStateOverwrittenColorMapSelector } from "src/services/state/viewerState/selectors"; import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState.store.helper"; import {BS_ENDPOINT} from "src/util/constants"; @@ -81,7 +81,6 @@ describe('ConnectivityComponent', () => { beforeEach(() => { const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateAllRegionsFlattenedRegionSelector, []) mockStore.overrideSelector(viewerStateOverwrittenColorMapSelector, null) mockStore.overrideSelector(ngViewerSelectorClearViewEntries, []) }) diff --git a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts index 638863a0893fc8a064fc1e7482b24b56bf956a39..0458757bbf0bfe396386003660dfe7de83cdc37d 100644 --- a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts +++ b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts @@ -10,19 +10,16 @@ import { OnInit, Inject, } from "@angular/core"; import {select, Store} from "@ngrx/store"; -import {fromEvent, Observable, Subscription, Subject, combineLatest} from "rxjs"; -import {distinctUntilChanged, filter, map} from "rxjs/operators"; - -import { viewerStateNavigateToRegion, viewerStateSetSelectedRegions } from "src/services/state/viewerState.store.helper"; +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 { - viewerStateAllRegionsFlattenedRegionSelector, - viewerStateOverwrittenColorMapSelector -} from "src/services/state/viewerState/selectors"; 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' @@ -139,8 +136,10 @@ export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDe public selectedDatasetKgSchema: string = '' public connectedAreas = [] + // TODO this may be incompatible private selectedParcellationFlatRegions$ = this.store$.pipe( - select(viewerStateAllRegionsFlattenedRegionSelector) + select(atlasSelection.selectors.selectedATP), + switchMap(({ atlas, template, parcellation }) => this.sapi.getParcRegions(atlas["@id"], parcellation["@id"], template["@id"])) ) public overwrittenColorMap$: Observable<any> @@ -159,10 +158,11 @@ export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDe private changeDetectionRef: ChangeDetectorRef, private httpClient: HttpClient, @Inject(BS_ENDPOINT) private siibraApiUrl: string, + private sapi: SAPI ) { this.overwrittenColorMap$ = this.store$.pipe( - select(viewerStateOverwrittenColorMapSelector), + select(atlasAppearance.selectors.getOverwrittenColormap), distinctUntilChanged() ) } @@ -188,7 +188,7 @@ export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDe public ngAfterViewInit(): void { this.subscriptions.push( this.store$.pipe( - select(viewerStateOverwrittenColorMapSelector), + select(atlasAppearance.selectors.getOverwrittenColormap), ).subscribe(value => { if (this.accordionIsExpanded) { this.setColorMap$.next(!!value) @@ -348,18 +348,18 @@ export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDe } } - navigateToRegion(region) { + navigateToRegion(region: SapiRegionModel) { this.store$.dispatch( - viewerStateNavigateToRegion({ - payload: {region: this.getRegionWithName(region)} + atlasSelection.actions.navigateToRegion({ + region }) ) } - selectRegion(region) { + selectRegion(region: SapiRegionModel) { this.store$.dispatch( - viewerStateSetSelectedRegions({ - selectRegions: [ region ] + actions.selectRegions({ + regions: [ region ] }) ) } diff --git a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html index 9dfdc054e09c6eba8ad1318a37cca2db792a8ef2..dbb5cb03e570d0730eae592c5b270053c8ad34b1 100644 --- a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html +++ b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html @@ -36,16 +36,7 @@ </mat-select> </mat-form-field> <ng-container *ngIf="selectedDataset && (selectedDatasetDescription || selectedDatasetKgId)" > - <button class="flex-grow-0 flex-shrink-0" - mat-icon-button - iav-dataset-show-dataset-dialog - [iav-dataset-show-dataset-dialog-name]="selectedDatasetName" - [iav-dataset-show-dataset-dialog-description]="selectedDatasetDescription" - [iav-dataset-show-dataset-dialog-kgid]="selectedDatasetKgId? selectedDatasetKgId : null" - [iav-dataset-show-dataset-dialog-kgschema]="selectedDatasetKgSchema? selectedDatasetKgSchema : null" - > - <i class="fas fa-info"></i> - </button> + TODO please reimplmenent button to explore KG dataset <button class="flex-grow-0 flex-shrink-0" mat-icon-button (click)="exportFullConnectivity()" diff --git a/src/atlasComponents/connectivity/module.ts b/src/atlasComponents/connectivity/module.ts index a43e558c21da18e5998646bf63578f6ea00d8c28..c9231d4605589eeb8a7e5f2dd1f66a4a2b34089f 100644 --- a/src/atlasComponents/connectivity/module.ts +++ b/src/atlasComponents/connectivity/module.ts @@ -3,12 +3,10 @@ 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"; -import {KgDatasetModule} from "src/atlasComponents/regionalFeatures/bsFeatures/kgDataset"; @NgModule({ imports: [ CommonModule, - KgDatasetModule, AngularMaterialModule ], declarations: [ diff --git a/src/atlasComponents/parcellation/regionHierachy/regionHierarchy.component.ts b/src/atlasComponents/parcellation/regionHierachy/regionHierarchy.component.ts index 715f000757552e41ee4e67ace06e9de56006e69e..33030c60ba8f26e1283cab7aef1d25a4f1d87181 100644 --- a/src/atlasComponents/parcellation/regionHierachy/regionHierarchy.component.ts +++ b/src/atlasComponents/parcellation/regionHierachy/regionHierarchy.component.ts @@ -2,7 +2,7 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, E import { fromEvent, Subject, Subscription } from "rxjs"; import { buffer, debounceTime } from "rxjs/operators"; import { FilterNameBySearch } from "./filterNameBySearch.pipe"; -import { serialiseParcellationRegion } from "common/util" +import { serializeSegment } from "src/viewerModule/nehuba/util"; const insertHighlight: (name: string, searchTerm: string) => string = (name: string, searchTerm: string = '') => { const regex = new RegExp(searchTerm, 'gi') @@ -15,7 +15,7 @@ const getDisplayTreeNode: (searchTerm: string, selectedRegions: any[]) => (item: return !!labelIndex && !!ngId && selectedRegions.findIndex(re => - serialiseParcellationRegion({ labelIndex: re.labelIndex, ngId: re.ngId }) === serialiseParcellationRegion({ ngId, labelIndex }), + serializeSegment(re.labelIndex, re.ngId) === serializeSegment(ngId, labelIndex), ) >= 0 ? `<span class="cursor-default regionSelected">${insertHighlight(name, searchTerm)}</span>` + (status ? ` <span class="text-muted">(${insertHighlight(status, searchTerm)})</span>` : ``) : `<span class="cursor-default regionNotSelected">${insertHighlight(name, searchTerm)}</span>` + (status ? ` <span class="text-muted">(${insertHighlight(status, searchTerm)})</span>` : ``) diff --git a/src/atlasComponents/parcellation/regionSearch/regionSearch.component.ts b/src/atlasComponents/parcellation/regionSearch/regionSearch.component.ts index 6ff6301a2f59a77a959774d31299a679c317bed1..7f98db57dde3788da7a80f1430a86e93f3bbb67f 100644 --- a/src/atlasComponents/parcellation/regionSearch/regionSearch.component.ts +++ b/src/atlasComponents/parcellation/regionSearch/regionSearch.component.ts @@ -1,19 +1,18 @@ -import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core"; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core"; import { FormControl } from "@angular/forms"; import { select, Store } from "@ngrx/store"; import { combineLatest, Observable, Subject, merge } from "rxjs"; import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, take, tap, withLatestFrom } from "rxjs/operators"; -import { VIEWER_STATE_ACTION_TYPES } from "src/services/effect/effect"; -import { CHANGE_NAVIGATION, SELECT_REGIONS } from "src/services/state/viewerState.store"; import { getMultiNgIdsRegionsLabelIndexMap } from "src/services/stateStore.service"; import { LoggingService } from "src/logging"; import { MatDialog } from "@angular/material/dialog"; import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; import { PureContantService } from "src/util"; -import { viewerStateToggleRegionSelect, viewerStateNavigateToRegion, viewerStateSetSelectedRegions, viewerStateSetSelectedRegionsWithIds } from "src/services/state/viewerState.store.helper"; import { ARIA_LABELS, CONST } from 'common/constants' -import { serialiseParcellationRegion } from "common/util" -import { actionAddToRegionsSelectionWithIds } from "src/services/state/viewerState/actions"; +import { actions } from "src/state/atlasSelection"; +import { SapiRegionModel } from "src/atlasComponents/sapi"; +import { atlasSelection } from "src/state"; +import { serializeSegment } from "src/viewerModule/nehuba/util"; const filterRegionBasedOnText = searchTerm => region => `${region.name.toLowerCase()}${region.status? ' (' + region.status + ')' : null}`.includes(searchTerm.toLowerCase()) || (region.relatedAreas && region.relatedAreas.some(relatedArea => relatedArea.name && relatedArea.name.toLowerCase().includes(searchTerm.toLowerCase()))) @@ -82,7 +81,7 @@ export class RegionTextSearchAutocomplete { ...region, ngId, labelIndex, - labelIndexId: serialiseParcellationRegion({ ngId, labelIndex }), + labelIndexId: serializeSegment(ngId, labelIndex), }) } } @@ -95,17 +94,6 @@ export class RegionTextSearchAutocomplete { shareReplay(1), ) - this.regionsSelected$ = viewerState$.pipe( - select('regionsSelected'), - distinctUntilChanged(), - tap(regions => { - const arrLabelIndexId = regions.map(({ ngId, labelIndex }) => serialiseParcellationRegion({ ngId, labelIndex })) - this.selectedRegionLabelIndexSet = new Set(arrLabelIndexId) - }), - startWith([]), - shareReplay(1), - ) - this.autocompleteList$ = combineLatest( merge( this.manualRenderList$.pipe( @@ -134,35 +122,10 @@ export class RegionTextSearchAutocomplete { ) } - public toggleRegionWithId(id: string, removeFlag= false) { - if (removeFlag) { - this.store$.dispatch({ - type: VIEWER_STATE_ACTION_TYPES.DESELECT_REGIONS_WITH_ID, - deselecRegionIds: [id], - }) - } else { - this.store$.dispatch( - actionAddToRegionsSelectionWithIds({ - selectRegionIds : [id], - }) - ) - } - } - - public navigateTo(position) { - this.store$.dispatch({ - type: CHANGE_NAVIGATION, - navigation: { - position, - animation: {}, - }, - }) - } - - public optionSelected(_ev?: MatAutocompleteSelectedEvent) { + public optionSelected(_ev: MatAutocompleteSelectedEvent) { this.store$.dispatch( - viewerStateSetSelectedRegionsWithIds({ - selectRegionIds: _ev ? [ _ev.option.value ] : [] + actions.toggleRegionSelectById({ + id: _ev.option.value }) ) } @@ -174,7 +137,9 @@ export class RegionTextSearchAutocomplete { public filterNullFn(item: any){ return !!item } - public regionsSelected$: Observable<any> + public regionsSelected$: Observable<SapiRegionModel[]> = this.store$.pipe( + select(atlasSelection.selectors.selectedRegions) + ) public parcellationSelected$: Observable<any> @Output() @@ -189,26 +154,25 @@ export class RegionTextSearchAutocomplete { return this._focused } - public deselectAllRegions(_event: MouseEvent) { - this.store$.dispatch({ - type: SELECT_REGIONS, - selectRegions: [], - }) + public deselectAllRegions() { + this.store$.dispatch( + actions.clearSelectedRegions() + ) } // TODO handle mobile public handleRegionClick({ mode = null, region = null } = {}) { if (mode === 'single') { this.store$.dispatch( - viewerStateToggleRegionSelect({ - payload: { region } + actions.toggleRegionSelect({ + region }) ) } if (mode === 'double') { this.store$.dispatch( - viewerStateNavigateToRegion({ - payload: { region } + actions.navigateToRegion({ + region }) ) } @@ -243,8 +207,8 @@ export class RegionTextSearchAutocomplete { public deselectAndSelectRegion(region: any) { this.store$.dispatch( - viewerStateSetSelectedRegions({ - selectRegions: [ region ] + actions.selectRegions({ + regions: [ region ] }) ) } diff --git a/src/atlasComponents/parcellation/regionSearch/regionSearch.template.html b/src/atlasComponents/parcellation/regionSearch/regionSearch.template.html index 965cc3c65d508042857b48f489afaf00ee6e2364..f9f7e26ad60fbaee9531afc50d6b35cd2d3eace0 100644 --- a/src/atlasComponents/parcellation/regionSearch/regionSearch.template.html +++ b/src/atlasComponents/parcellation/regionSearch/regionSearch.template.html @@ -18,7 +18,7 @@ <button *ngIf="(regionsSelected$ | async)?.length > 0" mat-icon-button [attr.aria-label]="CLEAR_SELECTED_REGION" - (click)="optionSelected()" + (click)="deselectAllRegions()" matSuffix> <i class="fas fa-times"></i> </button> @@ -77,7 +77,7 @@ [selectedRegions]="regionsSelected$ | async | filterArray : filterNullFn" (singleClickRegion)="handleRegionClick({ mode: 'single', region: $event })" (doubleClickRegion)="handleRegionClick({ mode: 'double', region: $event })" - (clearAllRegions)="deselectAllRegions($event)" + (clearAllRegions)="deselectAllRegions()" [parcellationSelected]="parcellationSelected$ | async"> </region-hierarchy> diff --git a/src/atlasComponents/parcellationRegion/module.ts b/src/atlasComponents/parcellationRegion/module.ts index 03aceeb2d6353c60a3bdb08a634fbbd87a4199d8..8c995808ea52f2b2fa72d9e1272b221387d04200 100644 --- a/src/atlasComponents/parcellationRegion/module.ts +++ b/src/atlasComponents/parcellationRegion/module.ts @@ -7,13 +7,12 @@ import { RenderViewOriginDatasetLabelPipe } from "./region.base"; import { RegionDirective } from "./region.directive"; import { RegionMenuComponent } from "./regionMenu/regionMenu.component"; import { SimpleRegionComponent } from "./regionSimple/regionSimple.component"; -import { BSFeatureModule } from "../regionalFeatures/bsFeatures"; import { RegionAccordionTooltipTextPipe } from "./regionAccordionTooltipText.pipe"; import { AtlasCmptConnModule } from "../connectivity"; import { HttpClientModule } from "@angular/common/http"; import { RegionInOtherTmplPipe } from "./regionInOtherTmpl.pipe"; import { SiibraExplorerTemplateModule } from "../template"; -import { KgDatasetModule } from "../regionalFeatures/bsFeatures/kgDataset"; +import { RegionalFeaturesModule } from "./regionalFeatures/module"; @NgModule({ imports: [ @@ -21,11 +20,11 @@ import { KgDatasetModule } from "../regionalFeatures/bsFeatures/kgDataset"; UtilModule, AngularMaterialModule, ComponentsModule, - BSFeatureModule, AtlasCmptConnModule, HttpClientModule, SiibraExplorerTemplateModule, - KgDatasetModule, + + RegionalFeaturesModule, ], declarations: [ RegionMenuComponent, diff --git a/src/atlasComponents/parcellationRegion/region.base.spec.ts b/src/atlasComponents/parcellationRegion/region.base.spec.ts index 3e4085c70e1df483bd89f47e31dda4c3d167f1da..d3ead88e500af377ccbedc9ea0d2447187deddc5 100644 --- a/src/atlasComponents/parcellationRegion/region.base.spec.ts +++ b/src/atlasComponents/parcellationRegion/region.base.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from '@angular/core/testing' import { MockStore, provideMockStore } from '@ngrx/store/testing' import { viewerStateSelectTemplateWithId } from 'src/services/state/viewerState/actions' -import { RegionBase, getRegionParentParcRefSpace } from './region.base' +import { RegionBase } from './region.base' import { TSiibraExRegion } from './type' // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -39,7 +39,6 @@ describe('> region.base.ts', () => { ] }) mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(getRegionParentParcRefSpace, { template: null, parcellation: null }) }) describe('> position', () => { beforeEach(() => { @@ -116,7 +115,6 @@ describe('> region.base.ts', () => { beforeEach(() => { strToRgbSpy = spyOn(util, 'strToRgb') mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(getRegionParentParcRefSpace, { template: null, parcellation: null }) }) afterEach(() => { @@ -278,7 +276,7 @@ describe('> region.base.ts', () => { // }) // expect(dispatchSpy).toHaveBeenCalledWith( - // viewerStateNewViewer({ + // actionViewerStateNewViewer({ // selectTemplate: fakeTmpl, // selectParcellation: fakeParc, // navigation: {} @@ -320,7 +318,7 @@ describe('> region.base.ts', () => { // }) // expect(dispatchSpy).toHaveBeenCalledWith( - // viewerStateNewViewer({ + // actionViewerStateNewViewer({ // selectTemplate: fakeTmpl, // selectParcellation: fakeParc, // navigation: { diff --git a/src/atlasComponents/parcellationRegion/region.base.ts b/src/atlasComponents/parcellationRegion/region.base.ts index 5f4b7d327e4e1432abb128187ec7c738b6a1bd29..382c288a9a5801a994bcbd086e5a68d4583e3914 100644 --- a/src/atlasComponents/parcellationRegion/region.base.ts +++ b/src/atlasComponents/parcellationRegion/region.base.ts @@ -1,16 +1,11 @@ import { Directive, EventEmitter, Input, Output, Pipe, PipeTransform } from "@angular/core"; -import { select, Store, createSelector } from "@ngrx/store"; -import { uiStateOpenSidePanel, uiStateExpandSidePanel, uiActionShowSidePanelConnectivity } from 'src/services/state/uiState.store.helper' -import { map, tap } from "rxjs/operators"; -import { Observable, BehaviorSubject, combineLatest } from "rxjs"; -import { rgbToHsl } from 'common/util' -import { viewerStateSetConnectivityRegion, viewerStateNavigateToRegion, viewerStateToggleRegionSelect, viewerStateSelectTemplateWithId } from "src/services/state/viewerState.store.helper"; -import { viewerStateGetSelectedAtlas, viewerStateSelectedTemplatePureSelector } from "src/services/state/viewerState/selectors"; +import { select, Store } from "@ngrx/store"; +import { Observable, BehaviorSubject } from "rxjs"; +import { rgbToHsl, hexToRgb } from 'common/util' import { strToRgb, verifyPositionArg } from 'common/util' -import { getPosFromRegion } from "src/util/siibraApiConstants/fn"; -import { TRegionDetail } from "src/util/siibraApiConstants/types"; -import { IHasId } from "src/util/interfaces"; -import { TSiibraExTemplate } from "./type"; +import { actions } from "src/state/atlasSelection"; +import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "../sapi"; +import { atlasSelection } from "src/state"; @Directive() export class RegionBase { @@ -18,16 +13,9 @@ export class RegionBase { public rgbString: string public rgbDarkmode: boolean - private _region: TRegionDetail & { - context?: { - atlas: IHasId - template: IHasId - parcellation: IHasId - } - ngId?: string - } + private _region: SapiRegionModel - private _position: [number, number, number] + private _position: number[] set position(val){ if (verifyPositionArg(val)) { this._position = val @@ -40,39 +28,51 @@ export class RegionBase { return this._position } + public dois: string[] = [] + + @Input() + atlas: SapiAtlasModel + + @Input() + parcellation: SapiParcellationModel + + @Input() + template: SapiSpaceModel + @Input() set region(val) { this._region = val this.region$.next(this._region) - this.hasContext$.next(!!this._region?.context) + this.hasContext$.next(false) this.position = null // bug the centroid returned is currently nonsense // this.position = val?.props?.centroid_mm - if (!this._region) return - const pos = getPosFromRegion(val) + if (!val) return + const pos = val?.hasAnnotation?.bestViewPoint?.coordinates?.map(v => v.value * 1e6) if (pos) { this.position = pos } - const rgb = this._region.rgb - || (this._region.labelIndex > 65500 && [255, 255, 255]) - || strToRgb(`${this._region.ngId || this._region.name}${this._region.labelIndex}`) - || [255, 200, 200] - + let rgb = [255, 200, 200] + if (val.hasAnnotation?.displayColor) { + rgb = hexToRgb(val?.hasAnnotation?.displayColor) + } else { + rgb = strToRgb(JSON.stringify(val)) + } this.rgbString = `rgb(${rgb.join(',')})` const [_h, _s, l] = rgbToHsl(...rgb) this.rgbDarkmode = l < 0.4 + + this.dois = (val.hasAnnotation?.inspiredBy || []) + .map(insp => insp["@id"] as string) + .filter(id => /^https?:\/\/doi\.org/.test(id)) } get region(){ return this._region } - get originDatainfos(){ - if (!this._region) return [] - return (this._region._dataset_specs || []).filter(spec => spec['@type'] === 'minds/core/dataset/v1.0.0') - } public hasContext$: BehaviorSubject<boolean> = new BehaviorSubject(false) public region$: BehaviorSubject<any> = new BehaviorSubject(null) @@ -84,9 +84,8 @@ export class RegionBase { @Output() public closeRegionMenu: EventEmitter<boolean> = new EventEmitter() - public regionOriginDatasetLabels$: Observable<{ name: string }[]> public selectedAtlas$: Observable<any> = this.store$.pipe( - select(viewerStateGetSelectedAtlas) + select(atlasSelection.selectors.selectedAtlas) ) @@ -94,24 +93,19 @@ export class RegionBase { private store$: Store<any>, ) { - this.regionOriginDatasetLabels$ = combineLatest([ - this.store$, - this.region$ - ]).pipe( - map(([state, region]) => getRegionParentParcRefSpace(state, { region })), - map(({ template }) => (template && template.originalDatasetFormats) || []) - ) } public selectedTemplate$ = this.store$.pipe( - select(viewerStateSelectedTemplatePureSelector), + select(atlasSelection.selectors.selectedTemplate), ) public navigateToRegion() { this.closeRegionMenu.emit() const { region } = this this.store$.dispatch( - viewerStateNavigateToRegion({ payload: { region } }) + atlasSelection.actions.navigateToRegion({ + region + }) ) } @@ -119,101 +113,37 @@ export class RegionBase { this.closeRegionMenu.emit() const { region } = this this.store$.dispatch( - viewerStateToggleRegionSelect({ payload: { region } }) + actions.toggleRegionSelect({ + region + }) ) } public showConnectivity(regionName) { this.closeRegionMenu.emit() // ToDo trigger side panel opening with effect - this.store$.dispatch(uiStateOpenSidePanel()) - this.store$.dispatch(uiStateExpandSidePanel()) - this.store$.dispatch(uiActionShowSidePanelConnectivity()) - - this.store$.dispatch( - viewerStateSetConnectivityRegion({ connectivityRegion: regionName }) - ) + // this.store$.dispatch(uiStateOpenSidePanel()) + // this.store$.dispatch(uiStateExpandSidePanel()) + // this.store$.dispatch(uiActionShowSidePanelConnectivity()) + + // I think we can use viewerMode for this?? + // this.store$.dispatch( + // viewerStateSetConnectivityRegion({ connectivityRegion: regionName }) + // ) } - changeView(template: TSiibraExTemplate) { + changeView(template: SapiSpaceModel) { this.closeRegionMenu.emit() - - const { - parcellation - } = (this.region?.context || {}) - - /** - * TODO use createAction in future - * for now, not importing const because it breaks tests - */ this.store$.dispatch( - viewerStateSelectTemplateWithId({ - payload: { - '@id': template['@id'] || template['fullId'] - }, - config: { - selectParcellation: { - '@id': parcellation['@id'] || parcellation['fullId'] - } - } + atlasSelection.actions.viewSelRegionInNewSpace({ + region: this._region, + template, }) ) } } -export const getRegionParentParcRefSpace = createSelector( - (state: any) => state.viewerState, - viewerStateGetSelectedAtlas, - (viewerState, selectedAtlas, prop) => { - const { region: regionOfInterest } = prop - /** - * if region is undefined, return null - */ - if (!regionOfInterest || !viewerState.parcellationSelected || !viewerState.templateSelected) { - return { - template: null, - parcellation: null - } - } - /** - * first check if the region can be found in the currently selected parcellation - */ - const checkRegions = regions => { - for (const region of regions) { - - /** - * check ROI to iterating regions - */ - if (region.name === regionOfInterest.name) return true - - if (region && region.children && Array.isArray(region.children)) { - const flag = checkRegions(region.children) - if (flag) return true - } - } - return false - } - const regionInParcSelected = checkRegions(viewerState.parcellationSelected.regions) - - if (regionInParcSelected) { - const p = selectedAtlas.parcellations.find(p => p['@id'] === viewerState.parcellationSelected['@id']) - if (p) { - const refSpace = p.availableIn.find(refSpace => refSpace['@id'] === viewerState.templateSelected['@id']) - return { - template: refSpace, - parcellation: p - } - } - } - - return { - template: null, - parcellation: null - } - } -) - @Pipe({ name: 'renderViewOriginDatasetlabel' }) diff --git a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html index 979ed7a4f93b1544066e828425e241e5d341d5bb..ad8c518d75db71e06e2b116fd7ca0c34ff91a5f6 100644 --- a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html @@ -33,17 +33,13 @@ </button> <!-- explore doi --> - <ng-template let-infos [ngIf]="originDatainfos"> - <ng-container *ngFor="let info of infos"> - <a *ngFor="let url of info.urls" - [href]="url.doi | doiParserPipe" - [matTooltip]="ARIA_LABELS.EXPLORE_DATASET_IN_KG" - target="_blank" - mat-icon-button> - <i class="fas fa-external-link-alt"></i> - </a> - </ng-container> - </ng-template> + <a *ngFor="let doi of dois" + [href]="doi | doiParserPipe" + [matTooltip]="ARIA_LABELS.EXPLORE_DATASET_IN_KG" + target="_blank" + mat-icon-button> + <i class="fas fa-external-link-alt"></i> + </a> </mat-card-subtitle> @@ -53,7 +49,8 @@ <mat-accordion class="d-block mt-2"> <!-- description --> - <ng-template [ngIf]="(originDatainfos || []).length > 0"> + <!-- TODO how are we going to get desc for PEV? --> + <!-- <ng-template [ngIf]="(originDatainfos || []).length > 0"> <ng-container *ngFor="let originData of originDatainfos"> <ng-template #descTmpl> <markdown-dom [markdown]="originData.description" @@ -70,62 +67,22 @@ </ng-container> </ng-container> - </ng-template> + </ng-template> --> <!-- Explore in other template --> - <ng-template [ngIf]="region$ | async | regionInOtherTmpl" let-otherTmpls> - <ng-template #exploreInOtherTmpl> - <mat-grid-list cols="3" - rowHeight="2:3" - gutterSize="16" - class="position-relative"> - <mat-grid-tile *ngFor="let otherTmpl of otherTmpls"> - - <div [hidden] - iav-dataset-show-dataset-dialog - [iav-dataset-show-dataset-dialog-name]="otherTmpl.originDatainfos[0]?.name" - [iav-dataset-show-dataset-dialog-description]="otherTmpl.originDatainfos[0]?.description" - [iav-dataset-show-dataset-dialog-urls]="otherTmpl.originDatainfos[0]?.urls" - [iav-dataset-show-dataset-dialog-ignore-overwrite]="true" - #kgInfo="iavDatasetShowDatasetDialog"> - </div> - <tile-cmp [tile-image-src]="otherTmpl | getTemplatePreviewUrl" - class="cursor-pointer pe-all" - tile-image-alt="Preview of this tile" - [tile-text]="otherTmpl.displayName || otherTmpl.name" - [tile-show-info]="otherTmpl.originDatainfos?.length > 0" - [tile-image-darktheme]="otherTmpl | templateIsDarkTheme" - [tile-selected]="(selectedTemplate$ | async | getProperty : '@id') === otherTmpl['@id']" - (tile-on-click)="(tileCmp.selected || changeView(otherTmpl)); (tileCmp.selected || intentToChgTmpl$.next(true))" - (tile-on-info-click)="kgInfo && kgInfo.onClick()" - #tileCmp="tileCmp"> - </tile-cmp> - </mat-grid-tile> - </mat-grid-list> - - <div *ngIf="lockOtherTmpl$ | async" class="loading-overlay"> - <spinner-cmp class="spinner"></spinner-cmp> - </div> - - </ng-template> - - <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { - title: 'Explore in other templates', - desc: otherTmpls.length, - iconClass: 'fas fa-brain', - iconTooltip: otherTmpls.length | regionAccordionTooltipTextPipe : 'regionInOtherTmpl', - iavNgIf: otherTmpls.length > 0, - content: exploreInOtherTmpl - }"> - </ng-container> - </ng-template> + TODO reimplmenent explore in other templates + perhaps extract the tile into a separate ui component (?) <!-- kg regional features list --> <ng-template #kgRegionalFeatureList> - <regional-feature-wrapper [region]="region$ | async"> - </regional-feature-wrapper> + <regional-features-list + [region]="region$ | async" + [atlas]="atlas" + [template]="template" + [parcellation]="parcellation"> + </regional-features-list> </ng-template> <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { @@ -134,7 +91,7 @@ content: kgRegionalFeatureList, desc: '', iconTooltip: 'Regional Features', - iavNgIf: hasContext$ | async + iavNgIf: true }"> </ng-container> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.style.css b/src/atlasComponents/parcellationRegion/regionalFeatures/index.ts similarity index 100% rename from src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.style.css rename to src/atlasComponents/parcellationRegion/regionalFeatures/index.ts diff --git a/src/atlasComponents/parcellationRegion/regionalFeatures/module.ts b/src/atlasComponents/parcellationRegion/regionalFeatures/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ce100730a8d94dd18471110623f89364eab081d --- /dev/null +++ b/src/atlasComponents/parcellationRegion/regionalFeatures/module.ts @@ -0,0 +1,18 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { AngularMaterialModule } from "src/sharedModules"; +import { RegionalFeaturesList } from "./regionalFeaturesList/regionalFeaturesList.component"; + +@NgModule({ + imports: [ + CommonModule, + AngularMaterialModule, + ], + declarations: [ + RegionalFeaturesList + ], + exports: [ + RegionalFeaturesList + ] +}) +export class RegionalFeaturesModule{} diff --git a/src/atlasComponents/parcellationRegion/regionalFeatures/regionalFeaturesList/regionalFeaturesList.component.ts b/src/atlasComponents/parcellationRegion/regionalFeatures/regionalFeaturesList/regionalFeaturesList.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..07c94787ed920a6c58463c99b11343561813eeec --- /dev/null +++ b/src/atlasComponents/parcellationRegion/regionalFeatures/regionalFeaturesList/regionalFeaturesList.component.ts @@ -0,0 +1,57 @@ +import { Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core"; +import { BehaviorSubject, Observable, Subject } from "rxjs"; +import { filter, shareReplay, startWith, switchMap, tap } from "rxjs/operators"; +import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; + +@Component({ + selector: 'regional-features-list', + templateUrl: './regionalFeaturesList.template.html', + styleUrls: [ + './regionalFeaturesList.style.css' + ] +}) + +export class RegionalFeaturesList implements OnChanges, OnInit{ + + @Input() + atlas: SapiAtlasModel + + @Input() + template: SapiSpaceModel + + @Input() + parcellation: SapiParcellationModel + + @Input() + region: SapiRegionModel + + private ATPR$ = new BehaviorSubject<{ + atlas: SapiAtlasModel + template: SapiSpaceModel + parcellation: SapiParcellationModel + region: SapiRegionModel + }>(null) + + public listOfFeatures$: 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)), + startWith([]), + shareReplay(1), + ) + + ngOnChanges(): void { + const { atlas, template, parcellation, region } = this + this.ATPR$.next({ atlas, template, parcellation, region }) + } + + ngOnInit(): void { + // this.ngOnChanges() + } + + constructor(private sapi: SAPI){ + } +} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.style.css b/src/atlasComponents/parcellationRegion/regionalFeatures/regionalFeaturesList/regionalFeaturesList.style.css similarity index 100% rename from src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.style.css rename to src/atlasComponents/parcellationRegion/regionalFeatures/regionalFeaturesList/regionalFeaturesList.style.css diff --git a/src/atlasComponents/parcellationRegion/regionalFeatures/regionalFeaturesList/regionalFeaturesList.template.html b/src/atlasComponents/parcellationRegion/regionalFeatures/regionalFeaturesList/regionalFeaturesList.template.html new file mode 100644 index 0000000000000000000000000000000000000000..b6f9f0fb993a884b38b63cd050cd05f30331273a --- /dev/null +++ b/src/atlasComponents/parcellationRegion/regionalFeatures/regionalFeaturesList/regionalFeaturesList.template.html @@ -0,0 +1,9 @@ +<ng-template ngFor let-first="first" let-feature [ngForOf]="listOfFeatures$ | async"> + + <mat-divider *ngIf="!first"> + </mat-divider> + + <div mat-ripple class="pt-4 cursor-default"> + {{ feature.metadata.fullName }} + </div> +</ng-template> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/bsRegionInputBase.ts b/src/atlasComponents/regionalFeatures/bsFeatures/bsRegionInputBase.ts deleted file mode 100644 index 85d2e6d005bca99c7d003f148bc6a93fe7e2acb7..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/bsRegionInputBase.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { BehaviorSubject, throwError } from "rxjs"; -import { map, switchMap } from "rxjs/operators"; -import { TRegion, IBSSummaryResponse, IBSDetailResponse } from "./type"; -import { BsFeatureService, TFeatureCmpInput } from "./service"; -import { flattenReducer } from 'common/util' -import { Directive, Input } from "@angular/core"; - -@Directive() -export class BsRegionInputBase{ - - protected region$ = new BehaviorSubject<TRegion>(null) - private _region: TRegion - - @Input() - set region(val: TRegion) { - this._region = val - this.region$.next(val) - } - - get region() { - return this._region - } - - constructor( - private svc: BsFeatureService, - data?: TFeatureCmpInput - ){ - if (data) { - this.region = data.region - } - } - - protected featuresList$ = this.region$.pipe( - switchMap(region => this.svc.listFeatures(region)), - map(result => result.features.map(obj => Object.keys(obj).reduce(flattenReducer, []))) - ) - - protected getFeatureInstancesList<T extends keyof IBSSummaryResponse>(feature: T){ - if (!this._region) return throwError('#getFeatureInstancesList region needs to be defined') - return this.svc.getFeatures<T>(feature, this._region) - } - - protected getFeatureInstance<T extends keyof IBSDetailResponse>(feature: T, id: string) { - if (!this._region) return throwError('#getFeatureInstance region needs to be defined') - return this.svc.getFeature<T>(feature, this._region, id) - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts b/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts deleted file mode 100644 index 3746ac94431b1b414a26a5e5fb1d6aa3b46ab0a4..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { InjectionToken } from "@angular/core"; -import { Observable } from "rxjs"; -export { BS_ENDPOINT } from 'src/util/constants' - -export const BS_DARKTHEME = new InjectionToken<Observable<boolean>>('BS_DARKTHEME') -export const REGISTERED_FEATURE_INJECT_DATA = new InjectionToken('REGISTERED_FEATURE_INJECT_DATA') diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.component.ts deleted file mode 100644 index ca0d7c57c1c17a4fe011b05d05bd5d1cea13f6f5..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.component.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { AfterViewInit, ChangeDetectorRef, Component, Inject, Input, OnChanges, OnDestroy, Optional, TemplateRef, ViewChild, ViewContainerRef, ViewRef } from "@angular/core"; -import { BsRegionInputBase } from "../../bsRegionInputBase"; -import { KG_REGIONAL_FEATURE_KEY, TBSDetail, UNDER_REVIEW } from "../../kgRegionalFeature/type"; -import { ARIA_LABELS, CONST } from 'common/constants' -import { TBSSummary } from "../../kgDataset"; -import { BsFeatureService } from "../../service"; -import { MAT_DIALOG_DATA } from "@angular/material/dialog"; -import { TDatainfosDetail } from "src/util/siibraApiConstants/types"; -import { TRegion } from "../../type"; - -/** - * this component is specifically used to render side panel ebrains dataset view - */ - -export type TInjectableData = TDatainfosDetail & { - dataType?: string - view?: ViewRef | TemplateRef<any> - region?: TRegion - summary?: TBSSummary - isGdprProtected?: boolean -} - -@Component({ - selector: 'generic-info-cmp', - templateUrl: './genericInfo.template.html', - styleUrls: [ - './genericInfo.style.css' - ] -}) - -export class GenericInfoCmp extends BsRegionInputBase implements OnChanges, AfterViewInit, OnDestroy { - - public ARIA_LABELS = ARIA_LABELS - public CONST = CONST - - @Input() - public summary: TBSSummary - - @Input() - public detail: TBSDetail - - public loadingFlag = false - public error = null - - public nameFallback = `[This dataset cannot be fetched right now]` - public isGdprProtected = false - - public descriptionFallback = `[This dataset cannot be fetched right now]` - public useClassicUi = false - public dataType = 'ebrains regional dataset' - - public description: string - public name: string - public urls: { - cite: string - doi: string - }[] - - public doiUrls: { - cite: string - doi: string - }[] - - template: TemplateRef<any> - viewref: ViewRef - - @ViewChild('insertViewTarget', { read: ViewContainerRef }) - insertedViewVCR: ViewContainerRef - - constructor( - svc: BsFeatureService, - private cdr: ChangeDetectorRef, - @Optional() @Inject(MAT_DIALOG_DATA) data: TInjectableData - ){ - super(svc) - if (data) { - const { dataType, description, name, urls, useClassicUi, view, region, summary, isGdprProtected } = data - this.description = description - this.name = name - this.urls = urls || [] - this.doiUrls = this.urls.filter(d => !!d.doi) - this.useClassicUi = useClassicUi - if (dataType) this.dataType = dataType - if (typeof isGdprProtected !== 'undefined') this.isGdprProtected = isGdprProtected - - if (!!view) { - if (view instanceof TemplateRef){ - this.template = view - } else { - this.viewref = view - } - } - - if (region && summary) { - this.region = region - this.summary = summary - this.ngOnChanges() - } - } - } - - ngOnDestroy(){ - this.insertedViewVCR.clear() - } - - ngAfterViewInit(){ - if (this.insertedViewVCR && this.viewref) { - this.insertedViewVCR.insert(this.viewref) - } - } - - ngOnChanges(){ - if (!this.region) return - if (!this.summary) return - if (!!this.detail) return - this.loadingFlag = true - this.getFeatureInstance(KG_REGIONAL_FEATURE_KEY, this.summary['@id']).subscribe( - detail => { - this.detail = detail - - this.name = this.detail.src_name - this.description = this.detail.__detail?.description - this.urls = this.detail.__detail.kgReference.map(url => { - return { cite: null, doi: url } - }) - - this.isGdprProtected = detail.__detail.embargoStatus && detail.__detail.embargoStatus.some(status => status["@id"] === UNDER_REVIEW["@id"]) - }, - err => { - this.error = err.toString() - }, - () => { - this.loadingFlag = false - this.cdr.markForCheck() - } - ) - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/index.ts b/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/index.ts deleted file mode 100644 index d38b8440733fc665a74c75e3d9fa574ec90dfe26..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { GenericInfoModule } from './module' -export { GenericInfoCmp, TInjectableData } from './genericInfoCmp/genericInfo.component' diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/module.ts deleted file mode 100644 index 7f1391e10a378a65c95fd3c8261a5d35509106bf..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/module.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component, Inject, NgModule, Optional } from "@angular/core"; -import { MAT_DIALOG_DATA } from "@angular/material/dialog"; -import { ComponentsModule } from "src/components"; -import { AngularMaterialModule } from "src/sharedModules"; -import { UtilModule } from "src/util"; -import { IAV_DATASET_SHOW_DATASET_DIALOG_CMP } from "../kgDataset/showDataset/showDataset.directive"; -import { GenericInfoCmp } from "./genericInfoCmp/genericInfo.component"; - -@Component({ - selector: 'show-ds-dialog-cmp', - template: ` -<ng-template [ngIf]="useClassicUi" [ngIfElse]="modernUiTmpl"> - <generic-info-cmp></generic-info-cmp> -</ng-template> - -<ng-template #modernUiTmpl> - - <mat-dialog-content class="m-0 p-0"> - <generic-info-cmp></generic-info-cmp> - </mat-dialog-content> - - <mat-dialog-actions align="center"> - <button mat-button mat-dialog-close> - Close - </button> - </mat-dialog-actions> - -</ng-template> -` -}) - -export class ShowDsDialogCmp{ - public useClassicUi = false - constructor( - @Optional() @Inject(MAT_DIALOG_DATA) data: any - ){ - this.useClassicUi = data.useClassicUi - } -} - -@NgModule({ - imports: [ - CommonModule, - AngularMaterialModule, - UtilModule, - ComponentsModule, - ], - declarations: [ - GenericInfoCmp, - ShowDsDialogCmp, - ], - exports: [ - GenericInfoCmp, - ], - - providers: [ - { - provide: IAV_DATASET_SHOW_DATASET_DIALOG_CMP, - useValue: ShowDsDialogCmp - } - ] -}) - -export class GenericInfoModule{} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.component.ts deleted file mode 100644 index 1694e857d446519312a46d43254edd66323627ee..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.component.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Component, Inject, OnDestroy, Optional } from "@angular/core"; -import { Store } from "@ngrx/store"; -import { BehaviorSubject, forkJoin, merge, Observable, of, Subscription } from "rxjs"; -import { catchError, mapTo, switchMap } from "rxjs/operators"; -import { viewerStateAddUserLandmarks, viewerStateChangeNavigation, viewreStateRemoveUserLandmarks } from "src/services/state/viewerState/actions"; -import { BsRegionInputBase } from "../../bsRegionInputBase"; -import { REGISTERED_FEATURE_INJECT_DATA } from "../../constants"; -import { BsFeatureService, TFeatureCmpInput } from "../../service"; -import { SIIBRA_FEATURE_KEY, TContactPoint, TElectrode, TBSIeegSessionDetail } from '../type' -import { ARIA_LABELS, CONST } from 'common/constants' - -@Component({ - selector: 'bs-feature-ieeg.cmp', - templateUrl: './ieeg.template.html', - styleUrls: [ - './ieeg.style.css' - ] -}) - -export class BsFeatureIEEGCmp extends BsRegionInputBase implements OnDestroy{ - - public ARIA_LABELS = ARIA_LABELS - public CONST = CONST - - private featureId: string - - private results: TBSIeegSessionDetail[] = [] - constructor( - private store: Store<any>, - svc: BsFeatureService, - @Optional() @Inject(REGISTERED_FEATURE_INJECT_DATA) data: TFeatureCmpInput, - ){ - super(svc, data) - if (data.featureId) this.featureId = data.featureId - this.subs.push( - this.results$.subscribe(results => { - this.results = results - this.loadLandmarks() - }) - ) - } - - public results$: Observable<TBSIeegSessionDetail[]> = this.region$.pipe( - switchMap(() => this.getFeatureInstancesList(SIIBRA_FEATURE_KEY).pipe( - switchMap(arr => forkJoin(arr.filter(it => { - if (!this.featureId) return true - return it['@id'] === this.featureId - }).map(it => this.getFeatureInstance(SIIBRA_FEATURE_KEY, it["@id"])))), - catchError(() => of([])) - )), - ) - - public busy$ = this.region$.pipe( - switchMap(() => merge( - of(true), - this.results$.pipe( - mapTo(false) - ) - )), - ) - - private subs: Subscription[] = [] - ngOnDestroy(){ - this.unloadLandmarks() - while(this.subs.length) this.subs.pop().unsubscribe() - } - private openElectrodeSet = new Set<TElectrode>() - public openElectrode$ = new BehaviorSubject<TElectrode[]>([]) - handleDatumExpansion(electrode: TElectrode, state: boolean) { - if (state) this.openElectrodeSet.add(electrode) - else this.openElectrodeSet.delete(electrode) - this.openElectrode$.next(Array.from(this.openElectrodeSet)) - this.loadLandmarks() - } - - private loadedLms: { - '@id': string - id: string - name: string - position: [number, number, number] - color: [number, number, number] - showInSliceView: boolean - }[] = [] - - private unloadLandmarks(){ - /** - * unload all the landmarks first - */ - this.store.dispatch( - viewreStateRemoveUserLandmarks({ - payload: { - landmarkIds: this.loadedLms.map(l => l['@id']) - } - }) - ) - } - - private loadLandmarks(){ - this.unloadLandmarks() - this.loadedLms = [] - - const lms = [] as { - '@id': string - id: string - name: string - position: [number, number, number] - color: [number, number, number] - showInSliceView: boolean - }[] - - for (const detail of this.results) { - const subjectKey = detail.sub_id - for (const electrodeId in detail.electrodes){ - const electrode = detail.electrodes[electrodeId] - if (!electrode.inRoi) continue - for (const cpKey in electrode.contact_points) { - const cp = electrode.contact_points[cpKey] - const id=`${detail.name}:${subjectKey}#${cpKey}` - lms.push({ - "@id": id, - id: id, - name: id, - position: cp.location, - color: cp.inRoi ? [255, 100, 100]: [255, 255, 255], - showInSliceView: this.openElectrodeSet.has(electrode) - }) - } - } - } - this.loadedLms = lms - - this.store.dispatch( - viewerStateAddUserLandmarks({ - landmarks: lms - }) - ) - } - - handleContactPtClk(cp: TContactPoint) { - const { location } = cp - this.store.dispatch( - viewerStateChangeNavigation({ - navigation: { - position: location.map(v => v * 1e6), - positionReal: true, - animation: {} - }, - }) - ) - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.template.html deleted file mode 100644 index 2622d200acae890dc47352ef6f0859ccc7125369..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.template.html +++ /dev/null @@ -1,67 +0,0 @@ -<ng-template [ngIf]="busy$ | async" [ngIfElse]="contenttmpl"> - <spinner-cmp></spinner-cmp> -</ng-template> - -<ng-template #contenttmpl> - <ng-container *ngFor="let result of results$ | async"> - <ng-container *ngFor="let electrodeKeyVal of result | getProperty : 'electrodes' | keyvalue"> - <ng-template [ngIf]="electrodeKeyVal.value.inRoi"> - <ng-container *ngTemplateOutlet="electrodeTmpl; context: { $implicit: electrodeKeyVal.value }"> - </ng-container> - </ng-template> - - </ng-container> - </ng-container> -</ng-template> - -<!-- template for electrode --> -<ng-template #electrodeTmpl let-electrode> - - <mat-expansion-panel - [expanded]="openElectrode$ | async | includes : electrode" - (opened)="handleDatumExpansion(electrode, true)" - (closed)="handleDatumExpansion(electrode, false)" - togglePosition="before"> - <mat-expansion-panel-header> - <mat-panel-title> - Electrode - </mat-panel-title> - <mat-panel-description class="text-nowrap"> - {{ electrode.electrode_id }} - </mat-panel-description> - </mat-expansion-panel-header> - - - <!-- <label for="task-list" class="d-block mat-h4 mt-4 text-muted"> - Tasks - </label> - <section class="d-flex align-items-center mt-1"> - <section id="task-list" class="flex-grow-1 flex-shrink-1 overflow-x-auto"> - <div role="list"> - <mat-chip *ngFor="let task of datum['tasks']" class="ml-1"> - {{ task }} - </mat-chip> - </div> - </section> - </section> --> - - <mat-divider></mat-divider> - - <label for="contact-points-list" class="d-block mat-h4 mt-4 text-muted"> - Contact Points - </label> - <section class="d-flex align-items-center mt-1"> - <section id="contact-points-list" class="flex-grow-1 flex-shrink-1 overflow-x-auto"> - <div role="list"> - <mat-chip *ngFor="let cp_kv of electrode.contact_points | keyvalue" - [matTooltip]="cp_kv['value']['location']" - (click)="handleContactPtClk(cp_kv['value'])" - class="ml-1"> - {{ cp_kv['key'] }} - </mat-chip> - </div> - </section> - </section> - - </mat-expansion-panel> -</ng-template> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCtrl.directive.ts b/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCtrl.directive.ts deleted file mode 100644 index e7e79ef4a97103ec38993712490cd0ebbb6ebb1b..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCtrl.directive.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Directive, Inject, OnDestroy, Optional } from "@angular/core"; -import { merge, Observable, of, Subscription } from "rxjs"; -import { catchError, mapTo, switchMap } from "rxjs/operators"; -import { BsRegionInputBase } from "../bsRegionInputBase"; -import { REGISTERED_FEATURE_INJECT_DATA } from "../constants"; -import { BsFeatureService, TFeatureCmpInput } from "../service"; -import { IBSSummaryResponse, IRegionalFeatureReadyDirective } from "../type"; -import { SIIBRA_FEATURE_KEY } from './type' - -@Directive({ - selector: '[bs-features-ieeg-directive]', - exportAs: 'bsFeatureIeegDirective' -}) - -export class BsFeatureIEEGDirective extends BsRegionInputBase implements IRegionalFeatureReadyDirective, OnDestroy{ - - public results$: Observable<IBSSummaryResponse['IEEG_Session'][]> = this.region$.pipe( - switchMap(() => this.getFeatureInstancesList(SIIBRA_FEATURE_KEY).pipe( - catchError(() => of([])) - )), - ) - public busy$ = this.region$.pipe( - switchMap(() => merge( - of(true), - this.results$.pipe( - mapTo(false) - ) - )) - ) - - constructor( - svc: BsFeatureService, - @Optional() @Inject(REGISTERED_FEATURE_INJECT_DATA) data: TFeatureCmpInput, - ){ - super(svc, data) - } - - private sub: Subscription[] = [] - ngOnDestroy(){ - while(this.sub.length) this.sub.pop().unsubscribe() - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/index.ts b/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/index.ts deleted file mode 100644 index e4e8322a07b0f6a2a25ce0b8522e5d2550b2f09b..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { - BsFeatureIEEGModule -} from './module' - -export { - IEEG_FEATURE_NAME, - SIIBRA_FEATURE_KEY, -} from './type' diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/module.ts deleted file mode 100644 index 50ea68c3c313509efacea5473ad554e181bddc40..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; -import { ComponentsModule } from "src/components"; -import { AngularMaterialModule } from "src/sharedModules"; -import { UtilModule } from "src/util"; -import { BsFeatureService } from "../service"; -import { BsFeatureIEEGCmp } from "./ieegCmp/ieeg.component"; -import { BsFeatureIEEGDirective } from "./ieegCtrl.directive"; -import { IEEG_FEATURE_NAME } from "./type"; - -@NgModule({ - imports: [ - CommonModule, - ComponentsModule, - UtilModule, - AngularMaterialModule, - ], - declarations: [ - BsFeatureIEEGCmp, - BsFeatureIEEGDirective - ] -}) - -export class BsFeatureIEEGModule{ - constructor(svc: BsFeatureService){ - svc.registerFeature({ - name: IEEG_FEATURE_NAME, - icon: 'fas fa-info', - View: BsFeatureIEEGCmp, - Ctrl: BsFeatureIEEGDirective - }) - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/type.ts b/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/type.ts deleted file mode 100644 index 8300f694dde812df13206c56c403bd06ebb59be7..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/type.ts +++ /dev/null @@ -1,45 +0,0 @@ -export type TBSSummary = { - '@id': string - name: string - description: string -} - -export type TContactPoint = { - id: string - location: [number, number, number] - inRoi?: boolean -} - -export type TElectrode = { - electrode_id: string - subject_id: string - contact_points: { - [key: string]: TContactPoint - } - inRoi?: boolean -} - -export type TBSIeegSessionSummary = { - '@id': string - name: string - description: string - origin_datainfos: { - urls: { - doi: string - }[] - }[] -} - -type TDetail = { - sub_id: string - electrodes: { - [key: string]: TElectrode - } - inRoi?: boolean -} - -export type TBSIeegSessionDetail = TBSIeegSessionSummary & TDetail - -export const SIIBRA_FEATURE_KEY = 'IEEG_Session' -export const _SIIBRA_FEATURE_KEY = 'IEEG_Electrode' -export const IEEG_FEATURE_NAME = 'iEEG recordings' diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/index.ts b/src/atlasComponents/regionalFeatures/bsFeatures/index.ts deleted file mode 100644 index e571d3c4272b3c27af2343832de0f8d8aabd2399..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { BSFeatureModule } from './module' -export { BS_ENDPOINT, BS_DARKTHEME } from './constants' -export { TRegion } from './type' -// nb do not export BsRegionInputBase from here -// will result in cyclic imports \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/getTrailingHex.pipe.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/getTrailingHex.pipe.ts deleted file mode 100644 index 5314267e7e1f31b8226ab6eea1ac890277781a68..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/getTrailingHex.pipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; - -@Pipe({ - name: 'getTrailingHex', - pure: true -}) - -export class GetTrailingHexPipe implements PipeTransform{ - public transform(input: string) { - const match = /[0-9a-f-]+$/.exec(input) - return match && match[0] - } -} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/index.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/index.ts deleted file mode 100644 index 337cdf646dd4827cc0f0a9a14f6637d0f4cb129e..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { KgDatasetModule } from './module' -export { TCountedDataModality, TBSDetail, TBSSummary } from './type' \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.spec.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.spec.ts deleted file mode 100644 index 9618701aaa39492065095230cc37d7b524d3fbec..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { TCountedDataModality } from "../type" -import { SortModalityAlphabeticallyPipe } from "./modalityPicker.component" - -describe('> modalityPicker.component.ts', () => { - describe('> ModalityPicker', () => { - // TODO - }) - - describe('> SortModalityAlphabeticallyPipe', () => { - - const mods: TCountedDataModality[] = [{ - name: 'bbb', - occurance: 0, - visible: false - }, { - name: 'AAA', - occurance: 1, - visible: false - }, { - name: '007', - occurance: 17, - visible: false - }] - const beforeInput = [...mods] - const pipe = new SortModalityAlphabeticallyPipe() - - const output = pipe.transform(mods) - - it('> does not mutate', () => { - expect(mods).toEqual(beforeInput) - }) - it('> should sort modalities as expected', () => { - expect(output).toEqual([ - mods[2], mods[1], mods[0] - ]) - }) - }) -}) diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.ts deleted file mode 100644 index c00a636533018ec616110d4f366d967144254afa..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Component, EventEmitter, Input, OnChanges, Output, Pipe, PipeTransform } from "@angular/core"; -import { TCountedDataModality } from "../type"; -import { ARIA_LABELS } from 'common/constants' - - -@Component({ - selector: 'modality-picker', - templateUrl: './modalityPicker.template.html', - styleUrls: [ - './modalityPicker.style.css', - ], - host:{ - 'aria-label': ARIA_LABELS.LIST_OF_MODALITIES - } -}) - -export class ModalityPicker implements OnChanges { - - public modalityVisibility: Set<string> = new Set() - - @Input() - public countedDataM: TCountedDataModality[] = [] - - public checkedModality: TCountedDataModality[] = [] - - @Output() - public modalityFilterEmitter: EventEmitter<TCountedDataModality[]> = new EventEmitter() - - // filter(dataentries:DataEntry[]) { - // return this.modalityVisibility.size === 0 - // ? dataentries - // : dataentries.filter(de => de.activity.some(a => a.methods.some(m => this.modalityVisibility.has(this.dbService.temporaryFilterDataentryName(m))))) - // } - - public ngOnChanges() { - this.checkedModality = this.countedDataM.filter(d => d.visible) - } - - /** - * TODO - * togglemodailty should emit event, and let parent handle state - */ - public toggleModality(modality: Partial<TCountedDataModality>) { - this.modalityFilterEmitter.emit( - this.countedDataM.map(d => d.name === modality.name - ? { - ...d, - visible: !d.visible, - } - : d), - ) - } - - public uncheckModality(modality: string) { - this.toggleModality({name: modality}) - } - - public clearAll() { - this.modalityFilterEmitter.emit( - this.countedDataM.map(d => { - return { - ...d, - visible: false, - } - }), - ) - } -} - -const sortByFn = (a: TCountedDataModality, b: TCountedDataModality) => (a.name || '0').toLowerCase().charCodeAt(0) - (b.name || '0').toLowerCase().charCodeAt(0) - -@Pipe({ - name: 'sortModalityAlphabetically', - pure: true -}) - -export class SortModalityAlphabeticallyPipe implements PipeTransform{ - public transform(arr: TCountedDataModality[]): TCountedDataModality[]{ - return [...arr].sort(sortByFn) - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.style.css b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.style.css deleted file mode 100644 index 85feb59e81b8a38ede569688b605d82e39932cca..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.style.css +++ /dev/null @@ -1,10 +0,0 @@ -:host -{ - display: flex; - flex-direction: column; -} - -div -{ - white-space: nowrap; -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.template.html deleted file mode 100644 index 7bde04e8b2cc46f0872d978914bf9c254e79113a..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.template.html +++ /dev/null @@ -1,14 +0,0 @@ -<fieldset> - <legend class="sr-only"> - Dataset modalities - </legend> - - <mat-checkbox - - [checked]="datamodality.visible" - (change)="toggleModality(datamodality)" - [ngClass]="{'muted': datamodality.occurance === 0}" - *ngFor="let datamodality of countedDataM | sortModalityAlphabetically"> - {{ datamodality.name }} <span class="text-muted">({{ datamodality.occurance }})</span> - </mat-checkbox> -</fieldset> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/module.ts deleted file mode 100644 index 232dba3734ce321bc2fd3a9fb1a5c52d0d4c9256..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; -import { ShowDatasetDialogDirective } from "./showDataset/showDataset.directive"; -import { AngularMaterialModule } from "src/sharedModules"; -import { GetTrailingHexPipe } from "./getTrailingHex.pipe"; -import { ModalityPicker, SortModalityAlphabeticallyPipe } from "./modalityPicker/modalityPicker.component"; - -// TODO break down into smaller components -@NgModule({ - imports: [ - CommonModule, - AngularMaterialModule, - ], - declarations: [ - ShowDatasetDialogDirective, - GetTrailingHexPipe, - ModalityPicker, - SortModalityAlphabeticallyPipe, - ], - exports: [ - ShowDatasetDialogDirective, - GetTrailingHexPipe, - ModalityPicker, - ] -}) - -export class KgDatasetModule{} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.spec.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.spec.ts deleted file mode 100644 index e37fa07aa72c7366814b10ecdbebb458118795c5..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Component } from "@angular/core"; -import { async, TestBed } from "@angular/core/testing"; -import { AngularMaterialModule } from "src/sharedModules"; -import { ShowDatasetDialogDirective, IAV_DATASET_SHOW_DATASET_DIALOG_CMP } from "./showDataset.directive"; -import { By } from "@angular/platform-browser"; -import { MatDialog } from "@angular/material/dialog"; -import { MatSnackBar } from "@angular/material/snack-bar"; - -@Component({ - template: '' -}) - -class TestCmp{} - -const dummyMatDialog = { - open: val => {} -} - -const dummyMatSnackBar = { - open: val => {} -} - -class DummyDialogCmp{} - -describe('ShowDatasetDialogDirective', () => { - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - AngularMaterialModule - ], - declarations: [ - TestCmp, - ShowDatasetDialogDirective, - ], - providers: [ - { - provide: MatDialog, - useValue: dummyMatDialog - }, - { - provide: MatSnackBar, - useValue: dummyMatSnackBar - }, - { - provide: IAV_DATASET_SHOW_DATASET_DIALOG_CMP, - useValue: DummyDialogCmp - } - ] - }) - })) - - it('should be able to test directiv,', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: '<div iav-dataset-show-dataset-dialog></div>' - } - }).compileComponents() - - const fixutre = TestBed.createComponent(TestCmp) - const directive = fixutre.debugElement.query( By.directive( ShowDatasetDialogDirective ) ) - - expect(directive).not.toBeNull() - }) - - it('if neither kgId nor fullId is defined, should not call dialog', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: '<div iav-dataset-show-dataset-dialog></div>' - } - }).compileComponents() - - const snackbarOpenSpy = spyOn(dummyMatSnackBar, 'open').and.callThrough() - const dialogOpenSpy = spyOn(dummyMatDialog, 'open').and.callThrough() - - const fixutre = TestBed.createComponent(TestCmp) - fixutre.detectChanges() - - const directive = fixutre.debugElement.query( By.directive( ShowDatasetDialogDirective ) ) - directive.nativeElement.click() - - expect(snackbarOpenSpy).toHaveBeenCalled() - expect(dialogOpenSpy).not.toHaveBeenCalled() - - snackbarOpenSpy.calls.reset() - dialogOpenSpy.calls.reset() - }) - - it('if kgId is defined, should call dialogOpen', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: ` - <div iav-dataset-show-dataset-dialog - iav-dataset-show-dataset-dialog-kgid="aaa-bbb"> - </div> - ` - } - }).compileComponents() - - const snackbarOpenSpy = spyOn(dummyMatSnackBar, 'open').and.callThrough() - const dialogOpenSpy = spyOn(dummyMatDialog, 'open').and.callThrough() - - const fixutre = TestBed.createComponent(TestCmp) - fixutre.detectChanges() - - const directive = fixutre.debugElement.query( By.directive( ShowDatasetDialogDirective ) ) - directive.nativeElement.click() - - expect(snackbarOpenSpy).not.toHaveBeenCalled() - const mostRecentCall = dialogOpenSpy.calls.mostRecent() - const args = mostRecentCall.args as any[] - - expect(args[0]).toEqual(DummyDialogCmp) - expect(args[1]).toEqual({ - ...ShowDatasetDialogDirective.defaultDialogConfig, - panelClass: ['no-padding-dialog'], - data: { - fullId: `minds/core/dataset/v1.0.0/aaa-bbb` - } - }) - - snackbarOpenSpy.calls.reset() - dialogOpenSpy.calls.reset() - }) - - it('if fullId is defined, should call dialogOpen', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: ` - <div iav-dataset-show-dataset-dialog - iav-dataset-show-dataset-dialog-fullid="abc/ccc-ddd"> - </div> - ` - } - }).compileComponents() - - const snackbarOpenSpy = spyOn(dummyMatSnackBar, 'open').and.callThrough() - const dialogOpenSpy = spyOn(dummyMatDialog, 'open').and.callThrough() - - const fixutre = TestBed.createComponent(TestCmp) - fixutre.detectChanges() - - const directive = fixutre.debugElement.query( By.directive( ShowDatasetDialogDirective ) ) - directive.nativeElement.click() - - expect(snackbarOpenSpy).not.toHaveBeenCalled() - const mostRecentCall = dialogOpenSpy.calls.mostRecent() - const args = mostRecentCall.args as any[] - expect(args[0]).toEqual(DummyDialogCmp) - expect(args[1]).toEqual({ - ...ShowDatasetDialogDirective.defaultDialogConfig, - panelClass: ['no-padding-dialog'], - data: { - fullId: `abc/ccc-ddd` - } - }) - - snackbarOpenSpy.calls.reset() - dialogOpenSpy.calls.reset() - }) -}) \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/type.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/type.ts deleted file mode 100644 index e56921ce5c5e1e9adc110ba666dcf1d2eb192be3..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/type.ts +++ /dev/null @@ -1,82 +0,0 @@ -export type TCountedDataModality = { - name: string - occurance: number - visible: boolean -} - -export type TBSSummary = { - ['@id']: string - src_name: string -} - -export type TBSDetail = TBSSummary & { - __detail: { - formats: string[] - datasetDOI: { - cite: string - doi: string - }[] - activity: { - protocols: string[] - preparation: string[] - }[] - referenceSpaces: { - name: string - fullId: string - }[] - methods: string[] - custodians: { - "schema.org/shortName": string - identifier: string - name: string - '@id': string - shortName: string - }[] - project: string[] - description: string - parcellationAtlas: { - name: string - fullId: string - id: string[] - }[] - licenseInfo: { - name: string - url: string - }[] - embargoStatus: { - identifier: string[] - name: string - '@id': string - }[] - license: any[] - parcellationRegion: { - species: any[] - name: string - alias: string - fullId: string - }[] - species: string[] - name: string - files: { - byteSize: number - name: string - absolutePath: string - contentType: string - }[] - fullId: string - contributors: { - "schema.org/shortName": string - identifier: string - name: string - '@id': string - shortName: string - }[] - id: string - kgReference: string[] // aka doi - publications: { - name: string - cite: string - doi: string - }[] - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/index.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/index.ts deleted file mode 100644 index 3efa1d80ed5927d3777da4cc28faad043ce7795b..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { - KgRegionalFeatureModule -} from './module' - -export { - EbrainsRegionalFeatureName, - KG_REGIONAL_FEATURE_KEY, - UNDER_REVIEW, - TBSDetail, - TBSSummary -} from './type' diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.component.ts deleted file mode 100644 index fd5b0afac18cbfd515e7ef21718e71084f75c0b2..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.component.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { ChangeDetectionStrategy, Component, Inject, Input, OnDestroy, Optional } from "@angular/core"; -import { BehaviorSubject, Subscription } from "rxjs"; -import { filter, switchMap, tap } from "rxjs/operators"; -import { TCountedDataModality } from '../../kgDataset' -import { BsRegionInputBase } from "../../bsRegionInputBase"; -import { BsFeatureService, TFeatureCmpInput } from "../../service"; -import { KG_REGIONAL_FEATURE_KEY, TBSDetail, TBSSummary } from "../type"; -import { ARIA_LABELS } from 'common/constants' -import { REGISTERED_FEATURE_INJECT_DATA } from "../../constants"; - -@Component({ - selector: 'kg-regional-features-list', - templateUrl: './kgRegList.template.html', - styleUrls: [ - './kgRegList.style.css' - ], - changeDetection: ChangeDetectionStrategy.OnPush -}) - -export class KgRegionalFeaturesList extends BsRegionInputBase implements OnDestroy{ - - public ARIA_LABELS = ARIA_LABELS - - public dataModalities: TCountedDataModality[] = [] - - @Input() - public disableVirtualScroll = false - - public visibleRegionalFeatures: TBSSummary[] = [] - public kgRegionalFeatures: TBSSummary[] = [] - public kgRegionalFeatures$ = this.region$.pipe( - filter(v => { - this.busy$.next(false) - return !!v - }), - // must not use switchmapto here - switchMap(() => { - this.busy$.next(true) - return this.getFeatureInstancesList(KG_REGIONAL_FEATURE_KEY).pipe( - tap(() => { - this.busy$.next(false) - }) - ) - }) - ) - constructor( - svc: BsFeatureService, - @Optional() @Inject(REGISTERED_FEATURE_INJECT_DATA) data: TFeatureCmpInput - ){ - super(svc, data) - this.sub.push( - this.kgRegionalFeatures$.subscribe(val => { - this.kgRegionalFeatures = val - this.visibleRegionalFeatures = val - }) - ) - } - private sub: Subscription[] = [] - ngOnDestroy(){ - while (this.sub.length) this.sub.pop().unsubscribe() - } - - public trackByFn(_index: number, dataset: TBSSummary) { - return dataset['@id'] - } - - public detailDict: { - [key: string]: TBSDetail - } = {} - - public handlePopulatedDetailEv(detail: TBSDetail){ - this.detailDict = { - ...this.detailDict, - [detail["@id"]]: detail - } - for (const method of detail.__detail.methods) { - const found = this.dataModalities.find(v => v.name === method) - if (found) found.occurance = found.occurance + 1 - else this.dataModalities.push({ - name: method, - occurance: 1, - visible: false - }) - } - this.dataModalities = [...this.dataModalities] - } - - public busy$ = new BehaviorSubject(false) -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.style.css b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.style.css deleted file mode 100644 index a0d49c32422a4789531e0cdaf7bf02fd4d43e20f..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.style.css +++ /dev/null @@ -1,9 +0,0 @@ -cdk-virtual-scroll-viewport -{ - min-height: 24rem; -} - -modality-picker -{ - font-size: 90%; -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.template.html deleted file mode 100644 index 010f4c85971f683525a8f3a6ca7026d648ea0e45..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.template.html +++ /dev/null @@ -1,30 +0,0 @@ -<spinner-cmp *ngIf="busy$ | async; else contentTmpl"></spinner-cmp> - -<ng-template #contentTmpl> - <cdk-virtual-scroll-viewport - [attr.aria-label]="ARIA_LABELS.LIST_OF_DATASETS_ARIA_LABEL" - class="h-100" - minBufferPx="200" - maxBufferPx="400" - itemSize="50"> - <div *cdkVirtualFor="let dataset of visibleRegionalFeatures; trackBy: trackByFn; templateCacheSize: 20; let index = index" - class="h-50px overflow-hidden"> - - <!-- divider, show if not first --> - <mat-divider class="mt-1" *ngIf="index !== 0"></mat-divider> - - <kg-regional-feature-summary - mat-ripple - iav-dataset-show-dataset-dialog - [iav-dataset-show-dataset-dialog-fullid]="dataset['@id']" - [iav-dataset-show-dataset-dialog-contexted-region]="region" - class="d-block pb-1 pt-1" - [region]="region" - [loadFull]="false" - [summary]="dataset" - (loadedDetail)="handlePopulatedDetailEv($event)"> - </kg-regional-feature-summary> - - </div> - </cdk-virtual-scroll-viewport> -</ng-template> \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgReglist.directive.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgReglist.directive.ts deleted file mode 100644 index df51bbb7f7c90d589e6e5c843d25f3a7f711f027..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgReglist.directive.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Directive, EventEmitter, Inject, OnDestroy, Optional, Output } from "@angular/core"; -import { KG_REGIONAL_FEATURE_KEY, TBSSummary } from "../type"; -import { BsFeatureService, TFeatureCmpInput } from "../../service"; -import { BsRegionInputBase } from "../../bsRegionInputBase"; -import { merge, of, Subscription } from "rxjs"; -import { catchError, mapTo, startWith, switchMap, tap } from "rxjs/operators"; -import { IRegionalFeatureReadyDirective } from "../../type"; -import { REGISTERED_FEATURE_INJECT_DATA } from "../../constants"; - -@Directive({ - selector: '[kg-regional-features-list-directive]', - exportAs: 'kgRegionalFeaturesListDirective' -}) - -export class KgRegionalFeaturesListDirective extends BsRegionInputBase implements IRegionalFeatureReadyDirective, OnDestroy { - public kgRegionalFeatures: TBSSummary[] = [] - public kgRegionalFeatures$ = this.region$.pipe( - // must not use switchmapto here - switchMap(() => { - this.busyEmitter.emit(true) - return this.getFeatureInstancesList(KG_REGIONAL_FEATURE_KEY).pipe( - catchError(() => of([])), - tap(() => { - this.busyEmitter.emit(false) - }), - ) - }), - startWith([]) - ) - - constructor( - svc: BsFeatureService, - @Optional() @Inject(REGISTERED_FEATURE_INJECT_DATA) data: TFeatureCmpInput, - ){ - super(svc, data) - this.sub.push( - this.kgRegionalFeatures$.subscribe(val => { - this.kgRegionalFeatures = val - }) - ) - } - private sub: Subscription[] = [] - ngOnDestroy(){ - while (this.sub.length) this.sub.pop().unsubscribe() - } - - results$ = this.kgRegionalFeatures$ - busy$ = this.region$.pipe( - switchMap(() => merge( - of(true), - this.results$.pipe( - mapTo(false) - ) - )) - ) - - @Output('kg-regional-features-list-directive-busy') - busyEmitter = new EventEmitter<boolean>() -} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.component.ts deleted file mode 100644 index 841226ad69f75fd821954f624878c4e1a4081de6..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.component.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Component, EventEmitter, Input, OnChanges, Output } from "@angular/core"; -import { BsRegionInputBase } from "../../bsRegionInputBase"; -import { BsFeatureService } from "../../service"; -import { KG_REGIONAL_FEATURE_KEY, TBSDetail, TBSSummary } from '../type' - -@Component({ - selector: 'kg-regional-feature-summary', - templateUrl: './kgRegSummary.template.html', - styleUrls: [ - './kgRegSummary.style.css' - ], - exportAs: 'kgRegionalFeatureSummary' -}) - -export class KgRegSummaryCmp extends BsRegionInputBase implements OnChanges{ - - @Input() - public loadFull = false - - @Input() - public summary: TBSSummary = null - - public detailLoaded = false - public loadingDetail = false - public detail: TBSDetail = null - @Output() - public loadedDetail = new EventEmitter<TBSDetail>() - - public error: string = null - @Output() - public errorEmitter = new EventEmitter<string>() - - constructor(svc: BsFeatureService){ - super(svc) - } - - ngOnChanges(){ - if (this.loadFull && !!this.summary) { - if (this.loadingDetail || this.detailLoaded) { - return - } - this.loadingDetail = true - this.getFeatureInstance(KG_REGIONAL_FEATURE_KEY, this.summary["@id"]).subscribe( - detail => { - this.detail = detail - this.loadedDetail.emit(detail) - }, - err => { - this.error = err - this.errorEmitter.emit(err) - }, - () => { - this.detailLoaded = true - this.loadingDetail = false - } - ) - } - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.template.html deleted file mode 100644 index 5fcb11b2b8f7a550e66a3491430a0629d62f7533..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.template.html +++ /dev/null @@ -1,3 +0,0 @@ -<small> - {{ summary.src_name }} -</small> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/module.ts deleted file mode 100644 index 878eed872ca8ce48c90e6931eeb4351a2fd44892..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/module.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Inject, NgModule, Optional } from "@angular/core"; -import { AngularMaterialModule } from "src/sharedModules"; -import { KgRegSummaryCmp } from "./kgRegSummary/kgRegSummary.component"; -import { KgRegionalFeaturesList } from "./kgRegList/kgRegList.component"; -import { KgRegionalFeaturesListDirective } from "./kgRegList/kgReglist.directive"; -import { KgDatasetModule } from "../kgDataset"; -import { UtilModule } from "src/util"; -import { ComponentsModule } from "src/components"; -import { BsFeatureService } from "../service"; -import { EbrainsRegionalFeatureName } from "./type"; -import { GENERIC_INFO_INJ_TOKEN } from "../type"; - -@NgModule({ - imports: [ - CommonModule, - AngularMaterialModule, - KgDatasetModule, - UtilModule, - ComponentsModule, - ], - declarations:[ - KgRegSummaryCmp, - KgRegionalFeaturesList, - KgRegionalFeaturesListDirective, - ], - exports:[ - KgRegSummaryCmp, - KgRegionalFeaturesList, - KgRegionalFeaturesListDirective, - ], -}) - -export class KgRegionalFeatureModule{ - constructor( - svc: BsFeatureService, - @Optional() @Inject(GENERIC_INFO_INJ_TOKEN) Cmp: any - ){ - if (!Cmp) { - console.warn(`GENERIC_INFO_INJ_TOKEN not injected!`) - return - } - svc.registerFeature({ - name: EbrainsRegionalFeatureName, - icon: 'fas fa-ellipsis-h', - View: null, - Ctrl: KgRegionalFeaturesListDirective, - }) - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/type.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/type.ts deleted file mode 100644 index f7ebe60367da3805b95b90c03237f31d4a23b4f2..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/type.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { - TBSDetail, TBSSummary -} from '../kgDataset' - -export const EbrainsRegionalFeatureName = 'EBRAINS datasets' -export const KG_REGIONAL_FEATURE_KEY = 'EbrainsRegionalDataset' - -export const UNDER_REVIEW = { - ['@id']: "https://nexus.humanbrainproject.org/v0/data/minds/core/embargostatus/v1.0.0/1d726b76-b176-47ed-96f0-b4f2e17d5f19" -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/module.ts deleted file mode 100644 index 20939be689ba1885ac2129c9d8f24e10743df630..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/module.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; -import { ComponentsModule } from "src/components"; -import { AngularMaterialModule } from "src/sharedModules"; -import { GenericInfoCmp, GenericInfoModule } from "./genericInfo"; -import { BsFeatureIEEGModule } from "./ieeg/module"; -import { KgRegionalFeatureModule } from "./kgRegionalFeature"; -import { GetBadgeFromFeaturePipe } from "./pipes/getBadgeFromFeature.pipe"; -import { RenderRegionalFeatureSummaryPipe } from "./pipes/renderRegionalFeatureSummary.pipe"; -import { BSFeatureReceptorModule } from "./receptor"; -import { RegionalFeatureWrapperCmp } from "./regionalFeatureWrapper/regionalFeatureWrapper.component"; -import { BsFeatureService } from "./service"; -import { GENERIC_INFO_INJ_TOKEN } from "./type"; - -@NgModule({ - imports: [ - AngularMaterialModule, - CommonModule, - KgRegionalFeatureModule, - BSFeatureReceptorModule, - BsFeatureIEEGModule, - ComponentsModule, - GenericInfoModule, - ], - declarations: [ - RegionalFeatureWrapperCmp, - RenderRegionalFeatureSummaryPipe, - GetBadgeFromFeaturePipe, - ], - providers: [ - BsFeatureService, - { - provide: GENERIC_INFO_INJ_TOKEN, - useValue: GenericInfoCmp - } - ], - exports: [ - RegionalFeatureWrapperCmp, - GenericInfoModule, - ] -}) - -export class BSFeatureModule{} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/pipes/getBadgeFromFeature.pipe.ts b/src/atlasComponents/regionalFeatures/bsFeatures/pipes/getBadgeFromFeature.pipe.ts deleted file mode 100644 index 8fe67e163d116fb68ccbbff6524cb21d544db3f2..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/pipes/getBadgeFromFeature.pipe.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { IBSSummaryResponse, TContextedFeature } from "../type"; -import { - IEEG_FEATURE_NAME -} from '../ieeg' -import { - RECEPTOR_FEATURE_NAME -} from '../receptor' - -export type TBadge = { - text: string - color: 'primary' | 'warn' | 'accent' -} - -@Pipe({ - name: 'getBadgeFromFeaturePipe', - pure: true -}) - -export class GetBadgeFromFeaturePipe implements PipeTransform{ - public transform(input: TContextedFeature<keyof IBSSummaryResponse>): TBadge[]{ - if (input.featureName === IEEG_FEATURE_NAME) { - return [{ - text: IEEG_FEATURE_NAME, - color: 'primary', - }] - } - if (input.featureName === RECEPTOR_FEATURE_NAME) { - return [{ - text: RECEPTOR_FEATURE_NAME, - color: 'accent', - }] - } - return [] - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/pipes/renderRegionalFeatureSummary.pipe.ts b/src/atlasComponents/regionalFeatures/bsFeatures/pipes/renderRegionalFeatureSummary.pipe.ts deleted file mode 100644 index 829ed01981e3fb5a2061a058892f67742327dbb0..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/pipes/renderRegionalFeatureSummary.pipe.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { IBSSummaryResponse, TContextedFeature } from "../type"; -import { - IEEG_FEATURE_NAME -} from '../ieeg' -import { - RECEPTOR_FEATURE_NAME -} from '../receptor' -import { - EbrainsRegionalFeatureName -} from '../kgRegionalFeature' - -@Pipe({ - name: 'renderRegionalFeatureSummaryPipe', - pure: true, -}) - -export class RenderRegionalFeatureSummaryPipe implements PipeTransform{ - public transform(input: TContextedFeature<keyof IBSSummaryResponse>): string{ - if (input.featureName === IEEG_FEATURE_NAME) { - return input.result['name'] - } - if (input.featureName === RECEPTOR_FEATURE_NAME) { - return input.result['name'] - } - if (input.featureName === EbrainsRegionalFeatureName) { - return input.result['src_name'] - } - return `[Unknown feature type]` - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.component.ts deleted file mode 100644 index 83054ac26c45903d6e5cef7c64015a57454f2cf1..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.component.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Component, ElementRef, Input, OnChanges, ViewChild } from "@angular/core"; -import { BsFeatureReceptorBase } from "../base"; -import { CONST } from 'common/constants' -import { TBSDetail } from "../type"; -import { environment } from 'src/environments/environment' -import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; - -const { RECEPTOR_AR_CAPTION } = CONST - -export function isAr(detail: TBSDetail, label: string){ - if (label) return !!detail.__data.__autoradiographs[label] - return !!detail.__data.__autoradiographs -} - -@Component({ - selector: 'bs-features-receptor-autoradiograph', - templateUrl: './autoradiograph.template.html', - styleUrls: [ - './autoradiograph.style.css' - ] -}) - -export class BsFeatureReceptorAR extends BsFeatureReceptorBase implements OnChanges { - - public RECEPTOR_AR_CAPTION = RECEPTOR_AR_CAPTION - private DS_PREVIEW_URL = environment.DATASET_PREVIEW_URL - - @Input() - bsLabel: string - - @ViewChild('arContainer', { read: ElementRef }) - arContainer: ElementRef - - private renderBuffer: Uint8ClampedArray - private width: number - private height: number - private pleaseRender = false - - constructor(private worker: AtlasWorkerService){ - super() - } - async ngOnChanges(){ - this.error = null - this.urls = [] - if (!this.bsFeature) { - this.error = `bsFeature not populated` - return - } - if (!this.bsLabel) { - this.error = `bsLabel not populated` - return - } - - try { - const { - "x-channel": channel, - "x-height": height, - "x-width": width, - content_type: contentType, - content_encoding: contentEncoding, - content, - } = this.bsFeature.__data.__autoradiographs[this.bsLabel] - - if (contentType !== "application/octet-stream") { - throw new Error(`contentType expected to be application/octet-stream, but is instead ${contentType}`) - } - if (contentEncoding !== "gzip; base64") { - throw new Error(`contentEncoding expected to be gzip; base64, but is ${contentEncoding} instead.`) - } - - const bin = atob(content) - const { pako } = (window as any).export_nehuba - const uint8array: Uint8Array = pako.inflate(bin) - - this.width = width - this.height = height - - const rgbaBuffer = await this.worker.sendMessage({ - method: "PROCESS_TYPED_ARRAY", - param: { - inputArray: uint8array, - width, - height, - channel - }, - transfers: [ uint8array.buffer ] - }) - - this.renderBuffer = rgbaBuffer.result.buffer - this.renderCanvas() - } catch (e) { - this.error = e.toString() - } - } - - private renderCanvas(){ - if (!this.arContainer) { - this.pleaseRender = true - return - } - - const arContainer = (this.arContainer.nativeElement as HTMLElement) - while (arContainer.firstChild) { - arContainer.removeChild(arContainer.firstChild) - } - - const canvas = document.createElement("canvas") - canvas.height = this.height - canvas.width = this.width - arContainer.appendChild(canvas) - const ctx = canvas.getContext("2d") - const imgData = ctx.createImageData(this.width, this.height) - imgData.data.set(this.renderBuffer) - ctx.putImageData(imgData, 0, 0) - } - - ngAfterViewChecked(){ - if (this.pleaseRender) this.renderCanvas() - } -} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.style.css b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.style.css deleted file mode 100644 index c7ebabfec67ff4346377d1dfed4a4ce63a40c066..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.style.css +++ /dev/null @@ -1,5 +0,0 @@ -/* canvas created by createElement does not have encapsulation applied */ -.ar-container >>> canvas -{ - width: 100%; -} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.template.html deleted file mode 100644 index ad467adc5a5f15bf37343c49a0c02fbb96a2c4d7..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.template.html +++ /dev/null @@ -1,21 +0,0 @@ -<ng-template [ngIf]="error"> - {{ error }} -</ng-template> - -<a *ngFor="let url of urls" - [href]="url.url" - class="no-hover" - download> - <i class="fas fa-download"></i> - <span> - {{ url.text || (url.url | getFilenamePipe) }} - </span> -</a> - -<figure> - <figcaption class="text-muted"> - Autoradiograph: {{ RECEPTOR_AR_CAPTION }} - </figcaption> - <div class="ar-container" #arContainer> - </div> -</figure> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/base.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/base.ts deleted file mode 100644 index b149e359d7dca68da294e98e9138a557bfedf41a..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/base.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Directive, Input } from "@angular/core"; -import { TBSDetail } from "./type"; - -@Directive() -export class BsFeatureReceptorBase { - @Input() - bsFeature: TBSDetail - - public urls: { - url: string - text?: string - }[] = [] - - public error = null - - // eslint-disable-next-line @typescript-eslint/no-empty-function - constructor(){} -} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts deleted file mode 100644 index 33bc2bbde0ce20039d5bcf33de72782da242cc21..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ChangeDetectorRef, Component, Inject, OnDestroy, Optional } from "@angular/core"; -import { BehaviorSubject, Observable, of, Subscription } from "rxjs"; -import { filter, map, shareReplay, startWith, switchMap, tap } from "rxjs/operators"; -import { BsRegionInputBase } from "../../bsRegionInputBase"; -import { REGISTERED_FEATURE_INJECT_DATA } from "../../constants"; -import { BsFeatureService, TFeatureCmpInput } from "../../service"; -import { TBSDetail } from "../type"; -import { ARIA_LABELS } from 'common/constants' - -@Component({ - selector: 'bs-features-receptor-entry', - templateUrl: './entry.template.html', - styleUrls: [ - './entry.style.css' - ], -}) - -export class BsFeatureReceptorEntry extends BsRegionInputBase implements OnDestroy{ - - private sub: Subscription[] = [] - public ARIA_LABELS = ARIA_LABELS - - private selectedREntryId$ = new BehaviorSubject<string>(null) - private _selectedREntryId: string - set selectedREntryId(id: string){ - this.selectedREntryId$.next(id) - this._selectedREntryId = id - } - get selectedREntryId(){ - return this._selectedREntryId - } - - public selectedReceptor$: Observable<TBSDetail> = this.selectedREntryId$.pipe( - switchMap(id => id - ? this.getFeatureInstance('ReceptorDistribution', id) - : of(null) - ), - shareReplay(1), - ) - - public hasPrAr$: Observable<boolean> = this.selectedReceptor$.pipe( - map(detail => !!detail.__data.__profiles), - ) - - ngOnDestroy(){ - while (this.sub.length > 0) this.sub.pop().unsubscribe() - } - - public receptorsSummary$ = this.region$.pipe( - filter(v => !!v), - switchMap(() => this.getFeatureInstancesList('ReceptorDistribution')), - startWith([]), - shareReplay(1), - ) - - public onSelectReceptor(receptor: string){ - this.selectedReceptor = receptor - } - - public selectedReceptor = null - public allReceptors$ = this.selectedReceptor$.pipe( - map(rec => { - if (!rec) return [] - return Object.keys(rec.__receptor_symbols || {}) - }) - ) - - constructor( - svc: BsFeatureService, - cdr: ChangeDetectorRef, - @Optional() @Inject(REGISTERED_FEATURE_INJECT_DATA) data: TFeatureCmpInput - ){ - super(svc, data) - this.sub.push( - this.selectedReceptor$.subscribe(() => { - cdr.markForCheck() - }), - this.receptorsSummary$.subscribe(arr => { - if (arr && arr.length > 0) { - this.selectedREntryId = arr[0]['@id'] - } else { - this.selectedREntryId = null - } - }) - ) - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.style.css b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.style.css deleted file mode 100644 index 71beb4683eec695ddf613bfcf2eecac35e79773d..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.style.css +++ /dev/null @@ -1,8 +0,0 @@ -:host -{ - display: block; - width: 100%; - height: 100%; - padding-left:24px; - padding-right:24px; -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.template.html deleted file mode 100644 index 48a24c834f0ef7e943434a6242c12fe49b0f5b91..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.template.html +++ /dev/null @@ -1,50 +0,0 @@ -<mat-divider class="mt-2 mb-2"></mat-divider> - -<!-- potential selector for receptor data --> -<mat-select class="invisible" [(value)]="selectedREntryId"> - <mat-option *ngFor="let rec of receptorsSummary$ | async" - [value]="rec['@id']"> - {{ rec.name }} - </mat-option> -</mat-select> - -<ng-template [ngIf]="!(selectedReceptor$ | async)"> - <spinner-cmp></spinner-cmp> -</ng-template> - -<ng-template let-selectedRec [ngIf]="selectedReceptor$ | async"> - <bs-features-receptor-fingerprint - (onSelectReceptor)="onSelectReceptor($event)" - [bsFeature]="selectedRec"> - </bs-features-receptor-fingerprint> - - <ng-template [ngIf]="hasPrAr$ | async"> - <mat-divider></mat-divider> - - <mat-form-field class="mt-2 w-100"> - <mat-label> - Select a receptor - </mat-label> - <mat-select [(value)]="selectedReceptor"> - <mat-option - *ngFor="let receptorName of (allReceptors$ | async)" - [value]="receptorName"> - {{ receptorName }} - </mat-option> - </mat-select> - </mat-form-field> - - <bs-features-receptor-profile - *ngIf="selectedReceptor" - [bsFeature]="selectedRec" - [bsLabel]="selectedReceptor"> - </bs-features-receptor-profile> - - <bs-features-receptor-autoradiograph - *ngIf="selectedReceptor" - [bsFeature]="selectedRec" - [bsLabel]="selectedReceptor"> - </bs-features-receptor-autoradiograph> - </ng-template> - -</ng-template> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/fp/fp.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/fp/fp.component.ts deleted file mode 100644 index a061221118bf8420bbf6883b14c15f18680c4fb8..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/fp/fp.component.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, Inject, OnChanges, OnDestroy, OnInit, Optional, Output } from "@angular/core"; -import { fromEvent, Observable, Subscription } from "rxjs"; -import { distinctUntilChanged, map } from "rxjs/operators"; -import { BS_DARKTHEME } from "../../constants"; -import { BsFeatureReceptorBase } from "../base"; -import { CONST } from 'common/constants' - -const { RECEPTOR_FP_CAPTION } = CONST - -@Component({ - selector: 'bs-features-receptor-fingerprint', - templateUrl: './fp.template.html', - styleUrls: [ - './fp.style.css' - ], - changeDetection: ChangeDetectionStrategy.OnPush -}) - -export class BsFeatureReceptorFingerprint extends BsFeatureReceptorBase implements OnChanges, OnInit, OnDestroy{ - - public RECEPTOR_FP_CAPTION = RECEPTOR_FP_CAPTION - private sub: Subscription[] = [] - - @HostListener('click') - onClick(){ - if (this.selectedReceptor) { - this.onSelectReceptor.emit(this.selectedReceptor) - } - } - - @Output() - public onSelectReceptor = new EventEmitter() - private selectedReceptor: any - - constructor( - private elRef: ElementRef, - @Optional() @Inject(BS_DARKTHEME) public darktheme$: Observable<boolean>, - ){ - super() - } - - ngOnInit(){ - // without, when devtool is out, runs sluggishly - // informing angular that change occurs here will be handled by programmer, and not angular - - this.sub.push( - fromEvent<CustomEvent>(this.elRef.nativeElement, 'kg-ds-prv-regional-feature-mouseover').pipe( - map(ev => ev.detail?.data?.receptor?.label), - distinctUntilChanged(), - ).subscribe(label => { - this.selectedReceptor = label - }) - ) - } - - ngOnDestroy() { - while (this.sub.length > 0) this.sub.pop().unsubscribe() - } - - ngOnChanges(){ - this.error = null - this.urls = [] - - if (!this.bsFeature) { - this.error = `bsFeature is not populated` - return - } - - this.urls.push( - ...this.bsFeature.__files - .filter(u => /_fp_/.test(u)) - .map(url => { - return { - url, - } - }), - ...this.bsFeature.__files - .filter(u => !/_pr_|_ar_/.test(u) && /receptors\.tsv$/.test(u)) - .map(url => { - return { - url, - } - }) - ) - - const radarEl = (this.elRef.nativeElement as HTMLElement).querySelector<any>('kg-dataset-dumb-radar') - radarEl.radarBs = this.bsFeature.__data.__fingerprint - radarEl.metaBs = this.bsFeature.__receptor_symbols - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/fp/fp.style.css b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/fp/fp.style.css deleted file mode 100644 index e2a98b2709c53c5853eebdbfc84370f371d84868..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/fp/fp.style.css +++ /dev/null @@ -1,18 +0,0 @@ -kg-dataset-dumb-radar -{ - display: block; - min-height: 20em; -} - -/* figure -{ - width: 100%; - height: 100%; -} - -kg-dataset-dumb-radar -{ - display: block; - width: 100%; - height: 100%; -} */ \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/fp/fp.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/fp/fp.template.html deleted file mode 100644 index 50d505459bb9893b0d7b98ce20f596ba2ba241a9..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/fp/fp.template.html +++ /dev/null @@ -1,22 +0,0 @@ -<ng-template [ngIf]="error"> - {{ error }} -</ng-template> - -<a *ngFor="let url of urls" - [href]="url.url" - class="no-hover" - download> - <i class="fas fa-download"></i> - <span> - {{ url.text || (url.url | getFilenamePipe) }} - </span> -</a> - -<figure> - <figcaption class="text-muted"> - Fingerprint : {{ RECEPTOR_FP_CAPTION }} - </figcaption> - <kg-dataset-dumb-radar - [attr.kg-ds-prv-darkmode]="darktheme$ && (darktheme$ | async)"> - </kg-dataset-dumb-radar> -</figure> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/hasReceptor.directive.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/hasReceptor.directive.ts deleted file mode 100644 index 6ef215934ac65dda3977d9e938d328ca45956f22..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/hasReceptor.directive.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Directive, EventEmitter, Inject, OnDestroy, Optional, Output } from "@angular/core"; -import { merge, Observable, of, Subscription } from "rxjs"; -import { catchError, map, mapTo, switchMap } from "rxjs/operators"; -import { BsRegionInputBase } from "../bsRegionInputBase"; -import { REGISTERED_FEATURE_INJECT_DATA } from "../constants"; -import { BsFeatureService, TFeatureCmpInput } from "../service"; -import { IBSSummaryResponse, IRegionalFeatureReadyDirective } from "../type"; - -@Directive({ - selector: '[bs-features-receptor-directive]', - exportAs: 'bsFeatureReceptorDirective' -}) - -export class BsFeatureReceptorDirective extends BsRegionInputBase implements IRegionalFeatureReadyDirective, OnDestroy { - - private sub: Subscription[] = [] - - ngOnDestroy(){ - while (this.sub.length > 0) this.sub.pop().unsubscribe() - } - public results$: Observable<IBSSummaryResponse['ReceptorDistribution'][]> = this.region$.pipe( - switchMap(() => merge( - of([]), - this.getFeatureInstancesList('ReceptorDistribution').pipe( - catchError(() => of([])) - ) - )), - ) - - public hasReceptor$ = this.results$.pipe( - map(arr => arr.length > 0) - ) - - public busy$ = this.region$.pipe( - switchMap(() => merge( - of(true), - this.results$.pipe( - mapTo(false) - ) - )) - ) - - constructor( - svc: BsFeatureService, - @Optional() @Inject(REGISTERED_FEATURE_INJECT_DATA) data: TFeatureCmpInput, - ){ - super(svc, data) - this.sub.push( - this.busy$.subscribe(flag => this.fetchingFlagEmitter.emit(flag)) - ) - } - - @Output('bs-features-receptor-directive-fetching-flag') - public fetchingFlagEmitter = new EventEmitter<boolean>() -} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/index.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/index.ts deleted file mode 100644 index 79f8e55d97df512175ef1be0f913512bff41a16f..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { BSFeatureReceptorModule } from './module' -export { RECEPTOR_FEATURE_NAME } from './type' diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/module.ts deleted file mode 100644 index 94f11b9df04e239090ed67a1bca192d2bd924b9c..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/module.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; -import { FormsModule } from "@angular/forms"; -import { AngularMaterialModule } from "src/sharedModules"; -import { UtilModule } from "src/util"; -import { BsFeatureService } from "../service"; -import { BsFeatureReceptorAR } from "./ar/autoradiograph.component"; -import { BsFeatureReceptorEntry } from "./entry/entry.component"; -import { BsFeatureReceptorFingerprint } from "./fp/fp.component"; -import { BsFeatureReceptorDirective } from "./hasReceptor.directive"; -import { BsFeatureReceptorProfile } from "./profile/profile.component"; -import { RECEPTOR_FEATURE_NAME } from "./type"; - -@NgModule({ - imports: [ - CommonModule, - UtilModule, - AngularMaterialModule, - FormsModule, - ], - declarations: [ - BsFeatureReceptorProfile, - BsFeatureReceptorAR, - BsFeatureReceptorFingerprint, - BsFeatureReceptorEntry, - BsFeatureReceptorDirective, - ], - exports: [ - BsFeatureReceptorEntry, - BsFeatureReceptorDirective, - ], - schemas: [ - CUSTOM_ELEMENTS_SCHEMA - ] -}) - -export class BSFeatureReceptorModule{ - constructor(svc: BsFeatureService){ - svc.registerFeature({ - name: RECEPTOR_FEATURE_NAME, - icon: 'fas fa-info', - View: BsFeatureReceptorEntry, - Ctrl: BsFeatureReceptorDirective, - }) - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/profile/profile.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/profile/profile.component.ts deleted file mode 100644 index 6b33a73b4810c5828752a783b006b0dbb6928b94..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/profile/profile.component.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { ChangeDetectionStrategy, Component, ElementRef, Inject, Input, OnChanges, Optional } from "@angular/core"; -import { Observable } from "rxjs"; -import { BS_DARKTHEME } from "../../constants"; -import { BsFeatureReceptorBase } from "../base"; -import { CONST } from 'common/constants' - -export function isPr(filename: string, label: string = ''){ - return filename.indexOf(`_pr_${label}`) >= 0 -} - -const { RECEPTOR_PR_CAPTION } = CONST - -@Component({ - selector: 'bs-features-receptor-profile', - templateUrl: './profile.template.html', - styleUrls: [ - './profile.style.css' - ], - changeDetection: ChangeDetectionStrategy.OnPush -}) - -export class BsFeatureReceptorProfile extends BsFeatureReceptorBase implements OnChanges{ - - public RECEPTOR_PR_CAPTION = RECEPTOR_PR_CAPTION - - @Input() - bsLabel: string - - constructor( - private elRef: ElementRef, - @Optional() @Inject(BS_DARKTHEME) public darktheme$: Observable<boolean>, - ){ - super() - } - - ngOnChanges(){ - this.error = null - this.urls = [] - - if (!this.bsFeature) { - this.error = `bsFeature not populated` - return - } - if (!this.bsLabel) { - this.error = `bsLabel not populated` - return - } - - this.urls = this.bsFeature.__files - .filter(url => isPr(url, this.bsLabel)) - .map(url => { - return { url } - }) - - const profileBs = this.bsFeature.__data.__profiles[this.bsLabel] - const lineEl = (this.elRef.nativeElement as HTMLElement).querySelector<any>('kg-dataset-dumb-line') - lineEl.profileBs = profileBs - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/profile/profile.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/profile/profile.template.html deleted file mode 100644 index 0b71d2ac46cb92527b2a44af80a94a69961568b5..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/profile/profile.template.html +++ /dev/null @@ -1,22 +0,0 @@ -<ng-template [ngIf]="error"> - {{ error }} -</ng-template> - -<a *ngFor="let url of urls" - [href]="url.url" - class="no-hover" - download> - <i class="fas fa-download"></i> - <span> - {{ url.text || (url.url | getFilenamePipe) }} - </span> -</a> - -<figure> - <figcaption class="text-muted"> - Profile: {{ RECEPTOR_PR_CAPTION }} - </figcaption> - <kg-dataset-dumb-line - [attr.kg-ds-prv-darkmode]="darktheme$ && (darktheme$ | async)"> - </kg-dataset-dumb-line> -</figure> \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/type.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/type.ts deleted file mode 100644 index 9ef1efe8b17727ded7f382784c035d780d323fc6..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/type.ts +++ /dev/null @@ -1,63 +0,0 @@ -type TReceptorCommon = { - latex: string - markdown: string - name: string -} - -type TReceptor = string // TODO complete all possible neuroreceptor - -type TReceptorSymbol = { - [key: string]: { - receptor: TReceptorCommon - neurotransmitter: TReceptorCommon & { label: string } - } -} - -type TProfile = { - [key: number]: number -} - -type TBSFingerprint = { - unit: string - labels: TReceptor[] - meanvals: number[] - stdvals: number[] - n: 1 -} - -export type TBSSummary = { - ['@id']: string - name: string - info: string - origin_datainfos?: ({ - name: string - description: string - urls: { - doi: string - cite?: string - }[] - })[] -} - -export type TBSDetail = TBSSummary & { - __files: string[] - __receptor_symbols: TReceptorSymbol - __data: { - __profiles: { - [key: string]: TProfile - } - __autoradiographs: { - [key: string]: { - content_type: string - content_encoding: string - ['x-width']: number - ['x-height']: number - ['x-channel']: number - content: string - } - } - __fingerprint: TBSFingerprint - } -} - -export const RECEPTOR_FEATURE_NAME = 'receptor density' diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.component.ts deleted file mode 100644 index 76a2ad1f2f9bdffbbb598992d542989a48127279..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.component.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { Component, ComponentFactory, ComponentFactoryResolver, Inject, Injector, Input, OnChanges, OnDestroy, Optional, ViewChild, ViewContainerRef } from "@angular/core"; -import { IBSSummaryResponse, TContextedFeature, TRegion } from "../type"; -import { BsFeatureService, TFeatureCmpInput } from "../service"; -import { combineLatest, Observable, Subject } from "rxjs"; -import { debounceTime, map, shareReplay, startWith } from "rxjs/operators"; -import { REGISTERED_FEATURE_INJECT_DATA } from "../constants"; -import { ARIA_LABELS } from 'common/constants' -import { - IEEG_FEATURE_NAME -} from '../ieeg' -import { - RECEPTOR_FEATURE_NAME -} from '../receptor' -import { - EbrainsRegionalFeatureName -} from '../kgRegionalFeature' -import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, TOverwriteShowDatasetDialog } from "src/util/interfaces"; - -@Component({ - selector: 'regional-feature-wrapper', - templateUrl: './regionalFeatureWrapper.template.html', - styleUrls: [ - './regionalFeatureWrapper.style.css' - ] -}) - -export class RegionalFeatureWrapperCmp implements OnChanges, OnDestroy{ - - public useVirtualScroll = false - - public ARIA_LABELS = ARIA_LABELS - - @Input() - region: TRegion - - @ViewChild('regionalFeatureContainerTmpl', { read: ViewContainerRef }) - regionalFeatureContainerRef: ViewContainerRef - - private weakmap = new WeakMap<(new () => any), ComponentFactory<any>>() - - private ondestroyCb: (() => void)[] = [] - constructor( - private svc: BsFeatureService, - private cfr: ComponentFactoryResolver, - @Optional() @Inject(OVERWRITE_SHOW_DATASET_DIALOG_TOKEN) private overwriteFn: TOverwriteShowDatasetDialog - ){ - const sub = this.registeredFeatures$.subscribe(arr => this.registeredFeatures = arr) - this.ondestroyCb.push(() => sub.unsubscribe()) - } - - private regionOnDestroyCb: (() => void)[] = [] - private setupRegionalFeatureCtrl(){ - if (!this.region) return - const { region } = this - for (const feat of this.svc.registeredFeatures){ - const { name, icon } = feat - const ctrl = new feat.Ctrl(this.svc, { region }) - const sub = combineLatest([ - ctrl.busy$, - ctrl.results$.pipe( - startWith([]) - ) - ]).subscribe( - ([busy, results]) => { - this.registeredFeatureRawRegister[name] = { busy, results, icon } - this.registeredFeatureFireStream$.next(true) - } - ) - this.regionOnDestroyCb.push(() => sub.unsubscribe()) - } - } - private cleanUpRegionalFeature(){ - while (this.regionOnDestroyCb.length) this.regionOnDestroyCb.pop()() - /** - * emit null to signify flush out of existing scan map - */ - this.registeredFeatureRawRegister = {} - this.registeredFeatureFireStream$.next(true) - } - - private registeredFeatureRawRegister: { - [key: string]: { - icon: string - busy: boolean - results: any[] - } - } = {} - private registeredFeatureFireStream$ = new Subject() - private registeredFeatureMasterStream$ = this.registeredFeatureFireStream$.pipe( - debounceTime(16), - /** - * must not use mapTo operator - * otherwise the emitted value will not change - */ - map(() => this.registeredFeatureRawRegister), - shareReplay(1), - ) - public busy$: Observable<boolean> = this.registeredFeatureMasterStream$.pipe( - map(obj => { - for (const key in obj) { - if(obj[key].busy) return true - } - return false - }), - ) - - public registeredFeatures: TContextedFeature<keyof IBSSummaryResponse>[] = [] - private registeredFeatures$: Observable<TContextedFeature<keyof IBSSummaryResponse>[]> = this.registeredFeatureMasterStream$.pipe( - map(obj => { - const returnArr = [] - for (const name in obj) { - if (obj[name].busy || obj[name].results.length === 0) { - continue - } - for (const result of obj[name].results) { - const objToBeInserted = { - featureName: name, - icon: obj[name].icon, - result - } - /** - * place ebrains regional features at the end - */ - if (name === EbrainsRegionalFeatureName) { - returnArr.push(objToBeInserted) - } else { - returnArr.unshift(objToBeInserted) - } - } - } - - return returnArr - }), - ) - - ngOnChanges(){ - this.cleanUpRegionalFeature() - this.setupRegionalFeatureCtrl() - } - - ngOnDestroy(){ - this.cleanUpRegionalFeature() - while(this.ondestroyCb.length) this.ondestroyCb.pop()() - } - - public handleFeatureClick(contextedFeature: TContextedFeature<any>){ - if (!this.overwriteFn) { - console.warn(`show dialog function not overwritten!`) - return - } - - const featureId = contextedFeature.result['@id'] - const arg = {} - if (contextedFeature.featureName === RECEPTOR_FEATURE_NAME) { - arg['name'] = contextedFeature.result['name'] - arg['description'] = contextedFeature.result['info'] - arg['urls'] = [] - for (const info of contextedFeature.result['origin_datainfos']) { - arg['urls'].push(...info.urls) - } - } - - if (contextedFeature.featureName === IEEG_FEATURE_NAME) { - arg['name'] = contextedFeature.result['name'] - arg['description'] = contextedFeature.result['description'] || ' ' - arg['isGdprProtected'] = true - arg['urls'] = [] - for (const info of contextedFeature.result['origin_datainfos']) { - arg['urls'].push(...(info.urls || [])) - } - } - - if (contextedFeature.featureName === EbrainsRegionalFeatureName) { - arg['summary'] = contextedFeature.result - } - - const { region } = this - - const feat = this.svc.registeredFeatures.find(f => f.name === contextedFeature.featureName) - if (!feat) { - console.log(`cannot find feature with name ${contextedFeature.featureName}`) - return - } - - const cf = (() => { - if (!feat.View) return null - const mapped = this.weakmap.get(feat.View) - if (mapped) return mapped - const _cf = this.cfr.resolveComponentFactory(feat.View) - this.weakmap.set(feat.View ,_cf) - return _cf - })() - - this.overwriteFn({ - region, - dataType: contextedFeature.featureName, - view: (() => { - if (!cf) return null - const injector = Injector.create({ - providers: [{ - provide: REGISTERED_FEATURE_INJECT_DATA, - useValue: { region, featureId } as TFeatureCmpInput - }], - }) - const cmp = cf.create(injector) - return cmp.hostView - })(), - ...arg, - }) - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.style.css b/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.style.css deleted file mode 100644 index 816e7ba25d2192ebd17900e60655db92bf17063b..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.style.css +++ /dev/null @@ -1,19 +0,0 @@ -.button-text -{ - white-space: normal; - line-height: 1.5rem; - text-align: center; -} - -.feature-container, -cdk-virtual-scroll-viewport -{ - min-height: 24rem; -} - - -.feature-container -{ - height: 24rem; - overflow-y: scroll; -} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.template.html deleted file mode 100644 index 020145c3c36c406a74c3575422068d0b43700892..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.template.html +++ /dev/null @@ -1,85 +0,0 @@ -<ng-template [ngTemplateOutlet]="resultTmpl"> -</ng-template> - -<ng-template #busyTmpl> - <spinner-cmp></spinner-cmp> -</ng-template> - -<ng-template #resultTmpl> - - <!-- virtual scroll. do not activate until autosize is supported --> - <cdk-virtual-scroll-viewport - *ngIf="useVirtualScroll; else regularScrollTmpl" - [attr.aria-label]="ARIA_LABELS.LIST_OF_DATASETS_ARIA_LABEL" - class="h-100" - minBufferPx="200" - maxBufferPx="400" - itemSize="50"> - <div *cdkVirtualFor="let feature of registeredFeatures; templateCacheSize: 20; let index = index" - class="h-50px overflow-hidden"> - - <!-- divider, show if not first --> - <mat-divider *ngIf="index !== 0"></mat-divider> - <ng-container *ngTemplateOutlet="itemContainer; context: { $implicit: feature }"> - </ng-container> - - </div> - </cdk-virtual-scroll-viewport> - - <!-- fallback, regular scroll --> - <!-- less efficient on large list, but for now should do --> - <ng-template #regularScrollTmpl> - <div class="feature-container" - [attr.aria-label]="ARIA_LABELS.LIST_OF_DATASETS_ARIA_LABEL"> - - <!-- if busy, show spinner --> - <ng-template [ngIf]="busy$ | async" [ngIfElse]="notBusyTmpl"> - <ng-template [ngTemplateOutlet]="busyTmpl"></ng-template> - </ng-template> - - <ng-template #notBusyTmpl> - <ng-template [ngIf]="registeredFeatures.length === 0"> - <span class="text-muted"> - No regional features found. - </span> - </ng-template> - </ng-template> - <div *ngFor="let feature of registeredFeatures; let index = index" - class="overflow-hidden"> - - <!-- divider, show if not first --> - <mat-divider *ngIf="index !== 0"></mat-divider> - <ng-container *ngTemplateOutlet="itemContainer; context: { $implicit: feature }"> - </ng-container> - - </div> - </div> - </ng-template> - -</ng-template> - -<!-- feature template --> -<ng-template #itemContainer let-feature> - <div class="d-block pt-4 cursor-default" - (click)="handleFeatureClick(feature)" - mat-ripple> - - <!-- mat-chip container --> - <!-- do not use mat-chip-list to avoid adding incorrect a11y info --> - <div class="transform-origin-left-center scale-80"> - <mat-chip *ngFor="let badge of feature | getBadgeFromFeaturePipe" - [color]="badge.color" - selected> - {{ badge.text }} - </mat-chip> - </div> - - <small> - {{ feature | renderRegionalFeatureSummaryPipe }} - </small> - </div> -</ng-template> - -<!-- dummy container --> -<ng-template #regionalFeatureContainerTmpl> -</ng-template> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/service.ts b/src/atlasComponents/regionalFeatures/bsFeatures/service.ts deleted file mode 100644 index 7f52201e0619227b8ba17f23d0b4ce6cb710ce23..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/service.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { HttpClient } from "@angular/common/http"; -import { Inject, Injectable } from "@angular/core"; -import { BehaviorSubject } from "rxjs"; -import { shareReplay } from "rxjs/operators"; -import { CachedFunction } from "src/util/fn"; -import { BS_ENDPOINT } from "./constants"; -import { IBSSummaryResponse, IBSDetailResponse, TRegion, IFeatureList, IRegionalFeatureReadyDirective } from './type' -import { SIIBRA_FEATURE_KEY as IEEG_FEATURE_KEY } from '../bsFeatures/ieeg/type' - -function processRegion(region: TRegion) { - return `${region.name} ${region.status ? region.status : '' }` -} - -export type TFeatureCmpInput = { - region: TRegion - featureId?: string -} - -export type TRegisteredFeature<V = any> = { - name: string - icon: string // fontawesome font class, e.g. `fas fa-link-alt` - View: new (...arg: any[]) => V - Ctrl: new (svc: BsFeatureService, data: TFeatureCmpInput) => IRegionalFeatureReadyDirective -} - -@Injectable({ - providedIn: 'root' -}) -export class BsFeatureService{ - - static SpaceFeatureSet = new Set([ - IEEG_FEATURE_KEY - ]) - - public registeredFeatures: TRegisteredFeature[] = [] - public registeredFeatures$ = new BehaviorSubject<TRegisteredFeature[]>(this.registeredFeatures) - public getAllFeatures$ = this.http.get(`${this.bsEndpoint}/features`).pipe( - shareReplay(1) - ) - - public listFeatures(region: TRegion){ - const { context } = region - const { atlas, parcellation } = context - return this.http.get<IFeatureList>( - `${this.bsEndpoint}/atlases/${encodeURIComponent(atlas["@id"])}/parcellations/${encodeURIComponent(parcellation['@id'])}/regions/${encodeURIComponent(processRegion(region))}/features` - ) - } - - private getUrl(arg: { - atlasId: string - parcId: string - spaceId: string - region: TRegion - featureName: string - featureId?: string - }){ - const { - atlasId, - parcId, - spaceId, - region, - featureName, - featureId, - } = arg - - if (BsFeatureService.SpaceFeatureSet.has(featureName)) { - - const url = new URL(`${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/spaces/${encodeURIComponent(spaceId)}/features/${encodeURIComponent(featureName)}${ featureId ? ('/' + encodeURIComponent(featureId)) : '' }`) - url.searchParams.set('parcellation_id', parcId) - url.searchParams.set('region', processRegion(region)) - - return url.toString() - } - - if (!featureId) { - return `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations/${encodeURIComponent(parcId)}/regions/${encodeURIComponent(processRegion(region))}/features/${encodeURIComponent(featureName)}` - } - return `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations/${encodeURIComponent(parcId)}/regions/${encodeURIComponent(processRegion(region))}/features/${encodeURIComponent(featureName)}/${encodeURIComponent(featureId)}` - } - - @CachedFunction({ - serialization: (featureName, region) => `${featureName}::${processRegion(region)}` - }) - public getFeatures<T extends keyof IBSSummaryResponse>(featureName: T, region: TRegion){ - const { context } = region - const { atlas, parcellation, template } = context - const url = this.getUrl({ - atlasId: atlas['@id'], - parcId: parcellation['@id'], - region, - featureName, - spaceId: template['@id'] - }) - - return this.http.get<IBSSummaryResponse[T][]>( - url - ).pipe( - shareReplay(1) - ) - } - - @CachedFunction({ - serialization: (featureName, region, featureId) => `${featureName}::${processRegion(region)}::${featureId}` - }) - public getFeature<T extends keyof IBSDetailResponse>(featureName: T, region: TRegion, featureId: string) { - const { context } = region - const { atlas, parcellation, template } = context - const url = this.getUrl({ - atlasId: atlas['@id'], - parcId: parcellation['@id'], - spaceId: template['@id'], - region, - featureName, - featureId - }) - return this.http.get<IBSSummaryResponse[T]&IBSDetailResponse[T]>(url).pipe( - shareReplay(1) - ) - } - - public registerFeature(feature: TRegisteredFeature){ - if (this.registeredFeatures.find(v => v.name === feature.name)) { - throw new Error(`feature ${feature.name} already registered`) - } - this.registeredFeatures.push(feature) - this.registeredFeatures$.next(this.registeredFeatures) - } - - public deregisterFeature(name: string){ - this.registeredFeatures = this.registeredFeatures.filter(v => v.name !== name) - this.registeredFeatures$.next(this.registeredFeatures) - } - - constructor( - private http: HttpClient, - @Inject(BS_ENDPOINT) private bsEndpoint: string, - ){ - - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/type.ts b/src/atlasComponents/regionalFeatures/bsFeatures/type.ts deleted file mode 100644 index 84731e48984af2dd020be92f5d06e3b91429f558..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/type.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { IHasId } from "src/util/interfaces"; -import { TBSDetail as TReceptorDetail, TBSSummary as TReceptorSummary } from "./receptor/type"; -import { KG_REGIONAL_FEATURE_KEY, TBSDetail as TKGDetail, TBSSummary as TKGSummary } from './kgRegionalFeature/type' -import { SIIBRA_FEATURE_KEY, TBSSummary as TIEEGSummary, TBSIeegSessionDetail as TIEEGDetail } from './ieeg/type' -import { Observable } from "rxjs"; -import { InjectionToken } from "@angular/core"; - -/** - * change KgRegionalFeature -> EbrainsRegionalDataset in prod - */ - -export interface IBSSummaryResponse { - 'ReceptorDistribution': TReceptorSummary - [KG_REGIONAL_FEATURE_KEY]: TKGSummary - [SIIBRA_FEATURE_KEY]: TIEEGSummary -} - -export interface IBSDetailResponse { - 'ReceptorDistribution': TReceptorDetail - [KG_REGIONAL_FEATURE_KEY]: TKGDetail - [SIIBRA_FEATURE_KEY]: TIEEGDetail -} - -export type TRegion = { - name: string - status?: string - context: { - atlas: IHasId - template: IHasId - parcellation: IHasId - } -} - -export interface IFeatureList { - features: { - [key: string]: string - }[] -} - -export interface IRegionalFeatureReadyDirective { - ngOnDestroy(): void - busy$: Observable<boolean> - results$: Observable<IBSSummaryResponse[keyof IBSSummaryResponse][]> -} - -export type TContextedFeature<T extends keyof IBSSummaryResponse> = { - featureName: string - icon: string - result: IBSSummaryResponse[T] -} - -export const GENERIC_INFO_INJ_TOKEN = new InjectionToken('GENERIC_INFO_INJ_TOKEN') diff --git a/src/atlasComponents/regionalFeatures/featureContainer/featureContainer.component.spec.ts b/src/atlasComponents/regionalFeatures/featureContainer/featureContainer.component.spec.ts deleted file mode 100644 index 6821e237ff0aae95e6eaa42c99989dec7d5e9b65..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/featureContainer/featureContainer.component.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { CommonModule } from "@angular/common" -import { ChangeDetectorRef, Component, ComponentRef, EventEmitter, NgModule } from "@angular/core" -import { async, TestBed } from "@angular/core/testing" -import { By } from "@angular/platform-browser" -import { RegionalFeaturesService } from "../regionalFeature.service" -import { ISingleFeature } from "../singleFeatures/interfaces" -import { FeatureContainer } from "./featureContainer.component" - -const dummyCmpType = 'dummyType' - -@Component({ - template: `{{ text }}` -}) - -class DummyComponent implements ISingleFeature{ - text = 'hello world' - feature: any - region: any - viewChanged = new EventEmitter<boolean>() -} - -@Component({ - template: '' -}) - -class HostCmp{ - public feature: any - public region: any - - constructor(public cdr: ChangeDetectorRef){ - - } - - detectChange(){ - this.cdr.detectChanges() - } -} - -const serviceStub = { - mapFeatToCmp: new Map([ - [dummyCmpType, DummyComponent] - ]) -} - -describe('> featureContainer.component.ts', () => { - describe('> FeatureContainer', () => { - - beforeEach(async () => { - - await TestBed.configureTestingModule({ - imports: [ - CommonModule, - ], - declarations: [ - FeatureContainer, - DummyComponent, - HostCmp, - ], - providers: [ - { - provide: RegionalFeaturesService, - useValue: serviceStub - } - ] - }).overrideComponent(HostCmp, { - set: { - template: ` - <feature-container - [feature]="feature" - [region]="region" - (viewChanged)="detectChange()"> - </feature-container>` - } - }).compileComponents() - - }) - - it('> can be created', () => { - const fixture = TestBed.createComponent(HostCmp) - expect(fixture).toBeTruthy() - const featContainer = fixture.debugElement.query(By.directive(FeatureContainer)) - expect(featContainer).toBeTruthy() - }) - - describe('> if inputs change', () => { - it('> if input changed, but feature is not one of them, map.get will not be called', () => { - const fixture = TestBed.createComponent(HostCmp) - // const featContainer = fixture.debugElement.query(By.directive(FeatureContainer)) - spyOn(serviceStub.mapFeatToCmp, 'get').and.callThrough() - fixture.componentInstance.region = { - name: 'tesla' - } - fixture.detectChanges() - expect(serviceStub.mapFeatToCmp.get).not.toHaveBeenCalled() - }) - - it('> if input changed, feature is one of them, will not call map.get', () => { - const fixture = TestBed.createComponent(HostCmp) - const dummyFeature = { - type: dummyCmpType - } - spyOn(serviceStub.mapFeatToCmp, 'get').and.callThrough() - fixture.componentInstance.feature = dummyFeature - fixture.detectChanges() - expect(serviceStub.mapFeatToCmp.get).toHaveBeenCalledWith(dummyCmpType) - }) - - it('> should render default txt', () => { - const fixture = TestBed.createComponent(HostCmp) - const dummyFeature = { - type: dummyCmpType - } - fixture.componentInstance.feature = dummyFeature - fixture.detectChanges() - const text = fixture.nativeElement.textContent - expect(text).toContain('hello world') - }) - - it('> if inner component changes, if view changed does not emit, will not change ui', () => { - - const fixture = TestBed.createComponent(HostCmp) - const dummyFeature = { - type: dummyCmpType - } - fixture.componentInstance.feature = dummyFeature - fixture.detectChanges() - const featureContainer = fixture.debugElement.query( - By.directive(FeatureContainer) - ) - const cr = (featureContainer.componentInstance as FeatureContainer)['cr'] as ComponentRef<DummyComponent> - cr.instance.text = 'foo bar' - const text = fixture.nativeElement.textContent - expect(text).toContain('hello world') - }) - - it('> if inner component changes, and viewChanged is emitted, ui should change accordingly', () => { - - const fixture = TestBed.createComponent(HostCmp) - const dummyFeature = { - type: dummyCmpType - } - fixture.componentInstance.feature = dummyFeature - fixture.detectChanges() - const featureContainer = fixture.debugElement.query( - By.directive(FeatureContainer) - ) - const cr = (featureContainer.componentInstance as FeatureContainer)['cr'] as ComponentRef<DummyComponent> - cr.instance.text = 'foo bar' - cr.instance.viewChanged.emit(true) - const text = fixture.nativeElement.textContent - expect(text).toContain('foo bar') - }) - }) - }) -}) diff --git a/src/atlasComponents/regionalFeatures/featureContainer/featureContainer.component.ts b/src/atlasComponents/regionalFeatures/featureContainer/featureContainer.component.ts deleted file mode 100644 index 2b2391599494ac22cadc250fa479255fa39d2179..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/featureContainer/featureContainer.component.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ChangeDetectionStrategy, Component, ComponentFactoryResolver, ComponentRef, Input, OnChanges, Output, SimpleChanges, ViewContainerRef, EventEmitter } from "@angular/core"; -import { Subscription } from "rxjs"; -import { IFeature, RegionalFeaturesService } from "../regionalFeature.service"; -import { ISingleFeature } from "../singleFeatures/interfaces"; - -@Component({ - selector: 'feature-container', - template: '', - changeDetection: ChangeDetectionStrategy.OnPush, -}) - -export class FeatureContainer implements OnChanges{ - @Input() - feature: IFeature - - @Input() - region: any - - @Output() - viewChanged: EventEmitter<boolean> = new EventEmitter() - - private cr: ComponentRef<ISingleFeature> - - constructor( - private vCRef: ViewContainerRef, - private rService: RegionalFeaturesService, - private cfr: ComponentFactoryResolver, - ){ - } - - private viewChangedSub: Subscription - - ngOnChanges(simpleChanges: SimpleChanges){ - if (!simpleChanges.feature) return - const { currentValue, previousValue } = simpleChanges.feature - if (currentValue === previousValue) return - this.clear() - - /** - * catching instances where currentValue for feature is falsy - */ - if (!currentValue) return - - /** - * TODO catch if map is undefined - */ - const comp = this.rService.mapFeatToCmp.get(currentValue.type) - if (!comp) throw new Error(`mapFeatToCmp for ${currentValue.type} not defined`) - const cf = this.cfr.resolveComponentFactory<ISingleFeature>(comp) - this.cr = this.vCRef.createComponent(cf) - this.cr.instance.feature = this.feature - this.cr.instance.region = this.region - this.viewChangedSub = this.cr.instance.viewChanged.subscribe(() => this.viewChanged.emit(true)) - } - - clear(){ - if (this.viewChangedSub) this.viewChangedSub.unsubscribe() - this.vCRef.clear() - } -} diff --git a/src/atlasComponents/regionalFeatures/index.ts b/src/atlasComponents/regionalFeatures/index.ts deleted file mode 100644 index a71230d7107939f7da63d18d843339937ed89290..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { RegionalFeaturesModule } from './module' -export { IFeature } from './regionalFeature.service' \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/module.ts b/src/atlasComponents/regionalFeatures/module.ts deleted file mode 100644 index 573acb57dfd9b3f0f0e303a622be2926f8013f27..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/module.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { HttpClientModule } from "@angular/common/http"; -import { NgModule } from "@angular/core"; -import { UtilModule } from "src/util"; -import { AngularMaterialModule } from "src/sharedModules"; -import { FeatureContainer } from "./featureContainer/featureContainer.component"; -import { FilterRegionalFeaturesByTypePipe } from "./pipes/filterRegionalFeaturesByType.pipe"; -import { FilterRegionFeaturesById } from "./pipes/filterRegionFeaturesById.pipe"; -import { FindRegionFEatureById } from "./pipes/findRegionFeatureById.pipe"; -import { RegionalFeaturesService } from "./regionalFeature.service"; -import { RegionGetAllFeaturesDirective } from "./regionGetAllFeatures.directive"; -import { FeatureIEEGRecordings } from "./singleFeatures/iEEGRecordings/module"; -import { ReceptorDensityModule } from "./singleFeatures/receptorDensity/module"; - -@NgModule({ - imports: [ - CommonModule, - UtilModule, - AngularMaterialModule, - FeatureIEEGRecordings, - ReceptorDensityModule, - HttpClientModule, - ], - declarations: [ - /** - * components - */ - FeatureContainer, - - /** - * Directives - */ - RegionGetAllFeaturesDirective, - - /** - * pipes - */ - FilterRegionalFeaturesByTypePipe, - FindRegionFEatureById, - FilterRegionFeaturesById, - ], - exports: [ - RegionGetAllFeaturesDirective, - FilterRegionFeaturesById, - FeatureContainer, - ], - providers: [ - RegionalFeaturesService, - ] -}) - -export class RegionalFeaturesModule{} diff --git a/src/atlasComponents/regionalFeatures/pipes/filterRegionFeaturesById.pipe.ts b/src/atlasComponents/regionalFeatures/pipes/filterRegionFeaturesById.pipe.ts deleted file mode 100644 index ed20ef67cfc96dcf3f21b1789cc7cdf82ac2fa01..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/pipes/filterRegionFeaturesById.pipe.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { IFeature } from "../regionalFeature.service"; -import { getIdFromFullId } from 'common/util' -@Pipe({ - name: 'filterRegionFeaturesById', - pure: true -}) - -export class FilterRegionFeaturesById implements PipeTransform{ - public transform(features: IFeature[], id: string){ - const filterId = getIdFromFullId(id) - return features.filter(f => getIdFromFullId(f['@id']) === filterId) - } -} diff --git a/src/atlasComponents/regionalFeatures/pipes/filterRegionalFeaturesByType.pipe.ts b/src/atlasComponents/regionalFeatures/pipes/filterRegionalFeaturesByType.pipe.ts deleted file mode 100644 index f5bf231bddf4cbb5fa1ae97121c6315074b2305e..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/pipes/filterRegionalFeaturesByType.pipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { IFeature } from "../regionalFeature.service"; - -@Pipe({ - name: 'filterRegionalFeaturesBytype', - pure: true, -}) - -export class FilterRegionalFeaturesByTypePipe implements PipeTransform{ - public transform(array: IFeature[], featureType: string){ - return array.filter(f => featureType ? f.type === featureType : true ) - } -} diff --git a/src/atlasComponents/regionalFeatures/pipes/findRegionFeatureById.pipe.ts b/src/atlasComponents/regionalFeatures/pipes/findRegionFeatureById.pipe.ts deleted file mode 100644 index a083a28a5a843ef36c47c29183efd72ed7e28936..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/pipes/findRegionFeatureById.pipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { IFeature } from "../regionalFeature.service"; - -@Pipe({ - name: 'findRegionFeaturebyId', - pure: true -}) - -export class FindRegionFEatureById implements PipeTransform{ - public transform(features: IFeature[], id: string){ - return features.find(f => f['@id'] === id) - } -} diff --git a/src/atlasComponents/regionalFeatures/regionGetAllFeatures.directive.ts b/src/atlasComponents/regionalFeatures/regionGetAllFeatures.directive.ts deleted file mode 100644 index 920e1f0a98c7cd791514755d48351b63d4005684..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/regionGetAllFeatures.directive.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Directive, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core"; -import { Subscription } from "rxjs"; -import { RegionalFeaturesService } from "./regionalFeature.service"; -import { RegionFeatureBase } from "./singleFeatures/base/regionFeature.base"; - -@Directive({ - selector: '[region-get-all-features-directive]', - exportAs: 'rfGetAllFeatures' -}) - -export class RegionGetAllFeaturesDirective extends RegionFeatureBase implements OnDestroy, OnInit{ - @Output() - loadingStateChanged: EventEmitter<boolean> = new EventEmitter() - - private subscriptions: Subscription[] = [] - - /** - * since the base class has DI - * sub class needs to call super() with the correct DI - */ - constructor( - rfService: RegionalFeaturesService - ){ - super(rfService) - } - - ngOnInit(){ - this.subscriptions.push( - this.isLoading$.subscribe(val => { - this.loadingStateChanged.emit(val) - }) - ) - } - ngOnDestroy(){ - while (this.subscriptions.length > 0) this.subscriptions.pop().unsubscribe() - } -} diff --git a/src/atlasComponents/regionalFeatures/regionalFeature.service.ts b/src/atlasComponents/regionalFeatures/regionalFeature.service.ts deleted file mode 100644 index 0525ba80e033d9db9245d54f368b152b210fda11..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/regionalFeature.service.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { HttpClient } from "@angular/common/http"; -import { Inject, Injectable, OnDestroy, Optional } from "@angular/core"; -import { PureContantService } from "src/util"; -import { getIdFromFullId, getRegionHemisphere, getStringIdsFromRegion, flattenReducer } from 'common/util' -import { forkJoin, from, Observable, of, Subject, Subscription, throwError } from "rxjs"; -import { catchError, map, mapTo, shareReplay, switchMap } from "rxjs/operators"; -import { IHasId } from "src/util/interfaces"; -import { select, Store } from "@ngrx/store"; -import { viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; -import { viewerStateAddUserLandmarks, viewreStateRemoveUserLandmarks } from "src/services/state/viewerState/actions"; -import { uiStateMouseoverUserLandmark } from "src/services/state/uiState/selectors"; -import { APPEND_SCRIPT_TOKEN } from "src/util/constants"; - -const libraries = [ - 'https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js', - 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.1.2/es5/tex-svg.js' -] - -export interface IFeature extends IHasId{ - type: string - name: string - data?: IHasId[] -} - -@Injectable({ - providedIn: 'root' -}) - -export class RegionalFeaturesService implements OnDestroy{ - - public depScriptLoaded$: Observable<boolean> - - private subs: Subscription[] = [] - private templateSelected: any - constructor( - private http: HttpClient, - private pureConstantService: PureContantService, - private store$: Store<any>, - @Optional() @Inject(APPEND_SCRIPT_TOKEN) private appendScript: (src: string) => Promise<HTMLScriptElement> - ){ - this.subs.push( - this.store$.pipe( - select(viewerStateSelectedTemplateSelector) - ).subscribe(val => this.templateSelected = val) - ) - - this.depScriptLoaded$ = this.appendScript - ? from( - libraries.map(this.appendScript) - ).pipe( - mapTo(true), - catchError(() => of(false)), - shareReplay(1), - ) - : of(false) - } - - public mapFeatToCmp = new Map<string, any>() - - ngOnDestroy(){ - while (this.subs.length > 0) this.subs.pop().unsubscribe() - } - - public onHoverLandmarks$ = this.store$.pipe( - select(uiStateMouseoverUserLandmark) - ) - - public getAllFeaturesByRegion(_region: {['fullId']: string} | { id: { kg: {kgSchema: string, kgId: string} } }){ - - const region = { - ..._region, - } - if (!region['fullId']) { - const { kgSchema, kgId } = region['id']?.kg || {} - if (kgSchema && kgId) region['fullId'] = `${kgSchema}/${kgId}` - } - - if (!region['fullId']) throw new Error(`getAllFeaturesByRegion - region does not have fullId defined`) - const regionFullIds = getStringIdsFromRegion(region) - const hemisphereObj = (() => { - const hemisphere = getRegionHemisphere(region) - return hemisphere ? { hemisphere } : {} - })() - - const refSpaceObj = this.templateSelected && this.templateSelected.fullId - ? { referenceSpaceId: getIdFromFullId(this.templateSelected.fullId) } - : {} - - return forkJoin( - regionFullIds.map(regionFullId => this.http.get<{features: IHasId[]}>( - `${this.pureConstantService.backendUrl}regionalFeatures/byRegion/${encodeURIComponent( regionFullId )}`, - { - params: { - ...hemisphereObj, - ...refSpaceObj, - }, - responseType: 'json' - } - ).pipe( - switchMap(({ features }) => forkJoin( - features.map(({ ['@id']: featureId }) => - this.http.get<IFeature>( - `${this.pureConstantService.backendUrl}regionalFeatures/byRegion/${encodeURIComponent( regionFullId )}/${encodeURIComponent( featureId )}`, - { - params: { - ...hemisphereObj, - ...refSpaceObj, - }, - responseType: 'json' - } - ) - ) - )), - )) - ).pipe( - map((arr: IFeature[][]) => arr.reduce(flattenReducer, [])) - ) - } - - public getFeatureData(region: any,feature: IFeature, data: IHasId){ - if (!feature['@id']) throw new Error(`@id attribute for feature is required`) - if (!data['@id']) throw new Error(`@id attribute for data is required`) - const refSpaceObj = this.templateSelected && this.templateSelected.fullId - ? { referenceSpaceId: getIdFromFullId(this.templateSelected.fullId) } - : {} - const hemisphereObj = (() => { - const hemisphere = getRegionHemisphere(region) - return hemisphere ? { hemisphere } : {} - })() - - const regionId = getIdFromFullId(region && region.fullId) - const url = regionId - ? `${this.pureConstantService.backendUrl}regionalFeatures/byRegion/${encodeURIComponent(regionId)}/${encodeURIComponent(feature['@id'])}/${encodeURIComponent(data['@id'])}` - : `${this.pureConstantService.backendUrl}regionalFeatures/byFeature/${encodeURIComponent(feature['@id'])}/${encodeURIComponent(data['@id'])}` - return this.http.get<IHasId>( - url, - { - params: { - ...hemisphereObj, - ...refSpaceObj, - }, - responseType: 'json' - } - ) - } - - public addLandmarks(lms: IHasId[]) { - this.store$.dispatch( - viewerStateAddUserLandmarks({ - landmarks: lms.map(lm => ({ - ...lm, - id: lm['@id'], - name: `region feature: ${lm['@id']}` - })) - }) - ) - } - - public removeLandmarks(lms: IHasId[]) { - this.store$.dispatch( - viewreStateRemoveUserLandmarks({ - payload: { - landmarkIds: lms.map(l => l['@id']) - } - }) - ) - } - - showDatafeatureInfo$ = new Subject<{ fullId: string } | { name: string, description: string }>() -} diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/base/regionFeature.base.ts b/src/atlasComponents/regionalFeatures/singleFeatures/base/regionFeature.base.ts deleted file mode 100644 index 21dde06920b0ddf29f384683c5851de9a171445f..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/base/regionFeature.base.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Directive, EventEmitter, Input, Output, SimpleChanges } from "@angular/core" -import { BehaviorSubject, forkJoin, Observable, of } from "rxjs" -import { catchError, shareReplay, switchMap, tap } from "rxjs/operators" -import { IHasId } from "src/util/interfaces" -import { IFeature, RegionalFeaturesService } from "../../regionalFeature.service" - -@Directive() -export class RegionFeatureBase{ - - private _feature: IFeature - - private feature$ = new BehaviorSubject(null) - @Input() - set feature(val) { - this._feature = val - this.feature$.next(val) - } - get feature(){ - return this._feature - } - - @Input() - public region: any - - @Output('feature-explorer-is-loading') - public dataIsLoadingEventEmitter: EventEmitter<boolean> = new EventEmitter() - - public features: IFeature[] = [] - public data$: Observable<IHasId[]> - - /** - * using isLoading flag for conditional rendering of root element (or display loading spinner) - * this is necessary, or the transcluded tab will always be the active tab, - * as this.features as populated via async - */ - public isLoading$ = new BehaviorSubject(false) - private _isLoading: boolean = false - get isLoading(){ - return this._isLoading - } - set isLoading(val){ - if (val !== this._isLoading) - this._isLoading = val - this.isLoading$.next(val) - } - - public dataIsLoading$ = new BehaviorSubject(false) - private _dataIsLoading = false - set dataIsLoading(val) { - if (val === this._dataIsLoading) return - this._dataIsLoading = val - this.dataIsLoading$.next(val) - this.dataIsLoadingEventEmitter.emit(val) - } - get dataIsLoading(){ - return this._dataIsLoading - } - - ngOnChanges(changes: SimpleChanges){ - if (changes.region && changes.region.previousValue !== changes.region.currentValue) { - this.isLoading = true - this.features = [] - - const _ = (changes.region.currentValue - ? this._regionalFeatureService.getAllFeaturesByRegion(changes.region.currentValue) - : of([]) - ).pipe( - /** - * region may have no fullId defined - * in this case, just emit [] - */ - catchError(() => of([])) - ).subscribe({ - next: features => this.features = features, - complete: () => this.isLoading = false - }) - } - } - - constructor( - private _regionalFeatureService: RegionalFeaturesService - ){ - - /** - * once feature stops loading, watch for input feature - */ - this.data$ = this.feature$.pipe( - tap(() => this.dataIsLoading = true), - switchMap((feature: IFeature) => forkJoin( - feature.data.map(datum => this._regionalFeatureService.getFeatureData(this.region, feature, datum))) - ), - tap(() => this.dataIsLoading = false), - shareReplay(1), - ) - } -} diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.component.ts b/src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.component.ts deleted file mode 100644 index 11fd197fbd45b569e35f15f5a9ae54ab13f07777..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.component.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { Component, Inject, Optional, EventEmitter } from "@angular/core"; -import { Store } from "@ngrx/store"; -import { merge, Subject, Subscription } from "rxjs"; -import { debounceTime, map, scan, take } from "rxjs/operators"; -import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; -import { RegionalFeaturesService } from "src/atlasComponents/regionalFeatures/regionalFeature.service"; -import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; -import { IHasId } from "src/util/interfaces"; -import { RegionFeatureBase } from "../../base/regionFeature.base"; -import { ISingleFeature } from '../../interfaces' - -const selectedColor = [ 255, 0, 0 ] - -@Component({ - templateUrl: './iEEGRecordings.template.html', - styleUrls: [ - './iEEGRecordings.style.css' - ] -}) - -export class IEEGRecordingsCmp extends RegionFeatureBase implements ISingleFeature{ - private landmarksLoaded: IHasId[] = [] - private onDestroyCb: (() => void)[] = [] - private sub: Subscription[] = [] - - constructor( - private regionFeatureService: RegionalFeaturesService, - private store: Store<any>, - @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) private regClickIntp: ClickInterceptor, - ){ - super(regionFeatureService) - } - - public viewChanged = new EventEmitter<boolean>() - - ngOnInit(){ - if (this.regClickIntp) { - const { deregister, register } = this.regClickIntp - const clickIntp = this.clickIntp.bind(this) - register(clickIntp) - this.onDestroyCb.push(() => { - deregister(clickIntp) - }) - } - this.sub.push( - this.data$.subscribe(data => { - const landmarksTobeLoaded: IHasId[] = [] - - for (const datum of data) { - const electrodeId = datum['@id'] - landmarksTobeLoaded.push( - ...datum['contactPoints'].map(({ ['@id']: contactPtId, position }) => { - return { - _: { - electrodeId, - contactPtId - }, - ['@id']: `${electrodeId}#${contactPtId}`, - position - } - }) - ) - } - /** - * remove first, then add - */ - if (this.landmarksLoaded.length > 0) this.regionFeatureService.removeLandmarks(this.landmarksLoaded) - if (landmarksTobeLoaded.length > 0) this.regionFeatureService.addLandmarks(landmarksTobeLoaded) - this.landmarksLoaded = landmarksTobeLoaded - }) - ) - - this.sub.push( - this.dataIsLoading$.subscribe(() => this.viewChanged.emit(true)) - ) - - this.onDestroyCb.push(() => { - if (this.landmarksLoaded.length > 0) this.regionFeatureService.removeLandmarks(this.landmarksLoaded) - }) - - this.sub.push( - this.openElectrodeId$.pipe( - debounceTime(200) - ).subscribe(arr => { - - if (this.landmarksLoaded.length > 0) { - this.regionFeatureService.removeLandmarks(this.landmarksLoaded) - this.regionFeatureService.addLandmarks(this.landmarksLoaded.map(lm => { - const selected = arr.some(id => id === lm['_']['electrodeId']) - return { - ...lm, - color: selected ? selectedColor : null, - showInSliceView: selected - } - })) - } - }) - ) - } - - - ngOnDestroy(){ - while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() - while(this.sub.length > 0) this.sub.pop().unsubscribe() - } - - handleContactPtClk(contactPt: IHasId & { position: number[] }){ - const { position } = contactPt - this.store.dispatch( - viewerStateChangeNavigation({ - navigation: { - position: position.map(v => v * 1e6), - positionReal: true, - animation: {} - }, - }) - ) - } - - handleDatumExpansion(electrodeId: string, open: boolean){ - /** - * TODO either debounce call here, or later down stream - */ - if (open) this.exploreElectrode$.next(electrodeId) - else this.unExploreElectrode$.next(electrodeId) - } - - private unExploreElectrode$ = new Subject<string>() - private exploreElectrode$ = new Subject<string>() - public openElectrodeId$ = merge( - this.unExploreElectrode$.pipe( - map(id => ({ - add: null, - remove: id - })) - ), - this.exploreElectrode$.pipe( - map(id => ({ - add: id, - remove: null - })) - ) - ).pipe( - scan((acc, curr) => { - const { add, remove } = curr - const set = new Set(acc) - if (add) set.add(add) - if (remove) set.delete(remove) - return Array.from(set) - }, []) - ) - - private clickIntp(ev: any): boolean { - let hoveredLandmark = null - this.regionFeatureService.onHoverLandmarks$.pipe( - take(1) - ).subscribe(val => { - hoveredLandmark = val - }) - if (!hoveredLandmark) return true - const isOne = this.landmarksLoaded.some(lm => { - return lm['_']['electrodeId'] === hoveredLandmark['_']['electrodeId'] - }) - if (!isOne) return true - this.exploreElectrode$.next(hoveredLandmark['_']['electrodeId']) - } -} diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.template.html b/src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.template.html deleted file mode 100644 index 7a4b70d88a55e2e8325446c946c2035ed29d59bf..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.template.html +++ /dev/null @@ -1,56 +0,0 @@ -<ng-container *ngIf="!dataIsLoading; else loadingTmpl"> - - <mat-accordion - class="ml-24px-n mr-24px-n d-block"> - <mat-expansion-panel *ngFor="let datum of (data$ | async)" - [expanded]="openElectrodeId$ | async | includes : datum['@id']" - (opened)="handleDatumExpansion(datum['@id'], true)" - (closed)="handleDatumExpansion(datum['@id'], false)" - togglePosition="before"> - <mat-expansion-panel-header> - <mat-panel-title> - Electrode - </mat-panel-title> - <mat-panel-description class="text-nowrap"> - {{ datum['@id'] }} - </mat-panel-description> - </mat-expansion-panel-header> - - <label for="task-list" class="d-block mat-h4 mt-4 text-muted"> - Tasks - </label> - <section class="d-flex align-items-center mt-1"> - <section id="task-list" class="flex-grow-1 flex-shrink-1 overflow-x-auto"> - <div role="list"> - <mat-chip *ngFor="let task of datum['tasks']" class="ml-1"> - {{ task }} - </mat-chip> - </div> - </section> - </section> - - <label for="contact-points-list" class="d-block mat-h4 mt-4 text-muted"> - Contact Points - </label> - <section class="d-flex align-items-center mt-1"> - <section id="contact-points-list" class="flex-grow-1 flex-shrink-1 overflow-x-auto"> - <div role="list"> - <mat-chip *ngFor="let contactPt of datum['contactPoints']" - [matTooltip]="contactPt['position']" - (click)="handleContactPtClk(contactPt)" - class="ml-1"> - {{ contactPt['@id'] }} - </mat-chip> - </div> - </section> - </section> - - </mat-expansion-panel> - </mat-accordion> - -</ng-container> - -<!-- loading template --> -<ng-template #loadingTmpl> - <spinner-cmp></spinner-cmp> -</ng-template> diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/module.ts b/src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/module.ts deleted file mode 100644 index 76d6cd9f67dba1c93bc6e51ee9c845bb035d4766..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; -import { ComponentsModule } from "src/components"; -import { AngularMaterialModule } from "src/sharedModules"; -import { UtilModule } from "src/util"; -import { RegionalFeaturesService } from "../../regionalFeature.service"; -import { IEEGRecordingsCmp } from "./iEEGRecordings/iEEGRecordings.component"; - -@NgModule({ - imports: [ - CommonModule, - UtilModule, - AngularMaterialModule, - ComponentsModule, - ], - declarations: [ - IEEGRecordingsCmp - ], - exports: [ - IEEGRecordingsCmp - ] -}) - -export class FeatureIEEGRecordings{ - constructor( - rService: RegionalFeaturesService - ){ - rService.mapFeatToCmp.set('iEEG recording', IEEGRecordingsCmp) - } -} diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/interfaces.ts b/src/atlasComponents/regionalFeatures/singleFeatures/interfaces.ts deleted file mode 100644 index bd3861d391b4bcc90d262ee16e124b51e4affee2..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/interfaces.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { EventEmitter } from "@angular/core"; -import { IFeature } from "../regionalFeature.service"; - -export interface ISingleFeature{ - feature: IFeature - region: any - viewChanged: EventEmitter<boolean> -} diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/filterReceptorBytype.pipe.ts b/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/filterReceptorBytype.pipe.ts deleted file mode 100644 index 1b0a91dd80079acf6e3cc6709c4ac3cc74ec5f97..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/filterReceptorBytype.pipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { IHasId } from "src/util/interfaces"; - -@Pipe({ - name: 'filterReceptorByType', - pure: true -}) - -export class FilterReceptorByType implements PipeTransform{ - public transform(arr: IHasId[], qualifer: string): IHasId[]{ - return (arr || []).filter(({ ['@id']: dId }) => dId.indexOf(qualifer) >= 0) - } -} diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/getAllReceptors.pipe.ts b/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/getAllReceptors.pipe.ts deleted file mode 100644 index ba3016186957b4851e469792cc834110152ddb1b..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/getAllReceptors.pipe.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { IHasId } from "src/util/interfaces"; - -@Pipe({ - name: 'getAllReceptors', - pure: true -}) - -export class GetAllReceptorsPipe implements PipeTransform{ - public transform(arr: IHasId[]): string[]{ - return (arr || []).reduce((acc, curr) => { - const thisType = /_(pr|ar)_([a-zA-Z0-9_]+)\./.exec(curr['@id']) - if (!thisType) return acc - return new Set(acc).has(thisType) ? acc : acc.concat(thisType[2]) - }, []) - } -} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/getId.pipe.ts b/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/getId.pipe.ts deleted file mode 100644 index 3af1a02bd4a98455bc9b0166ff4adfc5afa53646..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/getId.pipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; - -@Pipe({ - name: 'getId', - pure: true -}) - -export class GetIdPipe implements PipeTransform{ - public transform(fullId: string): string{ - const re = /\/([a-f0-9-]+)$/.exec(fullId) - return (re && re[1]) || null - } -} diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/getUrl.pipe.ts b/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/getUrl.pipe.ts deleted file mode 100644 index f4fdfab5613c225aa293a05b45c9d0ab8be666ab..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/getUrl.pipe.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { IHasId } from "src/util/interfaces"; - -interface IReceptorDatum extends IHasId{ - ['@context']: { - [key: string]: string - } - filename: string - mimetype: string - url: string | { - url: string - ['receptors.tsv']: string - } -} - -interface IHRef{ - url: string - filename: string -} - -@Pipe({ - name: 'getUrls', - pure: true -}) - -export class GetUrlsPipe implements PipeTransform{ - public transform(input: IReceptorDatum): IHRef[]{ - const output: IHRef[] = [] - let _url = typeof input.url === 'string' - ? input.url - : input.url.url - - for (const key in (input['@context'] || {})) { - _url = _url.replace(`${key}:`, input['@context'][key]) - } - - const match = /\/([\w-.]+)$/.exec(_url) - output.push({ - url: _url, - filename: match ? match[1] : 'download' - }) - return output - } -} - diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/module.ts b/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/module.ts deleted file mode 100644 index e8ab67e4273b33310171ba1b36f7199bf5286008..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/module.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; -import { AngularMaterialModule } from "src/sharedModules"; -import { RegionalFeaturesService } from "../../regionalFeature.service"; -import { FilterReceptorByType } from "./filterReceptorBytype.pipe"; -import { GetAllReceptorsPipe } from "./getAllReceptors.pipe"; -import { GetIdPipe } from "./getId.pipe"; -import { GetUrlsPipe } from "./getUrl.pipe"; -import { ReceptorDensityFeatureCmp } from "./receptorDensity/receptorDensity.component"; - -@NgModule({ - imports: [ - CommonModule, - AngularMaterialModule, - ], - declarations: [ - ReceptorDensityFeatureCmp, - FilterReceptorByType, - GetIdPipe, - GetAllReceptorsPipe, - GetUrlsPipe, - ], - schemas: [ - CUSTOM_ELEMENTS_SCHEMA - ] -}) - -export class ReceptorDensityModule{ - constructor( - rService: RegionalFeaturesService - ){ - rService.mapFeatToCmp.set(`Receptor density measurement`, ReceptorDensityFeatureCmp) - } -} diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.component.ts b/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.component.ts deleted file mode 100644 index 52cda125d7aa02bfaa062a121b62b94d365fa612..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Component, ElementRef, EventEmitter, HostListener, OnDestroy, Optional } from "@angular/core"; -import { fromEvent, Observable, of, Subscription } from "rxjs"; -import { RegionalFeaturesService } from "src/atlasComponents/regionalFeatures/regionalFeature.service"; -import { PureContantService } from "src/util"; -import { RegionFeatureBase } from "../../base/regionFeature.base"; -import { ISingleFeature } from "../../interfaces"; -import { CONST } from 'common/constants' -import { environment } from 'src/environments/environment' - -const { - RECEPTOR_FP_CAPTION, - RECEPTOR_PR_CAPTION, - RECEPTOR_AR_CAPTION, -} = CONST -@Component({ - templateUrl: './receptorDensity.template.html', - styleUrls: [ - './receptorDensity.style.css' - ] -}) - -export class ReceptorDensityFeatureCmp extends RegionFeatureBase implements ISingleFeature, OnDestroy{ - - public RECEPTOR_FP_CAPTION = RECEPTOR_FP_CAPTION - public RECEPTOR_PR_CAPTION = RECEPTOR_PR_CAPTION - public RECEPTOR_AR_CAPTION = RECEPTOR_AR_CAPTION - - public DS_PREVIEW_URL = environment.DATASET_PREVIEW_URL - viewChanged: EventEmitter<null> = new EventEmitter() - - private WEB_COMPONENT_MOUSEOVER_EVENT_NAME = 'kg-ds-prv-regional-feature-mouseover' - private webComponentOnHover: string = null - - public selectedReceptor: string - - public darktheme$: Observable<boolean> - - private subs: Subscription[] = [] - public depScriptLoaded$: Observable<boolean> - constructor( - regService: RegionalFeaturesService, - el: ElementRef, - @Optional() pureConstantService: PureContantService - ){ - super(regService) - this.depScriptLoaded$ = regService.depScriptLoaded$ - if (pureConstantService) { - this.darktheme$ = pureConstantService.darktheme$ - } else { - this.darktheme$ = of(false) - } - - this.subs.push( - fromEvent(el.nativeElement, this.WEB_COMPONENT_MOUSEOVER_EVENT_NAME).subscribe((ev: CustomEvent) => { - this.webComponentOnHover = ev.detail?.data?.receptor?.label - }) - ) - } - - @HostListener('click') - onClick(){ - if (this.webComponentOnHover) { - this.selectedReceptor = this.webComponentOnHover - } - } - - ngOnDestroy(){ - while(this.subs.length > 0) this.subs.pop().unsubscribe() - } -} diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.style.css b/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.style.css deleted file mode 100644 index 0437be86256eb2a7039e72e33e80de0a92ea62ef..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.style.css +++ /dev/null @@ -1,11 +0,0 @@ -kg-dataset-previewer -{ - display: block; - height: 20em; -} - -kg-ds-prv-regional-feature-view -{ - display: block; - min-height: 20em; -} diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.template.html b/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.template.html deleted file mode 100644 index f1454e06ca2bef47d6629a80a3907497b650ed76..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.template.html +++ /dev/null @@ -1,143 +0,0 @@ -<label for="fingerprint-cmp" class="d-inline mat-h4 mt-4 text-muted"> - Fingerprint : -</label> - -<ng-container *ngFor="let datum of (data$ | async | filterReceptorByType : '_fp_')"> - - <ng-container *ngTemplateOutlet="labelTmpl; context: { - for: 'fingerprint-cmp', - label: RECEPTOR_FP_CAPTION - }"> - </ng-container> - - <div class="d-block"> - <ng-container *ngTemplateOutlet="datasetPreviewTmpl; context: { - id: 'fingerprint-cmp', - kgId: (feature['@id'] | getId), - filename: datum['@id'], - datum: datum - }"> - </ng-container> - </div> - -</ng-container> - -<mat-divider></mat-divider> - -<ng-container *ngIf="data$ | async | getAllReceptors as allReceptors; else selectPlaceHolderTmpl"> - - <mat-form-field class="mt-2 w-100" *ngIf="allReceptors.length > 0; else selectPlaceHolderTmpl"> - <mat-label> - Select a receptor - </mat-label> - <mat-select [(value)]="selectedReceptor"> - <mat-option - *ngFor="let receptor of allReceptors" - [value]="receptor"> - {{ receptor }} - </mat-option> - </mat-select> - </mat-form-field> - -</ng-container> - -<ng-template #selectPlaceHolderTmpl> - <span class="text-muted">No profile or autoradiographs available.</span> -</ng-template> - -<ng-template [ngIf]="selectedReceptor"> - <ng-container *ngTemplateOutlet="prArTmpl; context: { filter: '_pr_', label: 'Profile' }"> - </ng-container> - <ng-container *ngTemplateOutlet="prArTmpl; context: { filter: '_ar_', label: 'Autoradiograph' }"> - </ng-container> -</ng-template> - -<!-- ar/pr template --> -<ng-template #prArTmpl let-label="label" let-filter="filter"> - <ng-container *ngFor="let datum of (data$ | async | filterReceptorByType : selectedReceptor | filterReceptorByType : filter); let first = first"> - <ng-template [ngIf]="first"> - <label [attr.for]="label + '-cmp'" class="d-inline mat-h4 mt-4 text-muted"> - {{ label }} : - </label> - </ng-template> - - <ng-container *ngTemplateOutlet="labelTmpl; context: { - for: label + '-cmp', - label: 'Autoradiograph' ? RECEPTOR_AR_CAPTION : RECEPTOR_PR_CAPTION - }"> - </ng-container> - - <div class="d-block"> - <ng-container *ngTemplateOutlet="datasetPreviewTmpl; context: { - id: label + '-cmp', - kgId: (feature['@id'] | getId), - filename: datum['@id'], - datum: datum - }"> - </ng-container> - </div> - - </ng-container> -</ng-template> - - -<!-- display preview tmpl --> -<ng-template #datasetPreviewTmpl - let-id="id" - let-kgId="kgId" - let-filename="filename" - let-datum="datum"> - - <!-- download btns --> - <ng-container *ngFor="let urlObj of datum | getUrls"> - <ng-container *ngTemplateOutlet="downloadBtnTmpl; context: { - url: urlObj.url, - filename: urlObj.filename, - tooltip: 'download ' + urlObj.filename - }"> - </ng-container> - </ng-container> - - <!-- render preview --> - <kg-ds-prv-regional-feature-view - *ngIf="depScriptLoaded$ | async; else fallbackTmpl" - [attr.id]="id" - [darkmode]="darktheme$ | async" - (renderEvent)="viewChanged.emit()" - [backendUrl]="DS_PREVIEW_URL" - [kgId]="kgId" - [filename]="filename"> - </kg-ds-prv-regional-feature-view> - - <ng-template #fallbackTmpl> - <kg-dataset-previewer - [attr.id]="id" - (renderEvent)="viewChanged.emit()" - [backendUrl]="DS_PREVIEW_URL" - [kgId]="kgId" - [filename]="filename"> - </kg-dataset-previewer> - </ng-template> -</ng-template> - -<ng-template #downloadBtnTmpl - let-url="url" - let-filename="filename" - let-tooltip="tooltip"> - <a [href]="url" - class="d-inline-block" - [download]="filename" - [matTooltip]="tooltip"> - <button mat-icon-button> - <i class="fas fa-download"></i> - </button> - </a> -</ng-template> - -<ng-template #labelTmpl let-label="label" let-for="for"> - <label [attr.for]="for" class="d-inline text-muted"> - <span> - {{ label }} - </span> - </label> -</ng-template> \ No newline at end of file diff --git a/src/atlasComponents/sapi/core/index.ts b/src/atlasComponents/sapi/core/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7fc4cbb696db25e9fa2159b5862e23c0bdb260e --- /dev/null +++ b/src/atlasComponents/sapi/core/index.ts @@ -0,0 +1,4 @@ +export { SAPIAtlas } from "./sapiAtlas" +export { SAPISpace } from "./sapiSpace" +export { SAPIParcellation } from "./sapiParcellation" +export { SAPIRegion } from "./sapiRegion" diff --git a/src/atlasComponents/sapi/core/sapiAtlas.ts b/src/atlasComponents/sapi/core/sapiAtlas.ts new file mode 100644 index 0000000000000000000000000000000000000000..788a803bd32eb8cf8b5dcc98545284abbf4fe056 --- /dev/null +++ b/src/atlasComponents/sapi/core/sapiAtlas.ts @@ -0,0 +1,5 @@ +import { SAPI } from "../sapi.service"; + +export class SAPIAtlas{ + constructor(private sapi: SAPI, public id: string){} +} diff --git a/src/atlasComponents/sapi/core/sapiParcellation.ts b/src/atlasComponents/sapi/core/sapiParcellation.ts new file mode 100644 index 0000000000000000000000000000000000000000..05e367a54c71aa42f312c5232ff69dab251f8ccf --- /dev/null +++ b/src/atlasComponents/sapi/core/sapiParcellation.ts @@ -0,0 +1,29 @@ +import { SapiVolumeModel } from ".." +import { SAPI } from "../sapi.service" +import { SapiParcellationModel, SapiRegionModel } from "../type" + +export class SAPIParcellation{ + constructor(private sapi: SAPI, public atlasId: string, public id: string){ + + } + getDetail(): Promise<SapiParcellationModel>{ + return this.sapi.cachedGet<SapiParcellationModel>( + `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}` + ) + } + getRegions(spaceId: string): Promise<SapiRegionModel[]> { + return this.sapi.cachedGet<SapiRegionModel[]>( + `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`, + { + params: { + space_id: spaceId + } + } + ) + } + getVolumes(): Promise<SapiVolumeModel[]>{ + return this.sapi.cachedGet<SapiVolumeModel[]>( + `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes` + ) + } +} diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts new file mode 100644 index 0000000000000000000000000000000000000000..6eb6996df0868ee07edbd65e8e7c30e79fba79eb --- /dev/null +++ b/src/atlasComponents/sapi/core/sapiRegion.ts @@ -0,0 +1,35 @@ +import { SAPI } from ".."; +import { SapiRegionalFeatureModel } from "../type"; + +export class SAPIRegion{ + constructor( + private sapi: SAPI, + public atlasId: string, + public parcId: string, + public id: string, + ){ + + } + + 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`, + { + params: { + space_id: spaceId + } + } + ).toPromise() + } + + getFeatureInstance(instanceId: string, 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/${encodeURIComponent(instanceId)}`, + { + params: { + space_id: spaceId + } + } + ).toPromise() + } +} diff --git a/src/atlasComponents/sapi/sapiSpace.ts b/src/atlasComponents/sapi/core/sapiSpace.ts similarity index 55% rename from src/atlasComponents/sapi/sapiSpace.ts rename to src/atlasComponents/sapi/core/sapiSpace.ts index 9999a6d3e3332a8909981ac72719ab4e9a06af72..1b3ef810f646100f381e4d847013e4354b3cc8bb 100644 --- a/src/atlasComponents/sapi/sapiSpace.ts +++ b/src/atlasComponents/sapi/core/sapiSpace.ts @@ -1,7 +1,8 @@ import { Observable } from "rxjs" -import { SAPI } from './sapi' +import { SAPI } from '../sapi.service' import { camelToSnake } from 'common/util' import { IVolumeTypeDetail } from "src/util/siibraApiConstants/types" +import { SapiSpaceModel, SapiSpatialFeatureModel, SapiVolumeModel } from "../type" type FeatureResponse = { features: { @@ -32,47 +33,38 @@ type Volume = { } } -type SapiVoiResponse = { - "@id": string - name: string - description: string - urls: { - cite?: string - doi: string - }[] - location: { - space: { - "@id": string - center: Point - minpoint: Point - maxpoint: Point - } - } - volumes: Volume[] -} - -type SapiSpatialResp = SapiVoiResponse - export class SAPISpace{ - constructor(private sapi: SAPI, private atlasId: string, public id: string){} + constructor(private sapi: SAPI, public atlasId: string, public id: string){} getModalities(): Observable<FeatureResponse> { return this.sapi.http.get<FeatureResponse>( - `${this.sapi.bsEndpoint}/atlases/${this.atlasId}/spaces/${this.id}/features` + `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features` ) } - getFeatures(modalityId: string, opts: SpatialFeatureOpts): Observable<SapiSpatialResp[]> { + getFeatures(modalityId: string, opts: SpatialFeatureOpts): Promise<SapiSpatialFeatureModel[]> { const query = {} for (const [key, value] of Object.entries(opts)) { query[camelToSnake(key)] = value } - return this.sapi.http.get<SapiSpatialResp[]>( + return this.sapi.cachedGet<SapiSpatialFeatureModel[]>( `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features/${encodeURIComponent(modalityId)}`, { params: query } ) } + + getDetail(): Promise<SapiSpaceModel>{ + return this.sapi.cachedGet<SapiSpaceModel>( + `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`, + ) + } + + getVolumes(): Promise<SapiVolumeModel[]>{ + return this.sapi.cachedGet<SapiVolumeModel[]>( + `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/volumes`, + ) + } } diff --git a/src/atlasComponents/sapi/directives/spatialFeatureBBox.directive.ts b/src/atlasComponents/sapi/directives/spatialFeatureBBox.directive.ts index 88a23c7f1a227c7e02044ceeda54f467cdc0b9f5..bdd15c9b221c01dc699d1959de3eade353c416c9 100644 --- a/src/atlasComponents/sapi/directives/spatialFeatureBBox.directive.ts +++ b/src/atlasComponents/sapi/directives/spatialFeatureBBox.directive.ts @@ -1,8 +1,8 @@ import { combineLatest, Observable, BehaviorSubject, Subject, Subscription, of, merge } from 'rxjs'; import { debounceTime, map, distinctUntilChanged, switchMap, tap, startWith, filter } from 'rxjs/operators'; import { Directive, EventEmitter, Input, OnDestroy, Output } from "@angular/core"; -import { BoundingBoxConcept, SapiVoiResponse } from '../type' -import { SAPI } from '../sapi' +import { BoundingBoxConcept, SapiSpatialFeatureModel, SapiVOIDataResponse } from '../type' +import { SAPI } from '../sapi.service' import { environment } from "src/environments/environment" function validateBbox(input: any): boolean { @@ -52,8 +52,8 @@ export class SpatialFeatureBBox implements OnDestroy{ } @Output('sii-xp-spatial-feat-bbox-features') - featureOutput = new EventEmitter<SapiVoiResponse[]>() - features$ = new BehaviorSubject<SapiVoiResponse[]>([]) + featureOutput = new EventEmitter<SapiVOIDataResponse[]>() + features$ = new BehaviorSubject<SapiVOIDataResponse[]>([]) @Output('sii-xp-spatial-feat-bbox-busy') busy$ = new EventEmitter<boolean>() @@ -96,12 +96,12 @@ export class SpatialFeatureBBox implements OnDestroy{ }) => { if (!atlasId || !spaceId || !bbox) { this.busy$.emit(false) - return of([]) + return of([] as SapiSpatialFeatureModel[]) } const space = this.svc.getSpace(atlasId, spaceId) return space.getFeatures(SpatialFeatureBBox.FEATURE_NAME, { bbox: JSON.stringify(bbox) }) }) - ).subscribe(results => { + ).subscribe((results: SapiVOIDataResponse[]) => { this.featureOutput.emit(results) this.features$.next(results) this.busy$.emit(false) diff --git a/src/atlasComponents/sapi/index.ts b/src/atlasComponents/sapi/index.ts index 00d79eee47547d480e353d9d29a062b65fc37420..092ebd6e947b5c63cad8074d1e3d311539f62a10 100644 --- a/src/atlasComponents/sapi/index.ts +++ b/src/atlasComponents/sapi/index.ts @@ -1,8 +1,21 @@ export { SAPIModule } from './module' export { SpatialFeatureBBox } from './directives/spatialFeatureBBox.directive' -import { SapiVoiResponse } from './type' -export type SpatialFeatureResponse = SapiVoiResponse - -export type FeatureResponse = SpatialFeatureResponse +export { + SapiAtlasModel, + SapiParcellationModel, + SapiSpaceModel, + SapiRegionModel, + SapiVolumeModel, + SapiDatasetModel, + SapiRegionalFeatureModel, + SapiSpatialFeatureModel +} from "./type" +export { SAPI } from "./sapi.service" +export { + SAPIAtlas, + SAPISpace, + SAPIParcellation, + SAPIRegion +} from "./core" diff --git a/src/atlasComponents/sapi/module.ts b/src/atlasComponents/sapi/module.ts index 85957778367792123a5eb321082fb30f7e3346ae..452d71f70804eedce57e4c5b051e3c3abdff4be4 100644 --- a/src/atlasComponents/sapi/module.ts +++ b/src/atlasComponents/sapi/module.ts @@ -1,11 +1,18 @@ import { NgModule } from "@angular/core"; -import { SAPI } from "./sapi"; +import { SAPI } from "./sapi.service"; import { SpatialFeatureBBox } from "./directives/spatialFeatureBBox.directive" import { CommonModule } from "@angular/common"; +import { EffectsModule } from "@ngrx/effects"; +import { SapiEffects } from "./sapi.effects"; +import { HTTP_INTERCEPTORS } from "@angular/common/http"; +import { PriorityHttpInterceptor } from "src/util/priority"; @NgModule({ imports: [ CommonModule, + EffectsModule.forFeature([ + SapiEffects + ]) ], declarations: [ SpatialFeatureBBox, @@ -14,7 +21,12 @@ import { CommonModule } from "@angular/common"; SpatialFeatureBBox, ], providers: [ - SAPI + SAPI, + { + provide: HTTP_INTERCEPTORS, + useClass: PriorityHttpInterceptor, + multi: true + } ] }) export class SAPIModule{} diff --git a/src/atlasComponents/sapi/sapi.effects.ts b/src/atlasComponents/sapi/sapi.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1cb06fb33676d803368afaac8f39bbd0c7887ef --- /dev/null +++ b/src/atlasComponents/sapi/sapi.effects.ts @@ -0,0 +1,12 @@ +import { Injectable } from "@angular/core"; +import { SAPI } from "./sapi.service" + +@Injectable() +export class SapiEffects{ + + constructor( + private sapiSvc: SAPI + ){ + + } +} diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..aacc57ee38a39d2a8fc27395fa85216ed25dcd13 --- /dev/null +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -0,0 +1,133 @@ +import { Inject, Injectable } from "@angular/core"; +import { HttpClient } from '@angular/common/http'; +import { BS_ENDPOINT } from 'src/util/constants'; +import { map, shareReplay, take, tap } from "rxjs/operators"; +import { SAPIAtlas, SAPISpace } from './core' +import { SapiAtlasModel, SapiParcellationModel, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel } from "./type"; +import { CachedFunction } from "src/util/fn"; +import { SAPIParcellation } from "./core/sapiParcellation"; +import { SAPIRegion } from "./core/sapiRegion" +import { MatSnackBar } from "@angular/material/snack-bar"; + +export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version' +export const SIIBRA_API_VERSION = '0.2.0' + +type RegistryType = SAPIAtlas | SAPISpace | SAPIParcellation + +@Injectable() +export class SAPI{ + + public bsEndpoint = 'https://siibra-api-dev.apps-dev.hbp.eu/v1_0' + + registry = { + _map: {} as Record<string, { + func: (...arg: any[]) => RegistryType, + args: string[] + }>, + get<T>(id: string): T { + if (!this._map[id]) return null + const { func, args } = this._map[id] + return func(...args) + }, + set(id: string, func: (...args: any[]) => RegistryType, args: string[]) { + if (this._map[id]) { + console.warn(`id ${id} already mapped as ${this._map[id]}`) + } + this._map[id] = { func, args } + } + } + + getAtlas(atlasId: string): SAPIAtlas { + return new SAPIAtlas(this, atlasId) + } + + getSpace(atlasId: string, spaceId: string): SAPISpace { + return new SAPISpace(this, atlasId, spaceId) + } + + getParcellation(atlasId: string, parcId: string): SAPIParcellation { + return new SAPIParcellation(this, atlasId, parcId) + } + + getRegion(atlasId: string, parcId: string, regionId: string): SAPIRegion{ + return new SAPIRegion(this, atlasId, parcId, regionId) + } + + @CachedFunction({ + serialization: (atlasId, spaceId, ...args) => `sapi::getSpaceDetail::${atlasId}::${spaceId}` + }) + getSpaceDetail(atlasId: string, spaceId: string, priority = 0): Promise<SapiSpaceModel> { + return this.getSpace(atlasId, spaceId).getDetail() + } + + @CachedFunction({ + serialization: (atlasId, parcId, ...args) => `sapi::getParcDetail::${atlasId}::${parcId}` + }) + getParcDetail(atlasId: string, parcId: string, priority = 0): Promise<SapiParcellationModel> { + return this.getParcellation(atlasId, parcId).getDetail() + } + + @CachedFunction({ + serialization: (atlasId, parcId, spaceId, ...args) => `sapi::getRegions::${atlasId}::${parcId}::${spaceId}` + }) + getParcRegions(atlasId: string, parcId: string, spaceId: string, priority = 0): Promise<SapiRegionModel[]> { + const parc = this.getParcellation(atlasId, parcId) + return parc.getRegions(spaceId) + } + + @CachedFunction({ + serialization: (atlasId, parcId, spaceId, regionId, ...args) => `sapi::getRegions::${atlasId}::${parcId}::${spaceId}::${regionId}` + }) + getRegionFeatures(atlasId: string, parcId: string, spaceId: string, regionId: string, priority = 0): Promise<SapiRegionalFeatureModel[]>{ + + const reg = this.getRegion(atlasId, parcId, regionId) + return reg.getFeatures(spaceId) + } + + @CachedFunction({ + serialization: (url, params) => `sapi::cachedGet::${url}::${JSON.stringify(params)}` + }) + cachedGet<T>(url: string, option?: Record<string, any>) { + return this.http.get<T>(url, option).toPromise() + } + + + public atlases$ = this.http.get<SapiAtlasModel[]>( + `${this.bsEndpoint}/atlases`, + { + observe: "response" + } + ).pipe( + tap(resp => { + + const respVersion = resp.headers.get(SIIBRA_API_VERSION_HEADER_KEY) + if (respVersion !== SIIBRA_API_VERSION) { + this.snackbar.open(`Expecting ${SIIBRA_API_VERSION}, got ${respVersion}. Some functionalities may not work as expected.`, 'Dismiss', { + duration: 5000 + }) + } + console.log(`siibra-api::version::${respVersion}, expecting::${SIIBRA_API_VERSION}`) + }), + map(resp => resp.body), + shareReplay(1) + ) + + constructor( + public http: HttpClient, + private snackbar: MatSnackBar, + // @Inject(BS_ENDPOINT) public bsEndpoint: string, + ){ + this.atlases$.subscribe(atlases => { + for (const atlas of atlases) { + for (const space of atlas.spaces) { + this.registry.set(space["@id"], this.getSpace.bind(this), [atlas["@id"], space["@id"]]) + this.getSpaceDetail(atlas["@id"], space["@id"]) + } + for (const parc of atlas.parcellations) { + this.registry.set(parc["@id"], this.getParcellation.bind(this), [atlas["@id"], parc["@id"]]) + this.getParcDetail(atlas["@id"], parc["@id"]) + } + } + }) + } +} diff --git a/src/atlasComponents/sapi/sapi.ts b/src/atlasComponents/sapi/sapi.ts deleted file mode 100644 index 0e3e141e01c8ae9e18f4ad0c211b64af3feff4d9..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapi/sapi.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BS_ENDPOINT } from 'src/util/constants'; -import { Inject, Injectable } from "@angular/core"; -import { SAPISpace } from './sapiSpace' -import { HttpClient } from '@angular/common/http'; - -@Injectable() -export class SAPI{ - - getSpace(atlasId: string, spaceId: string): SAPISpace { - return new SAPISpace(this, atlasId, spaceId) - } - - constructor( - public http: HttpClient, - @Inject(BS_ENDPOINT) public bsEndpoint: string, - ){ - - } -} diff --git a/src/atlasComponents/sapi/schema.ts b/src/atlasComponents/sapi/schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1f52d82ee9d74a9687466aa12867ad3ee4f54fa --- /dev/null +++ b/src/atlasComponents/sapi/schema.ts @@ -0,0 +1,1769 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions": { + /** Returns all regions for a given parcellation id. */ + get: operations["get_all_regions_from_atlas_parc_space_atlases__atlas_id__parcellations__parcellation_id__regions_get"]; + }; + "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}/features": { + /** Returns all regional features for a region. */ + get: operations["get_all_features_for_region_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features_get"]; + }; + "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}/features/{feature_id}": { + /** Returns a feature for a region, as defined by by the modality and feature ID */ + get: operations["get_regional_modality_by_id_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features__feature_id__get"]; + }; + "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}/regional_map/info": { + /** Returns information about a regional map for given region name. */ + get: operations["get_regional_map_info_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__regional_map_info_get"]; + }; + "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}/regional_map/map": { + /** Returns a regional map for given region name. */ + get: operations["get_regional_map_file_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__regional_map_map_get"]; + }; + "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}": { + get: operations["get_single_region_detail_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__get"]; + }; + "/atlases/{atlas_id}/parcellations": { + /** Returns all parcellations that are defined in the siibra client for given atlas. */ + get: operations["get_all_parcellations_atlases__atlas_id__parcellations_get"]; + }; + "/atlases/{atlas_id}/parcellations/{parcellation_id}/features/{feature_id}": { + /** Returns a global feature for a specific modality id. */ + get: operations["get_single_global_feature_detail_atlases__atlas_id__parcellations__parcellation_id__features__feature_id__get"]; + }; + "/atlases/{atlas_id}/parcellations/{parcellation_id}/features": { + /** Returns all global features for a parcellation. */ + get: operations["get_global_features_names_atlases__atlas_id__parcellations__parcellation_id__features_get"]; + }; + "/atlases/{atlas_id}/parcellations/{parcellation_id}/volumes": { + /** Returns one parcellation for given id. */ + get: operations["get_volumes_by_id_atlases__atlas_id__parcellations__parcellation_id__volumes_get"]; + }; + "/atlases/{atlas_id}/parcellations/{parcellation_id}": { + /** Returns one parcellation for given id. */ + get: operations["get_parcellation_by_id_atlases__atlas_id__parcellations__parcellation_id__get"]; + }; + "/atlases/{atlas_id}/spaces": { + /** Returns all spaces that are defined in the siibra client. */ + get: operations["get_all_spaces_atlases__atlas_id__spaces_get"]; + }; + "/atlases/{atlas_id}/spaces/{space_id}/templates": { + /** Returns a template for a given space id. */ + get: operations["get_template_by_space_id_atlases__atlas_id__spaces__space_id__templates_get"]; + }; + "/atlases/{atlas_id}/spaces/{space_id}/parcellation_maps": { + /** Returns all parcellation maps for a given space id. */ + get: operations["get_parcellation_map_for_space_atlases__atlas_id__spaces__space_id__parcellation_maps_get"]; + }; + "/atlases/{atlas_id}/spaces/{space_id}/features/{feature_id}": { + /** + * Get a detailed view on a single spatial feature. + * A parcellation id and region id can be provided optional to get more details. + */ + get: operations["get_single_spatial_feature_detail_atlases__atlas_id__spaces__space_id__features__feature_id__get"]; + }; + "/atlases/{atlas_id}/spaces/{space_id}/features": { + /** Return all possible feature names and links to get more details */ + get: operations["get_spatial_features_from_space_atlases__atlas_id__spaces__space_id__features_get"]; + }; + "/atlases/{atlas_id}/spaces/{space_id}/volumes": { + get: operations["get_one_space_volumes_atlases__atlas_id__spaces__space_id__volumes_get"]; + }; + "/atlases/{atlas_id}/spaces/{space_id}": { + /** Returns one space for given id, with links to further resources */ + get: operations["get_one_space_by_id_atlases__atlas_id__spaces__space_id__get"]; + }; + "/atlases": { + /** Get all atlases known by siibra. */ + get: operations["get_all_atlases_atlases_get"]; + }; + "/atlases/{atlas_id}": { + /** Get more information for a specific atlas with links to further objects. */ + get: operations["get_atlas_by_id_atlases__atlas_id__get"]; + }; + "/genes": { + /** Return all genes (name, acronym) in siibra */ + get: operations["get_gene_names_genes_get"]; + }; + "/features": { + /** Return all possible modalities */ + get: operations["get_all_available_modalities_features_get"]; + }; +} + +export interface components { + schemas: { + /** AutoradiographyDataModel */ + AutoradiographyDataModel: { + /** + * Content Type + * @default application/octet-stream + */ + content_type?: string; + /** + * Content Encoding + * @default gzip; base64 + */ + content_encoding?: string; + /** X-Width */ + "x-width": number; + /** X-Height */ + "x-height": number; + /** X-Channel */ + "x-channel": number; + /** Dtype */ + dtype: string; + /** Content */ + content: string; + }; + /** AxesOrigin */ + AxesOrigin: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * typeOfUncertainty + * @description Distinct technique used to quantify the uncertainty of a measurement. + */ + typeOfUncertainty?: unknown; + /** + * uncertainty + * @description Quantitative value range defining the uncertainty of a measurement. + */ + uncertainty?: number[]; + /** + * unit + * @description Determinate quantity adopted as a standard of measurement. + */ + unit?: unknown; + /** + * value + * @description Entry for a property. + */ + value: number; + }; + /** BaseDatasetJsonModel */ + BaseDatasetJsonModel: { + /** @Id */ + "@id": string; + /** + * Type + * @constant + */ + type?: "siibra/base-dataset"; + metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"]; + /** Urls */ + urls: components["schemas"]["Url"][]; + }; + /** BestViewPoint */ + BestViewPoint: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * coordinateSpace + * @description Two or three dimensional geometric setting. + */ + coordinateSpace: unknown; + /** + * Coordinates + * @description Structured information on a quantitative value. + */ + coordinates: components["schemas"]["siibra__openminds__SANDS__v3__atlas__parcellationEntityVersion__Coordinates"][]; + }; + /** BoundingBoxModel */ + BoundingBoxModel: { + /** Space */ + space: { [key: string]: string }; + center: components["schemas"]["siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Model"]; + minpoint: components["schemas"]["siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Model"]; + maxpoint: components["schemas"]["siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Model"]; + /** Shape */ + shape: number[]; + /** Is Planar */ + is_planar: boolean; + }; + /** ConnectivityMatrixDataModel */ + ConnectivityMatrixDataModel: { + /** @Id */ + "@id": string; + /** Name */ + name: string; + matrix: components["schemas"]["NpArrayDataModel"]; + /** Columns */ + columns: string[]; + /** Parcellations */ + parcellations: { [key: string]: string }[]; + }; + /** DatasetJsonModel */ + DatasetJsonModel: { + /** @Id */ + "@id": string; + /** + * Type + * @constant + */ + type?: "siibra/base-dataset"; + metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"]; + /** Urls */ + urls: components["schemas"]["Url"][]; + }; + /** FingerPrintDataModel */ + FingerPrintDataModel: { + /** Mean */ + mean: number; + /** Std */ + std: number; + /** Unit */ + unit: string; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** HasAnnotation */ + HasAnnotation: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * Bestviewpoint + * @description Structured information on a coordinate point. + */ + bestViewPoint?: components["schemas"]["BestViewPoint"]; + /** + * criteria + * @description Aspects or standards on which a judgement or decision is based. + */ + criteria?: unknown; + /** + * criteriaQualityType + * @description Distinct class that defines how the judgement or decision was made for a particular criteria. + */ + criteriaQualityType: unknown; + /** + * displayColor + * @description Preferred coloring. + */ + displayColor?: string; + /** + * inspiredBy + * @description Reference to an inspiring element. + */ + inspiredBy?: unknown[]; + /** + * internalIdentifier + * @description Term or code that identifies someone or something within a particular product. + */ + internalIdentifier: string; + /** + * laterality + * @description Differentiation between a pair of lateral homologous parts of the body. + */ + laterality?: unknown[]; + /** + * visualizedIn + * @description Reference to an image in which something is visible. + */ + visualizedIn?: unknown; + }; + /** HasTerminologyVersion */ + HasTerminologyVersion: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * definedIn + * @description Reference to a file instance in which something is stored. + */ + definedIn?: unknown[]; + /** hasEntityVersion */ + hasEntityVersion: unknown[]; + /** + * ontologyIdentifier + * @description Term or code used to identify something or someone registered within a particular ontology. + */ + ontologyIdentifier?: string[]; + }; + /** HrefModel */ + HrefModel: { + /** Href */ + href: string; + }; + /** IEEGContactPointModel */ + IEEGContactPointModel: { + /** Id */ + id: string; + point: components["schemas"]["siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Model"]; + }; + /** IEEGElectrodeModel */ + IEEGElectrodeModel: { + /** Electrode Id */ + electrode_id: string; + /** Contact Points */ + contact_points: { + [key: string]: components["schemas"]["IEEGContactPointModel"]; + }; + }; + /** IEEGSessionModel */ + IEEGSessionModel: { + /** @Id */ + "@id": string; + dataset: components["schemas"]["DatasetJsonModel"]; + /** Sub Id */ + sub_id: string; + /** Electrodes */ + electrodes: { + [key: string]: components["schemas"]["IEEGElectrodeModel"]; + }; + }; + /** NiiMetadataModel */ + NiiMetadataModel: { + /** Min */ + min: number; + /** Max */ + max: number; + }; + /** NpArrayDataModel */ + NpArrayDataModel: { + /** + * Content Type + * @default application/octet-stream + */ + content_type?: string; + /** + * Content Encoding + * @default gzip; base64 + */ + content_encoding?: string; + /** X-Width */ + "x-width": number; + /** X-Height */ + "x-height": number; + /** X-Channel */ + "x-channel": number; + /** Dtype */ + dtype: string; + /** Content */ + content: string; + }; + /** ProfileDataModel */ + ProfileDataModel: { + density: components["schemas"]["NpArrayDataModel"]; + /** Unit */ + unit: string; + }; + /** QuantitativeOverlapItem */ + QuantitativeOverlapItem: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * typeOfUncertainty + * @description Distinct technique used to quantify the uncertainty of a measurement. + */ + typeOfUncertainty?: unknown; + /** + * uncertainty + * @description Quantitative value range defining the uncertainty of a measurement. + */ + uncertainty?: number[]; + /** + * unit + * @description Determinate quantity adopted as a standard of measurement. + */ + unit?: unknown; + /** + * value + * @description Entry for a property. + */ + value: number; + }; + /** QuantitativeOverlapItem1 */ + QuantitativeOverlapItem1: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * maxValue + * @description Greatest quantity attained or allowed. + */ + maxValue: number; + /** maxValueUnit */ + maxValueUnit?: unknown; + /** + * minValue + * @description Smallest quantity attained or allowed. + */ + minValue: number; + /** minValueUnit */ + minValueUnit?: unknown; + }; + /** ReceptorDataModel */ + ReceptorDataModel: { + /** Autoradiographs */ + autoradiographs: { + [key: string]: components["schemas"]["AutoradiographyDataModel"]; + }; + /** Profiles */ + profiles: { [key: string]: components["schemas"]["ProfileDataModel"] }; + /** Fingerprints */ + fingerprints: { + [key: string]: components["schemas"]["FingerPrintDataModel"]; + }; + }; + /** ReceptorDatasetModel */ + ReceptorDatasetModel: { + /** @Id */ + "@id": string; + /** + * Type + * @constant + */ + type?: "siibra/receptor"; + metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"]; + /** Urls */ + urls: components["schemas"]["Url"][]; + data?: components["schemas"]["ReceptorDataModel"]; + }; + /** RelationAssessmentItem */ + RelationAssessmentItem: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * criteria + * @description Aspects or standards on which a judgement or decision is based. + */ + criteria?: unknown; + /** + * inRelationTo + * @description Reference to a related element. + */ + inRelationTo: unknown; + /** + * qualitativeOverlap + * @description Semantic characterization of how much two things occupy the same space. + */ + qualitativeOverlap: unknown; + }; + /** RelationAssessmentItem1 */ + RelationAssessmentItem1: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * criteria + * @description Aspects or standards on which a judgement or decision is based. + */ + criteria?: unknown; + /** + * inRelationTo + * @description Reference to a related element. + */ + inRelationTo: unknown; + /** Quantitativeoverlap */ + quantitativeOverlap: Partial< + components["schemas"]["QuantitativeOverlapItem"] + > & + Partial<components["schemas"]["QuantitativeOverlapItem1"]>; + }; + /** SapiAtlasModel */ + SapiAtlasModel: { + /** Links */ + links: { [key: string]: components["schemas"]["HrefModel"] }; + /** @Id */ + "@id": string; + /** Name */ + name: string; + /** + * @Type + * @constant + */ + "@type"?: "juelich/iav/atlas/v1.0.0"; + /** Spaces */ + spaces: components["schemas"]["SiibraAtIdModel"][]; + /** Parcellations */ + parcellations: components["schemas"]["SiibraAtIdModel"][]; + species: components["schemas"]["SpeciesModel"]; + }; + /** SapiParcellationModel */ + SapiParcellationModel: { + /** Links */ + links: { [key: string]: components["schemas"]["HrefModel"] }; + /** @Id */ + "@id": string; + /** + * @Type + * @constant + */ + "@type"?: "minds/core/parcellationatlas/v1.0.0"; + /** Name */ + name: string; + /** Datasets */ + datasets: components["schemas"]["DatasetJsonModel"][]; + /** Brainatlasversions */ + brainAtlasVersions: components["schemas"]["siibra__openminds__SANDS__v3__atlas__brainAtlasVersion__Model"][]; + }; + /** SapiSpaceModel */ + SapiSpaceModel: { + /** Links */ + links: { [key: string]: components["schemas"]["HrefModel"] }; + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * @Id + * @description Metadata node identifier. + */ + "@id": string; + /** @Type */ + "@type": string; + /** + * anatomicalAxesOrientation + * @description Relation between reference planes used in anatomy and mathematics. + */ + anatomicalAxesOrientation: { [key: string]: unknown }; + /** + * Axesorigin + * @description Structured information on a quantitative value. + */ + axesOrigin: components["schemas"]["AxesOrigin"][]; + /** + * defaultImage + * @description Two or three dimensional image that particluarly represents a specific coordinate space. + */ + defaultImage?: unknown[]; + /** + * digitalIdentifier + * @description Digital handle to identify objects or legal persons. + */ + digitalIdentifier?: { [key: string]: unknown }; + /** + * fullName + * @description Whole, non-abbreviated name of something or somebody. + */ + fullName: string; + /** + * homepage + * @description Main website of something or someone. + */ + homepage?: { [key: string]: unknown }; + /** + * howToCite + * @description Preferred format for citing a particular object or legal person. + */ + howToCite?: string; + /** + * nativeUnit + * @description Determinate quantity used in the original measurement. + */ + nativeUnit: { [key: string]: unknown }; + /** + * ontologyIdentifier + * @description Term or code used to identify something or someone registered within a particular ontology. + */ + ontologyIdentifier?: string[]; + /** + * releaseDate + * Format: date + * @description Fixed date on which a product is due to become or was made available for the general public to see or buy + */ + releaseDate: string; + /** + * shortName + * @description Shortened or fully abbreviated name of something or somebody. + */ + shortName: string; + /** + * versionIdentifier + * @description Term or code used to identify the version of something. + */ + versionIdentifier: string; + }; + /** SiibraAtIdModel */ + SiibraAtIdModel: { + /** @Id */ + "@id": string; + }; + /** SpeciesModel */ + SpeciesModel: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * @Id + * @description Metadata node identifier. + */ + "@id": string; + /** @Type */ + "@type": string; + /** + * definition + * @description Short, but precise statement of the meaning of a word, word group, sign or a symbol. + */ + definition?: string; + /** + * description + * @description Longer statement or account giving the characteristics of someone or something. + */ + description?: string; + /** + * interlexIdentifier + * @description Persistent identifier for a term registered in the InterLex project. + */ + interlexIdentifier?: string; + /** + * knowledgeSpaceLink + * @description Persistent link to an encyclopedia entry in the Knowledge Space project. + */ + knowledgeSpaceLink?: string; + /** + * name + * @description Word or phrase that constitutes the distinctive designation of a being or thing. + */ + name: string; + /** + * preferredOntologyIdentifier + * @description Persistent identifier of a preferred ontological term. + */ + preferredOntologyIdentifier?: string; + /** + * synonym + * @description Words or expressions used in the same language that have the same or nearly the same meaning in some or all senses. + */ + synonym?: string[]; + /** Kgv1Id */ + kgV1Id: string; + }; + /** Url */ + Url: { + /** Doi */ + doi: string; + /** Cite */ + cite?: string; + }; + /** VOIDataModel */ + VOIDataModel: { + /** @Id */ + "@id": string; + /** + * Type + * @constant + */ + type?: "siibra/base-dataset"; + metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"]; + /** Urls */ + urls: components["schemas"]["Url"][]; + /** Volumes */ + volumes: components["schemas"]["VolumeModel"][]; + location: components["schemas"]["BoundingBoxModel"]; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: string[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + }; + /** VocabModel */ + VocabModel: { + /** @Vocab */ + "@vocab": string; + }; + /** VolumeDataModel */ + VolumeDataModel: { + /** Type */ + type: string; + /** Is Volume */ + is_volume: boolean; + /** Is Surface */ + is_surface: boolean; + /** Detail */ + detail: { [key: string]: unknown }; + space: components["schemas"]["SiibraAtIdModel"]; + /** Url */ + url?: string; + /** Url Map */ + url_map?: { [key: string]: string }; + /** Map Type */ + map_type?: string; + /** Volume Type */ + volume_type?: string; + }; + /** VolumeModel */ + VolumeModel: { + /** @Id */ + "@id": string; + /** + * Type + * @constant + */ + type?: "siibra/base-dataset"; + metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"]; + /** Urls */ + urls: components["schemas"]["Url"][]; + data: components["schemas"]["VolumeDataModel"]; + }; + /** Copyright */ + siibra__openminds__SANDS__v3__atlas__brainAtlasVersion__Copyright: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * holder + * @description Legal person in possession of something. + */ + holder: unknown[]; + /** + * year + * @description Cycle in the Gregorian calendar specified by a number and comprised of 365 or 366 days divided into 12 months beginning with January and ending with December. + */ + year: string; + }; + /** Model */ + siibra__openminds__SANDS__v3__atlas__brainAtlasVersion__Model: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * @Id + * @description Metadata node identifier. + */ + "@id": string; + /** @Type */ + "@type": string; + /** abbreviation */ + abbreviation?: string; + /** + * accessibility + * @description Level to which something is accessible to someone or something. + */ + accessibility: { [key: string]: unknown }; + /** atlasType */ + atlasType?: { [key: string]: unknown }; + /** + * author + * @description Creator of a literary or creative work, as well as a dataset publication. + */ + author?: unknown[]; + /** + * coordinateSpace + * @description Two or three dimensional geometric setting. + */ + coordinateSpace: { [key: string]: unknown }; + /** + * Copyright + * @description Structured information on the copyright. + */ + copyright?: components["schemas"]["siibra__openminds__SANDS__v3__atlas__brainAtlasVersion__Copyright"]; + /** + * custodian + * @description The 'custodian' is a legal person who is responsible for the content and quality of the data, metadata, and/or code of a research product. + */ + custodian?: unknown[]; + /** + * description + * @description Longer statement or account giving the characteristics of someone or something. + */ + description?: string; + /** + * digitalIdentifier + * @description Digital handle to identify objects or legal persons. + */ + digitalIdentifier?: { [key: string]: unknown }; + /** + * fullDocumentation + * @description Non-abridged instructions, comments, and information for using a particular product. + */ + fullDocumentation: { [key: string]: unknown }; + /** + * fullName + * @description Whole, non-abbreviated name of something or somebody. + */ + fullName?: string; + /** + * funding + * @description Money provided by a legal person for a particular purpose. + */ + funding?: unknown[]; + hasTerminologyVersion: components["schemas"]["HasTerminologyVersion"]; + /** + * homepage + * @description Main website of something or someone. + */ + homepage?: { [key: string]: unknown }; + /** + * howToCite + * @description Preferred format for citing a particular object or legal person. + */ + howToCite?: string; + /** + * isAlternativeVersionOf + * @description Reference to an original form where the essence was preserved, but presented in an alternative form. + */ + isAlternativeVersionOf?: unknown[]; + /** + * isNewVersionOf + * @description Reference to a previous (potentially outdated) particular form of something. + */ + isNewVersionOf?: { [key: string]: unknown }; + /** + * keyword + * @description Significant word or concept that are representative of something or someone. + */ + keyword?: unknown[]; + /** + * license + * @description Grant by a party to another party as an element of an agreement between those parties that permits to do, use, or own something. + */ + license: { [key: string]: unknown }; + /** + * ontologyIdentifier + * @description Term or code used to identify something or someone registered within a particular ontology. + */ + ontologyIdentifier?: string[]; + /** + * Othercontribution + * @description Structured information on the contribution made to a research product. + */ + otherContribution?: components["schemas"]["siibra__openminds__SANDS__v3__atlas__brainAtlasVersion__OtherContribution"]; + /** + * relatedPublication + * @description Reference to something that was made available for the general public to see or buy. + */ + relatedPublication?: unknown[]; + /** + * releaseDate + * Format: date + * @description Fixed date on which a product is due to become or was made available for the general public to see or buy + */ + releaseDate: string; + /** + * repository + * @description Place, room, or container where something is deposited or stored. + */ + repository?: { [key: string]: unknown }; + /** + * shortName + * @description Shortened or fully abbreviated name of something or somebody. + */ + shortName: string; + /** + * supportChannel + * @description Way of communication used to interact with users or customers. + */ + supportChannel?: string[]; + /** + * versionIdentifier + * @description Term or code used to identify the version of something. + */ + versionIdentifier: string; + /** + * versionInnovation + * @description Documentation on what changed in comparison to a previously published form of something. + */ + versionInnovation: string; + }; + /** OtherContribution */ + siibra__openminds__SANDS__v3__atlas__brainAtlasVersion__OtherContribution: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * contributionType + * @description Distinct class of what was given or supplied as a part or share. + */ + contributionType: unknown[]; + /** + * contributor + * @description Legal person that gave or supplied something as a part or share. + */ + contributor: unknown; + }; + /** Coordinates */ + siibra__openminds__SANDS__v3__atlas__parcellationEntityVersion__Coordinates: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * typeOfUncertainty + * @description Distinct technique used to quantify the uncertainty of a measurement. + */ + typeOfUncertainty?: unknown; + /** + * uncertainty + * @description Quantitative value range defining the uncertainty of a measurement. + */ + uncertainty?: number[]; + /** + * unit + * @description Determinate quantity adopted as a standard of measurement. + */ + unit?: unknown; + /** + * value + * @description Entry for a property. + */ + value: number; + }; + /** Model */ + siibra__openminds__SANDS__v3__atlas__parcellationEntityVersion__Model: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * @Id + * @description Metadata node identifier. + */ + "@id": string; + /** @Type */ + "@type": string; + hasAnnotation?: components["schemas"]["HasAnnotation"]; + /** + * hasParent + * @description Reference to a parent object or legal person. + */ + hasParent?: unknown[]; + /** lookupLabel */ + lookupLabel?: string; + /** + * name + * @description Word or phrase that constitutes the distinctive designation of a being or thing. + */ + name?: string; + /** + * ontologyIdentifier + * @description Term or code used to identify something or someone registered within a particular ontology. + */ + ontologyIdentifier?: string[]; + /** Relationassessment */ + relationAssessment?: Partial< + components["schemas"]["RelationAssessmentItem"] + > & + Partial<components["schemas"]["RelationAssessmentItem1"]>; + /** + * versionIdentifier + * @description Term or code used to identify the version of something. + */ + versionIdentifier: string; + /** + * versionInnovation + * @description Documentation on what changed in comparison to a previously published form of something. + */ + versionInnovation?: string; + }; + /** Coordinates */ + siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Coordinates: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * typeOfUncertainty + * @description Distinct technique used to quantify the uncertainty of a measurement. + */ + typeOfUncertainty?: unknown; + /** + * uncertainty + * @description Quantitative value range defining the uncertainty of a measurement. + */ + uncertainty?: number[]; + /** + * unit + * @description Determinate quantity adopted as a standard of measurement. + */ + unit?: unknown; + /** + * value + * @description Entry for a property. + */ + value: number; + }; + /** Model */ + siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Model: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * @Id + * @description Metadata node identifier. + */ + "@id": string; + /** @Type */ + "@type": string; + /** + * coordinateSpace + * @description Two or three dimensional geometric setting. + */ + coordinateSpace: { [key: string]: unknown }; + /** + * Coordinates + * @description Structured information on a quantitative value. + */ + coordinates: components["schemas"]["siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Coordinates"][]; + }; + /** Copyright */ + siibra__openminds__core__v4__products__datasetVersion__Copyright: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * holder + * @description Legal person in possession of something. + */ + holder: unknown[]; + /** + * year + * @description Cycle in the Gregorian calendar specified by a number and comprised of 365 or 366 days divided into 12 months beginning with January and ending with December. + */ + year: string; + }; + /** Model */ + siibra__openminds__core__v4__products__datasetVersion__Model: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * @Id + * @description Metadata node identifier. + */ + "@id": string; + /** @Type */ + "@type": string; + /** + * accessibility + * @description Level to which something is accessible to someone or something. + */ + accessibility: { [key: string]: unknown }; + /** + * author + * @description Creator of a literary or creative work, as well as a dataset publication. + */ + author?: unknown[]; + /** behavioralProtocol */ + behavioralProtocol?: unknown[]; + /** + * Copyright + * @description Structured information on the copyright. + */ + copyright?: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Copyright"]; + /** + * custodian + * @description The 'custodian' is a legal person who is responsible for the content and quality of the data, metadata, and/or code of a research product. + */ + custodian?: unknown[]; + /** dataType */ + dataType: unknown[]; + /** + * description + * @description Longer statement or account giving the characteristics of someone or something. + */ + description?: string; + /** + * digitalIdentifier + * @description Digital handle to identify objects or legal persons. + */ + digitalIdentifier: { [key: string]: unknown }; + /** + * ethicsAssessment + * @description Judgment about the applied principles of conduct governing an individual or a group. + */ + ethicsAssessment: { [key: string]: unknown }; + /** experimentalApproach */ + experimentalApproach: unknown[]; + /** + * fullDocumentation + * @description Non-abridged instructions, comments, and information for using a particular product. + */ + fullDocumentation: { [key: string]: unknown }; + /** + * fullName + * @description Whole, non-abbreviated name of something or somebody. + */ + fullName?: string; + /** + * funding + * @description Money provided by a legal person for a particular purpose. + */ + funding?: unknown[]; + /** + * homepage + * @description Main website of something or someone. + */ + homepage?: { [key: string]: unknown }; + /** + * howToCite + * @description Preferred format for citing a particular object or legal person. + */ + howToCite?: string; + /** + * inputData + * @description Data that is put into a process or machine. + */ + inputData?: unknown[]; + /** + * isAlternativeVersionOf + * @description Reference to an original form where the essence was preserved, but presented in an alternative form. + */ + isAlternativeVersionOf?: unknown[]; + /** + * isNewVersionOf + * @description Reference to a previous (potentially outdated) particular form of something. + */ + isNewVersionOf?: { [key: string]: unknown }; + /** + * keyword + * @description Significant word or concept that are representative of something or someone. + */ + keyword?: unknown[]; + /** + * license + * @description Grant by a party to another party as an element of an agreement between those parties that permits to do, use, or own something. + */ + license: { [key: string]: unknown }; + /** + * Othercontribution + * @description Structured information on the contribution made to a research product. + */ + otherContribution?: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__OtherContribution"]; + /** preparationDesign */ + preparationDesign?: unknown[]; + /** + * relatedPublication + * @description Reference to something that was made available for the general public to see or buy. + */ + relatedPublication?: unknown[]; + /** + * releaseDate + * Format: date + * @description Fixed date on which a product is due to become or was made available for the general public to see or buy + */ + releaseDate: string; + /** + * repository + * @description Place, room, or container where something is deposited or stored. + */ + repository?: { [key: string]: unknown }; + /** + * shortName + * @description Shortened or fully abbreviated name of something or somebody. + */ + shortName: string; + /** studiedSpecimen */ + studiedSpecimen?: unknown[]; + /** + * studyTarget + * @description Structure or function that was targeted within a study. + */ + studyTarget?: unknown[]; + /** + * supportChannel + * @description Way of communication used to interact with users or customers. + */ + supportChannel?: string[]; + /** + * technique + * @description Method of accomplishing a desired aim. + */ + technique: unknown[]; + /** + * versionIdentifier + * @description Term or code used to identify the version of something. + */ + versionIdentifier: string; + /** + * versionInnovation + * @description Documentation on what changed in comparison to a previously published form of something. + */ + versionInnovation: string; + }; + /** OtherContribution */ + siibra__openminds__core__v4__products__datasetVersion__OtherContribution: { + /** + * @Context + * @default [object Object] + */ + "@context"?: components["schemas"]["VocabModel"]; + /** + * contributionType + * @description Distinct class of what was given or supplied as a part or share. + */ + contributionType: unknown[]; + /** + * contributor + * @description Legal person that gave or supplied something as a part or share. + */ + contributor: unknown; + }; + }; +} + +export interface operations { + /** Returns all regions for a given parcellation id. */ + get_all_regions_from_atlas_parc_space_atlases__atlas_id__parcellations__parcellation_id__regions_get: { + parameters: { + path: { + atlas_id: string; + parcellation_id: string; + }; + query: { + space_id?: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["siibra__openminds__SANDS__v3__atlas__parcellationEntityVersion__Model"][]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns all regional features for a region. */ + get_all_features_for_region_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features_get: { + parameters: { + path: { + atlas_id: string; + parcellation_id: string; + region_id: string; + }; + query: { + space_id?: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": (Partial< + components["schemas"]["ReceptorDatasetModel"] + > & + Partial<components["schemas"]["BaseDatasetJsonModel"]>)[]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns a feature for a region, as defined by by the modality and feature ID */ + get_regional_modality_by_id_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features__feature_id__get: { + parameters: { + path: { + atlas_id: string; + parcellation_id: string; + region_id: string; + feature_id: string; + }; + query: { + space_id?: string; + gene?: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": Partial< + components["schemas"]["ReceptorDatasetModel"] + > & + Partial<components["schemas"]["BaseDatasetJsonModel"]>; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns information about a regional map for given region name. */ + get_regional_map_info_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__regional_map_info_get: { + parameters: { + path: { + atlas_id: string; + parcellation_id: string; + region_id: string; + }; + query: { + space_id?: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["NiiMetadataModel"]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns a regional map for given region name. */ + get_regional_map_file_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__regional_map_map_get: { + parameters: { + path: { + atlas_id: string; + parcellation_id: string; + region_id: string; + }; + query: { + space_id?: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": unknown; + "application/octet-stream": string; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_single_region_detail_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__get: { + parameters: { + path: { + atlas_id: string; + parcellation_id: string; + region_id: string; + }; + query: { + space_id?: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["siibra__openminds__SANDS__v3__atlas__parcellationEntityVersion__Model"]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns all parcellations that are defined in the siibra client for given atlas. */ + get_all_parcellations_atlases__atlas_id__parcellations_get: { + parameters: { + path: { + atlas_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SapiParcellationModel"][]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns a global feature for a specific modality id. */ + get_single_global_feature_detail_atlases__atlas_id__parcellations__parcellation_id__features__feature_id__get: { + parameters: { + path: { + atlas_id: string; + parcellation_id: string; + feature_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ConnectivityMatrixDataModel"]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns all global features for a parcellation. */ + get_global_features_names_atlases__atlas_id__parcellations__parcellation_id__features_get: { + parameters: { + path: { + atlas_id: string; + parcellation_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ConnectivityMatrixDataModel"][]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns one parcellation for given id. */ + get_volumes_by_id_atlases__atlas_id__parcellations__parcellation_id__volumes_get: { + parameters: { + path: { + atlas_id: string; + parcellation_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["VolumeModel"][]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns one parcellation for given id. */ + get_parcellation_by_id_atlases__atlas_id__parcellations__parcellation_id__get: { + parameters: { + path: { + atlas_id: string; + parcellation_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SapiParcellationModel"]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns all spaces that are defined in the siibra client. */ + get_all_spaces_atlases__atlas_id__spaces_get: { + parameters: { + path: { + atlas_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SapiSpaceModel"][]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns a template for a given space id. */ + get_template_by_space_id_atlases__atlas_id__spaces__space_id__templates_get: { + parameters: { + path: { + atlas_id: string; + space_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": unknown; + "application/octet-stream": string; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns all parcellation maps for a given space id. */ + get_parcellation_map_for_space_atlases__atlas_id__spaces__space_id__parcellation_maps_get: { + parameters: { + path: { + atlas_id: string; + space_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": unknown; + "application/octet-stream": string; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** + * Get a detailed view on a single spatial feature. + * A parcellation id and region id can be provided optional to get more details. + */ + get_single_spatial_feature_detail_atlases__atlas_id__spaces__space_id__features__feature_id__get: { + parameters: { + path: { + feature_id: string; + atlas_id: string; + space_id: string; + }; + query: { + parcellation_id: string; + region: string; + bbox?: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": Partial< + components["schemas"]["IEEGSessionModel"] + > & + Partial<components["schemas"]["VOIDataModel"]>; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Return all possible feature names and links to get more details */ + get_spatial_features_from_space_atlases__atlas_id__spaces__space_id__features_get: { + parameters: { + path: { + atlas_id: string; + space_id: string; + }; + query: { + parcellation_id?: string; + region?: string; + bbox?: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": (Partial< + components["schemas"]["IEEGSessionModel"] + > & + Partial<components["schemas"]["VOIDataModel"]>)[]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_one_space_volumes_atlases__atlas_id__spaces__space_id__volumes_get: { + parameters: { + path: { + atlas_id: string; + space_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["VolumeModel"][]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Returns one space for given id, with links to further resources */ + get_one_space_by_id_atlases__atlas_id__spaces__space_id__get: { + parameters: { + path: { + atlas_id: string; + space_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SapiSpaceModel"]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Get all atlases known by siibra. */ + get_all_atlases_atlases_get: { + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SapiAtlasModel"][]; + }; + }; + }; + }; + /** Get more information for a specific atlas with links to further objects. */ + get_atlas_by_id_atlases__atlas_id__get: { + parameters: { + path: { + atlas_id: string; + }; + }; + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SapiAtlasModel"]; + }; + }; + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Return all genes (name, acronym) in siibra */ + get_gene_names_genes_get: { + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; + /** Return all possible modalities */ + get_all_available_modalities_features_get: { + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; +} + +export interface external {} diff --git a/src/atlasComponents/sapi/type.ts b/src/atlasComponents/sapi/type.ts index 803c294d8e219030fbc6184034488919e652b974..dba9a4897300b90b3c45d058501ee0f928c41ad1 100644 --- a/src/atlasComponents/sapi/type.ts +++ b/src/atlasComponents/sapi/type.ts @@ -1,4 +1,7 @@ +import { OperatorFunction } from "rxjs" +import { map } from "rxjs/operators" import { IVolumeTypeDetail } from "src/util/siibraApiConstants/types" +import { components } from "./schema" export type IdName = { id: string @@ -18,21 +21,39 @@ type Volume = { export type BoundingBoxConcept = [Point, Point] -export type SapiVoiResponse = { - "@id": string - name: string - description: string - url: { - cite?: string - doi: string - }[] - location: { - space: { - "@id": string - } - center: Point - minpoint: Point - maxpoint: Point - } - volumes: Volume[] +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 SapiSpatialFeatureModel = components["schemas"]["VOIDataModel"] +export type SapiVOIDataResponse = components["schemas"]["VOIDataModel"] + +export type SapiVolumeModel = components["schemas"]["VolumeModel"] +export type SapiDatasetModel = components["schemas"]["DatasetJsonModel"] + +export const guards = { + isSapiVolumeModel: (val: SapiVolumeModel) => val.type === "siibra/base-dataset" + && val.data.detail["neuroglancer/precomputed"] +} + +/** + * datafeatures + */ +export type SapiRegionalFeatureModel = components["schemas"]["BaseDatasetJsonModel"] | components["schemas"]["ReceptorDatasetModel"] + +export function guardPipe< + InputType, + GuardType extends InputType +>( + guardFn: (input: InputType) => input is GuardType +): OperatorFunction<InputType, GuardType> { + return src => src.pipe( + map(val => { + if (guardFn(val)) { + return val + } + throw new Error(`TypeGuard Error`) + }) + ) } diff --git a/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.component.ts b/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..9725ac61ea28969bf932c600e7b82521b9b0ab5f --- /dev/null +++ b/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.component.ts @@ -0,0 +1,26 @@ +import { Component } from "@angular/core"; + +type Landmark = {} & { + showInSliceView +} + +@Component({ + templateUrl: `./ieegSession.template.html`, + styleUrls: [ + `./ieegSession.style.css` + ] +}) + +export class IEEGSessionCmp{ + private loadedLms: Landmark[] + + private unloadLandmarks(){} + private loadlandmarks(lms: Landmark[]){} + private handleDatumExpansion(dataset: any){ + + } + private handleContactPtClick(contactPt){ + // navigate there + + } +} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.style.css b/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.style.css similarity index 100% rename from src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.style.css rename to src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.style.css diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/profile/profile.style.css b/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.template.html similarity index 100% rename from src/atlasComponents/regionalFeatures/bsFeatures/receptor/profile/profile.style.css rename to src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.template.html diff --git a/src/atlasComponents/sapiViews/features/ieeg/index.ts b/src/atlasComponents/sapiViews/features/ieeg/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..663fabd8603621b891428afeb0d68e861690cfd4 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/ieeg/index.ts @@ -0,0 +1 @@ +export { IEEGSessionCmp } from "./ieegSession/ieegSession.component" \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/index.ts b/src/atlasComponents/sapiViews/features/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..19e30dcb1873aa37868be5e8944eaad3a9b0ce05 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/index.ts @@ -0,0 +1,3 @@ +export { + SapiViewsFeaturesModule +} from "./module" \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/module.ts b/src/atlasComponents/sapiViews/features/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..87dbbcd9db1587bb2b121e8d144f384656f2b7f8 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/module.ts @@ -0,0 +1,34 @@ +import { CommonModule } from "@angular/common" +import { NgModule } from "@angular/core" +import { AngularMaterialModule } from "src/sharedModules" +import * as ieeg from "./ieeg" +import * as receptor from "./receptors" + +const { + IEEGSessionCmp +} = ieeg +const { + Autoradiography, + Fingerprint, + Profile, +} = receptor + +@NgModule({ + imports: [ + CommonModule, + AngularMaterialModule + ], + declarations: [ + IEEGSessionCmp, + Autoradiography, + Fingerprint, + Profile, + ], + exports: [ + IEEGSessionCmp, + Autoradiography, + Fingerprint, + Profile, + ] +}) +export class SapiViewsFeaturesModule{} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.component.ts b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7d764069608079a7c893d4df7aaa3662ae6f3c0 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; +import { BaseReceptor } from "../base"; + +@Component({ + templateUrl: './autoradiography.template.html', + styleUrls: [ + './autoradiography.style.css' + ] +}) + +export class Autoradiography extends BaseReceptor{} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.style.css b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.style.css similarity index 100% rename from src/atlasComponents/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.style.css rename to src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.style.css diff --git a/src/atlasComponents/regionalFeatures/util.ts b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.template.html similarity index 100% rename from src/atlasComponents/regionalFeatures/util.ts rename to src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.template.html diff --git a/src/atlasComponents/sapiViews/features/receptors/base.ts b/src/atlasComponents/sapiViews/features/receptors/base.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bff16e25247fa50c55ca4accf8d605d210f3611 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/receptors/base.ts @@ -0,0 +1,7 @@ +import { Input } from "@angular/core"; + +export class BaseReceptor{ + + @Input() + featureId: string +} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.component.ts b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a99e111980e482baf88d925d93133a2361d664b8 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; +import { BaseReceptor } from "../base"; + +@Component({ + templateUrl: './fingerprint.template.html', + styleUrls: [ + './fingerprint.style.css' + ] +}) + +export class Fingerprint extends BaseReceptor{} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.style.css b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.template.html b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.template.html new file mode 100644 index 0000000000000000000000000000000000000000..000884f59613304a5f5b600272fb7b157b327536 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.template.html @@ -0,0 +1,3 @@ + +<kg-dataset-dumb-radar [attr.kg-ds-prv-darkmode]="true"> +</kg-dataset-dumb-radar> \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/receptors/index.ts b/src/atlasComponents/sapiViews/features/receptors/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b82f950a1ab09f3d27b4cf990cbecea0ac06902d --- /dev/null +++ b/src/atlasComponents/sapiViews/features/receptors/index.ts @@ -0,0 +1,3 @@ +export { Autoradiography } from "./autoradiography/autoradiography.component"; +export { Fingerprint } from "./fingerprint/fingerprint.component" +export { Profile } from "./profile/profile.component" \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/receptors/profile/profile.component.ts b/src/atlasComponents/sapiViews/features/receptors/profile/profile.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a62d72eb74b28b6c413d0e59c7de2bce0828148 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/receptors/profile/profile.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; +import { BaseReceptor } from "../base"; + +@Component({ + templateUrl: './profile.template.html', + styleUrls: [ + './profile.style.css' + ] +}) + +export class Profile extends BaseReceptor{} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/receptors/profile/profile.style.css b/src/atlasComponents/sapiViews/features/receptors/profile/profile.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/atlasComponents/sapiViews/features/receptors/profile/profile.template.html b/src/atlasComponents/sapiViews/features/receptors/profile/profile.template.html new file mode 100644 index 0000000000000000000000000000000000000000..a143d8a0e325953339a1771a3a6b5036d826148e --- /dev/null +++ b/src/atlasComponents/sapiViews/features/receptors/profile/profile.template.html @@ -0,0 +1,4 @@ + +<kg-dataset-dumb-line + [attr.kg-ds-prv-darkmode]="true"> +</kg-dataset-dumb-line> \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/index.ts b/src/atlasComponents/sapiViews/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1afef564d73a7aaa4f0923b2ba62656418d76ca8 --- /dev/null +++ b/src/atlasComponents/sapiViews/index.ts @@ -0,0 +1,3 @@ +export { + SapiViewsModule +} from "./module" diff --git a/src/atlasComponents/sapiViews/module.ts b/src/atlasComponents/sapiViews/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..6da0504892874497880c399b7df6e8426ef8baa9 --- /dev/null +++ b/src/atlasComponents/sapiViews/module.ts @@ -0,0 +1,9 @@ +import { NgModule } from "@angular/core"; +import { SapiViewsFeaturesModule } from "./features"; + +@NgModule({ + imports: [ + SapiViewsFeaturesModule + ], +}) +export class SapiViewsModule{} \ No newline at end of file diff --git a/src/atlasComponents/splashScreen/index.ts b/src/atlasComponents/splashScreen/index.ts index 5927a7b64a7f19529a4998770dd5f27a3d3a2546..0750a03038aaecc1a9de8097d87ecd240121e418 100644 --- a/src/atlasComponents/splashScreen/index.ts +++ b/src/atlasComponents/splashScreen/index.ts @@ -1,2 +1,2 @@ -export { GetTemplateImageSrcPipe, SplashScreen } from "./splashScreen/splashScreen.component"; +export { SplashScreen } from "./splashScreen/splashScreen.component"; export { SplashUiModule } from './module' \ No newline at end of file diff --git a/src/atlasComponents/splashScreen/module.ts b/src/atlasComponents/splashScreen/module.ts index cafa61559313528ffe6a1e74c2b4a88c240f8329..587e1217dad6d50acbe9f78b7f9074b3c0e844c8 100644 --- a/src/atlasComponents/splashScreen/module.ts +++ b/src/atlasComponents/splashScreen/module.ts @@ -4,7 +4,7 @@ import { ComponentsModule } from "src/components"; import { KgTosModule } from "src/ui/kgtos/module"; import { AngularMaterialModule } from "src/sharedModules"; import { UtilModule } from "src/util"; -import { GetTemplateImageSrcPipe, SplashScreen } from "./splashScreen/splashScreen.component"; +import { SplashScreen } from "./splashScreen/splashScreen.component"; @NgModule({ imports: [ @@ -16,7 +16,6 @@ import { GetTemplateImageSrcPipe, SplashScreen } from "./splashScreen/splashScre ], declarations: [ SplashScreen, - GetTemplateImageSrcPipe, ], exports: [ SplashScreen, diff --git a/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts b/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts index 3df8c71c099444e4be81158e3093c8c9e6d3dcde..775b1a486bc18ec333ce8aec1a32ccdcc8c14091 100644 --- a/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts +++ b/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts @@ -1,11 +1,9 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Pipe, PipeTransform, ViewChild } from "@angular/core"; -import { MatSnackBar } from "@angular/material/snack-bar"; -import { select, Store } from "@ngrx/store"; -import { Observable, Subject, Subscription } from "rxjs"; -import { filter } from 'rxjs/operators' -import { viewerStateHelperStoreName, viewerStateSelectAtlas } from "src/services/state/viewerState.store.helper"; -import { PureContantService } from "src/util"; -import { CONST } from 'common/constants' +import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { Store } from "@ngrx/store"; +import { tap } from 'rxjs/operators' +import { SAPI } from "src/atlasComponents/sapi/sapi.service"; +import { SapiAtlasModel } from "src/atlasComponents/sapi/type"; +import { atlasSelection } from "src/state" @Component({ selector : 'ui-splashscreen', @@ -20,56 +18,21 @@ export class SplashScreen { public finishedLoading: boolean = false - public loadedAtlases$: Observable<any[]> - - public filterNullFn(atlas: any){ - return !!atlas - } - - @ViewChild('parentContainer', {read: ElementRef}) - public activatedTemplate$: Subject<any> = new Subject() - - private subscriptions: Subscription[] = [] + public atlases$ = this.sapiSvc.atlases$.pipe( + tap(() => this.finishedLoading = true) + ) constructor( private store: Store<any>, - private snack: MatSnackBar, - private pureConstantService: PureContantService, - private cdr: ChangeDetectorRef, + private sapiSvc: SAPI, ) { - this.subscriptions.push( - this.pureConstantService.allFetchingReady$.subscribe(flag => { - this.finishedLoading = flag - this.cdr.markForCheck() - }) - ) - - this.loadedAtlases$ = this.store.pipe( - select(state => state[viewerStateHelperStoreName]), - select(state => state.fetchedAtlases), - filter(v => !!v) - ) } - public selectAtlas(atlas: any){ - if (!this.finishedLoading) { - this.snack.open(CONST.DATA_NOT_READY, null, { - duration: 3000 - }) - return - } + public selectAtlas(atlas: SapiAtlasModel){ this.store.dispatch( - viewerStateSelectAtlas({ atlas }) + atlasSelection.actions.selectAtlas({ + atlas + }) ) } } - -@Pipe({ - name: 'getTemplateImageSrcPipe', -}) - -export class GetTemplateImageSrcPipe implements PipeTransform { - public transform(name: string): string { - return `./res/image/${name.replace(/[|&;$%@()+,\s./]/g, '')}.png` - } -} diff --git a/src/atlasComponents/splashScreen/splashScreen/splashScreen.template.html b/src/atlasComponents/splashScreen/splashScreen/splashScreen.template.html index 6236bd010201f3b334dc28c58df491acdd3017d2..7c38d86fd400e559f57e07faa1babef2d13de84a 100644 --- a/src/atlasComponents/splashScreen/splashScreen/splashScreen.template.html +++ b/src/atlasComponents/splashScreen/splashScreen/splashScreen.template.html @@ -7,7 +7,7 @@ <mat-card (click)="selectAtlas(atlas)" matRipple - *ngFor="let atlas of loadedAtlases$ | async | filterArray : filterNullFn" + *ngFor="let atlas of atlases$ | async" class="m-3 col-md-12 col-lg-12 pe-all"> <mat-card-header> <mat-card-title class="text-nowrap font-stretch"> diff --git a/src/atlasComponents/template/templateIsDarkTheme.pipe.ts b/src/atlasComponents/template/templateIsDarkTheme.pipe.ts index 8454fd96e81e9815f8ab0bb627eb1e01d18b95b0..01590156217afe114936a0c2609e0a28c812ae61 100644 --- a/src/atlasComponents/template/templateIsDarkTheme.pipe.ts +++ b/src/atlasComponents/template/templateIsDarkTheme.pipe.ts @@ -1,7 +1,4 @@ -import { OnDestroy, Pipe, PipeTransform } from "@angular/core"; -import { select, Store } from "@ngrx/store"; -import { Subscription } from "rxjs"; -import { viewerStateSelectedTemplateFullInfoSelector } from "src/services/state/viewerState/selectors"; +import { Pipe, PipeTransform } from "@angular/core"; import { IHasId } from "src/util/interfaces"; @Pipe({ @@ -9,25 +6,9 @@ import { IHasId } from "src/util/interfaces"; pure: true, }) -export class TemplateIsDarkThemePipe implements OnDestroy, PipeTransform{ - - private templateFullInfo: any[] = [] - constructor(store: Store<any>){ - this.sub.push( - store.pipe( - select(viewerStateSelectedTemplateFullInfoSelector) - ).subscribe(val => this.templateFullInfo = val) - ) - } - - private sub: Subscription[] = [] - - ngOnDestroy(){ - while(this.sub.length) this.sub.pop().unsubscribe() - } +export class TemplateIsDarkThemePipe implements PipeTransform{ public transform(template: IHasId): boolean{ - const found = this.templateFullInfo.find(t => t['@id'] === template["@id"]) - return found && found.darktheme + return template["@id"] !== "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588" } } \ No newline at end of file diff --git a/src/atlasComponents/uiSelectors/atlasDropdown/atlasDropdown.component.ts b/src/atlasComponents/uiSelectors/atlasDropdown/atlasDropdown.component.ts index c44816155bcbb23701aa04e7a9de767b70dd056f..d3ef7a4d602c0b4298f1dff7caabbdbdce9aa3f4 100644 --- a/src/atlasComponents/uiSelectors/atlasDropdown/atlasDropdown.component.ts +++ b/src/atlasComponents/uiSelectors/atlasDropdown/atlasDropdown.component.ts @@ -1,10 +1,9 @@ import { Component } from "@angular/core"; import { Store, select } from "@ngrx/store"; import { Observable } from "rxjs"; -import { distinctUntilChanged } from "rxjs/operators"; -import { viewerStateHelperStoreName, viewerStateSelectAtlas } from "src/services/state/viewerState.store.helper"; import { ARIA_LABELS } from 'common/constants' -import { viewerStateGetSelectedAtlas } from "src/services/state/viewerState/selectors"; +import { atlasSelection } from "src/state" +import { SAPI } from "src/atlasComponents/sapi"; @Component({ selector: 'atlas-dropdown-selector', @@ -21,23 +20,20 @@ export class AtlasDropdownSelector{ public SELECT_ATLAS_ARIA_LABEL = ARIA_LABELS.SELECT_ATLAS - constructor(private store$: Store<any>){ - this.fetchedAtlases$ = this.store$.pipe( - select(viewerStateHelperStoreName), - select('fetchedAtlases'), - distinctUntilChanged() - ) + constructor( + private store$: Store<any>, + private sapi: SAPI, + ){ + this.fetchedAtlases$ = this.sapi.atlases$ this.selectedAtlas$ = this.store$.pipe( - select(viewerStateGetSelectedAtlas) + select(atlasSelection.selectors.selectedAtlas) ) } handleChangeAtlas({ value }) { this.store$.dispatch( - viewerStateSelectAtlas({ - atlas: { - ['@id']: value - } + atlasSelection.actions.selectATPById({ + atlasId: value }) ) } diff --git a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.spec.ts b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.spec.ts index 730f7b258819bf92a3a678976bead30a75817452..ac1cba2b02e1210747c708aadf634cd3ec219cf1 100644 --- a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.spec.ts +++ b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing" import { NoopAnimationsModule } from "@angular/platform-browser/animations" import { MockStore, provideMockStore } from "@ngrx/store/testing" import { ComponentsModule } from "src/components" -import { viewerStateSelectTemplateWithId, viewerStateToggleLayer } from "src/services/state/viewerState.store.helper" +import { viewerStateSelectTemplateWithId } from "src/services/state/viewerState.store.helper" import { AngularMaterialModule } from "src/sharedModules" import { QuickTourModule } from "src/ui/quickTour" import { GetGroupedParcPipe } from "../pipes/getGroupedParc.pipe" diff --git a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts index 9fbca8cce700ba9ef5bef01581a618e451308df3..927d62cb3d5fed7d12a1d3a5361aa0fd280a9883 100644 --- a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts +++ b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts @@ -1,15 +1,19 @@ -import { Component, OnInit, ViewChildren, QueryList, HostBinding, ViewChild, ElementRef, OnDestroy } from "@angular/core"; +import { Component, ViewChildren, QueryList, HostBinding, ViewChild, ElementRef, OnDestroy } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { distinctUntilChanged, map, withLatestFrom, shareReplay, mapTo } from "rxjs/operators"; import { merge, Observable, Subject, Subscription } from "rxjs"; -import { viewerStateSelectTemplateWithId, viewerStateToggleLayer } from "src/services/state/viewerState.store.helper"; import { MatMenuTrigger } from "@angular/material/menu"; -import { viewerStateGetSelectedAtlas, viewerStateAtlasLatestParcellationSelector, viewerStateSelectedTemplateFullInfoSelector, viewerStateSelectedTemplatePureSelector, viewerStateSelectedParcellationSelector } from "src/services/state/viewerState/selectors"; import { ARIA_LABELS, CONST, QUICKTOUR_DESC } from 'common/constants' import { IQuickTourData } from "src/ui/quickTour/constrants"; import { animate, state, style, transition, trigger } from "@angular/animations"; import { IHasId, OVERWRITE_SHOW_DATASET_DIALOG_TOKEN } from "src/util/interfaces"; import { CurrentTmplSupportsParcPipe } from "../pipes/currTmplSupportsParc.pipe"; +import { + actions, + fromRootStore +} from "src/state/atlasSelection" +import { SAPI } from "src/atlasComponents/sapi"; +import { atlasSelection } from "src/state"; @Component({ selector: 'atlas-layer-selector', @@ -43,7 +47,7 @@ import { CurrentTmplSupportsParcPipe } from "../pipes/currTmplSupportsParc.pipe" } ] }) -export class AtlasLayerSelector implements OnInit, OnDestroy { +export class AtlasLayerSelector implements OnDestroy { public ARIA_LABELS = ARIA_LABELS public CONST = CONST @@ -55,23 +59,22 @@ export class AtlasLayerSelector implements OnInit, OnDestroy { selectorPanelTemplateRef: ElementRef public selectedAtlas$: Observable<any> = this.store$.pipe( - select(viewerStateGetSelectedAtlas), + select(atlasSelection.selectors.selectedAtlas), distinctUntilChanged(), shareReplay(1) ) public atlasLayersLatest$ = this.store$.pipe( - select(viewerStateAtlasLatestParcellationSelector), + fromRootStore.allAvailParcs(this.sapi), shareReplay(1), ) - public availableTemplates$ = this.store$.pipe<any[]>( - select(viewerStateSelectedTemplateFullInfoSelector), + public availableTemplates$ = this.store$.pipe( + fromRootStore.allAvailSpaces(this.sapi), ) - private selectedTemplate: any public selectedTemplate$ = this.store$.pipe( - select(viewerStateSelectedTemplatePureSelector), + select(atlasSelection.selectors.selectedTemplate), withLatestFrom(this.availableTemplates$), map(([selectedTmpl, fullInfoTemplates]) => { return fullInfoTemplates.find(t => t['@id'] === selectedTmpl['@id']) @@ -90,7 +93,7 @@ export class AtlasLayerSelector implements OnInit, OnDestroy { ) public selectedParcellation$ = this.store$.pipe( - select(viewerStateSelectedParcellationSelector), + select(atlasSelection.selectors.selectedParcellation), ) private subscriptions: Subscription[] = [] @@ -103,15 +106,10 @@ export class AtlasLayerSelector implements OnInit, OnDestroy { description: QUICKTOUR_DESC.LAYER_SELECTOR, } - constructor(private store$: Store<any>) {} - - ngOnInit(): void { - this.subscriptions.push( - this.selectedTemplate$.subscribe(st => { - this.selectedTemplate = st - }), - ) - } + constructor( + private store$: Store<any>, + private sapi: SAPI, + ) {} ngOnDestroy() { while(this.subscriptions.length) this.subscriptions.pop().unsubscribe() @@ -124,32 +122,21 @@ export class AtlasLayerSelector implements OnInit, OnDestroy { selectTemplatewithId(templateId: string) { this.showOverlayIntent$.next(true) - this.store$.dispatch(viewerStateSelectTemplateWithId({ - payload: { - '@id': templateId - } - })) + this.store$.dispatch( + actions.selectATPById({ + templateId + }) + ) } private currTmplSupportParcPipe = new CurrentTmplSupportsParcPipe() - selectParcellationWithName(layer: any) { - const tmplChangeReq = !this.currTmplSupportParcPipe.transform(this.selectedTemplate, layer) - if (!tmplChangeReq) { - this.store$.dispatch( - viewerStateToggleLayer({ payload: layer }) - ) - } else { - this.showOverlayIntent$.next(true) - this.store$.dispatch( - viewerStateSelectTemplateWithId({ - payload: layer.availableIn[0], - config: { - selectParcellation: layer - } - }) - ) - } + selectParcellationWithId(id: string) { + this.store$.dispatch( + actions.selectATPById({ + parcellationId: id + }) + ) } collapseExpandedGroup(){ diff --git a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html index ed78029cd9c644c84e4c809e9498ff21a5eece81..b93e61fe24b492d47d332d594cdc4d129caa4bee 100644 --- a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html +++ b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html @@ -22,9 +22,7 @@ <div [hidden] iav-dataset-show-dataset-dialog - [iav-dataset-show-dataset-dialog-name]="template.originDatainfos[0]?.name" - [iav-dataset-show-dataset-dialog-description]="template.originDatainfos[0]?.description" - [iav-dataset-show-dataset-dialog-urls]="template.originDatainfos[0]?.urls" + [iav-dataset-show-dataset-dialog-name]="template.fullName" #kgInfo="iavDatasetShowDatasetDialog"> </div> <tile-cmp [tile-image-src]="template | getPreviewUrlPipe" @@ -53,10 +51,10 @@ gutterSize="16"> <!-- non grouped layers --> - <mat-grid-tile *ngFor="let layer of (atlasLayersLatest$ | async | getNonbaseParc | getIndividualParc); trackBy: trackbyAtId" + <mat-grid-tile *ngFor="let layer of (atlasLayersLatest$ | async ); trackBy: trackbyAtId" [attr.aria-checked]="selectedParcellation$ | async | groupParcSelected : layer"> - - <div [hidden] + {{ layer.name }} + <!-- <div [hidden] iav-dataset-show-dataset-dialog [iav-dataset-show-dataset-dialog-name]="layer.originDatainfos[0]?.name" [iav-dataset-show-dataset-dialog-description]="layer.originDatainfos[0]?.description" @@ -75,7 +73,7 @@ (tile-on-click)="selectParcellationWithName(layer)" (tile-on-info-click)="kgInfo && kgInfo.onClick()"> - </tile-cmp> + </tile-cmp> --> </mat-grid-tile> @@ -159,8 +157,7 @@ [tile-image-darktheme]="layer.darktheme" [tile-selected]="selectedParcellation$ | async | groupParcSelected : layer" - - (tile-on-click)="selectParcellationWithName(layer)" + (tile-on-click)="selectParcellationWithId(layer)" (tile-on-info-click)="kgInfo && kgInfo.onClick()"> </tile-cmp> diff --git a/src/atlasComponents/uiSelectors/module.ts b/src/atlasComponents/uiSelectors/module.ts index b6994ea3003f3dbe9815b2986507cd7fdeea20a5..2ab98df7ca53d16da552731ba4b67abb725e4301 100644 --- a/src/atlasComponents/uiSelectors/module.ts +++ b/src/atlasComponents/uiSelectors/module.ts @@ -5,7 +5,6 @@ import { UtilModule } from "src/util"; import { AtlasDropdownSelector } from "./atlasDropdown/atlasDropdown.component"; import { AtlasLayerSelector } from "./atlasLayerSelector/atlasLayerSelector.component"; import {QuickTourModule} from "src/ui/quickTour/module"; -import { KgDatasetModule } from "../regionalFeatures/bsFeatures/kgDataset"; import { AtlaslayerTooltipPipe } from "./pipes/atlasLayerTooltip.pipe"; import { ComponentsModule } from "src/components"; import { GetNonbaseParcPipe } from "./pipes/getNonBaseParc.pipe"; @@ -17,6 +16,7 @@ import { GetPreviewUrlPipe } from "./pipes/getPreviewUrl.pipe"; import { CurrParcSupportsTmplPipe } from "./pipes/currParcSupportsTmpl.pipe"; import { AtlasCmpParcellationModule } from "../parcellation"; import { SiibraExplorerTemplateModule } from "../template"; +import { DialogInfoModule } from "src/ui/dialogInfo"; @NgModule({ imports: [ @@ -24,10 +24,10 @@ import { SiibraExplorerTemplateModule } from "../template"; AngularMaterialModule, UtilModule, QuickTourModule, - KgDatasetModule, ComponentsModule, AtlasCmpParcellationModule, SiibraExplorerTemplateModule, + DialogInfoModule, ], declarations: [ AtlasDropdownSelector, diff --git a/src/atlasComponents/uiSelectors/pipes/getNonBaseParc.pipe.ts b/src/atlasComponents/uiSelectors/pipes/getNonBaseParc.pipe.ts index e0b7997bfa191ee6e790fc935a541e7c8d9a5bb1..71e236bb56d6846458553c4e310e8f48829c60dc 100644 --- a/src/atlasComponents/uiSelectors/pipes/getNonBaseParc.pipe.ts +++ b/src/atlasComponents/uiSelectors/pipes/getNonBaseParc.pipe.ts @@ -1,4 +1,5 @@ import { Pipe, PipeTransform } from "@angular/core"; +import { SapiParcellationModel } from "src/atlasComponents/sapi"; @Pipe({ name: 'getNonbaseParc', @@ -6,7 +7,8 @@ import { Pipe, PipeTransform } from "@angular/core"; }) export class GetNonbaseParcPipe implements PipeTransform{ - public transform(arr: any[]){ - return arr.filter(p => !p['baseLayer']) + public transform(arr: SapiParcellationModel[]){ + if (!arr) return [] + return arr.filter(p => p.name.toLowerCase().indexOf("julich") < 0) } } diff --git a/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts b/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts index 8c9a11566b9f7f27350a684cc9ffc3976525c32e..327439092987a0dae77a60d0d409f5200f6b60c6 100644 --- a/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts +++ b/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts @@ -5,10 +5,11 @@ import { Polygon } from '../tools/poly' import { FormControl, FormGroup } from "@angular/forms"; import { Subscription } from "rxjs"; import { select, Store } from "@ngrx/store"; -import { viewerStateFetchedAtlasesSelector } from "src/services/state/viewerState/selectors"; import { ModularUserAnnotationToolService } from "../tools/service"; import { MatSnackBar } from "@angular/material/snack-bar"; import { Line } from "../tools/line"; +import { atlasSelection } from "src/state"; +import { map } from "rxjs/operators"; @Component({ selector: 'single-annotation-unit', @@ -31,7 +32,6 @@ export class SingleAnnotationUnit implements OnDestroy, AfterViewInit{ private subs: Subscription[] = [] public templateSpaces: { ['@id']: string - name: string }[] = [] ngOnChanges(){ while(this.chSubs.length > 0) this.chSubs.pop().unsubscribe() @@ -52,30 +52,22 @@ export class SingleAnnotationUnit implements OnDestroy, AfterViewInit{ this.managedAnnotation.desc = desc }) ) - } + public tmpls$ = this.store.pipe( + select(atlasSelection.selectors.selectedTemplate), + map(val => { + return [val] + }) + ) + constructor( - store: Store<any>, + private store: Store<any>, private snackbar: MatSnackBar, private svc: ModularUserAnnotationToolService, private cfr: ComponentFactoryResolver, private injector: Injector, ){ - this.subs.push( - store.pipe( - select(viewerStateFetchedAtlasesSelector), - ).subscribe(atlases => { - for (const atlas of atlases) { - for (const tmpl of atlas.templateSpaces) { - this.templateSpaces.push({ - '@id': tmpl['@id'], - name: tmpl.name - }) - } - } - }) - ) } ngAfterViewInit(){ diff --git a/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.template.html b/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.template.html index e03b3068ebc2360cc9e6a08c537c73c750c4ece9..51f1c004700f834b92c3afd81d6cd93ee3d01026 100644 --- a/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.template.html +++ b/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.template.html @@ -4,8 +4,8 @@ Space </mat-label> <mat-select formControlName="spaceId"> - <mat-option *ngFor="let tmpl of templateSpaces" [value]="tmpl['@id']"> - {{ tmpl.name }} + <mat-option *ngFor="let tmpl of tmpls$ | async" [value]="tmpl['@id']"> + {{ tmpl.fullName }} </mat-option> </mat-select> </mat-form-field> diff --git a/src/atlasComponents/userAnnotations/tools/line/line.component.ts b/src/atlasComponents/userAnnotations/tools/line/line.component.ts index bf3d32401fc9cfc2378b2a4d17000e53cef2390d..a98e126c64223483c2bbaf9131f9db091d3cb2f2 100644 --- a/src/atlasComponents/userAnnotations/tools/line/line.component.ts +++ b/src/atlasComponents/userAnnotations/tools/line/line.component.ts @@ -4,10 +4,10 @@ import { Store } from "@ngrx/store"; import { Line, LINE_ICON_CLASS } from "../line"; import { ToolCmpBase } from "../toolCmp.base"; import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; -import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; import { Point } from "../point"; import { ARIA_LABELS } from 'common/constants' import { ComponentStore } from "src/viewerModule/componentStore"; +import { actions } from "src/state/atlasSelection"; @Component({ selector: 'line-update-cmp', @@ -59,12 +59,12 @@ export class LineUpdateCmp extends ToolCmpBase implements OnDestroy{ const { x, y, z } = roi this.store.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { - position: [x, y, z], - positionReal: true, - animation: {} - } + position: [x, y, z] + }, + physical: true, + animation: true }) ) return @@ -79,12 +79,12 @@ export class LineUpdateCmp extends ToolCmpBase implements OnDestroy{ const { x, y, z } = this.updateAnnotation.points[0] this.store.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { - position: [x, y, z], - positionReal: true, - animation: {} - } + position: [x, y, z] + }, + physical: true, + animation: true }) ) } diff --git a/src/atlasComponents/userAnnotations/tools/point/point.component.ts b/src/atlasComponents/userAnnotations/tools/point/point.component.ts index c28152125a1a006ac1f69d8543792364dcb8b4ab..b4ef51006975e34cdcc753ad85cf1dc10056a9dd 100644 --- a/src/atlasComponents/userAnnotations/tools/point/point.component.ts +++ b/src/atlasComponents/userAnnotations/tools/point/point.component.ts @@ -3,9 +3,9 @@ import { Point, POINT_ICON_CLASS } from "../point"; import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; import { ToolCmpBase } from "../toolCmp.base"; import { Store } from "@ngrx/store"; -import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; import { ComponentStore } from "src/viewerModule/componentStore"; import { ARIA_LABELS } from 'common/constants' +import { actions } from "src/state/atlasSelection"; @Component({ selector: 'point-update-cmp', @@ -55,12 +55,12 @@ export class PointUpdateCmp extends ToolCmpBase implements OnDestroy{ } const { x, y, z } = this.updateAnnotation this.store.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { - position: [x, y, z], - positionReal: true, - animation: {} - } + position: [x, y, z] + }, + physical: true, + animation: true }) ) } diff --git a/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts b/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts index 9d8371b404a5eec8d4bc4ca60bbcf07cdfac2c5e..6e8852d88b665d2b988ed59f1340c311d0086515 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts @@ -3,11 +3,11 @@ import { MatSnackBar } from "@angular/material/snack-bar"; import { Polygon, POLY_ICON_CLASS } from "../poly"; import { ToolCmpBase } from "../toolCmp.base"; import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; -import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; import { Store } from "@ngrx/store"; import { Point } from "../point"; import { ARIA_LABELS } from 'common/constants' import { ComponentStore } from "src/viewerModule/componentStore"; +import { actions } from "src/state/atlasSelection"; @Component({ selector: 'poly-update-cmp', @@ -68,12 +68,12 @@ export class PolyUpdateCmp extends ToolCmpBase implements OnDestroy{ const { x, y, z } = roi this.store.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { - position: [x, y, z], - positionReal: true, - animation: {} - } + position: [x, y, z] + }, + physical: true, + animation: true }) ) return @@ -88,12 +88,12 @@ export class PolyUpdateCmp extends ToolCmpBase implements OnDestroy{ const { x, y, z } = this.updateAnnotation.points[0] this.store.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { - position: [x, y, z], - positionReal: true, - animation: {} - } + position: [x, y, z] + }, + physical: true, + animation: true }) ) } diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index 26fc809549b7f7fd59740539aaaec0e09ae66a4a..c6f075adc864af1874b54662caab6eea5a362066 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -4,7 +4,6 @@ import { Inject, Optional } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { BehaviorSubject, combineLatest, fromEvent, merge, Observable, of, Subject, Subscription } from "rxjs"; import {map, switchMap, filter, shareReplay, pairwise } from "rxjs/operators"; -import { viewerStateSelectedTemplatePureSelector, viewerStateViewerModeSelector } from "src/services/state/viewerState/selectors"; import { NehubaViewerUnit } from "src/viewerModule/nehuba"; import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TCallbackFunction, TSands, TGeometryJson, TNgAnnotationLine, TCallback } from "./type"; @@ -15,7 +14,8 @@ import { Point } from "./point"; import { FilterAnnotationsBySpace } from "../filterAnnotationBySpace.pipe"; import { retry } from 'common/util' import { MatSnackBar } from "@angular/material/snack-bar"; -import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper"; +import { actions } from "src/state/atlasSelection"; +import { atlasSelection } from "src/state"; const LOCAL_STORAGE_KEY = 'userAnnotationKey' @@ -91,7 +91,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ private selectedTmpl: any private selectedTmpl$ = this.store.pipe( - select(viewerStateSelectedTemplatePureSelector), + select(atlasSelection.selectors.selectedTemplate), ) public moduleAnnotationTypes: {instance: {name: string, iconClass: string, toolSelected$: Observable<boolean>}, onClick: () => void}[] = [] private managedAnnotationsStream$ = new Subject<{ @@ -502,7 +502,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ */ this.subscription.push( store.pipe( - select(viewerStateViewerModeSelector) + select(atlasSelection.selectors.viewerMode) ).subscribe(viewerMode => { this.currMode = viewerMode if (viewerMode === ModularUserAnnotationToolService.VIEWER_MODE) { @@ -761,7 +761,9 @@ export class ModularUserAnnotationToolService implements OnDestroy{ : ARIA_LABELS.VIEWER_MODE_ANNOTATING } this.store.dispatch( - viewerStateSetViewerMode({ payload }) + actions.setViewerMode({ + viewerMode: "annotating" + }) ) } } diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index ad6d2fb7e7057784edadb8b227f015f4e83f74ac..48906afedcd635979c4713c7f104c5f00a23b03f 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -3,23 +3,20 @@ import {Injectable, NgZone, Optional, Inject, OnDestroy, InjectionToken} from "@ import { MatSnackBar } from "@angular/material/snack-bar"; import { select, Store } from "@ngrx/store"; import { Observable, Subject, Subscription, from, race, of, } from "rxjs"; -import { distinctUntilChanged, map, filter, startWith, switchMap, catchError, mapTo, take } from "rxjs/operators"; +import { distinctUntilChanged, map, filter, startWith, switchMap, catchError, mapTo, take, shareReplay } from "rxjs/operators"; import { DialogService } from "src/services/dialogService.service"; -import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors"; -import { - viewerStateFetchedTemplatesSelector, -} from "src/services/state/viewerState/selectors"; import { getLabelIndexMap, getMultiNgIdsRegionsLabelIndexMap, IavRootStoreInterface, - safeFilter } from "src/services/stateStore.service"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { FRAGMENT_EMIT_RED } from "src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component"; import { IPluginManifest, PluginServices } from "src/plugin"; import { ILoadMesh } from 'src/messaging/types' import { CANCELLABLE_DIALOG } from "src/util/interfaces"; +import { atlasSelection, userInteraction } from "src/state" +import { SapiRegionModel } from "src/atlasComponents/sapi"; declare let window @@ -88,9 +85,11 @@ export class AtlasViewerAPIServices implements OnDestroy{ const { rs, spec } = this.getNextUserRegionSelectHandler() || {} if (!!rs) { - let moSegments + let moSegments: SapiRegionModel this.store.pipe( - select(uiStateMouseOverSegmentsSelector), + select(userInteraction.selectors.mousingOverRegions), + filter(val => val.length > 0), + map(val => val[0]), take(1) ).subscribe(val => moSegments = val) @@ -203,38 +202,29 @@ export class AtlasViewerAPIServices implements OnDestroy{ ) } - this.loadedTemplates$ = this.store.pipe( - select(viewerStateFetchedTemplatesSelector) - ) - this.selectParcellation$ = this.store.pipe( - select('viewerState'), - safeFilter('parcellationSelected'), - map(state => state.parcellationSelected), + select(atlasSelection.selectors.selectedParcellation), + shareReplay(1), ) this.interactiveViewer = { metadata : { selectedTemplateBSubject : this.store.pipe( - select('viewerState'), - safeFilter('templateSelected'), - map(state => state.templateSelected)), + select(atlasSelection.selectors.selectedTemplate), + shareReplay(1), + ), - selectedParcellationBSubject : this.store.pipe( - select('viewerState'), - safeFilter('parcellationSelected'), - map(state => state.parcellationSelected)), + selectedParcellationBSubject : this.selectParcellation$, selectedRegionsBSubject : this.store.pipe( - select('viewerState'), - safeFilter('regionsSelected'), - map(state => state.regionsSelected), - distinctUntilChanged((arr1, arr2) => - arr1.length === arr2.length && - (arr1 as any[]).every((item, index) => item.name === arr2[index].name)), + select(atlasSelection.selectors.selectedRegions), + shareReplay(1), ), - loadedTemplates : [], + get loadedTemplates(){ + throw new Error(`loadedTemplates is being deprecated`) + return [] + }, // TODO deprecate regionsLabelIndexMap : new Map(), @@ -338,7 +328,6 @@ export class AtlasViewerAPIServices implements OnDestroy{ } private init() { - this.loadedTemplates$.subscribe(templates => this.interactiveViewer.metadata.loadedTemplates = templates) this.selectParcellation$.pipe( filter(p => !!p && p.regions), distinctUntilChanged() @@ -454,8 +443,7 @@ export interface IVIewerHandle { mouseEvent: Observable<{eventName: string, event: MouseEvent}> mouseOverNehuba: Observable<{labelIndex: number, foundRegion: any | null}> - mouseOverNehubaLayers: Observable<Array<{layer: {name: string}, segment: any | number }>> - mouseOverNehubaUI: Observable<{ annotation: any, segments: any, landmark: any, customLandmark: any }> + mouseOverNehubaUI: Observable<{ annotation: any, landmark: any, customLandmark: any }> getNgHash: () => string } diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index f407f68944dad36fb049901fcc59e9e91fc77f47..341b6c2688a33d045f589cbb154cb72e08e19721 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -12,18 +12,14 @@ import { } from "@angular/core"; import { Store, select } from "@ngrx/store"; import { Observable, Subscription, merge, timer, fromEvent } from "rxjs"; -import { map, filter, distinctUntilChanged, delay, switchMapTo, take, startWith } from "rxjs/operators"; +import { map, filter, delay, switchMapTo, take, startWith } from "rxjs/operators"; import { IavRootStoreInterface, isDefined, - safeFilter, } from "../services/stateStore.service"; -import { WidgetServices } from "src/widget"; -import { LocalFileService } from "src/services/localFile.service"; import { AGREE_COOKIE } from "src/services/state/uiState.store"; -import { isSame } from "src/util/fn"; import { colorAnimation } from "./atlasViewer.animation" import { MouseHoverDirective } from "src/mouseoverModule"; import {MatSnackBar, MatSnackBarRef} from "@angular/material/snack-bar"; @@ -71,27 +67,22 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public meetsRequirement: boolean = true public sidePanelView$: Observable<string|null> - private newViewer$: Observable<any> private snackbarRef: MatSnackBarRef<any> - public snackbarMessage$: Observable<symbol> public onhoverLandmark$: Observable<{landmarkName: string, datasets: any} | null> private subscriptions: Subscription[] = [] - private selectedParcellation$: Observable<any> public selectedParcellation: any private cookieDialogRef: MatDialogRef<any> constructor( private store: Store<IavRootStoreInterface>, - private widgetServices: WidgetServices, private pureConstantService: PureContantService, private matDialog: MatDialog, private rd: Renderer2, - public localFileService: LocalFileService, private snackbar: MatSnackBar, private el: ElementRef, private slService: SlServiceService, @@ -99,36 +90,12 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @Inject(DOCUMENT) private document, ) { - this.snackbarMessage$ = this.store.pipe( - select('uiState'), - select("snackbarMessage"), - ) - this.sidePanelView$ = this.store.pipe( select('uiState'), filter(state => isDefined(state)), map(state => state.focusedSidePanel), ) - this.newViewer$ = this.store.pipe( - select('viewerState'), - select('templateSelected'), - distinctUntilChanged(isSame), - ) - - this.selectedParcellation$ = this.store.pipe( - select('viewerState'), - safeFilter('parcellationSelected'), - map(state => state.parcellationSelected), - distinctUntilChanged(), - ) - - this.subscriptions.push( - this.selectedParcellation$.subscribe(parcellation => { - this.selectedParcellation = parcellation - }), - - ) const error = this.el.nativeElement.getAttribute('data-error') @@ -168,30 +135,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.pureConstantService.useTouchUI$.subscribe(bool => this.ismobile = bool), ) - this.subscriptions.push( - this.snackbarMessage$.pipe( - // angular material issue - // see https://github.com/angular/angular/issues/15634 - // and https://github.com/angular/components/issues/11357 - delay(0), - ).subscribe(messageSymbol => { - if (this.snackbarRef) { this.snackbarRef.dismiss() } - - if (!messageSymbol) { return } - - const message = messageSymbol.description - this.snackbarRef = this.snackbar.open(message, 'Dismiss', { - duration: 5000, - }) - }), - ) - - this.subscriptions.push( - this.newViewer$.subscribe(() => { - this.widgetServices.clearAllWidgets() - }), - ) - this.subscriptions.push( this.pureConstantService.darktheme$.subscribe(flag => { this.rd.setAttribute(this.document.body, 'darktheme', this.meetsRequirement && flag.toString()) diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 5549a8e38914948e7157fcfc6cde6a4f63bd9153..0e1a5b2450c1f945d1487ba79b57c495ed225728 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -47,6 +47,7 @@ (iav-captureClickListenerDirective-onUnmovedClick)="mouseClickDocument($event)"> </iav-cmp-viewer-container> + <!-- TODO move to viewerCmp.template.html --> <layout-floating-container zIndex="13" #floatingOverlayContainer> diff --git a/src/atlasViewerExports/export.html b/src/atlasViewerExports/export.html index b4a41a088e065668815706796260dc8f76e1faf9..7d8ff5f05bab2139e9889b98b6d1b06e4ed4ca9f 100644 --- a/src/atlasViewerExports/export.html +++ b/src/atlasViewerExports/export.html @@ -156,9 +156,6 @@ console.log('GOTCHA') </div> <div class = "col-md-3"> <sample-box sample-box-title = "tree component" id = "tree-element-sample-box"> -<tree-element children-expanded = "true" id = 'tree-element' treebase> - -</tree-element> </sample-box> </div> </div> diff --git a/src/components/components.module.ts b/src/components/components.module.ts index c8d9ff210b5530a6e001dcd983a3fb8487c560cf..1ed9c40fe619fa0a10e851d89206e4b4e93d0b09 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -19,8 +19,6 @@ import { FlattenTreePipe } from './flatTree/flattener.pipe'; import { FlatTreeComponent } from './flatTree/flatTree.component'; import { HighlightPipe } from './flatTree/highlight.pipe'; import { RenderPipe } from './flatTree/render.pipe'; -import { TreeComponent } from './tree/tree.component'; -import { TreeBaseDirective } from './tree/treeBase.directive'; import { IAVVerticalButton } from './vButton/vButton.component'; import { DynamicMaterialBtn } from './dynamicMaterialBtn/dynamicMaterialBtn.component'; import { SpinnerCmp } from './spinner/spinner.component'; @@ -40,7 +38,6 @@ import { TileCmp } from './tile/tile.component'; declarations : [ /* components */ MarkdownDom, - TreeComponent, FlatTreeComponent, DialogComponent, ConfirmDialogComponent, @@ -49,9 +46,6 @@ import { TileCmp } from './tile/tile.component'; SpinnerCmp, TileCmp, - /* directive */ - TreeBaseDirective, - /* pipes */ SafeHtmlPipe, TreeSearchPipe, @@ -67,7 +61,6 @@ import { TileCmp } from './tile/tile.component'; ReadmoreModule, MarkdownDom, - TreeComponent, FlatTreeComponent, DialogComponent, ConfirmDialogComponent, @@ -77,7 +70,6 @@ import { TileCmp } from './tile/tile.component'; TileCmp, TreeSearchPipe, - TreeBaseDirective, ], }) diff --git a/src/components/tree/tree.animation.ts b/src/components/tree/tree.animation.ts deleted file mode 100644 index eca9be9c9530b480c4fdc7e696cfa252c61e2728..0000000000000000000000000000000000000000 --- a/src/components/tree/tree.animation.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { animate, state, style, transition, trigger } from "@angular/animations"; - -export const treeAnimations = trigger('collapseState', [ - state('collapsed', - style({ - 'margin-top' : '-{{ fullHeight }}px', - }), - { params : { fullHeight : 0 } }, - ), - state('visible', - style({ - 'margin-top' : '0px', - }), - { params : { fullHeight : 0 } }, - ), - transition('collapsed => visible', [ - animate('180ms'), - ]), - transition('visible => collapsed', [ - animate('180ms'), - ]), -]) diff --git a/src/components/tree/tree.component.ts b/src/components/tree/tree.component.ts deleted file mode 100644 index b462dbc2c314626f1052bc552af39ed62ae5b6db..0000000000000000000000000000000000000000 --- a/src/components/tree/tree.component.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { AfterContentChecked, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, Input, OnChanges, OnDestroy, OnInit, Optional, Output, QueryList, ViewChild, ViewChildren } from "@angular/core"; -import { Subscription } from "rxjs"; -import { ParseAttributeDirective } from "../parseAttribute.directive"; -import { treeAnimations } from "./tree.animation"; -import { TreeService } from "./treeService.service"; - -@Component({ - selector : 'tree-component', - templateUrl : './tree.template.html', - styleUrls : [ - './tree.style.css', - ], - animations : [ - treeAnimations, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) - -export class TreeComponent extends ParseAttributeDirective implements OnChanges, OnInit, OnDestroy, AfterContentChecked { - @Input() public inputItem: any = { - name : 'Untitled', - children : [], - } - @Input() public childrenExpanded: boolean = true - - @Output() public mouseentertree: EventEmitter<any> = new EventEmitter() - @Output() public mouseleavetree: EventEmitter<any> = new EventEmitter() - @Output() public mouseclicktree: EventEmitter<any> = new EventEmitter() - - @ViewChildren(TreeComponent) public treeChildren: QueryList<TreeComponent> - @ViewChild('childrenContainer', { read : ElementRef }) public childrenContainer: ElementRef - - constructor( - private cdr: ChangeDetectorRef, - @Optional() public treeService: TreeService, - ) { - super() - } - - public subscriptions: Subscription[] = [] - - public ngOnInit() { - if ( this.treeService ) { - this.subscriptions.push( - this.treeService.markForCheck.subscribe(() => this.cdr.markForCheck()), - ) - } - } - - public ngOnDestroy() { - this.subscriptions.forEach(s => s.unsubscribe()) - } - - public _fullHeight: number = 9999 - - set fullHeight(num: number) { - this._fullHeight = num - } - - get fullHeight() { - return this._fullHeight - } - - public ngAfterContentChecked() { - this.fullHeight = this.childrenContainer ? this.childrenContainer.nativeElement.offsetHeight : 0 - this.cdr.detectChanges() - } - - public mouseenter(ev: MouseEvent) { - this.treeService.mouseenter.next({ - inputItem : this.inputItem, - node : this, - event : ev, - }) - } - - public mouseleave(ev: MouseEvent) { - this.treeService.mouseleave.next({ - inputItem : this.inputItem, - node : this, - event : ev, - }) - } - - public mouseclick(ev: MouseEvent) { - this.treeService.mouseclick.next({ - inputItem : this.inputItem, - node : this, - event : ev, - }) - } - - get chevronClass(): string { - return this.children ? - this.children.length > 0 ? - this.childrenExpanded ? - 'fa-chevron-down' : - 'fa-chevron-right' : - 'fa-none' : - 'fa-none' - } - - public handleEv(event: Event) { - event.preventDefault(); - event.stopPropagation(); - } - - public toggleChildrenShow(event: Event) { - this.childrenExpanded = !this.childrenExpanded - event.stopPropagation() - event.preventDefault() - } - - get children(): any[] { - return this.treeService ? - this.treeService.findChildren(this.inputItem) : - this.inputItem.children - } - - @HostBinding('attr.filterHidden') - get visibilityOnFilter(): boolean { - return this.treeService ? - this.treeService.searchFilter(this.inputItem) : - true - } - public handleMouseEnter(fullObj: any) { - - this.mouseentertree.emit(fullObj) - - if (this.treeService) { - this.treeService.mouseenter.next(fullObj) - } - } - - public handleMouseLeave(fullObj: any) { - - this.mouseleavetree.emit(fullObj) - - if (this.treeService) { - this.treeService.mouseleave.next(fullObj) - } - } - - public handleMouseClick(fullObj: any) { - - this.mouseclicktree.emit(fullObj) - - if (this.treeService) { - this.treeService.mouseclick.next(fullObj) - } - } - - public defaultSearchFilter = () => true -} diff --git a/src/components/tree/tree.style.css b/src/components/tree/tree.style.css deleted file mode 100644 index b05982290b10f2674d9868aa9d958274d762979d..0000000000000000000000000000000000000000 --- a/src/components/tree/tree.style.css +++ /dev/null @@ -1,111 +0,0 @@ -tree-component -{ - display:block; - margin-left:1em; -} - -div[itemContainer] -{ - display:flex; -} - -div[itemContainer] > [fas] -{ - flex: 0 0 1.2em; - align-self: center; - text-align: center; - z-index: 1; -} - -div[itemContainer] > [itemName] -{ - flex: 1 1 0px; - white-space: nowrap; -} - -div[itemContainer] > span[itemName]:hover -{ - cursor:default; - color:rgba(219, 181, 86,1); -} - -/* dashed guiding line */ - -tree-component:not(:last-child) > div[itemMasterContainer]:before -{ - - pointer-events: none; - top:-0.8em; - left:-0.5em; - height:100%; - box-sizing: border-box; - - border-left: rgba(255,255,255,1) 1px dashed; -} - -tree-component:not(:last-child) div[itemMasterContainer] > [itemContainer] -{ - position:relative; -} - - -tree-component:not(:last-child) div[itemMasterContainer] > [itemContainer]:before -{ - pointer-events: none; - content : ''; - position:absolute; - width:1.5em; - height:100%; - top:-50%; - left:-0.5em; - z-index: 0; -} - -tree-component:last-child div[itemMasterContainer] > [itemContainer] -{ - position:relative; -} - -tree-component:last-child div[itemMasterContainer] > [itemContainer]:before -{ - pointer-events: none; - content : ''; - position:absolute; - width:1.5em; - height:100%; - top:-50%; - left:-0.5em; - z-index: 0; -} - -tree-component:not(:last-child) div[itemMasterContainer]:before -{ - border-left: rgba(128,128,128,0.6) 1px dashed; -} - -tree-component:not(:last-child) div[itemMasterContainer] > [itemContainer]:before -{ - border-bottom: rgba(128,128,128,0.6) 1px dashed; -} - -tree-component div[itemMasterContainer]:last-child > [itemContainer]:before -{ - border-bottom: rgba(128,128,128,0.6) 1px dashed; - border-left : rgba(128,128,128,0.6) 1px dashed; -} - -div[itemMasterContainer] -{ - position:relative; -} -div[itemMasterContainer]:before -{ - content:' '; - position:absolute; - z-index: 0; -} - -div[childrenOverflowContainer] -{ - overflow:hidden; -} \ No newline at end of file diff --git a/src/components/tree/tree.template.html b/src/components/tree/tree.template.html deleted file mode 100644 index 10b6318bfe862cd8da5813da78aabd16e3ff6782..0000000000000000000000000000000000000000 --- a/src/components/tree/tree.template.html +++ /dev/null @@ -1,30 +0,0 @@ -<div - itemMasterContainer> - <div itemContainer> - <i - (click) = "toggleChildrenShow($event)" - [ngClass] = "chevronClass" - class = "fas" - fas> - </i> - <span - (mouseleave)="handleMouseLeave({inputItem:inputItem,node:this});handleEv($event)" - (mouseenter)="handleMouseEnter({inputItem:inputItem,node:this});handleEv($event)" - (click)="handleMouseClick({inputItem:inputItem,node:this});handleEv($event)" - [innerHTML] = "treeService ? treeService.renderNode(inputItem) : inputItem.name" - itemName> - </span> - </div> - <div childrenOverflowContainer> - <div - [@collapseState] = "{ value : childrenExpanded ? 'visible' : 'collapsed' , params : { fullHeight : fullHeight }}" - #childrenContainer> - <tree-component - *ngFor = "let child of ( treeService ? (treeService.findChildren(inputItem) | treeSearch : treeService.searchFilter : treeService.findChildren ) : inputItem.children )" - [childrenExpanded] = "childrenExpanded" - [inputItem] = "child"> - - </tree-component> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/components/tree/treeBase.directive.ts b/src/components/tree/treeBase.directive.ts deleted file mode 100644 index 43fdd5954cb1b4c0145aa5a9ffb10c4d5979f46f..0000000000000000000000000000000000000000 --- a/src/components/tree/treeBase.directive.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ChangeDetectorRef, Directive, EventEmitter, Input, OnChanges, OnDestroy, Output } from "@angular/core"; -import { Subscription } from "rxjs"; -import { TreeService } from "./treeService.service"; - -@Directive({ - selector : '[treebase]', - host : { - style : ` - - `, - }, - providers : [ - TreeService, - ], -}) - -export class TreeBaseDirective implements OnDestroy, OnChanges { - @Output() public treeNodeClick: EventEmitter<any> = new EventEmitter() - @Output() public treeNodeEnter: EventEmitter<any> = new EventEmitter() - @Output() public treeNodeLeave: EventEmitter<any> = new EventEmitter() - - @Input() public renderNode: (item: any) => string = (item) => item.name - @Input() public findChildren: (item: any) => any[] = (item) => item.children - @Input() public searchFilter: (item: any) => boolean | null = () => true - - private subscriptions: Subscription[] = [] - - constructor( - public changeDetectorRef: ChangeDetectorRef, - public treeService: TreeService, - ) { - this.subscriptions.push( - this.treeService.mouseclick.subscribe((obj) => this.treeNodeClick.emit(obj)), - ) - this.subscriptions.push( - this.treeService.mouseenter.subscribe((obj) => this.treeNodeEnter.emit(obj)), - ) - this.subscriptions.push( - this.treeService.mouseleave.subscribe((obj) => this.treeNodeLeave.emit(obj)), - ) - } - - public ngOnChanges() { - this.treeService.findChildren = this.findChildren - this.treeService.renderNode = this.renderNode - this.treeService.searchFilter = this.searchFilter - - this.treeService.markForCheck.next(true) - } - - public ngOnDestroy() { - this.subscriptions.forEach(s => s.unsubscribe()) - } -} diff --git a/src/components/tree/treeService.service.ts b/src/components/tree/treeService.service.ts deleted file mode 100644 index 2dc6bfc4dc1810038bdb008907a5d2dd5e0c27d9..0000000000000000000000000000000000000000 --- a/src/components/tree/treeService.service.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; - -@Injectable() -export class TreeService { - public mouseclick: Subject<any> = new Subject() - public mouseenter: Subject<any> = new Subject() - public mouseleave: Subject<any> = new Subject() - - public findChildren: (item: any) => any[] = (item) => item.children - public searchFilter: (item: any) => boolean | null = () => true - public renderNode: (item: any) => string = (item) => item.name - - public searchTerm: string = `` - - public markForCheck: Subject<any> = new Subject() -} diff --git a/src/databrowser.fallback.ts b/src/databrowser.fallback.ts index 86c9c626f104954b9f28f9b3992894c6b2ea3a71..6d23e7fb4ec9d40e35c4b2c8cafed9578b68db64 100644 --- a/src/databrowser.fallback.ts +++ b/src/databrowser.fallback.ts @@ -6,8 +6,6 @@ import { IHasId } from "./util/interfaces" * TODO gradually move to relevant. */ -export const OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN = new InjectionToken<(file: any, dataset: any) => void>('OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN') -export const GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME = new InjectionToken<({ datasetSchema, datasetId, filename }) => Observable<any|null>>('GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME') export const kgTos = `The interactive viewer queries HBP Knowledge Graph Data Platform ("KG") for published datasets. @@ -85,7 +83,6 @@ export interface IKgDataEntry { } export type TypePreviewDispalyed = (file, dataset) => Observable<boolean> -export const IAV_DATASET_PREVIEW_ACTIVE = new InjectionToken<TypePreviewDispalyed>('IAV_DATASET_PREVIEW_ACTIVE') export enum EnumPreviewFileTypes{ diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 85a5e68ae63408c740790bd514fb6ad3fdbe8bb2..7634811836054d36fb8d6c38e622993b4d2ca2df 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -5,7 +5,7 @@ export const environment = { PRODUCTION: true, BACKEND_URL: null, DATASET_PREVIEW_URL: 'https://hbp-kg-dataset-previewer.apps.hbp.eu/v2', - BS_REST_URL: 'https://siibra-api-latest.apps-dev.hbp.eu/v1_0', + BS_REST_URL: 'http://localhost:5000/v1_0', SPATIAL_TRANSFORM_BACKEND: 'https://hbp-spatial-backend.apps.hbp.eu', MATOMO_URL: null, MATOMO_ID: null, diff --git a/src/glue.spec.ts b/src/glue.spec.ts index 63009e7a8914a09f7bf31672669438cce70cb4f2..e398540319d21c6cf282f31bbe73359a39e760c6 100644 --- a/src/glue.spec.ts +++ b/src/glue.spec.ts @@ -1,16 +1,5 @@ -import { TestBed, tick, fakeAsync, discardPeriodicTasks } from "@angular/core/testing" -import { DatasetPreviewGlue, glueSelectorGetUiStatePreviewingFiles, glueActionRemoveDatasetPreview, datasetPreviewMetaReducer, glueActionAddDatasetPreview, GlueEffects, ClickInterceptorService } from "./glue" -import { provideMockStore, MockStore } from "@ngrx/store/testing" -import { getRandomHex, getIdObj } from 'common/util' -import { EnumWidgetTypes, TypeOpenedWidget, uiActionSetPreviewingDatasetFiles, uiStatePreviewingDatasetFilesSelector } from "./services/state/uiState.store.helper" -import { hot } from "jasmine-marbles" -import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" -import { glueActionToggleDatasetPreview } from './glue' -import { DS_PREVIEW_URL } from 'src/util/constants' -import { NgLayersService } from "./ui/layerbrowser/ngLayerService.service" -import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "./databrowser.fallback" -import { viewerStateSelectedTemplateSelector } from "./services/state/viewerState/selectors" -import { generalActionError } from "./services/stateStore.helper" +import { ClickInterceptorService } from "./glue" +import { getRandomHex } from 'common/util' const mockActionOnSpyReturnVal0 = { id: getRandomHex(), @@ -91,587 +80,6 @@ const dataset1 = { } describe('> glue.ts', () => { - describe('> DatasetPreviewGlue', () => { - - const initialState = { - uiState: { - previewingDatasetFiles: [] - }, - viewerState: { - regionsSelected: [] - } - } - beforeEach(() => { - actionOnWidgetSpy = jasmine.createSpy('actionOnWidget').and.returnValues( - mockActionOnSpyReturnVal0, - mockActionOnSpyReturnVal1 - ) - - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ], - providers: [ - DatasetPreviewGlue, - provideMockStore({ - initialState - }), - NgLayersService - ] - }) - }) - - afterEach(() => { - actionOnWidgetSpy.calls.reset() - const ctrl = TestBed.inject(HttpTestingController) - ctrl.verify() - }) - - describe('> #datasetPreviewDisplayed', () => { - - it('> correctly emits true when store changes', () => { - const glue = TestBed.inject(DatasetPreviewGlue) - const store = TestBed.inject(MockStore) - - const obs = glue.datasetPreviewDisplayed(file1) - - store.setState({ - uiState: { - previewingDatasetFiles: [] - } - }) - const uiStateSelector = store.overrideSelector( - glueSelectorGetUiStatePreviewingFiles, - [] - ) - - uiStateSelector.setResult([ file1 ] ) - store.refreshState() - expect(obs).toBeObservable( - hot('a', { - a: true, - b: false - }) - ) - }) - - - it('> correctly emits false when store changes', () => { - const store = TestBed.inject(MockStore) - - const glue = TestBed.inject(DatasetPreviewGlue) - store.setState({ - uiState: { - previewingDatasetFiles: [ file2 ] - } - }) - const obs = glue.datasetPreviewDisplayed(file1) - store.refreshState() - - expect(obs).toBeObservable( - hot('b', { - a: true, - b: false - }) - ) - }) - }) - - describe('> #displayDatasetPreview', () => { - - it('> calls dispatch', () => { - - const glue = TestBed.inject(DatasetPreviewGlue) - const mockStore = TestBed.inject(MockStore) - const dispatchSpy = spyOn(mockStore, 'dispatch').and.callThrough() - - glue.displayDatasetPreview(file1, dataset1 as any) - - expect(dispatchSpy).toHaveBeenCalled() - }) - - it('> dispatches glueActionToggleDatasetPreview with the correct filename', () => { - - const glue = TestBed.inject(DatasetPreviewGlue) - const mockStore = TestBed.inject(MockStore) - const dispatchSpy = spyOn(mockStore, 'dispatch').and.callThrough() - - glue.displayDatasetPreview(file1, dataset1 as any) - - const args = dispatchSpy.calls.allArgs() - const [ action ] = args[0] - - expect(action.type).toEqual(glueActionToggleDatasetPreview.type) - expect((action as any).datasetPreviewFile.filename).toEqual(file1.filename) - }) - - it('> uses datasetId of file if present', () => { - - const glue = TestBed.inject(DatasetPreviewGlue) - const mockStore = TestBed.inject(MockStore) - const dispatchSpy = spyOn(mockStore, 'dispatch').and.callThrough() - - glue.displayDatasetPreview(file1, dataset1 as any) - - const args = dispatchSpy.calls.allArgs() - const [ action ] = args[0] - - expect((action as any).datasetPreviewFile.datasetId).toEqual(file1.datasetId) - }) - - it('> falls back to dataset fullId if datasetId not present on file', () => { - - const glue = TestBed.inject(DatasetPreviewGlue) - const mockStore = TestBed.inject(MockStore) - const dispatchSpy = spyOn(mockStore, 'dispatch').and.callThrough() - - const { datasetId, ...noDsIdFile1 } = file1 - glue.displayDatasetPreview(noDsIdFile1 as any, dataset1 as any) - - const { fullId } = dataset1 - const { kgId } = getIdObj(fullId) - - const args = dispatchSpy.calls.allArgs() - const [ action ] = args[0] - - expect((action as any).datasetPreviewFile.datasetId).toEqual(kgId) - }) - }) - - describe('> http interceptor', () => { - it('> on no state, does not call', fakeAsync(() => { - - const store = TestBed.inject(MockStore) - const ctrl = TestBed.inject(HttpTestingController) - const glue = TestBed.inject(DatasetPreviewGlue) - - store.setState({ - uiState: { - previewingDatasetFiles: [] - } - }) - - const { datasetId, filename } = file1 - // debounce at 100ms - tick(200) - ctrl.expectNone({}) - - discardPeriodicTasks() - })) - it('> on set state, calls end point to fetch full data', fakeAsync(() => { - - const store = TestBed.inject(MockStore) - const ctrl = TestBed.inject(HttpTestingController) - const glue = TestBed.inject(DatasetPreviewGlue) - - store.setState({ - uiState: { - previewingDatasetFiles: [ file1 ] - } - }) - - const { datasetId, filename } = file1 - // debounce at 100ms - tick(200) - - const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent('minds/core/dataset/v1.0.0')}/${datasetId}/${encodeURIComponent(filename)}`) - req.flush(nifti) - discardPeriodicTasks() - })) - - it('> if returns 404, should be handled gracefully', fakeAsync(() => { - - const ctrl = TestBed.inject(HttpTestingController) - const glue = TestBed.inject(DatasetPreviewGlue) - - const { datasetId, filename } = file3 - - const obs$ = glue.getDatasetPreviewFromId({ datasetId, filename }) - let expectedVal = 'defined' - obs$.subscribe(val => expectedVal = val) - tick(200) - - const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent('minds/core/dataset/v1.0.0')}/${encodeURIComponent(datasetId)}/${encodeURIComponent(filename)}`) - req.flush(null, { status: 404, statusText: 'Not found' }) - - expect(expectedVal).toBeNull() - discardPeriodicTasks() - })) - }) - }) - - - describe('> datasetPreviewMetaReducer', () => { - - const obj1: TypeOpenedWidget = { - type: EnumWidgetTypes.DATASET_PREVIEW, - data: file1 - } - - const stateEmpty = { - uiState: { - previewingDatasetFiles: [] - } - } as { uiState: { previewingDatasetFiles: {datasetId: string, filename: string}[] } } - - const stateObj1 = { - uiState: { - previewingDatasetFiles: [ file1 ] - } - } as { uiState: { previewingDatasetFiles: {datasetId: string, filename: string}[] } } - - const reducer = jasmine.createSpy('reducer') - const metaReducer = datasetPreviewMetaReducer(reducer) - - afterEach(() => { - reducer.calls.reset() - }) - describe('> on glueActionAddDatasetPreview', () => { - describe('> if preview does not yet exist in state', () => { - beforeEach(() => { - metaReducer(stateEmpty, glueActionAddDatasetPreview({ datasetPreviewFile: file1 })) - }) - - it('> expect reducer to be called once', () => { - expect(reducer).toHaveBeenCalled() - expect(reducer.calls.count()).toEqual(1) - }) - - it('> expect call sig of reducer call to be correct', () => { - - const [ args ] = reducer.calls.allArgs() - expect(args[0]).toEqual(stateEmpty) - expect(args[1].type).toEqual(uiActionSetPreviewingDatasetFiles.type) - expect(args[1].previewingDatasetFiles).toEqual([ file1 ]) - }) - }) - - describe('> if preview already exist in state', () => { - beforeEach(() => { - metaReducer(stateObj1, glueActionAddDatasetPreview({ datasetPreviewFile: file1 })) - }) - it('> should still call reducer', () => { - expect(reducer).toHaveBeenCalled() - expect(reducer.calls.count()).toEqual(1) - }) - - it('> there should now be two previews in dispatched action', () => { - - const [ args ] = reducer.calls.allArgs() - expect(args[0]).toEqual(stateObj1) - expect(args[1].type).toEqual(uiActionSetPreviewingDatasetFiles.type) - expect(args[1].previewingDatasetFiles).toEqual([ file1, file1 ]) - }) - }) - }) - describe('> on glueActionRemoveDatasetPreview', () => { - it('> removes id as expected', () => { - metaReducer(stateObj1, glueActionRemoveDatasetPreview({ datasetPreviewFile: file1 })) - expect(reducer).toHaveBeenCalled() - expect(reducer.calls.count()).toEqual(1) - const [ args ] = reducer.calls.allArgs() - expect(args[0]).toEqual(stateObj1) - expect(args[1].type).toEqual(uiActionSetPreviewingDatasetFiles.type) - expect(args[1].previewingDatasetFiles).toEqual([ ]) - }) - }) - }) - - describe('> GlueEffects', () => { - - /** - * related to previews - */ - const mockTemplate = { - fullId: 'bar' - } - const mockPreviewFileIds = { - datasetId: 'foo', - filename: 'bar' - } - const mockPreviewFileIds2 = { - datasetId: 'foo2', - filename: 'bar2' - } - const mockPreviewFileIds3 = { - datasetId: 'foo3', - filename: 'bar3' - } - const mockPreviewFileIds4 = { - datasetId: 'foo4', - filename: 'bar4' - } - const previewFileNoRefSpace = { - name: 'bla bla 4', - datasetId: 'foo4', - filename: 'bar4' - } - const fittingMockPreviewFile = { - name: 'bla bla2', - datasetId: 'foo2', - filename: 'bar2', - referenceSpaces: [{ - fullId: 'bar' - }] - } - const mockPreviewFile = { - name: 'bla bla', - datasetId: 'foo', - filename: 'bar', - referenceSpaces: [{ - fullId: 'hello world' - }] - } - - const defaultState = { - viewerState: { - templateSelected: null, - parcellationSelected: null, - regionsSelected: [] - }, - uiState: { - previewingDatasetFiles: [] - } - } - - const mockGetDatasetPreviewFromId = jasmine.createSpy('getDatasetPreviewFromId') - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - GlueEffects, - provideMockStore({ - initialState: defaultState - }), - { - provide: GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME, - useValue: mockGetDatasetPreviewFromId - } - ] - }) - mockGetDatasetPreviewFromId.withArgs(mockPreviewFileIds2).and.returnValue( - hot('(a|)', { - a: fittingMockPreviewFile - }) - ) - mockGetDatasetPreviewFromId.withArgs({ datasetId: 'foo', filename: 'bar' }).and.returnValue( - hot('(a|)', { - a: mockPreviewFile - }) - ) - mockGetDatasetPreviewFromId.withArgs(mockPreviewFileIds3).and.returnValue( - hot('(a|)', { - a: null - }) - ) - mockGetDatasetPreviewFromId.withArgs(mockPreviewFileIds4).and.returnValue( - hot('(a|)', { - a: previewFileNoRefSpace - }) - ) - }) - - afterEach(() => { - mockGetDatasetPreviewFromId.calls.reset() - }) - - describe('> regionTemplateParcChange$', () => { - - const copiedState0 = JSON.parse(JSON.stringify(defaultState)) - copiedState0.viewerState.regionsSelected = [{ name: 'coffee' }] - copiedState0.viewerState.parcellationSelected = { name: 'chicken' } - copiedState0.viewerState.templateSelected = { name: 'spinach' } - - const generateTest = (m1, m2) => { - - const mockStore = TestBed.inject(MockStore) - mockStore.setState(copiedState0) - const glueEffects = TestBed.inject(GlueEffects) - /** - * couldn't get jasmine-marble to coopoerate - * TODO test proper with jasmine marble (?) - */ - let numOfEmit = 0 - const sub = glueEffects.regionTemplateParcChange$.subscribe(() => { - numOfEmit += 1 - }) - - const copiedState1 = JSON.parse(JSON.stringify(copiedState0)) - m1(copiedState1) - mockStore.setState(copiedState1) - expect(numOfEmit).toEqual(1) - - const copiedState2 = JSON.parse(JSON.stringify(copiedState0)) - m2(copiedState2) - mockStore.setState(copiedState2) - expect(numOfEmit).toEqual(2) - - sub.unsubscribe() - } - - it('> on change of region, should emit', () => { - generateTest( - copiedState1 => copiedState1.viewerState.regionsSelected = [{ name: 'cake' }], - copiedState2 => copiedState2.viewerState.regionsSelected = [{ name: 't bone' }] - ) - }) - - it('> on change of parcellation, should emit', () => { - generateTest( - copiedState1 => copiedState1.viewerState.parcellationSelected = { name: 'pizza' }, - copiedState2 => copiedState2.viewerState.parcellationSelected = { name: 'pizza on pineapple' } - ) - }) - - it('> on change of template, should emit', () => { - generateTest( - copiedState1 => copiedState1.viewerState.templateSelected = { name: 'calzone' }, - copiedState2 => copiedState2.viewerState.templateSelected = { name: 'calzone on pineapple' } - ) - }) - }) - - - describe('> unsuitablePreviews$', () => { - - it('> calls injected getDatasetPreviewFromId', () => { - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, mockTemplate) - mockStore.overrideSelector(uiStatePreviewingDatasetFilesSelector, [mockPreviewFileIds2]) - - const glueEffects = TestBed.inject(GlueEffects) - expect(glueEffects.unsuitablePreviews$).toBeObservable( - hot('') - ) - /** - * calling twice, once to check if the dataset preview can be retrieved, the other to check the referenceSpace - */ - expect(mockGetDatasetPreviewFromId).toHaveBeenCalledTimes(2) - expect(mockGetDatasetPreviewFromId).toHaveBeenCalledWith(mockPreviewFileIds2) - }) - - it('> if getDatasetPreviewFromId throws in event stream, handles gracefully', () => { - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, mockTemplate) - mockStore.overrideSelector(uiStatePreviewingDatasetFilesSelector, [mockPreviewFileIds3]) - - const glueEffects = TestBed.inject(GlueEffects) - - expect(glueEffects.unsuitablePreviews$).toBeObservable( - hot('a', { - a: [ mockPreviewFileIds3 ] - }) - ) - }) - - describe('> filtering out dataset previews that do not satisfy reference space requirements', () => { - it('> if reference spaces does not match the selected reference template, will emit', () => { - const mockStore = TestBed.inject(MockStore) - - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, mockTemplate) - mockStore.overrideSelector(uiStatePreviewingDatasetFilesSelector, [mockPreviewFileIds]) - const glueEffects = TestBed.inject(GlueEffects) - expect(glueEffects.unsuitablePreviews$).toBeObservable( - hot('a', { - a: [ mockPreviewFile ] - }) - ) - }) - }) - - describe('> keeping dataset previews that satisfy reference space criteria', () => { - it('> if ref space is undefined, keep preview', () => { - - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, mockTemplate) - mockStore.overrideSelector(uiStatePreviewingDatasetFilesSelector, [mockPreviewFileIds4]) - const glueEffects = TestBed.inject(GlueEffects) - expect(glueEffects.unsuitablePreviews$).toBeObservable( - hot('') - ) - }) - - it('> if ref space is defined, and matches, keep preview', () => { - - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, mockTemplate) - mockStore.overrideSelector(uiStatePreviewingDatasetFilesSelector, [mockPreviewFileIds2]) - const glueEffects = TestBed.inject(GlueEffects) - expect(glueEffects.unsuitablePreviews$).toBeObservable( - hot('') - ) - }) - }) - - }) - - describe('> uiRemoveUnsuitablePreviews$', () => { - it('> emits whenever unsuitablePreviews$ emits', () => { - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, mockTemplate) - mockStore.overrideSelector(uiStatePreviewingDatasetFilesSelector, [mockPreviewFileIds]) - const glueEffects = TestBed.inject(GlueEffects) - expect(glueEffects.uiRemoveUnsuitablePreviews$).toBeObservable( - hot('a', { - a: generalActionError({ - message: `Dataset previews ${mockPreviewFile.name} cannot be displayed.` - }) - }) - ) - }) - }) - - describe('> filterDatasetPreviewByTemplateSelected$', () => { - - it('> remove 1 preview datasetfile depending on unsuitablepreview$', () => { - const mockStore = TestBed.inject(MockStore) - - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, mockTemplate) - mockStore.overrideSelector(uiStatePreviewingDatasetFilesSelector, [mockPreviewFileIds]) - const glueEffects = TestBed.inject(GlueEffects) - expect(glueEffects.filterDatasetPreviewByTemplateSelected$).toBeObservable( - hot('a', { - a: uiActionSetPreviewingDatasetFiles({ - previewingDatasetFiles: [ ] - }) - }) - ) - - }) - it('> remove 1 preview datasetfile (get preview info fail) depending on unsuitablepreview$', () => { - const mockStore = TestBed.inject(MockStore) - - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, mockTemplate) - mockStore.overrideSelector(uiStatePreviewingDatasetFilesSelector, [mockPreviewFileIds3]) - const glueEffects = TestBed.inject(GlueEffects) - expect(glueEffects.filterDatasetPreviewByTemplateSelected$).toBeObservable( - hot('a', { - a: uiActionSetPreviewingDatasetFiles({ - previewingDatasetFiles: [ ] - }) - }) - ) - - }) - it('> remove 2 preview datasetfile depending on unsuitablepreview$', () => { - const mockStore = TestBed.inject(MockStore) - - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, mockTemplate) - mockStore.overrideSelector(uiStatePreviewingDatasetFilesSelector, [mockPreviewFileIds, mockPreviewFileIds2, mockPreviewFileIds4]) - const glueEffects = TestBed.inject(GlueEffects) - expect(glueEffects.filterDatasetPreviewByTemplateSelected$).toBeObservable( - hot('a', { - a: uiActionSetPreviewingDatasetFiles({ - previewingDatasetFiles: [ mockPreviewFileIds2, mockPreviewFileIds4 ] - }) - }) - ) - - }) - - }) - - }) describe('> ClickInterceptorService', () => { /** diff --git a/src/glue.ts b/src/glue.ts index 027858745e6ac5223431e586aa6bd016e1ff3611..89b97a7f5647a8ff7e99a7bcf1dfa52837ab9c85 100644 --- a/src/glue.ts +++ b/src/glue.ts @@ -1,447 +1,6 @@ -import { uiActionSetPreviewingDatasetFiles, IDatasetPreviewData, uiStatePreviewingDatasetFilesSelector } from "./services/state/uiState.store.helper" -import { OnDestroy, Injectable, Inject, InjectionToken } from "@angular/core" -import { DatasetPreview, determinePreviewFileType, EnumPreviewFileTypes, IKgDataEntry, getKgSchemaIdFromFullId, GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "./databrowser.fallback" -import { Subscription, Observable, forkJoin, of, merge, combineLatest } from "rxjs" -import { select, Store, ActionReducer, createAction, props, createSelector, Action } from "@ngrx/store" -import { startWith, map, shareReplay, pairwise, debounceTime, distinctUntilChanged, tap, switchMap, withLatestFrom, mapTo, switchMapTo, filter, skip, catchError } from "rxjs/operators" -import { getIdObj } from 'common/util' -import { MatDialogRef } from "@angular/material/dialog" -import { HttpClient } from "@angular/common/http" -import { DS_PREVIEW_URL } from 'src/util/constants' -import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer } from "./services/state/ngViewerState.store.helper" -import { ARIA_LABELS } from 'common/constants' -import { Effect } from "@ngrx/effects" -import { viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector, viewerStateSelectedParcellationSelector } from "./services/state/viewerState/selectors" -import { ngViewerActionClearView } from './services/state/ngViewerState/actions' -import { generalActionError } from "./services/stateStore.helper" +import { Injectable } from "@angular/core" import { RegDeregController } from "./util/regDereg.base" -const prvFilterNull = ({ prvToDismiss, prvToShow }) => ({ - prvToDismiss: prvToDismiss.filter(v => !!v), - prvToShow: prvToShow.filter(v => !!v), -}) - -export const glueActionToggleDatasetPreview = createAction( - '[glue] toggleDatasetPreview', - props<{ datasetPreviewFile: IDatasetPreviewData }>() -) - -export const glueActionAddDatasetPreview = createAction( - '[glue] addDatasetPreview', - props<{ datasetPreviewFile: IDatasetPreviewData }>() -) - -export const glueActionRemoveDatasetPreview = createAction( - '[glue] removeDatasetPreview', - props<{ datasetPreviewFile: IDatasetPreviewData }>() -) - -export const glueSelectorGetUiStatePreviewingFiles = createSelector( - (state: any) => state.uiState, - uiState => uiState.previewingDatasetFiles -) - -export interface IDatasetPreviewGlue{ - datasetPreviewDisplayed(file: DatasetPreview, dataset: IKgDataEntry): Observable<boolean> - displayDatasetPreview(previewFile: DatasetPreview, dataset: IKgDataEntry): void -} - -@Injectable({ - providedIn: 'root' -}) - -export class GlueEffects { - - public regionTemplateParcChange$ = merge( - this.store$.pipe( - select(viewerStateSelectedRegionsSelector), - map(rs => (rs || []).map(r => r['name']).sort().join(',')), - distinctUntilChanged(), - skip(1), - ), - this.store$.pipe( - select(viewerStateSelectedTemplateSelector), - map(tmpl => tmpl - ? tmpl['@id'] || tmpl['name'] - : null), - distinctUntilChanged(), - skip(1) - ), - this.store$.pipe( - select(viewerStateSelectedParcellationSelector), - map(parc => parc - ? parc['@id'] || parc['name'] - : null), - distinctUntilChanged(), - skip(1) - ) - ).pipe( - mapTo(true) - ) - - @Effect() - resetDatasetPreview$: Observable<any> = this.store$.pipe( - select(uiStatePreviewingDatasetFilesSelector), - distinctUntilChanged(), - filter(previews => previews?.length > 0), - switchMapTo(this.regionTemplateParcChange$) - ).pipe( - mapTo(uiActionSetPreviewingDatasetFiles({ - previewingDatasetFiles: [] - })) - ) - - unsuitablePreviews$: Observable<any> = merge( - /** - * filter out the dataset previews, whose details cannot be fetchd from getdatasetPreviewFromId method - */ - - this.store$.pipe( - select(uiStatePreviewingDatasetFilesSelector), - switchMap(previews => - forkJoin( - previews.map(prev => this.getDatasetPreviewFromId(prev).pipe( - // filter out the null's - filter(val => !val), - mapTo(prev) - )) - ).pipe( - filter(previewFiles => previewFiles.length > 0) - ) - ) - ), - /** - * filter out the dataset previews, whose details can be fetched from getDatasetPreviewFromId method - */ - combineLatest([ - this.store$.pipe( - select(viewerStateSelectedTemplateSelector) - ), - this.store$.pipe( - select(uiStatePreviewingDatasetFilesSelector), - switchMap(previews => - forkJoin( - previews.map(prev => this.getDatasetPreviewFromId(prev).pipe( - filter(val => !!val) - )) - ).pipe( - // filter out the null's - filter(previewFiles => previewFiles.length > 0) - ) - ), - ) - ]).pipe( - map(([ templateSelected, previewFiles ]) => - previewFiles.filter(({ referenceSpaces }) => - // if referenceSpaces of the dataset preview is undefined, assume it is suitable for all reference spaces - (!referenceSpaces) - ? false - : !referenceSpaces.some(({ fullId }) => fullId === '*' || fullId === templateSelected.fullId) - ) - ), - ) - ).pipe( - filter(arr => arr.length > 0), - shareReplay(1), - ) - - @Effect() - uiRemoveUnsuitablePreviews$: Observable<any> = this.unsuitablePreviews$.pipe( - map(previews => generalActionError({ - message: `Dataset previews ${previews.map(v => v.name)} cannot be displayed.` - })) - ) - - @Effect() - filterDatasetPreviewByTemplateSelected$: Observable<any> = this.unsuitablePreviews$.pipe( - withLatestFrom( - this.store$.pipe( - select(uiStatePreviewingDatasetFilesSelector), - ) - ), - map(([ unsuitablePreviews, previewFiles ]) => uiActionSetPreviewingDatasetFiles({ - previewingDatasetFiles: previewFiles.filter( - ({ datasetId: dsId, filename: fName }) => !unsuitablePreviews.some( - ({ datasetId, filename }) => datasetId === dsId && fName === filename - ) - ) - })) - ) - - @Effect() - resetConnectivityMode: Observable<any> = this.store$.pipe( - select(viewerStateSelectedRegionsSelector), - pairwise(), - filter(([o, n]) => o.length > 0 && n.length === 0), - mapTo( - ngViewerActionClearView({ - payload: { - 'Connectivity': false - } - }) - ) - ) - - constructor( - private store$: Store<any>, - @Inject(GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME) private getDatasetPreviewFromId: (arg) => Observable<any|null> - ){ - } -} - -@Injectable({ - providedIn: 'root' -}) - -export class DatasetPreviewGlue implements IDatasetPreviewGlue, OnDestroy{ - - static readonly DEFAULT_DIALOG_OPTION = { - ariaLabel: ARIA_LABELS.DATASET_FILE_PREVIEW, - hasBackdrop: false, - disableClose: true, - autoFocus: false, - panelClass: 'mat-card-sm', - height: '50vh', - width: '350px', - position: { - left: '5px' - }, - } - - static GetDatasetPreviewId(data: IDatasetPreviewData ){ - const { datasetSchema = 'minds/core/dataset/v1.0.0', datasetId, filename } = data - return `${datasetSchema}/${datasetId}:${filename}` - } - - static GetDatasetPreviewFromId(id: string): IDatasetPreviewData{ - const re = /([a-f0-9-]+):(.+)$/.exec(id) - if (!re) throw new Error(`id cannot be decoded: ${id}`) - return { datasetId: re[1], filename: re[2] } - } - - static PreviewFileIsInCorrectSpace(previewFile, templateSelected): boolean{ - - const re = getKgSchemaIdFromFullId( - (templateSelected && templateSelected.fullId) || '' - ) - const templateId = re && re[0] && `${re[0]}/${re[1]}` - const { referenceSpaces } = previewFile - return referenceSpaces.findIndex(({ fullId }) => fullId === '*' || fullId === templateId) >= 0 - } - - private subscriptions: Subscription[] = [] - private openedPreviewMap = new Map<string, {id: string, matDialogRef: MatDialogRef<any>}>() - - private previewingDatasetFiles$: Observable<IDatasetPreviewData[]> = this.store$.pipe( - select(glueSelectorGetUiStatePreviewingFiles), - startWith([]), - shareReplay(1), - ) - - public _volumePreview$ = this.previewingDatasetFiles$.pipe( - switchMap(arr => arr.length > 0 - ? forkJoin(arr.map(v => this.getDatasetPreviewFromId(v))) - : of([])), - map(arr => arr.filter(v => determinePreviewFileType(v) === EnumPreviewFileTypes.VOLUMES)) - ) - - private diffPreviewingDatasetFiles$= this.previewingDatasetFiles$.pipe( - debounceTime(100), - startWith([] as IDatasetPreviewData[]), - pairwise(), - map(([ oldPreviewWidgets, newPreviewWidgets ]) => { - const oldPrvWgtIdSet = new Set(oldPreviewWidgets.map(DatasetPreviewGlue.GetDatasetPreviewId)) - const newPrvWgtIdSet = new Set(newPreviewWidgets.map(DatasetPreviewGlue.GetDatasetPreviewId)) - - const prvToShow = newPreviewWidgets.filter(obj => !oldPrvWgtIdSet.has(DatasetPreviewGlue.GetDatasetPreviewId(obj))) - const prvToDismiss = oldPreviewWidgets.filter(obj => !newPrvWgtIdSet.has(DatasetPreviewGlue.GetDatasetPreviewId(obj))) - - return { prvToShow, prvToDismiss } - }), - ) - - ngOnDestroy(){ - while(this.subscriptions.length > 0){ - this.subscriptions.pop().unsubscribe() - } - } - - private sharedDiffObs$ = this.diffPreviewingDatasetFiles$.pipe( - switchMap(({ prvToShow, prvToDismiss }) => { - return forkJoin({ - prvToShow: prvToShow.length > 0 - ? forkJoin(prvToShow.map(val => this.getDatasetPreviewFromId(val))) - : of([]), - prvToDismiss: prvToDismiss.length > 0 - ? forkJoin(prvToDismiss.map(val => this.getDatasetPreviewFromId(val))) - : of([]) - }) - }), - map(prvFilterNull), - shareReplay(1) - ) - - private getDiffDatasetFilesPreviews(filterFn: (prv: any) => boolean = () => true): Observable<{prvToShow: any[], prvToDismiss: any[]}>{ - return this.sharedDiffObs$.pipe( - map(({ prvToDismiss, prvToShow }) => { - return { - prvToShow: prvToShow.filter(filterFn), - prvToDismiss: prvToDismiss.filter(filterFn), - } - }) - ) - } - - private fetchedDatasetPreviewCache: Map<string, Observable<any>> = new Map() - public getDatasetPreviewFromId({ datasetSchema = 'minds/core/dataset/v1.0.0', datasetId, filename }: IDatasetPreviewData){ - const dsPrvId = DatasetPreviewGlue.GetDatasetPreviewId({ datasetSchema, datasetId, filename }) - const cachedPrv$ = this.fetchedDatasetPreviewCache.get(dsPrvId) - const filteredDsId = /[a-f0-9-]+$/.exec(datasetId) - if (cachedPrv$) return cachedPrv$ - const url = `${DS_PREVIEW_URL}/${encodeURIComponent(datasetSchema)}/${filteredDsId}/${encodeURIComponent(filename)}` - const filedetail$ = this.http.get(url, { responseType: 'json' }).pipe( - map(json => { - return { - ...json, - filename, - datasetId, - datasetSchema - } - }), - catchError((_err, _obs) => of(null)) - ) - this.fetchedDatasetPreviewCache.set(dsPrvId, filedetail$) - return filedetail$.pipe( - tap(val => this.fetchedDatasetPreviewCache.set(dsPrvId, of(val))) - ) - } - - constructor( - private store$: Store<any>, - private http: HttpClient, - ){ - - // managing dataset previews without UI - - // managing registeredVolumes - this.subscriptions.push( - this.getDiffDatasetFilesPreviews( - dsPrv => determinePreviewFileType(dsPrv) === EnumPreviewFileTypes.VOLUMES - ).pipe( - withLatestFrom(this.store$.pipe( - select(state => state?.viewerState?.templateSelected || null), - distinctUntilChanged(), - )) - ).subscribe(([ { prvToShow, prvToDismiss }, templateSelected ]) => { - const filterdPrvs = prvToShow.filter(prv => DatasetPreviewGlue.PreviewFileIsInCorrectSpace(prv, templateSelected)) - for (const prv of filterdPrvs) { - const { volumes } = prv['data']['iav-registered-volumes'] - this.store$.dispatch(ngViewerActionAddNgLayer({ - layer: volumes - })) - } - - for (const prv of prvToDismiss) { - const { volumes } = prv['data']['iav-registered-volumes'] - this.store$.dispatch(ngViewerActionRemoveNgLayer({ - layer: volumes - })) - } - }) - ) - } - - public datasetPreviewDisplayed(file: DatasetPreview, dataset?: IKgDataEntry){ - if (!file) return of(false) - return this.previewingDatasetFiles$.pipe( - map(datasetPreviews => { - const { filename, datasetId } = file - const { fullId } = dataset || {} - const { kgId } = getIdObj(fullId) || {} - - return datasetPreviews.findIndex(({ datasetId: dsId, filename: fName }) => { - return (datasetId || kgId) === dsId && fName === filename - }) >= 0 - }) - ) - } - - public displayDatasetPreview(previewFile: DatasetPreview, dataset: IKgDataEntry){ - const { filename, datasetId } = previewFile - const { fullId } = dataset - const { kgId, kgSchema } = getIdObj(fullId) - - const datasetPreviewFile = { - datasetSchema: kgSchema, - datasetId: datasetId || kgId, - filename - } - - this.store$.dispatch(glueActionToggleDatasetPreview({ datasetPreviewFile })) - } -} - -export function datasetPreviewMetaReducer(reducer: ActionReducer<any>): ActionReducer<any>{ - return function (state, action) { - switch(action.type) { - case glueActionToggleDatasetPreview.type: { - - const previewingDatasetFiles = (state?.uiState?.previewingDatasetFiles || []) as IDatasetPreviewData[] - const ids = new Set(previewingDatasetFiles.map(DatasetPreviewGlue.GetDatasetPreviewId)) - const { datasetPreviewFile } = action as Action & { datasetPreviewFile: IDatasetPreviewData } - const newId = DatasetPreviewGlue.GetDatasetPreviewId(datasetPreviewFile) - if (ids.has(newId)) { - const removeId = DatasetPreviewGlue.GetDatasetPreviewId(datasetPreviewFile) - const filteredOpenedWidgets = previewingDatasetFiles.filter(obj => { - const id = DatasetPreviewGlue.GetDatasetPreviewId(obj) - return id !== removeId - }) - return reducer(state, uiActionSetPreviewingDatasetFiles({ previewingDatasetFiles: filteredOpenedWidgets })) - } else { - return reducer(state, uiActionSetPreviewingDatasetFiles({ previewingDatasetFiles: [ ...previewingDatasetFiles, datasetPreviewFile ] })) - } - } - case glueActionAddDatasetPreview.type: { - const previewingDatasetFiles = (state?.uiState?.previewingDatasetFiles || []) as IDatasetPreviewData[] - const { datasetPreviewFile } = action as Action & { datasetPreviewFile: IDatasetPreviewData } - return reducer(state, uiActionSetPreviewingDatasetFiles({ previewingDatasetFiles: [ ...previewingDatasetFiles, datasetPreviewFile] })) - } - case glueActionRemoveDatasetPreview.type: { - const previewingDatasetFiles = (state?.uiState?.previewingDatasetFiles || []) as IDatasetPreviewData[] - const { datasetPreviewFile } = action as any - - const removeId = DatasetPreviewGlue.GetDatasetPreviewId(datasetPreviewFile) - const filteredOpenedWidgets = previewingDatasetFiles.filter(obj => { - const id = DatasetPreviewGlue.GetDatasetPreviewId(obj) - return id !== removeId - }) - return reducer(state, uiActionSetPreviewingDatasetFiles({ previewingDatasetFiles: filteredOpenedWidgets })) - } - default: return reducer(state, action) - } - } -} - -export const SAVE_USER_DATA = new InjectionToken<TypeSaveUserData>('SAVE_USER_DATA') - -type TypeSaveUserData = (key: string, value: string) => void - -export const gluActionFavDataset = createAction( - '[glue] favDataset', - props<{dataentry: Partial<IKgDataEntry>}>() -) -export const gluActionUnfavDataset = createAction( - '[glue] favDataset', - props<{dataentry: Partial<IKgDataEntry>}>() -) -export const gluActionToggleDataset = createAction( - '[glue] favDataset', - props<{dataentry: Partial<IKgDataEntry>}>() -) -export const gluActionSetFavDataset = createAction( - '[glue] favDataset', - props<{dataentries: Partial<IKgDataEntry>[]}>() -) @Injectable({ providedIn: 'root' @@ -467,28 +26,3 @@ export class ClickInterceptorService extends RegDeregController<any, boolean>{ // called when the call has not been intercepted } } - -export type _TPLIVal = { - name: string - filename: string - datasetSchema: string - datasetId: string - data: { - 'iav-registered-volumes': { - volumes: { - name: string - source: string - shader: string - transform: any - opacity: string - }[] - } - } - referenceSpaces: { - name: string - fullId: string - }[] - mimetype: 'application/json' -} - -export const _PLI_VOLUME_INJ_TOKEN = new InjectionToken<Observable<_TPLIVal[]>>('_PLI_VOLUME_INJ_TOKEN') diff --git a/src/keyframesModule/service.ts b/src/keyframesModule/service.ts index b807aa15ba8414c823bf48ab132fb01c29c19590..98575c6b3fc1f6de5439791eaf01d0609c580d92 100644 --- a/src/keyframesModule/service.ts +++ b/src/keyframesModule/service.ts @@ -1,11 +1,8 @@ import { Injectable, OnDestroy } from "@angular/core"; -import { MatDialog } from "@angular/material/dialog"; import { Store } from "@ngrx/store"; import { BehaviorSubject, Subscription } from "rxjs"; import { distinctUntilChanged } from "rxjs/operators"; -import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper"; -import { KEYFRAME_VIEWMODE } from "./constants"; -import { KeyFrameCtrlCmp } from "./keyframeCtrl/keyframeCtrl.component"; +import { actions } from "src/state/atlasSelection"; @Injectable() export class KeyFrameService implements OnDestroy { @@ -42,8 +39,10 @@ export class KeyFrameService implements OnDestroy { // TODO enable side bar when ready this.store.dispatch( - viewerStateSetViewerMode({ - payload: flag && KEYFRAME_VIEWMODE + actions.setViewerMode({ + viewerMode: flag + ? "key frame" + : null }) ) }) diff --git a/src/main.module.ts b/src/main.module.ts index ea82fa7eb23dbd27e2037f7f52ad1f3e7bdd43e1..5ed5774ef07f50825dc22e721129e76e524f0c94 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -7,7 +7,7 @@ import { AngularMaterialModule } from 'src/sharedModules' import { AtlasViewer } from "./atlasViewer/atlasViewer.component"; import { ComponentsModule } from "./components/components.module"; import { LayoutModule } from "./layouts/layout.module"; -import { ngViewerState, pluginState, uiState, userConfigState, UserConfigStateUseEffect, viewerConfigState, viewerState } from "./services/stateStore.service"; +import { ngViewerState, uiState, userConfigState, UserConfigStateUseEffect, viewerConfigState } from "./services/stateStore.service"; import { UIModule } from "./ui/ui.module"; import { HttpClientModule } from "@angular/common/http"; @@ -18,29 +18,22 @@ import { WINDOW_MESSAGING_HANDLER_TOKEN } from 'src/messaging/types' import { ConfirmDialogComponent } from "./components/confirmDialog/confirmDialog.component"; import { DialogComponent } from "./components/dialog/dialog.component"; import { DialogService } from "./services/dialogService.service"; -import { UseEffects } from "./services/effect/effect"; -import { LocalFileService } from "./services/localFile.service"; import { NgViewerUseEffect } from "./services/state/ngViewerState.store"; -import { ViewerStateUseEffect } from "./services/state/viewerState.store"; import { UIService } from "./services/uiService.service"; -import { ViewerStateControllerUseEffect } from "src/state"; import { FloatingContainerDirective } from "./util/directives/floatingContainer.directive"; import { FloatingMouseContextualContainerDirective } from "./util/directives/floatingMouseContextualContainer.directive"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR, PureContantService, UtilModule } from "src/util"; import { SpotLightModule } from 'src/spotlight/spot-light.module' import { TryMeComponent } from "./ui/tryme/tryme.component"; import { UiStateUseEffect } from "src/services/state/uiState.store"; -import { PluginServiceUseEffect } from './services/effect/pluginUseEffect'; import { TemplateCoordinatesTransformation } from "src/services/templateCoordinatesTransformation.service"; -import { NewTemplateUseEffect } from './services/effect/newTemplate.effect'; import { WidgetModule } from 'src/widget'; import { PluginModule } from './plugin/plugin.module'; import { LoggingModule } from './logging/logging.module'; import { AuthService } from './auth' import 'src/theme.scss' -import { DatasetPreviewGlue, datasetPreviewMetaReducer, IDatasetPreviewGlue, GlueEffects, ClickInterceptorService, _PLI_VOLUME_INJ_TOKEN } from './glue'; -import { viewerStateHelperReducer, viewerStateMetaReducers, ViewerStateHelperEffect } from './services/state/viewerState.store.helper'; +import { ClickInterceptorService } from './glue'; import { TOS_OBS_INJECTION_TOKEN } from './ui/kgtos'; import { UiEffects } from './services/state/uiState/ui.effects'; import { MesssagingModule } from './messaging/module'; @@ -54,11 +47,19 @@ import { MessagingGlue } from './messagingGlue'; import { BS_ENDPOINT } from './util/constants'; import { QuickTourModule } from './ui/quickTour'; import { of } from 'rxjs'; -import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME, OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN, kgTos, IAV_DATASET_PREVIEW_ACTIVE } from './databrowser.fallback' +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, + annotation, + userInterface, + userInteraction, + plugins, +} from "./state" + export function debug(reducer: ActionReducer<any>): ActionReducer<any> { return function(state, action) { console.log('state', state); @@ -93,31 +94,26 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> { QuickTourModule, EffectsModule.forRoot([ - UseEffects, UserConfigStateUseEffect, - ViewerStateControllerUseEffect, - ViewerStateUseEffect, NgViewerUseEffect, - PluginServiceUseEffect, + plugins.Effects, UiStateUseEffect, - NewTemplateUseEffect, - ViewerStateHelperEffect, - GlueEffects, UiEffects, + atlasSelection.Effect, ]), StoreModule.forRoot({ - pluginState, viewerConfigState, ngViewerState, - viewerState, - viewerStateHelper: viewerStateHelperReducer, uiState, userConfigState, + [atlasSelection.nameSpace]: atlasSelection.reducer, + [userInterface.nameSpace]: userInterface.reducer, + [userInteraction.nameSpace]: userInteraction.reducer, + [annotation.nameSpace]: annotation.reducer, + [plugins.nameSpace]: plugins.reducer, },{ metaReducers: [ // debug, - ...viewerStateMetaReducers, - datasetPreviewMetaReducer, ] }), HttpClientModule, @@ -139,16 +135,10 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> { providers : [ AtlasWorkerService, AuthService, - LocalFileService, DialogService, UIService, TemplateCoordinatesTransformation, ClickInterceptorService, - { - provide: OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN, - useFactory: (glue: IDatasetPreviewGlue) => glue.displayDatasetPreview.bind(glue), - deps: [ DatasetPreviewGlue ] - }, { provide: CANCELLABLE_DIALOG, useFactory: (uiService: UIService) => { @@ -187,23 +177,6 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> { }, deps: [ UIService ] }, - { - provide: _PLI_VOLUME_INJ_TOKEN, - useFactory: (glue: DatasetPreviewGlue) => glue._volumePreview$, - deps: [ DatasetPreviewGlue ] - }, - { - provide: IAV_DATASET_PREVIEW_ACTIVE, - useFactory: (glue: DatasetPreviewGlue) => glue.datasetPreviewDisplayed.bind(glue), - deps: [ DatasetPreviewGlue ] - }, - { - provide: GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME, - useFactory: (glue: DatasetPreviewGlue) => glue.getDatasetPreviewFromId.bind(glue), - deps: [ DatasetPreviewGlue ] - }, - DatasetPreviewGlue, - { provide: TOS_OBS_INJECTION_TOKEN, useValue: of(kgTos) @@ -252,9 +225,6 @@ export class MainModule { constructor( authServce: AuthService, - // bandaid fix: required to init glueService on startup - // TODO figure out why, then init service without this hack - glueService: DatasetPreviewGlue ) { authServce.authReloadState() } diff --git a/src/messaging/types.ts b/src/messaging/types.ts index eda7bc06a35931dd508ead545b9bb7f6df38c174..0e7cc13eba6bba6817222122592a29a0707eaf9d 100644 --- a/src/messaging/types.ts +++ b/src/messaging/types.ts @@ -8,10 +8,10 @@ interface IResourceType { swc: string } -export type TVec4 = [number, number, number, number] -export type TVec3 = [number, number, number] -export type TMat3 = [TVec3, TVec3, TVec3] -export type TMat4 = [TVec4, TVec4, TVec4, TVec4] +export type TVec4 = number[] +export type TVec3 = number[] +export type TMat3 = TVec3[] +export type TMat4 = TVec4[] interface ICommonResParam { transform: TMat4 diff --git a/src/messagingGlue.ts b/src/messagingGlue.ts index 80e459157a717ec90b21f426596e531acbb48843..96bce9f87b0a723b592abc5b8f78a03ffde7ee9d 100644 --- a/src/messagingGlue.ts +++ b/src/messagingGlue.ts @@ -2,9 +2,9 @@ import { Injectable, OnDestroy } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { IMessagingActionTmpl, IWindowMessaging } from "./messaging/types"; import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer } from "./services/state/ngViewerState/actions"; -import { viewerStateSelectAtlas } from "./services/state/viewerState/actions"; -import { viewerStateFetchedAtlasesSelector } from "./services/state/viewerState/selectors"; import { generalActionError } from "./services/stateStore.helper"; +import { atlasSelection } from "src/state" +import { SAPI } from "./atlasComponents/sapi"; @Injectable() export class MessagingGlue implements IWindowMessaging, OnDestroy { @@ -17,14 +17,15 @@ export class MessagingGlue implements IWindowMessaging, OnDestroy { while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() } - constructor(private store: Store<any>){ + constructor( + private store: Store<any>, + sapi: SAPI, + ){ - const sub = this.store.pipe( - select(viewerStateFetchedAtlasesSelector) - ).subscribe((atlases: any[]) => { + const sub = sapi.atlases$.subscribe(atlases => { for (const atlas of atlases) { - const { ['@id']: atlasId, templateSpaces } = atlas - for (const tmpl of templateSpaces) { + const { ['@id']: atlasId, spaces } = atlas + for (const tmpl of spaces) { const { ['@id']: tmplId } = tmpl this.tmplSpIdToAtlasId.set(tmplId, atlasId) } @@ -48,13 +49,9 @@ export class MessagingGlue implements IWindowMessaging, OnDestroy { ) } this.store.dispatch( - viewerStateSelectAtlas({ - atlas: { - ['@id']: atlasId, - template: { - ['@id']: payload['@id'] - } - } + atlasSelection.actions.selectATPById({ + atlasId, + templateId: payload["@id"] }) ) } diff --git a/src/mouseoverModule/mouseOverCvt.pipe.ts b/src/mouseoverModule/mouseOverCvt.pipe.ts index b0a86234237ad87ab895e70575e8dd1964ba5d51..f325feacc32b30c67e39451bac74cff846782185 100644 --- a/src/mouseoverModule/mouseOverCvt.pipe.ts +++ b/src/mouseoverModule/mouseOverCvt.pipe.ts @@ -4,16 +4,14 @@ import { TOnHoverObj } from "./util"; function render<T extends keyof TOnHoverObj>(key: T, value: TOnHoverObj[T]){ if (!value) return [] switch (key) { - case 'segments': { - return (value as TOnHoverObj['segments']).map(seg => { + case 'regions': { + return (value as TOnHoverObj['regions']).map(seg => { return { icon: { fontSet: 'fas', fontIcon: 'fa-brain' }, - text: typeof seg.segment === 'string' - ? seg.segment - : seg.segment.name + text: seg.name } }) } diff --git a/src/mouseoverModule/mouseover.directive.ts b/src/mouseoverModule/mouseover.directive.ts index cc4ae00872d32f4463aeb4d6a9ce7fbdee37ebe3..b05625a943fea38ccf10203ec518138b9b2cdd29 100644 --- a/src/mouseoverModule/mouseover.directive.ts +++ b/src/mouseoverModule/mouseover.directive.ts @@ -3,9 +3,10 @@ import { select, Store } from "@ngrx/store" import { merge, Observable } from "rxjs" import { distinctUntilChanged, map, scan, shareReplay } from "rxjs/operators" import { LoggingService } from "src/logging" -import { uiStateMouseOverLandmarkSelector, uiStateMouseOverSegmentsSelector, uiStateMouseoverUserLandmark } from "src/services/state/uiState/selectors" +import { uiStateMouseOverLandmarkSelector, uiStateMouseoverUserLandmark } from "src/services/state/uiState/selectors" import { TOnHoverObj, temporalPositveScanFn } from "./util" import { ModularUserAnnotationToolService } from "src/atlasComponents/userAnnotations/tools/service"; +import { userInteraction } from "src/state" @Directive({ selector: '[iav-mouse-hover]', @@ -45,7 +46,7 @@ export class MouseHoverDirective { ) const onHoverSegments$ = this.store$.pipe( - select(uiStateMouseOverSegmentsSelector), + select(userInteraction.selectors.mousingOverRegions), // TODO fix aux mesh filtering @@ -59,7 +60,7 @@ export class MouseHoverDirective { // ? arr.filter(({ segment }) => { // // if segment is not a string (i.e., not labelIndexId) return true // if (typeof segment !== 'string') { return true } - // const { labelIndex } = deserialiseParcRegionId(segment) + // const { label: labelIndex } = deserializeSegment(segment) // return parcellationSelected.auxillaryMeshIndices.indexOf(labelIndex) < 0 // }) // : arr), @@ -70,8 +71,8 @@ export class MouseHoverDirective { const mergeObs = merge( onHoverSegments$.pipe( distinctUntilChanged(), - map(segments => { - return { segments } + map(regions => { + return { regions } }), ), onHoverAnnotation$.pipe( @@ -101,7 +102,7 @@ export class MouseHoverDirective { map(arr => { let returnObj = { - segments: null, + regions: null, annotation: null, landmark: null, userLandmark: null, diff --git a/src/mouseoverModule/type.ts b/src/mouseoverModule/type.ts index 07c4312fa7896c53a7f4a8ca58b842e3cd88dc59..38033f903e9047b4164c90137564a19d0f75ae86 100644 --- a/src/mouseoverModule/type.ts +++ b/src/mouseoverModule/type.ts @@ -1,13 +1,5 @@ import { TRegionSummary } from "src/util/siibraApiConstants/types"; -export type TMouseOverSegment = { - layer: { - name: string - } - segmentId: number - segment: TRegionSummary | string // if cannot decode, then segment will be {ngId}#{labelIndex} -} - export type TMouseOverVtkLandmark = { landmarkName: string } \ No newline at end of file diff --git a/src/mouseoverModule/util.ts b/src/mouseoverModule/util.ts index ed4fbe50f42c9580f554e19b6e98678438c1b038..0811db26a592492f396f27f1d61cae1b4c7e2cb6 100644 --- a/src/mouseoverModule/util.ts +++ b/src/mouseoverModule/util.ts @@ -1,8 +1,8 @@ +import { SapiRegionModel } from "src/atlasComponents/sapi" import { IAnnotationGeometry } from "src/atlasComponents/userAnnotations/tools/type" -import { TMouseOverSegment } from "./type" export type TOnHoverObj = { - segments: TMouseOverSegment[] + regions: SapiRegionModel[] annotation: IAnnotationGeometry landmark: { landmarkName: number diff --git a/src/plugin/atlasViewer.pluginService.service.ts b/src/plugin/atlasViewer.pluginService.service.ts index 668027764cf639fe3be327119a12fb4140352f08..3707c087d376b589ff673fe0482bf4a59b300046 100644 --- a/src/plugin/atlasViewer.pluginService.service.ts +++ b/src/plugin/atlasViewer.pluginService.service.ts @@ -1,6 +1,5 @@ import { HttpClient } from '@angular/common/http' import { ComponentFactory, ComponentFactoryResolver, Injectable, ViewContainerRef, Inject, SecurityContext } from "@angular/core"; -import { PLUGINSTORE_ACTION_TYPES } from "src/services/state/pluginState.helper"; import { PluginUnit } from "./pluginUnit/pluginUnit.component"; import { select, Store } from "@ngrx/store"; import { BehaviorSubject, from, merge, Observable, of } from "rxjs"; @@ -14,6 +13,7 @@ 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" const requiresReloadMd = `\n\n***\n\n**warning**: interactive atlas viewer **will** be reloaded in order for the change to take effect.` @@ -306,13 +306,12 @@ export class PluginServices { ? plugin.initStateUrl : null - handler.setInitManifestUrl = (url) => this.store.dispatch({ - type : PLUGINSTORE_ACTION_TYPES.SET_INIT_PLUGIN, - manifest : { - name : plugin.name, - initManifestUrl : url, - }, - }) + handler.setInitManifestUrl = url => this.store.dispatch( + actions.setInitMan({ + nameSpace: plugin.name, + url + }) + ) const shutdownCB = [ () => { diff --git a/src/routerModule/parseRouteToTmplParcReg.ts b/src/routerModule/parseRouteToTmplParcReg.ts index a1ed2a7c47003fbaae169cc49bdd0b7e8be0e3bb..ee4d505a1fa390d55c9aed507998897f896cf203 100644 --- a/src/routerModule/parseRouteToTmplParcReg.ts +++ b/src/routerModule/parseRouteToTmplParcReg.ts @@ -1,5 +1,3 @@ -import { getGetRegionFromLabelIndexId } from 'src/util/fn' -import { serialiseParcellationRegion } from "common/util" import { decodeToNumber, separator } from './cipher' import { @@ -7,6 +5,7 @@ import { TUrlPathObj, } from './type' import { UrlTree } from '@angular/router' +import { serializeSegment } from "src/viewerModule/nehuba/util" export const PARSE_ERROR = { @@ -55,7 +54,7 @@ export function parseSearchParamForTemplateParcellationRegion(obj: TUrlPathObj<s // TODO deprecate. Fallback (defaultNgId) (should) already exist // if (!viewerState.parcellationSelected.updated) throw new Error(PARCELLATION_NOT_UPDATED) - const getRegionFromlabelIndexId = getGetRegionFromLabelIndexId({ parcellation: parcellationSelected }) + /** * either or both parcellationToLoad and .regions maybe empty */ @@ -80,18 +79,19 @@ export function parseSearchParamForTemplateParcellationRegion(obj: TUrlPathObj<s } }).filter(v => !!v) for (const labelIndex of labelIndicies) { - selectRegionIds.push( serialiseParcellationRegion({ ngId, labelIndex }) ) + selectRegionIds.push( serializeSegment(ngId, labelIndex) ) } } - return selectRegionIds - .map(labelIndexId => { - const region = getRegionFromlabelIndexId({ labelIndexId }) - if (!region) { - // cb && cb({ type: ID_ERROR, message: `region with id ${labelIndexId} not found, and will be ignored.` }) - } - return region - }) - .filter(r => !!r) + return [] + // selectRegionIds + // .map(labelIndexId => { + // const region = getRegionFromlabelIndexId({ labelIndexId }) + // if (!region) { + // // cb && cb({ type: ID_ERROR, message: `region with id ${labelIndexId} not found, and will be ignored.` }) + // } + // return region + // }) + // .filter(r => !!r) } catch (e) { /** diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts index a17b554adcc8d430c279d7c241f8f96438ca018d..2837af008e31978422ba494aabdc8223f9d188ba 100644 --- a/src/routerModule/router.service.ts +++ b/src/routerModule/router.service.ts @@ -7,7 +7,7 @@ import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith import { generalApplyState } from "src/services/stateStore.helper"; import { PureContantService } from "src/util"; import { cvtStateToHashedRoutes, cvtFullRouteToState, encodeCustomState, decodeCustomState, verifyCustomState } from "./util"; -import { BehaviorSubject, combineLatest, merge, Observable, of } from 'rxjs' +import { BehaviorSubject, combineLatest, merge, NEVER, Observable, of } from 'rxjs' import { scan } from 'rxjs/operators' @Injectable({ @@ -51,7 +51,8 @@ export class RouterService { navEnd$.subscribe() - const ready$ = pureConstantService.allFetchingReady$.pipe( + // TODO fix + const ready$ = NEVER.pipe( filter(flag => !!flag), take(1), shareReplay(1), diff --git a/src/routerModule/util.ts b/src/routerModule/util.ts index 39ec477aebc60ba1bd201971923bc5c3a6f539c5..127c593470102dcab030df1ca508c22f22e82e30 100644 --- a/src/routerModule/util.ts +++ b/src/routerModule/util.ts @@ -1,12 +1,10 @@ -import { viewerStateGetSelectedAtlas, viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector, viewerStateSelectorNavigation, viewerStateSelectorStandaloneVolumes } from "src/services/state/viewerState/selectors" import { encodeNumber, decodeToNumber, separator, encodeURIFull } from './cipher' import { UrlSegment, UrlTree } from "@angular/router" import { getShader, PMAP_DEFAULT_CONFIG } from "src/util/constants" import { mixNgLayers } from "src/services/state/ngViewerState.store" -import { PLUGINSTORE_CONSTANTS } from 'src/services/state/pluginState.helper' -import { viewerStateHelperStoreName } from "src/services/state/viewerState.store.helper" import { uiStatePreviewingDatasetFilesSelector } from "src/services/state/uiState/selectors" import { Component } from "@angular/core" +import { atlasSelection, plugins } from "src/state" import { TUrlStandaloneVolume, @@ -19,6 +17,7 @@ import { encodeId, } from './parseRouteToTmplParcReg' import { spaceMiscInfoMap } from "src/util/pureConstant.service" +import { getRegionLabelIndex, getParcNgId } from "src/viewerModule/nehuba/config.service" const endcodePath = (key: string, val: string|string[]) => key[0] === '?' @@ -124,7 +123,7 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: (ar if (pluginStates) { try { const arrPluginStates = JSON.parse(pluginStates) - pluginState.initManifests = arrPluginStates.map(url => [PLUGINSTORE_CONSTANTS.INIT_MANIFEST_SRC, url] as [string, string]) + pluginState.initManifests = arrPluginStates.map(url => [plugins.INIT_MANIFEST_SRC, url] as [string, string]) } catch (e) { /** * parsing plugin error @@ -199,33 +198,21 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: (ar /** * parsing template to get atlasId */ - (() => { - - const viewreHelperState = returnState[viewerStateHelperStoreName] || {} - const { templateSelected, parcellationSelected } = returnState['viewerState'] - const { fetchedAtlases, ...rest } = viewreHelperState - - const selectedAtlas = (fetchedAtlases || []).find(a => a['templateSpaces'].find(t => t['@id'] === (templateSelected && templateSelected['@id']))) - - const overlayLayer = selectedAtlas && selectedAtlas['parcellations'].find(p => p['@id'] === (parcellationSelected && parcellationSelected['@id'])) - - viewreHelperState['selectedAtlasId'] = selectedAtlas && selectedAtlas['@id'] - viewreHelperState['overlayingAdditionalParcellations'] = (overlayLayer && !overlayLayer['baseLayer']) - ? [ overlayLayer ] - : [] - })() - + // TODO return returnState } export const cvtStateToHashedRoutes = (state): string => { // TODO check if this causes memleak - const selectedAtlas = viewerStateGetSelectedAtlas(state) - const selectedTemplate = viewerStateSelectedTemplateSelector(state) - const selectedParcellation = viewerStateSelectedParcellationSelector(state) - const selectedRegions = viewerStateSelectedRegionsSelector(state) - const standaloneVolumes = viewerStateSelectorStandaloneVolumes(state) - const navigation = viewerStateSelectorNavigation(state) + const { + atlas: selectedAtlas, + parcellation: selectedParcellation, + template: selectedTemplate, + } = atlasSelection.selectors.selectedATP(state) + + const selectedRegions = atlasSelection.selectors.selectedRegions(state) + const standaloneVolumes = atlasSelection.selectors.standaloneVolumes(state) + const navigation = atlasSelection.selectors.navigation(state) const previewingDatasetFiles = uiStatePreviewingDatasetFilesSelector(state) let dsPrvString: string @@ -258,10 +245,12 @@ export const cvtStateToHashedRoutes = (state): string => { } // encoding selected regions - let selectedRegionsString + let selectedRegionsString: string if (selectedRegions.length === 1) { const region = selectedRegions[0] - const { ngId, labelIndex } = region + const labelIndex = getRegionLabelIndex(selectedAtlas, selectedTemplate, selectedParcellation, region) + + const ngId = getParcNgId(selectedAtlas, selectedTemplate, selectedParcellation, region) selectedRegionsString = `${ngId}::${encodeNumber(labelIndex, { float: false })}` } let routes: any diff --git a/src/services/effect/effect.spec.ts b/src/services/effect/effect.spec.ts deleted file mode 100644 index 906e23daf3f5d76b601306fa10fff91e6c912611..0000000000000000000000000000000000000000 --- a/src/services/effect/effect.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {} from 'jasmine' -import { UseEffects } from './effect' -import { TestBed } from '@angular/core/testing' -import { Observable } from 'rxjs' -import { SELECT_PARCELLATION, SELECT_REGIONS } from '../state/viewerState.store' -import { provideMockActions } from '@ngrx/effects/testing' -import { hot } from 'jasmine-marbles' -import { provideMockStore } from '@ngrx/store/testing' -import { defaultRootState } from '../stateStore.service' -import { viewerStateNewViewer } from '../state/viewerState/actions' - -describe('effect.ts', () => { - - describe('UseEffects', () => { - let actions$:Observable<any> - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - UseEffects, - provideMockActions(() => actions$), - provideMockStore({ initialState: defaultRootState }) - ] - }) - }) - - it('both SELECT_PARCELLATION and viewerStateNewViewer.type actions should trigger onParcellationSelected$', () => { - const useEffectsInstance: UseEffects = TestBed.inject(UseEffects) - actions$ = hot( - 'ab', - { - a: { type: SELECT_PARCELLATION }, - b: { type: viewerStateNewViewer.type } - } - ) - expect( - useEffectsInstance.onParcChange$ - ).toBeObservable( - hot( - 'aa', - { - a: { type: SELECT_REGIONS, selectRegions: [] } - } - ) - ) - }) - }) -}) diff --git a/src/services/effect/effect.ts b/src/services/effect/effect.ts deleted file mode 100644 index 10e5506143f0963ddaea23b81bd3e09509c8623b..0000000000000000000000000000000000000000 --- a/src/services/effect/effect.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { Injectable, OnDestroy } from "@angular/core"; -import { Actions, Effect, ofType } from "@ngrx/effects"; -import { select, Store } from "@ngrx/store"; -import { merge, Observable, Subscription, combineLatest } from "rxjs"; -import { filter, map, shareReplay, switchMap, take, withLatestFrom, mapTo, distinctUntilChanged } from "rxjs/operators"; -import { LoggingService } from "src/logging"; -import { IavRootStoreInterface, recursiveFindRegionWithLabelIndexId } from '../stateStore.service'; -import { viewerStateNewViewer, viewerStateSelectAtlas, viewerStateSetSelectedRegionsWithIds, viewerStateToggleLayer } from "../state/viewerState.store.helper"; -import { deserialiseParcRegionId, serialiseParcellationRegion } from "common/util" -import { getGetRegionFromLabelIndexId } from 'src/util/fn' -import { actionAddToRegionsSelectionWithIds, actionSelectLandmarks, viewerStateSelectParcellation, viewerStateSelectRegionWithIdDeprecated, viewerStateSetSelectedRegions } from "../state/viewerState/actions"; - -@Injectable({ - providedIn: 'root', -}) -export class UseEffects implements OnDestroy { - - @Effect() - setRegionsSelected$: Observable<any> - - constructor( - private actions$: Actions, - private store$: Store<IavRootStoreInterface>, - private log: LoggingService, - ) { - - this.regionsSelected$ = this.store$.pipe( - select('viewerState'), - select('regionsSelected'), - shareReplay(1), - ) - - this.setRegionsSelected$ = combineLatest( - this.actions$.pipe( - ofType(viewerStateSetSelectedRegionsWithIds), - map(action => { - const { selectRegionIds } = action - return selectRegionIds - }) - ), - this.store$.pipe( - select('viewerState'), - select('parcellationSelected'), - filter(v => !!v), - distinctUntilChanged() - ), - ).pipe( - map(([ids, parcellation]) => { - const getRegionFromlabelIndexId = getGetRegionFromLabelIndexId({ parcellation }) - const selectRegions = !!ids && Array.isArray(ids) - ? ids.map(id => getRegionFromlabelIndexId({ labelIndexId: id })).filter(v => !!v) - : [] - /** - * only allow 1 selection at a time - */ - return viewerStateSetSelectedRegions({ - selectRegions: selectRegions.slice(0,1) - }) - }) - ) - - this.onDeselectRegionsWithId$ = this.actions$.pipe( - ofType(ACTION_TYPES.DESELECT_REGIONS_WITH_ID), - map(action => { - const { deselecRegionIds } = action as any - return deselecRegionIds - }), - withLatestFrom(this.regionsSelected$), - map(([ deselecRegionIds, alreadySelectedRegions ]) => { - const deselectSet = new Set(deselecRegionIds) - return viewerStateSetSelectedRegions({ - selectRegions: alreadySelectedRegions - .filter(({ ngId, labelIndex }) => !deselectSet.has(serialiseParcellationRegion({ ngId, labelIndex }))), - }) - }), - ) - - this.addToSelectedRegions$ = this.actions$.pipe( - ofType(actionAddToRegionsSelectionWithIds.type), - map(action => { - const { selectRegionIds } = action - return selectRegionIds - }), - switchMap(selectRegionIds => this.updatedParcellation$.pipe( - filter(p => !!p), - take(1), - map(p => [selectRegionIds, p]), - )), - map(this.convertRegionIdsToRegion), - withLatestFrom(this.regionsSelected$), - map(([ selectedRegions, alreadySelectedRegions ]) => { - return viewerStateSetSelectedRegions({ - selectRegions: this.removeDuplicatedRegions(selectedRegions, alreadySelectedRegions), - }) - }), - ) - } - - private regionsSelected$: Observable<any[]> - - public ngOnDestroy() { - while (this.subscriptions.length > 0) { - this.subscriptions.pop().unsubscribe() - } - } - - private subscriptions: Subscription[] = [] - - private parcellationSelected$ = this.actions$.pipe( - ofType(viewerStateSelectParcellation.type), - ) - - - private updatedParcellation$ = this.store$.pipe( - select('viewerState'), - select('parcellationSelected'), - map(p => p.updated ? p : null), - shareReplay(1), - ) - - @Effect() - public onDeselectRegionsWithId$: Observable<any> - - private convertRegionIdsToRegion = ([selectRegionIds, parcellation]) => { - const { ngId: defaultNgId } = parcellation - return (selectRegionIds as any[]) - .map(labelIndexId => deserialiseParcRegionId(labelIndexId)) - .map(({ ngId, labelIndex }) => { - return { - labelIndexId: serialiseParcellationRegion({ - ngId: ngId || defaultNgId, - labelIndex, - }), - } - }) - .map(({ labelIndexId }) => { - return recursiveFindRegionWithLabelIndexId({ - regions: parcellation.regions, - labelIndexId, - inheritedNgId: defaultNgId, - }) - }) - .filter(v => { - if (!v) { - this.log.log(`SELECT_REGIONS_WITH_ID, some ids cannot be parsed intto label index`) - } - return !!v - }) - } - - private removeDuplicatedRegions = (...args) => { - const set = new Set() - const returnArr = [] - for (const regions of args) { - for (const region of regions) { - if (!set.has(region.name)) { - returnArr.push(region) - set.add(region.name) - } - } - } - return returnArr - } - - @Effect() - public addToSelectedRegions$: Observable<any> - - /** - * for backwards compatibility. - * older versions of atlas viewer may only have labelIndex as region identifier - */ - @Effect() - public onSelectRegionWithId = this.actions$.pipe( - ofType(viewerStateSelectRegionWithIdDeprecated.type), - map(action => { - const { selectRegionIds } = action - return selectRegionIds - }), - switchMap(selectRegionIds => this.updatedParcellation$.pipe( - filter(p => !!p), - take(1), - map(parcellation => [selectRegionIds, parcellation]), - )), - map(this.convertRegionIdsToRegion), - map(selectRegions => { - return viewerStateSetSelectedRegions({ - selectRegions - }) - }), - ) - - /** - * side effect of selecting a parcellation means deselecting all regions - */ - @Effect() - public onParcChange$ = merge( - this.actions$.pipe( - ofType(viewerStateToggleLayer.type) - ), - this.parcellationSelected$, - this.actions$.pipe( - ofType(viewerStateNewViewer.type) - ), - this.actions$.pipe( - ofType(viewerStateSelectAtlas.type) - ) - ).pipe( - mapTo( - viewerStateSetSelectedRegions({ - selectRegions: [] - }) - ) - ) - - /** - * side effects of loading a new template space - * Landmarks will no longer be accurate (differente template space) - */ - - @Effect() - public onNewViewerResetLandmarkSelected$ = this.actions$.pipe( - ofType(viewerStateNewViewer.type), - mapTo( - actionSelectLandmarks({ - landmarks: [] - }) - ) - ) -} - -export const compareRegions: (r1: any, r2: any) => boolean = (r1, r2) => { - if (!r1) { return !r2 } - if (!r2) { return !r1 } - return r1.ngId === r2.ngId - && r1.labelIndex === r2.labelIndex - && r1.name === r2.name -} - -const ACTION_TYPES = { - DESELECT_REGIONS_WITH_ID: 'DESELECT_REGIONS_WITH_ID', -} - -export const VIEWER_STATE_ACTION_TYPES = ACTION_TYPES diff --git a/src/services/effect/newTemplate.effect.ts b/src/services/effect/newTemplate.effect.ts deleted file mode 100644 index f2c2541fe6fee218be482ad7f67b0b84443a8928..0000000000000000000000000000000000000000 --- a/src/services/effect/newTemplate.effect.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable } from "@angular/core"; -import { Actions, Effect, ofType } from "@ngrx/effects"; -import { Observable } from "rxjs"; -import { mapTo } from "rxjs/operators"; -import { DATASETS_ACTIONS_TYPES } from "../state/dataStore.store"; -import { viewerStateNewViewer } from "../state/viewerState/actions"; - -@Injectable({ - providedIn: 'root' -}) - -export class NewTemplateUseEffect{ - - @Effect() - public onNewTemplateShouldClearPreviewDataset$: Observable<any> - - constructor( - private actions$: Actions - ){ - this.onNewTemplateShouldClearPreviewDataset$ = this.actions$.pipe( - ofType(viewerStateNewViewer.type), - mapTo({ - type: DATASETS_ACTIONS_TYPES.CLEAR_PREVIEW_DATASETS - }) - ) - } -} diff --git a/src/services/effect/pluginUseEffect.ts b/src/services/effect/pluginUseEffect.ts deleted file mode 100644 index bd0a74772c60d75cceda439cb5402df584c4e2af..0000000000000000000000000000000000000000 --- a/src/services/effect/pluginUseEffect.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Injectable } from "@angular/core" -import { Effect } from "@ngrx/effects" -import { select, Store } from "@ngrx/store" -import { Observable, forkJoin } from "rxjs" -import { filter, map, startWith, switchMap } from "rxjs/operators" -import { PluginServices } from "src/plugin/atlasViewer.pluginService.service" -import { PLUGINSTORE_CONSTANTS, PLUGINSTORE_ACTION_TYPES, pluginStateSelectorInitManifests } from 'src/services/state/pluginState.helper' -import { HttpClient } from "@angular/common/http" -import { getHttpHeader } from "src/util/constants" - -@Injectable({ - providedIn: 'root', -}) - -export class PluginServiceUseEffect { - - @Effect() - public initManifests$: Observable<any> - - constructor( - store$: Store<any>, - pluginService: PluginServices, - http: HttpClient - ) { - this.initManifests$ = store$.pipe( - select(pluginStateSelectorInitManifests), - startWith([]), - map(arr => { - // only launch plugins that has init manifest src label on it - return arr.filter(([ source ]) => source === PLUGINSTORE_CONSTANTS.INIT_MANIFEST_SRC) - }), - filter(arr => arr.length > 0), - switchMap(arr => forkJoin( - arr.map(([_source, url]) => - http.get(url, { - headers: getHttpHeader(), - responseType: 'json' - }) - ) - )), - map((jsons: any[]) => { - for (const json of jsons){ - pluginService.launchNewWidget(json) - } - - // clear init manifest - return { - type: PLUGINSTORE_ACTION_TYPES.CLEAR_INIT_PLUGIN, - } - }), - ) - } -} diff --git a/src/services/localFile.service.ts b/src/services/localFile.service.ts deleted file mode 100644 index 045ac686b54f7b65e2985f7d75c2bce85ce705c6..0000000000000000000000000000000000000000 --- a/src/services/localFile.service.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Injectable } from "@angular/core"; -import { Store } from "@ngrx/store"; -import { SNACKBAR_MESSAGE } from "./state/uiState.store"; -import { IavRootStoreInterface } from "./stateStore.service"; -import { DATASETS_ACTIONS_TYPES } from "./state/dataStore.store"; - -/** - * experimental service handling local user files such as nifti and gifti - */ - -@Injectable({ - providedIn: 'root', -}) - -export class LocalFileService { - public SUPPORTED_EXT = SUPPORTED_EXT - private supportedExtSet = new Set(SUPPORTED_EXT) - - constructor( - private store: Store<IavRootStoreInterface>, - ) { - - } - - private niiUrl: string - - public handleFileDrop(files: File[]) { - try { - this.validateDrop(files) - for (const file of files) { - const ext = this.getExtension(file.name) - switch (ext) { - case NII: { - this.handleNiiFile(file) - break; - } - default: - throw new Error(`File ${file.name} does not have a file handler`) - } - } - } catch (e) { - this.store.dispatch({ - type: SNACKBAR_MESSAGE, - snackbarMessage: `Opening local NIFTI error: ${e.toString()}`, - }) - } - } - - private getExtension(filename: string) { - const match = /(\.\w*?)$/i.exec(filename) - return (match && match[1]) || '' - } - - private validateDrop(files: File[]) { - if (files.length !== 1) { - throw new Error('Interactive atlas viewer currently only supports drag and drop of one file at a time') - } - for (const file of files) { - const ext = this.getExtension(file.name) - if (!this.supportedExtSet.has(ext)) { - throw new Error(`File ${file.name}${ext === '' ? ' ' : (' with extension ' + ext)} cannot be loaded. The supported extensions are: ${this.SUPPORTED_EXT.join(', ')}`) - } - } - } - - private handleNiiFile(file: File) { - - if (this.niiUrl) { - URL.revokeObjectURL(this.niiUrl) - } - this.niiUrl = URL.createObjectURL(file) - - this.store.dispatch({ - type: DATASETS_ACTIONS_TYPES.PREVIEW_DATASET, - payload: { - file: { - mimetype: 'application/json', - url: this.niiUrl - } - } - }) - - this.showLocalWarning() - } - - private showLocalWarning() { - this.store.dispatch({ - type: SNACKBAR_MESSAGE, - snackbarMessage: `Warning: sharing URL will not share the loaded local file`, - }) - } -} - -const NII = `.nii` -const GII = '.gii' - -const SUPPORTED_EXT = [ - NII, - GII, -] diff --git a/src/services/state/dataState/actions.ts b/src/services/state/dataState/actions.ts deleted file mode 100644 index 34af72ff7364bb0c575eb3cbf21aae9a739a9890..0000000000000000000000000000000000000000 --- a/src/services/state/dataState/actions.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createAction, props } from "@ngrx/store"; -import { IKgDataEntry } from "src/databrowser.fallback"; - -export const datastateActionToggleFav = createAction( - `[datastate] toggleFav`, - props<{payload: { fullId: string }}>() -) - -export const datastateActionUpdateFavDataset = createAction( - `[datastate] updateFav`, - props<{ favDataEntries: any[] }>() -) - -export const datastateActionUnfavDataset = createAction( - `[datastate] unFav`, - props<{ payload: { fullId: string } }>() -) - -export const datastateActionFavDataset = createAction( - `[datastate] fav`, - props<{ payload: { fullId: string } }>() -) - -export const datastateActionFetchedDataentries = createAction( - `[datastate] fetchedDatastate`, - props<{ fetchedDataEntries: IKgDataEntry[] }>() -) \ No newline at end of file diff --git a/src/services/state/dataStore.store.ts b/src/services/state/dataStore.store.ts deleted file mode 100644 index 962168dd111b4219e69c6f595f5ccf596df2fc95..0000000000000000000000000000000000000000 --- a/src/services/state/dataStore.store.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * TODO move to databrowser module - */ - -import { Action } from '@ngrx/store' -import { LOCAL_STORAGE_CONST } from 'src/util/constants' -import { datastateActionFetchedDataentries, datastateActionUpdateFavDataset } from './dataState/actions' -import { IHasId } from 'src/util/interfaces' -import { generalApplyState } from '../stateStore.helper' - -/** - * TODO merge with databrowser.usereffect.ts - */ - -export interface DatasetPreview { - datasetId: string - filename: string -} - -export interface IStateInterface { - fetchedDataEntries: IDataEntry[] - favDataEntries: Partial<IDataEntry>[] - fetchedSpatialData: IDataEntry[] -} - -// TODO deprecate -export const defaultState = { - fetchedDataEntries: [], - favDataEntries: (() => { - try { - const saved = localStorage.getItem(LOCAL_STORAGE_CONST.FAV_DATASET) - const arr = JSON.parse(saved) as any[] - return arr.every(item => item && !!item.fullId) - ? arr - : [] - } catch (e) { - // TODO propagate error - return [] - } - })(), - fetchedSpatialData: [], -} - -export const getStateStore = ({ state: state = defaultState } = {}) => (prevState: IStateInterface = state, action: Partial<IActionInterface>) => { - - switch (action.type) { - case datastateActionFetchedDataentries.type: - case FETCHED_DATAENTRIES: { - return { - ...prevState, - fetchedDataEntries : action.fetchedDataEntries, - } - } - case FETCHED_SPATIAL_DATA: { - return { - ...prevState, - fetchedSpatialData : action.fetchedDataEntries, - } - } - case datastateActionUpdateFavDataset.type: { - const { favDataEntries = [] } = action - return { - ...prevState, - favDataEntries, - } - } - case generalApplyState.type: { - const { dataStore } = (action as any).state - return dataStore - } - default: return prevState - } -} - -// must export a named function for aot compilation -// see https://github.com/angular/angular/issues/15587 -// https://github.com/amcdnl/ngrx-actions/issues/23 -// or just google for: -// -// angular function expressions are not supported in decorators - -const defaultStateStore = getStateStore() - -export function stateStore(state, action) { - return defaultStateStore(state, action) -} - -export interface IActionInterface extends Action { - favDataEntries: IDataEntry[] - fetchedDataEntries: IDataEntry[] - fetchedSpatialData: IDataEntry[] - payload?: any -} - -export const FETCHED_DATAENTRIES = 'FETCHED_DATAENTRIES' -export const FETCHED_SPATIAL_DATA = `FETCHED_SPATIAL_DATA` - -// TODO deprecate in favour of src/ui/datamodule/constants.ts - -export interface IActivity { - methods: string[] - preparation: string[] - protocols: string[] -} - -export interface IDataEntry { - activity: IActivity[] - name: string - description: string - license: string[] - licenseInfo: string[] - parcellationRegion: IParcellationRegion[] - formats: string[] - custodians: string[] - contributors: string[] - referenceSpaces: IReferenceSpace[] - files: File[] - publications: IPublication[] - embargoStatus: IHasId[] - - methods: string[] - protocols: string[] - - preview?: boolean - - /** - * TODO typo, should be kgReferences - */ - kgReference: string[] - - id: string - fullId: string -} - -export interface IParcellationRegion { - id?: string - name: string -} - -export interface IReferenceSpace { - name: string -} - -export interface IPublication { - name: string - doi: string - cite: string -} - -export interface IProperty { - description: string - publications: IPublication[] -} - -export interface ILandmark { - type: string // e.g. sEEG recording site, etc - name: string - templateSpace: string // possibily inherited from LandmarkBundle (?) - geometry: IPointLandmarkGeometry | IPlaneLandmarkGeometry | IOtherLandmarkGeometry - properties: IProperty - files: File[] -} - -export interface IDataStateInterface { - fetchedDataEntries: IDataEntry[] - - /** - * Map that maps parcellation name to a Map, which maps datasetname to Property Object - */ - fetchedMetadataMap: Map<string, Map<string, {properties: IProperty}>> -} - -export interface IPointLandmarkGeometry extends ILandmarkGeometry { - position: [number, number, number] -} - -export interface IPlaneLandmarkGeometry extends ILandmarkGeometry { - // corners have to be CW or CCW (no zigzag) - corners: [[number, number, number], [number, number, number], [number, number, number], [number, number, number]] -} - -export interface IOtherLandmarkGeometry extends ILandmarkGeometry { - vertices: Array<[number, number, number]> - meshIdx: Array<[number, number, number]> -} - -interface ILandmarkGeometry { - type: 'point' | 'plane' - space?: 'voxel' | 'real' -} - -export interface IFile { - name: string - absolutePath: string - byteSize: number - contentType: string -} - -export interface ViewerPreviewFile { - name: string - filename: string - mimetype: string - referenceSpaces: { - name: string - fullId: string - }[] - url?: string - data?: any - position?: any -} - -export interface IFileSupplementData { - data: any -} - -const ACTION_TYPES = { - PREVIEW_DATASET: 'PREVIEW_DATASET', - CLEAR_PREVIEW_DATASET: 'CLEAR_PREVIEW_DATASET', - CLEAR_PREVIEW_DATASETS: 'CLEAR_PREVIEW_DATASETS' -} - -export const DATASETS_ACTIONS_TYPES = ACTION_TYPES diff --git a/src/services/state/ngViewerState.store.ts b/src/services/state/ngViewerState.store.ts index 5b1758d60906076840930cafbf878ab834e7e88d..a27495313235764aceeabb3b8b43bb526b4f0726 100644 --- a/src/services/state/ngViewerState.store.ts +++ b/src/services/state/ngViewerState.store.ts @@ -14,7 +14,6 @@ import { generalApplyState } from '../stateStore.helper'; import { ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from './ngViewerState/selectors'; import { uiActionSnackbarMessage } from './uiState/actions'; import { TUserRouteError } from 'src/auth/auth.service'; -import { viewerStateSelectedTemplateSelector } from './viewerState.store.helper'; export function mixNgLayers(oldLayers: INgLayerInterface[], newLayers: INgLayerInterface|INgLayerInterface[]): INgLayerInterface[] { if (newLayers instanceof Array) { @@ -173,9 +172,6 @@ export class NgViewerUseEffect implements OnDestroy { @Effect() public cycleViews$: Observable<any> - @Effect() - public removeAllNonBaseLayers$: Observable<any> - private panelOrder$: Observable<string> private panelMode$: Observable<string> @@ -325,62 +321,6 @@ export class NgViewerUseEffect implements OnDestroy { snackbarMessage: CYCLE_PANEL_MESSAGE })), ) - - /** - * simplify with layer browser - */ - const baseNgLayerName$ = this.store$.pipe( - select(viewerStateSelectedTemplateSelector), - - map(templateSelected => { - if (!templateSelected) { return [] } - - const { ngId , otherNgIds = []} = templateSelected - - return [ - ngId, - ...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)), - ) - - const allLoadedNgLayers$ = this.store$.pipe( - select('viewerState'), - select('loadedNgLayers'), - ) - - this.removeAllNonBaseLayers$ = this.actions.pipe( - ofType(ACTION_TYPES.REMOVE_ALL_NONBASE_LAYERS), - withLatestFrom( - combineLatest( - baseNgLayerName$, - allLoadedNgLayers$, - ), - ), - map(([_, [baseNgLayerNames, loadedNgLayers] ]) => { - const baseNameSet = new Set(baseNgLayerNames) - return loadedNgLayers.filter(l => !baseNameSet.has(l.name)) - }), - map(layer => { - return ngViewerActionRemoveNgLayer({ - layer - }) - }), - ) } public ngOnDestroy() { @@ -392,11 +332,6 @@ export class NgViewerUseEffect implements OnDestroy { export { INgLayerInterface } -const ACTION_TYPES = { - - REMOVE_ALL_NONBASE_LAYERS: `REMOVE_ALL_NONBASE_LAYERS`, -} - export const SUPPORTED_PANEL_MODES = [ PANELS.FOUR_PANEL, PANELS.H_ONE_THREE, @@ -404,4 +339,3 @@ export const SUPPORTED_PANEL_MODES = [ PANELS.SINGLE_PANEL, ] -export const NG_VIEWER_ACTION_TYPES = ACTION_TYPES diff --git a/src/services/state/pluginState.helper.ts b/src/services/state/pluginState.helper.ts deleted file mode 100644 index e1c48c1941ab716f1bacc6c8980f62c2367a933a..0000000000000000000000000000000000000000 --- a/src/services/state/pluginState.helper.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createSelector } from "@ngrx/store" - -export const PLUGINSTORE_ACTION_TYPES = { - SET_INIT_PLUGIN: `SET_INIT_PLUGIN`, - CLEAR_INIT_PLUGIN: 'CLEAR_INIT_PLUGIN', -} - -export const pluginStateSelectorInitManifests = createSelector( - state => state['pluginState'], - pluginState => pluginState.initManifests -) - -export const PLUGINSTORE_CONSTANTS = { - INIT_MANIFEST_SRC: 'INIT_MANIFEST_SRC', -} diff --git a/src/services/state/pluginState.store.ts b/src/services/state/pluginState.store.ts deleted file mode 100644 index 85bfa20a915ec8ab2e11497321cd66d95edabb91..0000000000000000000000000000000000000000 --- a/src/services/state/pluginState.store.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Action } from '@ngrx/store' -import { generalApplyState } from '../stateStore.helper' -import { PLUGINSTORE_ACTION_TYPES, PLUGINSTORE_CONSTANTS } from './pluginState.helper' -export const defaultState: StateInterface = { - initManifests: [] -} - -export interface StateInterface { - initManifests: Array<[ string, string|null ]> -} - -export interface ActionInterface extends Action { - manifest: { - name: string - initManifestUrl?: string - } -} - - -export const getStateStore = ({ state = defaultState } = {}) => (prevState: StateInterface = state, action: ActionInterface): StateInterface => { - switch (action.type) { - case PLUGINSTORE_ACTION_TYPES.SET_INIT_PLUGIN: { - const newMap = new Map(prevState.initManifests ) - - // reserved source label for init manifest - if (action.manifest.name !== PLUGINSTORE_CONSTANTS.INIT_MANIFEST_SRC) { newMap.set(action.manifest.name, action.manifest.initManifestUrl) } - return { - ...prevState, - initManifests: Array.from(newMap), - } - } - case PLUGINSTORE_ACTION_TYPES.CLEAR_INIT_PLUGIN: { - const { initManifests } = prevState - const newManifests = initManifests.filter(([source]) => source !== PLUGINSTORE_CONSTANTS.INIT_MANIFEST_SRC) - return { - ...prevState, - initManifests: newManifests, - } - } - case generalApplyState.type: { - const { pluginState } = (action as any).state - return pluginState - } - default: return prevState - } -} - -// must export a named function for aot compilation -// see https://github.com/angular/angular/issues/15587 -// https://github.com/amcdnl/ngrx-actions/issues/23 -// or just google for: -// -// angular function expressions are not supported in decorators - -const defaultStateStore = getStateStore() - -export function stateStore(state, action) { - return defaultStateStore(state, action) -} diff --git a/src/services/state/uiState.store.helper.ts b/src/services/state/uiState.store.helper.ts index 6f1c187c15b94170fcdc5db6b5ba3d0664d6da75..2a89567a3896ec1cf986f2e070837b970eebed41 100644 --- a/src/services/state/uiState.store.helper.ts +++ b/src/services/state/uiState.store.helper.ts @@ -15,7 +15,6 @@ export { export { uiStatePreviewingDatasetFilesSelector, - uiStateMouseOverSegmentsSelector, uiStateMouseoverUserLandmark, } from './uiState/selectors' diff --git a/src/services/state/uiState.store.ts b/src/services/state/uiState.store.ts index cab51af1edb88fdbcf360fb5e5a3628e9809dc51..1df46f1de6ef7a7ee6bc223b2b8b488ec31de727 100644 --- a/src/services/state/uiState.store.ts +++ b/src/services/state/uiState.store.ts @@ -88,6 +88,7 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: IUiS ...prevState, sidePanelIsOpen: true, } + // src/state/userInteraction/actions.closeSidePanel case uiStateCloseSidePanel.type: case CLOSE_SIDE_PANEL: return { @@ -174,14 +175,6 @@ export class UiStateUseEffect implements OnDestroy{ private subscriptions: Subscription[] = [] - private numRegionSelectedWithHistory$: Observable<any[]> - - @Effect() - public sidePanelOpen$: Observable<any> - - @Effect() - public viewCurrentOpen$: Observable<any> - private bottomSheetRef: MatBottomSheetRef constructor( @@ -189,27 +182,6 @@ export class UiStateUseEffect implements OnDestroy{ actions$: Actions, bottomSheet: MatBottomSheet ) { - this.numRegionSelectedWithHistory$ = store$.pipe( - select('viewerState'), - select('regionsSelected'), - map(arr => arr.length), - startWith(0), - scan((acc, curr) => [curr, ...acc], []), - ) - - this.sidePanelOpen$ = this.numRegionSelectedWithHistory$.pipe( - filter(([curr, prev]) => prev === 0 && curr > 0), - mapTo({ - type: OPEN_SIDE_PANEL, - }), - ) - - this.viewCurrentOpen$ = this.numRegionSelectedWithHistory$.pipe( - filter(([curr, prev]) => prev === 0 && curr > 0), - mapTo({ - type: EXPAND_SIDE_PANEL_CURRENT_VIEW, - }), - ) this.subscriptions.push( actions$.pipe( diff --git a/src/services/state/uiState/selectors.spec.ts b/src/services/state/uiState/selectors.spec.ts index 2464c9c8a1a4770a1e1c14afd28cb7c3dd4a3833..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/src/services/state/uiState/selectors.spec.ts +++ b/src/services/state/uiState/selectors.spec.ts @@ -1,7 +0,0 @@ -import { uiStateMouseOverSegmentsSelector } from './selectors' - -describe('> uiState/selectors.ts', () => { - describe('> mouseOverSegments', () => { - - }) -}) diff --git a/src/services/state/uiState/selectors.ts b/src/services/state/uiState/selectors.ts index 483539b2d784f567564bc2963233b8dd91dabcb1..9d48fadedf36d09e6292a08bfce5d13cee787f8c 100644 --- a/src/services/state/uiState/selectors.ts +++ b/src/services/state/uiState/selectors.ts @@ -1,5 +1,4 @@ import { createSelector } from "@ngrx/store"; -import { TMouseOverSegment } from "src/mouseoverModule/type"; import { IUiState } from './common' export const uiStatePreviewingDatasetFilesSelector = createSelector( @@ -7,11 +6,6 @@ export const uiStatePreviewingDatasetFilesSelector = createSelector( (uiState: IUiState) => uiState['previewingDatasetFiles'] ) -export const uiStateMouseOverSegmentsSelector = createSelector( - state => state['uiState'], - uiState => uiState['mouseOverSegments'] as TMouseOverSegment[] -) - export const uiStateMouseOverLandmarkSelector = createSelector( state => state['uiState'], uiState => uiState['mouseOverLandmark'] as string diff --git a/src/services/state/userConfigState.store.ts b/src/services/state/userConfigState.store.ts index 81fef3205c4020651d6169012d43c19da67ec0f9..4c869cd8f9ee3a752e5572752f910882b1bad319 100644 --- a/src/services/state/userConfigState.store.ts +++ b/src/services/state/userConfigState.store.ts @@ -1,17 +1,13 @@ import { Injectable, OnDestroy } from "@angular/core"; import { Actions, Effect, ofType } from "@ngrx/effects"; import { Action, createAction, createReducer, props, select, Store, on, createSelector } from "@ngrx/store"; -import { combineLatest, from, Observable, of, Subscription } from "rxjs"; -import { catchError, distinctUntilChanged, filter, map, mapTo, share, shareReplay, switchMap, take, withLatestFrom } from "rxjs/operators"; -import { BACKENDURL, LOCAL_STORAGE_CONST } from "src/util//constants"; -import { DialogService } from "../dialogService.service"; -import { recursiveFindRegionWithLabelIndexId } from "src/util/fn"; -import { serialiseParcellationRegion } from 'common/util' +import { of, Subscription } from "rxjs"; +import { catchError, filter, map } from "rxjs/operators"; +import { LOCAL_STORAGE_CONST } from "src/util//constants"; // Get around the problem of importing duplicated string (ACTION_TYPES), even using ES6 alias seems to trip up the compiler // TODO file bug and reverse import { HttpClient } from "@angular/common/http"; -import { actionSetMobileUi, viewerStateNewViewer, viewerStateSelectParcellation, viewerStateSetSelectedRegions } from "./viewerState/actions"; -import { viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector } from "./viewerState/selectors"; +import { actionSetMobileUi } from "./viewerState/actions"; import { PureContantService } from "src/util"; interface ICsp{ @@ -62,11 +58,6 @@ export const defaultState: StateInterface = { pluginCsp: {} } -export const actionUpdateRegionSelections = createAction( - `[userConfig] updateRegionSelections`, - props<{ config: { savedRegionsSelection: RegionSelection[]} }>() -) - export const selectorAllPluginsCspPermission = createSelector( (state: any) => state.userConfigState, userConfigState => userConfigState.pluginCsp @@ -82,24 +73,12 @@ export const actionUpdatePluginCsp = createAction( ) export const ACTION_TYPES = { - UPDATE_REGIONS_SELECTIONS: actionUpdateRegionSelections.type, UPDATE_REGIONS_SELECTION: 'UPDATE_REGIONS_SELECTION', - SAVE_REGIONS_SELECTION: `SAVE_REGIONS_SELECTIONN`, - DELETE_REGIONS_SELECTION: 'DELETE_REGIONS_SELECTION', - - LOAD_REGIONS_SELECTION: 'LOAD_REGIONS_SELECTION', } export const userConfigReducer = createReducer( defaultState, - on(actionUpdateRegionSelections, (state, { config }) => { - const { savedRegionsSelection } = config - return { - ...state, - savedRegionsSelection - } - }), on(actionUpdatePluginCsp, (state, { payload }) => { return { ...state, @@ -118,187 +97,9 @@ export class UserConfigStateUseEffect implements OnDestroy { constructor( private actions$: Actions, private store$: Store<any>, - private dialogService: DialogService, private http: HttpClient, private constantSvc: PureContantService, ) { - const viewerState$ = this.store$.pipe( - select('viewerState'), - shareReplay(1), - ) - - this.parcellationSelected$ = this.store$.pipe( - select(viewerStateSelectedParcellationSelector), - distinctUntilChanged(), - ) - - this.tprSelected$ = combineLatest( - this.store$.pipe( - select(viewerStateSelectedTemplateSelector), - distinctUntilChanged(), - ), - this.parcellationSelected$, - this.store$.pipe( - select(viewerStateSelectedRegionsSelector) - /** - * TODO - * distinct selectedRegions - */ - ), - ).pipe( - map(([ templateSelected, parcellationSelected, regionsSelected ]) => { - return { - templateSelected, parcellationSelected, regionsSelected, - } - }), - ) - - this.savedRegionsSelections$ = this.store$.pipe( - select('userConfigState'), - select('savedRegionsSelection'), - shareReplay(1), - ) - - this.onSaveRegionsSelection$ = this.actions$.pipe( - ofType(ACTION_TYPES.SAVE_REGIONS_SELECTION), - withLatestFrom(this.tprSelected$), - withLatestFrom(this.savedRegionsSelections$), - - map(([[action, tprSelected], savedRegionsSelection]) => { - const { payload = {} } = action as UserConfigAction - const { name = 'Untitled' } = payload - - const { templateSelected, parcellationSelected, regionsSelected } = tprSelected - const newSavedRegionSelection: RegionSelection = { - id: Date.now().toString(), - name, - templateSelected, - parcellationSelected, - regionsSelected, - } - return { - type: ACTION_TYPES.UPDATE_REGIONS_SELECTIONS, - config: { - savedRegionsSelection: savedRegionsSelection.concat([newSavedRegionSelection]), - }, - } as UserConfigAction - }), - ) - - this.onDeleteRegionsSelection$ = this.actions$.pipe( - ofType(ACTION_TYPES.DELETE_REGIONS_SELECTION), - withLatestFrom(this.savedRegionsSelections$), - map(([ action, savedRegionsSelection ]) => { - const { payload = {} } = action as UserConfigAction - const { id } = payload - return { - type: ACTION_TYPES.UPDATE_REGIONS_SELECTIONS, - config: { - savedRegionsSelection: savedRegionsSelection.filter(srs => srs.id !== id), - }, - } - }), - ) - - this.onUpdateRegionsSelection$ = this.actions$.pipe( - ofType(ACTION_TYPES.UPDATE_REGIONS_SELECTION), - withLatestFrom(this.savedRegionsSelections$), - map(([ action, savedRegionsSelection]) => { - const { payload = {} } = action as UserConfigAction - const { id, ...rest } = payload - return { - type: ACTION_TYPES.UPDATE_REGIONS_SELECTIONS, - config: { - savedRegionsSelection: savedRegionsSelection - .map(srs => srs.id === id - ? { ...srs, ...rest } - : { ...srs }), - }, - } - }), - ) - - this.subscriptions.push( - this.actions$.pipe( - ofType(ACTION_TYPES.LOAD_REGIONS_SELECTION), - map(action => { - const { payload = {}} = action as UserConfigAction - const { savedRegionsSelection }: {savedRegionsSelection: RegionSelection} = payload - return savedRegionsSelection - }), - filter(val => !!val), - withLatestFrom(this.tprSelected$), - switchMap(([savedRegionsSelection, { parcellationSelected, templateSelected, regionsSelected }]) => - from(this.dialogService.getUserConfirm({ - title: `Load region selection: ${savedRegionsSelection.name}`, - message: `This action would cause the viewer to navigate away from the current view. Proceed?`, - })).pipe( - catchError((e, obs) => of(null)), - map(() => { - return { - savedRegionsSelection, - parcellationSelected, - templateSelected, - regionsSelected, - } - }), - filter(val => !!val), - ), - ), - switchMap(({ savedRegionsSelection, parcellationSelected, templateSelected, regionsSelected }) => { - if (templateSelected.name !== savedRegionsSelection.templateSelected.name ) { - /** - * template different, dispatch viewerStateNewViewer.type - */ - this.store$.dispatch( - viewerStateNewViewer({ - selectParcellation: savedRegionsSelection.parcellationSelected, - selectTemplate: savedRegionsSelection.templateSelected, - }) - ) - return this.parcellationSelected$.pipe( - filter(p => p.updated), - take(1), - map(() => { - return { - regionsSelected: savedRegionsSelection.regionsSelected, - } - }), - ) - } - - if (parcellationSelected.name !== savedRegionsSelection.parcellationSelected.name) { - /** - * parcellation different, dispatch SELECT_PARCELLATION - */ - this.store$.dispatch( - viewerStateSelectParcellation({ - selectParcellation: savedRegionsSelection.parcellationSelected, - }) - ) - return this.parcellationSelected$.pipe( - filter(p => p.updated), - take(1), - map(() => { - return { - regionsSelected: savedRegionsSelection.regionsSelected, - } - }), - ) - } - - return of({ - regionsSelected: savedRegionsSelection.regionsSelected, - }) - }), - ).subscribe(({ regionsSelected }) => { - this.store$.dispatch( - viewerStateSetSelectedRegions({ - selectRegions: regionsSelected, - }) - ) - }), - ) this.subscriptions.push( this.store$.pipe( @@ -328,61 +129,6 @@ export class UserConfigStateUseEffect implements OnDestroy { }), ) - this.subscriptions.push( - this.actions$.pipe( - ofType(ACTION_TYPES.UPDATE_REGIONS_SELECTIONS), - ).subscribe(action => { - const { config = {} } = action as UserConfigAction - const { savedRegionsSelection } = config - const simpleSRSs = savedRegionsSelection.map(({ id, name, templateSelected, parcellationSelected, regionsSelected }) => { - return { - id, - name, - tName: templateSelected.name, - pName: parcellationSelected.name, - rSelected: regionsSelected.map(({ ngId, labelIndex }) => serialiseParcellationRegion({ ngId, labelIndex })), - } as SimpleRegionSelection - }) - - /** - * TODO save server side on per user basis - */ - window.localStorage.setItem(LOCAL_STORAGE_CONST.SAVED_REGION_SELECTIONS, JSON.stringify(simpleSRSs)) - }), - ) - - const savedSRSsString = window.localStorage.getItem(LOCAL_STORAGE_CONST.SAVED_REGION_SELECTIONS) - const savedSRSs: SimpleRegionSelection[] = savedSRSsString && JSON.parse(savedSRSsString) - - this.restoreSRSsFromStorage$ = viewerState$.pipe( - filter(() => !!savedSRSs), - select('fetchedTemplates'), - distinctUntilChanged(), - map(fetchedTemplates => savedSRSs.map(({ id, name, tName, pName, rSelected }) => { - const templateSelected = fetchedTemplates.find(t => t.name === tName) - const parcellationSelected = templateSelected && templateSelected.parcellations.find(p => p.name === pName) - const regionsSelected = parcellationSelected && rSelected.map(labelIndexId => recursiveFindRegionWithLabelIndexId({ - regions: parcellationSelected.regions, - labelIndexId, - inheritedNgId: parcellationSelected.ngId - })) - return { - templateSelected, - parcellationSelected, - id, - name, - regionsSelected, - } as RegionSelection - })), - filter(restoredSavedRegions => restoredSavedRegions.every(rs => rs.regionsSelected && rs.regionsSelected.every(r => !!r))), - take(1), - map(savedRegionsSelection => { - return { - type: ACTION_TYPES.UPDATE_REGIONS_SELECTIONS, - config: { savedRegionsSelection }, - } - }), - ) } public ngOnDestroy() { @@ -391,24 +137,6 @@ export class UserConfigStateUseEffect implements OnDestroy { } } - /** - * Temmplate Parcellation Regions selected - */ - private tprSelected$: Observable<{templateSelected: any, parcellationSelected: any, regionsSelected: any[]}> - private savedRegionsSelections$: Observable<any[]> - private parcellationSelected$: Observable<any> - - @Effect() - public onSaveRegionsSelection$: Observable<any> - - @Effect() - public onDeleteRegionsSelection$: Observable<any> - - @Effect() - public onUpdateRegionsSelection$: Observable<any> - - @Effect() - public restoreSRSsFromStorage$: Observable<any> @Effect() public setInitPluginPermission$ = this.http.get(`${this.constantSvc.backendUrl}user/pluginPermissions`, { diff --git a/src/services/state/viewerState.store.helper.spec.ts b/src/services/state/viewerState.store.helper.spec.ts deleted file mode 100644 index fa0845554d221ea0051658ba8d096145913be883..0000000000000000000000000000000000000000 --- a/src/services/state/viewerState.store.helper.spec.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { TestBed } from "@angular/core/testing" -import { Action } from "@ngrx/store" -import { provideMockActions } from "@ngrx/effects/testing" -import { MockStore, provideMockStore } from "@ngrx/store/testing" -import { Observable, of } from "rxjs" -import { isNewerThan, ViewerStateHelperEffect } from "./viewerState.store.helper" -import { viewerStateGetSelectedAtlas, viewerStateSelectedTemplateSelector } from "./viewerState/selectors" -import { viewerStateHelperSelectParcellationWithId, viewerStateRemoveAdditionalLayer } from "./viewerState/actions" -import { generalActionError } from "../stateStore.helper" -import { hot } from "jasmine-marbles" - -describe('> viewerState.store.helper.ts', () => { - const tmplId = 'test-tmpl-id' - const tmplId0 = 'test-tmpl-id-0' - describe('> ViewerStateHelperEffect', () => { - let effect: ViewerStateHelperEffect - let mockStore: MockStore - let actions$: Observable<Action> - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - ViewerStateHelperEffect, - provideMockStore(), - provideMockActions(() => actions$) - ] - }) - - mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, { - ['@id']: tmplId - }) - - actions$ = of( - viewerStateRemoveAdditionalLayer({ - payload: { - ['@id']: 'bla' - } - }) - ) - }) - - describe('> if selected atlas has no matching tmpl space', () => { - beforeEach(() => { - mockStore.overrideSelector(viewerStateGetSelectedAtlas, { - templateSpaces: [{ - ['@id']: tmplId0 - }] - }) - }) - it('> should emit gernal error', () => { - effect = TestBed.inject(ViewerStateHelperEffect) - effect.onRemoveAdditionalLayer$.subscribe(val => { - expect(val.type === generalActionError.type) - }) - }) - }) - - describe('> if selected atlas has matching tmpl', () => { - - const parcId0 = 'test-parc-id-0' - const parcId1 = 'test-parc-id-1' - const tmpSp = { - ['@id']: tmplId, - availableIn: [{ - ['@id']: parcId0 - }], - } - beforeEach(() => { - mockStore.overrideSelector(viewerStateGetSelectedAtlas, { - templateSpaces: [ - tmpSp - ], - parcellations: [], - }) - }) - - describe('> if parc is empty array', () => { - it('> should emit with falsy as payload', () => { - effect = TestBed.inject(ViewerStateHelperEffect) - expect( - effect.onRemoveAdditionalLayer$ - ).toBeObservable( - hot('(a|)', { - a: viewerStateHelperSelectParcellationWithId({ - payload: undefined - }) - }) - ) - }) - }) - describe('> if no parc has eligible @id', () => { - beforeEach(() => { - mockStore.overrideSelector(viewerStateGetSelectedAtlas, { - templateSpaces: [ - tmpSp - ], - parcellations: [{ - ['@id']: parcId1 - }] - }) - }) - it('> should emit with falsy as payload', () => { - effect = TestBed.inject(ViewerStateHelperEffect) - expect( - effect.onRemoveAdditionalLayer$ - ).toBeObservable( - hot('(a|)', { - a: viewerStateHelperSelectParcellationWithId({ - payload: undefined - }) - }) - ) - }) - }) - - describe('> if some parc has eligible @id', () => { - describe('> if no @version is available', () => { - const parc1 = { - ['@id']: parcId0, - name: 'p0-0', - baseLayer: true - } - const parc2 = { - ['@id']: parcId0, - name: 'p0-1', - baseLayer: true - } - beforeEach(() => { - - mockStore.overrideSelector(viewerStateGetSelectedAtlas, { - templateSpaces: [ - tmpSp - ], - parcellations: [ - parc1, - parc2 - ] - }) - }) - it('> selects the first parc', () => { - - effect = TestBed.inject(ViewerStateHelperEffect) - expect( - effect.onRemoveAdditionalLayer$ - ).toBeObservable( - hot('(a|)', { - a: viewerStateHelperSelectParcellationWithId({ - payload: parc1 - }) - }) - ) - }) - }) - - describe('> if @version is available', () => { - - describe('> if there exist an entry without @next attribute', () => { - - const parc1 = { - ['@id']: parcId0, - name: 'p0-0', - baseLayer: true, - ['@version']: { - ['@next']: 'random-value' - } - } - const parc2 = { - ['@id']: parcId0, - name: 'p0-1', - baseLayer: true, - ['@version']: { - ['@next']: null - } - } - beforeEach(() => { - - mockStore.overrideSelector(viewerStateGetSelectedAtlas, { - templateSpaces: [ - tmpSp - ], - parcellations: [ - parc1, - parc2 - ] - }) - }) - it('> selects the first one without @next attribute', () => { - - effect = TestBed.inject(ViewerStateHelperEffect) - expect( - effect.onRemoveAdditionalLayer$ - ).toBeObservable( - hot('(a|)', { - a: viewerStateHelperSelectParcellationWithId({ - payload: parc2 - }) - }) - ) - }) - }) - describe('> if there exist no entry without @next attribute', () => { - - const parc1 = { - ['@id']: parcId0, - name: 'p0-0', - baseLayer: true, - ['@version']: { - ['@next']: 'random-value' - } - } - const parc2 = { - ['@id']: parcId0, - name: 'p0-1', - baseLayer: true, - ['@version']: { - ['@next']: 'another-random-value' - } - } - beforeEach(() => { - - mockStore.overrideSelector(viewerStateGetSelectedAtlas, { - templateSpaces: [ - tmpSp - ], - parcellations: [ - parc1, - parc2 - ] - }) - }) - it('> selects the first one without @next attribute', () => { - - effect = TestBed.inject(ViewerStateHelperEffect) - expect( - effect.onRemoveAdditionalLayer$ - ).toBeObservable( - hot('(a|)', { - a: viewerStateHelperSelectParcellationWithId({ - payload: parc1 - }) - }) - ) - }) - }) - }) - }) - }) - }) - - describe('> isNewerThan', () => { - describe('> ill formed versions', () => { - it('> in circular references, throws', () => { - - const parc0Circular = { - - "@version": { - "@next": "aaa-bbb", - "@this": "ccc-ddd", - "name": "", - "@previous": null, - } - } - const parc1Circular = { - - "@version": { - "@next": "ccc-ddd", - "@this": "aaa-bbb", - "name": "", - "@previous": null, - } - } - const p2 = { - ["@id"]: "foo-bar" - } - const p3 = { - ["@id"]: "baz" - } - expect(() => { - isNewerThan([parc0Circular, parc1Circular], p2, p3) - }).toThrow() - }) - - it('> if not found, will throw', () => { - - const parc0Circular = { - - "@version": { - "@next": "aaa-bbb", - "@this": "ccc-ddd", - "name": "", - "@previous": null, - } - } - const parc1Circular = { - - "@version": { - "@next": null, - "@this": "aaa-bbb", - "name": "", - "@previous": null, - } - } - const p2 = { - ["@id"]: "foo-bar" - } - const p3 = { - ["@id"]: "baz" - } - expect(() => { - isNewerThan([parc0Circular, parc1Circular], p2, p3) - }).toThrow() - }) - }) - - it('> works on well formed versions', () => { - - const parc0 = { - "@version": { - "@next": null, - "@this": "aaa-bbb", - "name": "", - "@previous": "ccc-ddd", - } - } - const parc1 = { - "@version": { - "@next": "aaa-bbb", - "@this": "ccc-ddd", - "name": "", - "@previous": null, - } - } - - const p0 = { - ['@id']: 'aaa-bbb' - } - const p1 = { - ['@id']: 'ccc-ddd' - } - expect( - isNewerThan([parc0, parc1], p0, p1) - ).toBeTrue() - }) - - }) -}) \ No newline at end of file diff --git a/src/services/state/viewerState.store.helper.ts b/src/services/state/viewerState.store.helper.ts deleted file mode 100644 index bf5f69fb89b4e9dbed57fd4066927a0254758944..0000000000000000000000000000000000000000 --- a/src/services/state/viewerState.store.helper.ts +++ /dev/null @@ -1,205 +0,0 @@ -// TODO merge with viewerstate.store.ts when refactor is done -import { createReducer, on, ActionReducer, Store, select } from "@ngrx/store"; -import { generalActionError, generalApplyState } from "../stateStore.helper"; -import { Effect, Actions, ofType } from "@ngrx/effects"; -import { Observable } from "rxjs"; -import { withLatestFrom, map } from "rxjs/operators"; -import { Injectable } from "@angular/core"; - -import { - viewerStateNewViewer, - viewerStateHelperSelectParcellationWithId, - viewerStateNavigateToRegion, - viewerStateRemoveAdditionalLayer, - viewerStateSelectAtlas, - viewerStateSelectParcellation, - viewerStateSelectTemplateWithId, - viewerStateSetConnectivityRegion, - viewerStateNehubaLayerchanged, - viewerStateSetFetchedAtlases, - viewerStateSetSelectedRegions, - viewerStateSetSelectedRegionsWithIds, - viewerStateToggleLayer, - viewerStateToggleRegionSelect, - viewerStateSelectRegionWithIdDeprecated, - viewerStateSetViewerMode, - viewerStateDblClickOnViewer, - viewerStateAddUserLandmarks, - viewreStateRemoveUserLandmarks, - viewerStateMouseOverCustomLandmark, - viewerStateMouseOverCustomLandmarkInPerspectiveView, - viewerStateSelectTemplateWithName, -} from './viewerState/actions' - -export { - viewerStateNewViewer, - viewerStateHelperSelectParcellationWithId, - viewerStateNavigateToRegion, - viewerStateRemoveAdditionalLayer, - viewerStateSelectAtlas, - viewerStateSelectParcellation, - viewerStateSelectTemplateWithId, - viewerStateSetConnectivityRegion, - viewerStateNehubaLayerchanged, - viewerStateSetFetchedAtlases, - viewerStateSetSelectedRegions, - viewerStateSetSelectedRegionsWithIds, - viewerStateToggleLayer, - viewerStateToggleRegionSelect, - viewerStateSelectRegionWithIdDeprecated, - viewerStateSetViewerMode, - viewerStateDblClickOnViewer, - viewerStateAddUserLandmarks, - viewreStateRemoveUserLandmarks, - viewerStateMouseOverCustomLandmark, - viewerStateMouseOverCustomLandmarkInPerspectiveView, - viewerStateSelectTemplateWithName, -} - -import { - viewerStateSelectedRegionsSelector, - viewerStateSelectedTemplateSelector, - viewerStateSelectedParcellationSelector, - viewerStateGetSelectedAtlas, - viewerStateCustomLandmarkSelector, - viewerStateFetchedTemplatesSelector, - viewerStateNavigationStateSelector, -} from './viewerState/selectors' -import { IHasId } from "src/util/interfaces"; - -export { - viewerStateSelectedRegionsSelector, - viewerStateSelectedTemplateSelector, - viewerStateSelectedParcellationSelector, - viewerStateCustomLandmarkSelector, - viewerStateFetchedTemplatesSelector, - viewerStateNavigationStateSelector, -} - -interface IViewerStateHelperStore{ - fetchedAtlases: any[] - selectedAtlasId: string - overlayingAdditionalParcellations: any[] -} - -const initialState: IViewerStateHelperStore = { - fetchedAtlases: [], - selectedAtlasId: null, - overlayingAdditionalParcellations: [] -} - -function handleToggleLayerAction(reducer: ActionReducer<any>): ActionReducer<any>{ - return function(state, action) { - switch(action.type){ - case viewerStateToggleLayer.type: { - const { payload } = action as any - const { templateSelected } = (state && state['viewerState']) || {} - - const selectParcellation = templateSelected?.parcellations.find(p => p['@id'] === payload['@id']) - return reducer(state, viewerStateSelectParcellation({ selectParcellation })) - } - default: reducer(state, action) - } - return reducer(state, action) - } -} - -export const viewerStateMetaReducers = [ - handleToggleLayerAction -] - -@Injectable({ - providedIn: 'root' -}) - -export class ViewerStateHelperEffect{ - @Effect() - onRemoveAdditionalLayer$: Observable<any> = this.actions$.pipe( - ofType(viewerStateRemoveAdditionalLayer.type), - withLatestFrom( - this.store$.pipe( - select(viewerStateGetSelectedAtlas) - ), - this.store$.pipe( - select(viewerStateSelectedTemplateSelector) - ) - ), - map(([ { payload }, selectedAtlas, selectedTemplate ]) => { - const tmpl = selectedAtlas['templateSpaces'].find(t => t['@id'] === selectedTemplate['@id']) - if (!tmpl) { - return generalActionError({ - message: `templateSpace with id ${selectedTemplate['@id']} cannot be found in atlas with id ${selectedAtlas['@id']}` - }) - } - - const eligibleParcIdSet = new Set( - tmpl.availableIn.map(p => p['@id']) - ) - const baseLayers = selectedAtlas['parcellations'].filter(fullP => fullP['baseLayer'] && eligibleParcIdSet.has(fullP['@id'])) - const baseLayer = baseLayers.find(layer => !!layer['@version'] && !layer['@version']['@next']) || baseLayers[0] - return viewerStateHelperSelectParcellationWithId({ payload: baseLayer }) - }) - ) - - constructor( - private store$: Store<any>, - private actions$: Actions - ){ - - } -} - -export const viewerStateHelperReducer = createReducer( - initialState, - on(viewerStateSetFetchedAtlases, (state, { fetchedAtlases }) => ({ ...state, fetchedAtlases })), - on(viewerStateSelectAtlas, (state, { atlas }) => ({ ...state, selectedAtlasId: atlas['@id'] })), - on(generalApplyState, (_prevState, { state }) => ({ ...state[viewerStateHelperStoreName] })), -) - -export const viewerStateHelperStoreName = 'viewerStateHelper' - -export const defaultState = initialState - -interface IVersion{ - "@next": string - "@this": string - "name": string - "@previous": string -} - -interface IHasVersion{ - ['@version']: IVersion -} - -export function isNewerThan(arr: IHasVersion[], srcObj: IHasId, compObj: IHasId): boolean { - - function* GenNewerVersions(flag){ - let it = 0 - const newest = arr.find((v => v['@version'] && v['@version']['@this'] === srcObj['@id'])) - if (!newest) throw new Error(`GenNewerVersions error newest element isn't found`) - yield newest - let currPreviousId = newest['@version'][ flag ? '@next' : '@previous' ] - while (currPreviousId) { - it += 1 - if (it>100) throw new Error(`iteration excced 100, did you include a loop?`) - - const curr = arr.find(v => v['@version']['@this'] === currPreviousId) - if (!curr) throw new Error(`GenNewerVersions error, version id ${currPreviousId} not found`) - currPreviousId = curr['@version'][ flag ? '@next' : '@previous' ] - yield curr - } - } - for (const obj of GenNewerVersions(true)) { - if (obj['@version']['@this'] === compObj['@id']) { - return false - } - } - - for (const obj of GenNewerVersions(false)) { - if (obj['@version']['@this'] === compObj['@id']) { - return true - } - } - - throw new Error(`isNewerThan error, neither srcObj nor compObj exist in array`) -} diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts deleted file mode 100644 index d4433a0db42c514a3d3ea64191cf7ee8404742bb..0000000000000000000000000000000000000000 --- a/src/services/state/viewerState.store.ts +++ /dev/null @@ -1,417 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Actions, Effect, ofType } from '@ngrx/effects'; -import { Action, select, Store } from '@ngrx/store' -import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, shareReplay, startWith, withLatestFrom, mapTo } from 'rxjs/operators'; -import { IUserLandmark } from 'src/atlasViewer/atlasViewer.apiService.service'; -import { INgLayerInterface } from 'src/atlasViewer/atlasViewer.component'; -import { getViewer } from 'src/util/fn'; -import { LoggingService } from 'src/logging'; -import { IavRootStoreInterface } from '../stateStore.service'; -import { GENERAL_ACTION_TYPES } from '../stateStore.service' -import { CLOSE_SIDE_PANEL } from './uiState.store'; -import { - viewerStateSetSelectedRegions, - viewerStateSetConnectivityRegion, - viewerStateSelectParcellation, - viewerStateSelectRegionWithIdDeprecated, - viewerStateCustomLandmarkSelector, - viewerStateDblClickOnViewer, - viewerStateAddUserLandmarks, - viewreStateRemoveUserLandmarks, - viewerStateMouseOverCustomLandmark, - viewerStateMouseOverCustomLandmarkInPerspectiveView, - viewerStateNewViewer -} from './viewerState.store.helper'; -import { cvtNehubaConfigToNavigationObj } from 'src/state'; -import { - viewerStateChangeNavigation, - viewerStateNehubaLayerchanged, - viewerStateSetViewerMode, - actionSelectLandmarks, - actionViewerStateSelectFeature -} from './viewerState/actions'; -import { serialiseParcellationRegion } from "common/util" -import { IViewerState, defaultViewerState } from "./viewerState/type" - -export { IViewerState, defaultViewerState } - -export interface ActionInterface extends Action { - fetchedTemplate?: any[] - - selectTemplate?: any - selectParcellation?: any - selectRegions?: any[] - selectRegionIds: string[] - deselectRegions?: any[] - - updatedParcellation?: any - - landmarks: IUserLandmark[] - deselectLandmarks: IUserLandmark[] - - navigation?: any - - payload: any - - connectivityRegion?: string -} - - -export const getStateStore = ({ state = defaultViewerState } = {}) => (prevState: Partial<IViewerState> = state, action: ActionInterface) => { - switch (action.type) { - case CLEAR_STANDALONE_VOLUMES: - return { - ...prevState, - standaloneVolumes: [] - } - case viewerStateNewViewer.type: { - - const { - selectParcellation: parcellation, - navigation, - selectTemplate, - } = action - const navigationFromTemplateSelected = cvtNehubaConfigToNavigationObj(selectTemplate?.nehubaConfig?.dataset?.initialNgState) - return { - ...prevState, - templateSelected : selectTemplate, - parcellationSelected : parcellation, - // taken care of by effect.ts - // regionsSelected : [], - - // taken care of by effect.ts - // landmarksSelected : [], - navigation : navigation || navigationFromTemplateSelected, - } - } - case FETCHED_TEMPLATE : { - return { - ...prevState, - fetchedTemplates: prevState.fetchedTemplates.concat(action.fetchedTemplate), - } - } - case viewerStateChangeNavigation.type: - case CHANGE_NAVIGATION : { - return { - ...prevState, - navigation : action.navigation, - } - } - case viewerStateSelectParcellation.type: - case SELECT_PARCELLATION : { - const { selectParcellation } = action - return { - ...prevState, - parcellationSelected: selectParcellation, - // taken care of by effect.ts - // regionsSelected: [] - } - } - case viewerStateSetSelectedRegions.type: - case SELECT_REGIONS: { - const { selectRegions } = action - return { - ...prevState, - regionsSelected: selectRegions, - } - } - case viewerStateSetViewerMode.type: { - return { - ...prevState, - viewerMode: action.payload - } - } - case DESELECT_LANDMARKS : { - return { - ...prevState, - landmarksSelected : prevState.landmarksSelected.filter(lm => action.deselectLandmarks.findIndex(dLm => dLm.name === lm.name) < 0), - } - } - case actionSelectLandmarks.type: { - return { - ...prevState, - landmarksSelected : action.landmarks, - } - } - case USER_LANDMARKS : { - return { - ...prevState, - userLandmarks: action.landmarks, - } - } - /** - * TODO - * duplicated with ngViewerState.layers ? - */ - case viewerStateNehubaLayerchanged.type: { - const viewer = getViewer() - if (!viewer) { - return { - ...prevState, - loadedNgLayers: [], - } - } else { - return { - ...prevState, - loadedNgLayers: (viewer.layerManager.managedLayers as any[]).map(obj => ({ - name : obj.name, - type : obj.initialSpecification.type, - source : obj.sourceUrl, - visible : obj.visible, - }) as INgLayerInterface), - } - } - } - case GENERAL_ACTION_TYPES.APPLY_STATE: { - const { viewerState } = (action as any).state - return viewerState - } - case viewerStateSetConnectivityRegion.type: - case SET_CONNECTIVITY_REGION: - return { - ...prevState, - connectivityRegion: action.connectivityRegion, - } - case CLEAR_CONNECTIVITY_REGION: - return { - ...prevState, - connectivityRegion: '', - } - case SET_OVERWRITTEN_COLOR_MAP: - return { - ...prevState, - overwrittenColorMap: action.payload || '', - } - case actionViewerStateSelectFeature.type: - return { - ...prevState, - featureSelected: (action as any).feature - } - default : - return prevState - } -} - -// must export a named function for aot compilation -// see https://github.com/angular/angular/issues/15587 -// https://github.com/amcdnl/ngrx-actions/issues/23 -// or just google for: -// -// angular function expressions are not supported in decorators - -const defaultStateStore = getStateStore() - -export function stateStore(state, action) { - return defaultStateStore(state, action) -} - -export const FETCHED_TEMPLATE = 'FETCHED_TEMPLATE' -export const CHANGE_NAVIGATION = viewerStateChangeNavigation.type - -export const SELECT_PARCELLATION = viewerStateSelectParcellation.type - -export const DESELECT_REGIONS = `DESELECT_REGIONS` -export const SELECT_REGIONS_WITH_ID = viewerStateSelectRegionWithIdDeprecated.type -// export const SET_VIEWER_MODE = viewerStateSetViewerMode.type -export const SELECT_LANDMARKS = `SELECT_LANDMARKS` -export const SELECT_REGIONS = viewerStateSetSelectedRegions.type -export const DESELECT_LANDMARKS = `DESELECT_LANDMARKS` -export const USER_LANDMARKS = `USER_LANDMARKS` - -export const SET_CONNECTIVITY_REGION = `SET_CONNECTIVITY_REGION` -export const CLEAR_CONNECTIVITY_REGION = `CLEAR_CONNECTIVITY_REGION` -export const SET_OVERWRITTEN_COLOR_MAP = `SET_OVERWRITTEN_COLOR_MAP` -export const CLEAR_STANDALONE_VOLUMES = `CLEAR_STANDALONE_VOLUMES` - -@Injectable({ - providedIn: 'root', -}) - -export class ViewerStateUseEffect { - constructor( - private actions$: Actions, - private store$: Store<IavRootStoreInterface>, - private log: LoggingService, - ) { - - const viewerState$ = this.store$.pipe( - select('viewerState'), - shareReplay(1) - ) - this.currentLandmarks$ = this.store$.pipe( - select(viewerStateCustomLandmarkSelector), - shareReplay(1), - ) - - this.removeUserLandmarks = this.actions$.pipe( - ofType(ACTION_TYPES.REMOVE_USER_LANDMARKS), - withLatestFrom(this.currentLandmarks$), - map(([action, currentLandmarks]) => { - const { landmarkIds } = (action as ActionInterface).payload - for ( const rmId of landmarkIds ) { - const idx = currentLandmarks.findIndex(({ id }) => id === rmId) - if (idx < 0) { this.log.warn(`remove userlandmark with id ${rmId} does not exist`) } - } - const removeSet = new Set(landmarkIds) - return { - type: USER_LANDMARKS, - landmarks: currentLandmarks.filter(({ id }) => !removeSet.has(id)), - } - }), - ) - - this.addUserLandmarks$ = this.actions$.pipe( - ofType(viewerStateAddUserLandmarks.type), - withLatestFrom(this.currentLandmarks$), - map(([action, currentLandmarks]) => { - const { landmarks } = action as ActionInterface - const landmarkMap = new Map() - for (const landmark of currentLandmarks) { - const { id } = landmark - landmarkMap.set(id, landmark) - } - for (const landmark of landmarks) { - const { id } = landmark - if (landmarkMap.has(id)) { - this.log.warn(`Attempting to add a landmark that already exists, id: ${id}`) - } else { - landmarkMap.set(id, landmark) - } - } - const userLandmarks = Array.from(landmarkMap).map(([_id, landmark]) => landmark) - return { - type: USER_LANDMARKS, - landmarks: userLandmarks, - } - }), - ) - - this.mouseoverUserLandmarks = this.actions$.pipe( - ofType(viewerStateMouseOverCustomLandmarkInPerspectiveView.type), - withLatestFrom(this.currentLandmarks$), - map(([ action, currentLandmarks ]) => { - const { payload } = action as any - const { label } = payload - if (!label) { - return viewerStateMouseOverCustomLandmark({ - payload: { - userLandmark: null - } - }) - } - - const idx = Number(label.replace('label=', '')) - if (isNaN(idx)) { - this.log.warn(`Landmark index could not be parsed as a number: ${idx}`) - return viewerStateMouseOverCustomLandmark({ - payload: { userLandmark: null } - }) - } - return viewerStateMouseOverCustomLandmark({ - payload: { - userLandmark: currentLandmarks[idx] - } - }) - }), - - ) - - const doubleClickOnViewer$ = this.actions$.pipe( - ofType(ACTION_TYPES.DOUBLE_CLICK_ON_VIEWER), - map(action => { - const { payload } = action as any - const { segments, landmark, userLandmark } = payload - return { segments, landmark, userLandmark } - }), - shareReplay(1), - ) - - this.doubleClickOnViewerToggleRegions$ = doubleClickOnViewer$.pipe( - filter(({ segments }) => segments && segments.length > 0), - withLatestFrom(viewerState$.pipe( - select('regionsSelected'), - distinctUntilChanged(), - startWith([]), - )), - map(([{ segments }, regionsSelected]) => { - const selectedSet = new Set<string>(regionsSelected.map(serialiseParcellationRegion)) - const toggleArr = segments.map(({ segment, layer }) => serialiseParcellationRegion({ ngId: layer.name, ...segment })) - - const deleteFlag = toggleArr.some(id => selectedSet.has(id)) - - for (const id of toggleArr) { - if (deleteFlag) { selectedSet.delete(id) } else { selectedSet.add(id) } - } - - return viewerStateSelectRegionWithIdDeprecated({ - selectRegionIds: [...selectedSet], - }) - }), - ) - - this.doubleClickOnViewerToggleLandmark$ = doubleClickOnViewer$.pipe( - filter(({ landmark }) => !!landmark), - withLatestFrom(viewerState$.pipe( - select('landmarksSelected'), - startWith([]), - )), - map(([{ landmark }, selectedSpatialDatas]) => { - - const selectedIdx = selectedSpatialDatas.findIndex(data => data.name === landmark.name) - - const newSelectedSpatialDatas = selectedIdx >= 0 - ? selectedSpatialDatas.filter((_, idx) => idx !== selectedIdx) - : selectedSpatialDatas.concat(landmark) - - return actionSelectLandmarks({ - landmarks: newSelectedSpatialDatas, - }) - }), - ) - - this.doubleClickOnViewerToogleUserLandmark$ = doubleClickOnViewer$.pipe( - filter(({ userLandmark }) => userLandmark), - ) - - this.onStandAloneVolumesExistCloseMatDrawer$ = viewerState$.pipe( - select('standaloneVolumes'), - filter(v => v && Array.isArray(v) && v.length > 0), - mapTo({ - type: CLOSE_SIDE_PANEL - }) - ) - } - - private currentLandmarks$: Observable<any[]> - - @Effect() - public onStandAloneVolumesExistCloseMatDrawer$: Observable<any> - - @Effect() - public mouseoverUserLandmarks: Observable<any> - - @Effect() - public removeUserLandmarks: Observable<any> - - @Effect() - public addUserLandmarks$: Observable<any> - - @Effect() - public doubleClickOnViewerToggleRegions$: Observable<any> - - @Effect() - public doubleClickOnViewerToggleLandmark$: Observable<any> - - // @Effect() - public doubleClickOnViewerToogleUserLandmark$: Observable<any> -} - -const ACTION_TYPES = { - REMOVE_USER_LANDMARKS: viewreStateRemoveUserLandmarks.type, - - SINGLE_CLICK_ON_VIEWER: 'SINGLE_CLICK_ON_VIEWER', - DOUBLE_CLICK_ON_VIEWER: viewerStateDblClickOnViewer.type -} - -export const VIEWERSTATE_ACTION_TYPES = ACTION_TYPES diff --git a/src/services/state/viewerState/actions.ts b/src/services/state/viewerState/actions.ts index 46f6c1e47c0af113ffe5f6c6f61cb1fe02416e8e..0157c15f83ad1cb8a1fcf84302db772d883a98f0 100644 --- a/src/services/state/viewerState/actions.ts +++ b/src/services/state/viewerState/actions.ts @@ -1,117 +1,20 @@ import { createAction, props } from "@ngrx/store" -import { IRegion } from './constants' -import { FeatureResponse } from "src/atlasComponents/sapi" - -export const viewerStateNewViewer = createAction( - `[viewerState] newViewer`, - props<{ - selectTemplate: any - selectParcellation: any - navigation?: any - }>() -) - -export const viewerStateSetSelectedRegionsWithIds = createAction( - `[viewerState] setSelectedRegionsWithIds`, - props<{ selectRegionIds: string[] }>() -) - -export const viewerStateSetSelectedRegions = createAction( - '[viewerState] setSelectedRegions', - props<{ selectRegions: IRegion[] }>() -) - -export const viewerStateSetConnectivityRegion = createAction( - `[viewerState] setConnectivityRegion`, - props<{ connectivityRegion: any }>() -) - -export const viewerStateNehubaLayerchanged = createAction( - `[viewerState] nehubaLayerChanged`, -) export const viewerStateNavigateToRegion = createAction( `[viewerState] navigateToRegion`, props<{ payload: { region: any } }>() ) -export const viewerStateToggleRegionSelect = createAction( - `[viewerState] toggleRegionSelect`, - props<{ payload: { region: any } }>() -) - -export const viewerStateSetFetchedAtlases = createAction( - '[viewerState] setFetchedatlases', - props<{ fetchedAtlases: any[] }>() -) - -export const viewerStateSelectAtlas = createAction( - `[viewerState] selectAtlas`, - props<{ - atlas: { - ['@id']: string - template?: { - ['@id']: string - } - } - }>() -) - -export const viewerStateHelperSelectParcellationWithId = createAction( - `[viewerStateHelper] selectParcellationWithId`, - props<{ payload: { ['@id']: string } }>() -) - -export const viewerStateSelectParcellation = createAction( - `[viewerState] selectParcellation`, - props<{ selectParcellation: any }>() -) - -export const viewerStateSelectTemplateWithName = createAction( - `[viewerState] selectTemplateWithName`, - props<{ payload: { name: string } }>() -) - export const viewerStateSelectTemplateWithId = createAction( `[viewerState] selectTemplateWithId`, props<{ payload: { ['@id']: string }, config?: { selectParcellation: { ['@id']: string } } }>() ) -export const viewerStateToggleLayer = createAction( - `[viewerState] toggleLayer`, - props<{ payload: { ['@id']: string } }>() -) - -export const viewerStateRemoveAdditionalLayer = createAction( - `[viewerState] removeAdditionalLayer`, - props<{ payload?: { ['@id']: string } }>() -) - -export const viewerStateSelectRegionWithIdDeprecated = createAction( - `[viewerState] [deprecated] selectRegionsWithId`, - props<{ selectRegionIds: string[] }>() -) - -export const viewerStateSetViewerMode = createAction( - `[viewerState] setViewerMode`, - props<{payload: string}>() -) - -export const viewerStateDblClickOnViewer = createAction( - `[viewerState] dblClickOnViewer`, - props<{ payload: { annotation: any, segments: any, landmark: any, userLandmark: any } }>() -) - export const viewerStateAddUserLandmarks = createAction( `[viewerState] addUserlandmark,`, props<{ landmarks: any[] }>() ) -export const viewreStateRemoveUserLandmarks = createAction( - `[viewerState] removeUserLandmarks`, - props<{ payload: { landmarkIds: string[] } }>() -) - export const viewerStateMouseOverCustomLandmark = createAction( '[viewerState] mouseOverCustomLandmark', props<{ payload: { userLandmark: any } }>() @@ -122,34 +25,7 @@ export const viewerStateMouseOverCustomLandmarkInPerspectiveView = createAction( props<{ payload: { label: string } }>() ) -export const viewerStateChangeNavigation = createAction( - `[viewerState] changeNavigation`, - props<{ navigation: any }>() -) - export const actionSetMobileUi = createAction( `[viewerState] setMobileUi`, props<{ payload: { useMobileUI: boolean } }>() ) - -export const actionAddToRegionsSelectionWithIds = createAction( - `[viewerState] addToRegionSelectionWithIds`, - props<{ - selectRegionIds: string[] - }>() -) - -export const actionSelectLandmarks = createAction( - `[viewerState] selectLandmarks`, - props<{ - landmarks: any[] - }>() -) - - -export const actionViewerStateSelectFeature = createAction( - `[viewerState] selectFeature`, - props<{ - feature: FeatureResponse - }>() -) diff --git a/src/services/state/viewerState/selectors.spec.ts b/src/services/state/viewerState/selectors.spec.ts index 6f13c133919aa50774b6e72eb15f2f872e1dfcc8..e5916b75e671cdb281508c796106e9de484ff7f3 100644 --- a/src/services/state/viewerState/selectors.spec.ts +++ b/src/services/state/viewerState/selectors.spec.ts @@ -3,6 +3,10 @@ import { viewerStateAtlasParcellationSelector, viewerStateAtlasLatestParcellationSelector, viewerStateParcVersionSelector, + selectorSelectedATP, + viewerStateGetSelectedAtlas, + viewerStateSelectedTemplateSelector, + viewerStateSelectedParcellationSelector, } from './selectors' @@ -153,95 +157,133 @@ const fetchedTemplates = [ ] 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] - }) + // 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([]) - }) - }) + // 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 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('> 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) => { + // 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 - } - } + // 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]) - }) - }) + // 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('> viewerStateAtlasLatestParcellationSelector', () => { - it('> should only show 1 parc', () => { - const parcs = viewerStateAtlasLatestParcellationSelector.projector(atlas2.parcellations) - expect(parcs.length).toEqual(1) + 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('> viewerStateParcVersionSelector', () => { - it('> should work', () => { - const parcs = viewerStateParcVersionSelector.projector(atlas2.parcellations, { - parcellationSelected: atlas2.parcellations[0] - }) - expect(parcs.length).toEqual(2) + describe("> selectorSelectedATP", () => { + const mockAtlas = { + "@id": "mock atlas" + } as any + const mockTmpl = { + "@id": "mock Tmpl" + } as any + const mockParc = { + "@id": "mock Parc" + } as any - expect(parcs[0]['@version']['@next']).toBeFalsy() - expect(parcs[parcs.length-1]['@version']['@previous']).toBeFalsy() + 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 index bef013ae8bdb5dc70f05f01d39ce34c0c39b8463..c752e8aeae91104be3fd789cf7829894361ff9e3 100644 --- a/src/services/state/viewerState/selectors.ts +++ b/src/services/state/viewerState/selectors.ts @@ -1,237 +1,188 @@ -import { createSelector } from "@ngrx/store" -import { viewerStateHelperStoreName } from "../viewerState.store.helper" -import { IViewerState } from "./type" - -export const viewerStateSelectedRegionsSelector = createSelector( - state => state['viewerState'], - viewerState => viewerState['regionsSelected'] -) - -export 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 ) -} - -export const viewerStateFetchedTemplatesSelector = createSelector( - state => state['viewerState'], - viewerState => viewerState['fetchedTemplates'] -) - -export const viewerStateSelectedTemplateSelector = createSelector( - state => state['viewerState'], - viewerState => viewerState?.['templateSelected'] -) - -export const viewerStateSelectorStandaloneVolumes = createSelector( - state => state['viewerState'], - viewerState => viewerState['standaloneVolumes'] -) - -export 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 - */ -export const viewerStateSelectedTemplatePureSelector = createSelector( - viewerStateFetchedTemplatesSelector, - viewerStateSelectedTemplateSelector, - (fetchedTemplates, selectedTemplate) => { - if (!selectedTemplate) return null - return fetchedTemplates.find(t => t['@id'] === selectedTemplate['@id']) - } -) - -export const viewerStateSelectedParcellationSelector = createSelector( - state => state['viewerState'], - viewerState => viewerState['parcellationSelected'] -) - -export const viewerStateNavigationStateSelector = createSelector( - state => state['viewerState'], - viewerState => viewerState['navigation'] -) - -export const viewerStateAllRegionsFlattenedRegionSelector = createSelector( - viewerStateSelectedParcellationSelector, - parc => { - const returnArr = [] - const processRegion = region => { - const { children, ...rest } = region - returnArr.push({ ...rest }) - region.children && Array.isArray(region.children) && region.children.forEach(processRegion) - } - if (parc && parc.regions && Array.isArray(parc.regions)) { - parc.regions.forEach(processRegion) - } - return returnArr - } -) - -export const viewerStateOverwrittenColorMapSelector = createSelector( - state => state['viewerState'], - viewerState => viewerState['overwrittenColorMap'] -) - -export const viewerStateStandAloneVolumes = createSelector( - state => state['viewerState'], - viewerState => viewerState['standaloneVolumes'] -) - -export const viewerStateSelectorNavigation = createSelector( - state => state['viewerState'], - viewerState => viewerState['navigation'] -) - -export const viewerStateViewerModeSelector = createSelector( - state => state['viewerState'], - viewerState => viewerState['viewerMode'] -) - -export 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 - }] : [] - } -) - -export const viewerStateFetchedAtlasesSelector = createSelector( - state => state[viewerStateHelperStoreName], - helperState => helperState['fetchedAtlases'] -) - -export const viewerStateGetSelectedAtlas = createSelector( - state => state[viewerStateHelperStoreName], - helperState => { - if (!helperState) return null - const { selectedAtlasId, fetchedAtlases } = helperState - if (!selectedAtlasId) return null - return selectedAtlasId && fetchedAtlases.find(a => a['@id'] === selectedAtlasId) - } -) - -export 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 - } -) - -export const viewerStateAtlasLatestParcellationSelector = createSelector( - viewerStateAtlasParcellationSelector, - parcs => (parcs && parcs.filter( p => !p['@version'] || !p['@version']['@next']) || []) -) - -export 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 - } -) - - -export const viewerStateSelectedTemplateFullInfoSelector = createSelector( - viewerStateGetSelectedAtlas, - viewerStateFetchedTemplatesSelector, - (selectedAtlas, fetchedTemplates) => { - if (!selectedAtlas) return null - const { templateSpaces } = selectedAtlas - return templateSpaces.map(templateSpace => { - const fullTemplateInfo = fetchedTemplates.find(t => t['@id'] === templateSpace['@id']) - return { - ...templateSpace, - ...(fullTemplateInfo || {}), - darktheme: (fullTemplateInfo || {}).useTheme === 'dark' - } - }) - } -) - -export const viewerStateContextedSelectedRegionsSelector = createSelector( - viewerStateSelectedRegionsSelector, - viewerStateGetSelectedAtlas, - viewerStateSelectedTemplatePureSelector, - viewerStateSelectedParcellationSelector, - (regions, atlas, template, parcellation) => regions.map(r => { - return { - ...r, - context: { - atlas, - template, - parcellation - } - } - }) -) +// 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/stateStore.service.ts b/src/services/stateStore.service.ts index 9169175790cd539fe2ffe400da0f772216df064c..558823d1ba54dced97da93a2a6c251833999bfda 100644 --- a/src/services/stateStore.service.ts +++ b/src/services/stateStore.service.ts @@ -1,9 +1,5 @@ import { filter } from 'rxjs/operators'; -export { - recursiveFindRegionWithLabelIndexId -} from 'src/util/fn' - export { getNgIds } from 'src/util/fn' import { @@ -12,11 +8,6 @@ import { StateInterface as NgViewerStateInterface, stateStore as ngViewerState, } from './state/ngViewerState.store' -import { - defaultState as pluginDefaultState, - StateInterface as PluginStateInterface, - stateStore as pluginState, -} from './state/pluginState.store' import { ActionInterface as UIActionInterface, defaultState as uiDefaultState, @@ -34,28 +25,14 @@ import { StateInterface as ViewerConfigStateInterface, stateStore as viewerConfigState, } from './state/viewerConfig.store' -import { - ActionInterface as ViewerActionInterface, - defaultViewerState, - IViewerState, - stateStore as viewerState, -} from './state/viewerState.store' - -import { - defaultState as defaultViewerHelperState, - viewerStateHelperStoreName -} from './state/viewerState.store.helper' - -export { pluginState } + + export { viewerConfigState } export { NgViewerStateInterface, NgViewerActionInterface, ngViewerState } -export { IViewerState, ViewerActionInterface, viewerState } export { IUiState, UIActionInterface, uiState } export { userConfigState, USER_CONFIG_ACTION_TYPES} -export { CHANGE_NAVIGATION, DESELECT_LANDMARKS, FETCHED_TEMPLATE, SELECT_PARCELLATION, SELECT_REGIONS, USER_LANDMARKS } from './state/viewerState.store' -export { IDataEntry, IParcellationRegion, FETCHED_DATAENTRIES, FETCHED_SPATIAL_DATA, ILandmark, IOtherLandmarkGeometry, IPlaneLandmarkGeometry, IPointLandmarkGeometry, IProperty, IPublication, IReferenceSpace, IFile, IFileSupplementData } from './state/dataStore.store' -export { CLOSE_SIDE_PANEL, MOUSE_OVER_SEGMENT, OPEN_SIDE_PANEL, COLLAPSE_SIDE_PANEL_CURRENT_VIEW, EXPAND_SIDE_PANEL_CURRENT_VIEW } from './state/uiState.store' +export { MOUSE_OVER_SEGMENT, OPEN_SIDE_PANEL, COLLAPSE_SIDE_PANEL_CURRENT_VIEW, EXPAND_SIDE_PANEL_CURRENT_VIEW } from './state/uiState.store' export { UserConfigStateUseEffect } from './state/userConfigState.store' export { GENERAL_ACTION_TYPES, generalActionError } from './stateStore.helper' @@ -140,22 +117,17 @@ export function isDefined(obj) { } export interface IavRootStoreInterface { - pluginState: PluginStateInterface viewerConfigState: ViewerConfigStateInterface ngViewerState: NgViewerStateInterface - viewerState: IViewerState dataStore: any uiState: IUiState userConfigState: UserConfigStateInterface } export const defaultRootState: any = { - pluginState: pluginDefaultState, dataStore: {}, ngViewerState: ngViewerDefaultState, uiState: uiDefaultState, userConfigState: userConfigDefaultState, viewerConfigState: viewerConfigDefaultState, - viewerState: defaultViewerState, - [viewerStateHelperStoreName]: defaultViewerHelperState } diff --git a/src/state/actions.ts b/src/state/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..2465c644ab5cd842e2e9f9f9537b10c1a51f155b --- /dev/null +++ b/src/state/actions.ts @@ -0,0 +1,13 @@ +import { createAction, props } from "@ngrx/store"; +import { nameSpace } from "./const" + +const generalActionError = createAction( + `${nameSpace} generalActionError`, + props<{ + message: string + }>() +) + +export const actions = { + generalActionError +} diff --git a/src/state/annotations/actions.ts b/src/state/annotations/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..da4ad8edeb6f86d3f80a5d8d3a310d56dc353428 --- /dev/null +++ b/src/state/annotations/actions.ts @@ -0,0 +1,19 @@ +import { createAction, props } from "@ngrx/store" +import { nameSpace } from "./const" +import { Annotation } from "./store" + +const clearAllAnnotations = createAction( + `${nameSpace} clearAllAnnotations` +) + +const rmAnnotations = createAction( + `${nameSpace} rmAnnotation`, + props<{ + annotations: Annotation[] + }>() +) + +export const actions = { + clearAllAnnotations, + rmAnnotations, +} diff --git a/src/state/annotations/const.ts b/src/state/annotations/const.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ca3a95036366f410633ade16db8d2a4e1665064 --- /dev/null +++ b/src/state/annotations/const.ts @@ -0,0 +1 @@ +export const nameSpace = `[state.annotations]` \ No newline at end of file diff --git a/src/state/annotations/index.ts b/src/state/annotations/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e044bea3dee02127fed851a564f49d95d3be6f26 --- /dev/null +++ b/src/state/annotations/index.ts @@ -0,0 +1,4 @@ +export { actions } from "./actions" +export { Annotation, AnnotationState, reducer } from "./store" +export { nameSpace } from "./const" +export * as selectors from "./selectors" \ No newline at end of file diff --git a/src/state/annotations/selectors.ts b/src/state/annotations/selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..abbb3444f4f659714226c63ed2e60f4d796a5489 --- /dev/null +++ b/src/state/annotations/selectors.ts @@ -0,0 +1,10 @@ +import { createSelector } from "@ngrx/store" +import { nameSpace } from "./const" +import { Annotation, AnnotationState } from "./store" + +const selectStore = state => state[nameSpace] as AnnotationState + +export const annotations = createSelector( + selectStore, + state => state.annotations +) diff --git a/src/state/annotations/store.ts b/src/state/annotations/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..6278ac4e6520ca06fc0ce4daf55c019685e22ed6 --- /dev/null +++ b/src/state/annotations/store.ts @@ -0,0 +1,21 @@ +import { createReducer } from "@ngrx/store" + +export type Annotation = { + "@id": string +} + +export type AnnotationState = { + annotations: Annotation[] +} + +const defaultState: AnnotationState = { + annotations: [] +} + +const reducer = createReducer( + defaultState +) + +export { + reducer +} diff --git a/src/state/atlasAppearance/action.ts b/src/state/atlasAppearance/action.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9065e07c2871b118ceb9a2bfd7d4d11eb488d87 --- /dev/null +++ b/src/state/atlasAppearance/action.ts @@ -0,0 +1,9 @@ +import { createAction, props } from "@ngrx/store"; +import { nameSpace } from "./const" + +export const overwriteColorMap = createAction( + `${nameSpace} overwriteColorMap`, + props<{ + colormap: Record<string, number[]> + }>() +) diff --git a/src/state/atlasAppearance/const.ts b/src/state/atlasAppearance/const.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee9d2e45f3c36217015b6c2a18e7e6cd4721ea9e --- /dev/null +++ b/src/state/atlasAppearance/const.ts @@ -0,0 +1 @@ +export const nameSpace = `[state.atlasAppearance]` \ No newline at end of file diff --git a/src/state/atlasAppearance/index.ts b/src/state/atlasAppearance/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf777e1fef5bcd30e900b26d381d0f0851cb441c --- /dev/null +++ b/src/state/atlasAppearance/index.ts @@ -0,0 +1,3 @@ +export * as actions from "./action" +export * as selectors from "./selector" +export { reducer } from "./store" \ No newline at end of file diff --git a/src/state/atlasAppearance/selector.ts b/src/state/atlasAppearance/selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..c43c619782f4d9e68eef4312269f0774e008c642 --- /dev/null +++ b/src/state/atlasAppearance/selector.ts @@ -0,0 +1,10 @@ +import { createSelector } from "@ngrx/store" +import { nameSpace } from "./const" +import { AtlasAppearanceStore } from "./store" + +const selectStore = state => state[nameSpace] as AtlasAppearanceStore + +export const getOverwrittenColormap = createSelector( + selectStore, + state => state.overwrittenColormap +) \ No newline at end of file diff --git a/src/state/atlasAppearance/store.ts b/src/state/atlasAppearance/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..817603c934596095de985e912183360f9427efca --- /dev/null +++ b/src/state/atlasAppearance/store.ts @@ -0,0 +1,23 @@ +import { createReducer, on } from "@ngrx/store" +import * as actions from "./action" + +export type AtlasAppearanceStore = { + overwrittenColormap: Record<string, number[]> +} + +const defaultState: AtlasAppearanceStore = { + overwrittenColormap: null +} + +export const reducer = createReducer( + defaultState, + on( + actions.overwriteColorMap, + (state, { colormap }) => { + return { + ...state, + overwrittenColormap: colormap + } + } + ) +) diff --git a/src/state/atlasSelection/actions.ts b/src/state/atlasSelection/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d114fa5cd3a2acb7811f3d8d08c594e9c0d5164 --- /dev/null +++ b/src/state/atlasSelection/actions.ts @@ -0,0 +1,127 @@ +import { createAction, props } from "@ngrx/store"; +import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; +import { nameSpace, ViewerMode } from "./const" + +export const selectAtlas = createAction( + `${nameSpace} selectAtlas`, + props<{ + atlas: SapiAtlasModel + }>() +) + +export const selectTemplate = createAction( + `${nameSpace} selectTemplate`, + props<{ + template: SapiSpaceModel + }>() +) + +export const selectParcellation = createAction( + `${nameSpace} selectParcellation`, + props<{ + parcellation: SapiParcellationModel + }>() +) + +export const selectRegions = createAction( + `${nameSpace} selectRegions`, + props<{ + regions: SapiRegionModel[] + }>() +) + +export const setStandAloneVolumes = createAction( + `${nameSpace} setStandAloneVolumes`, + props<{ + standAloneVolumes: string[] + }>() +) + +export const setNavigation = createAction( + `${nameSpace} setNavigation`, + props<{ + navigation: { + position: number[] + orientation: number[] + zoom: number + perspectiveOrientation: number[] + perspectiveZoom: number + } + }>() +) + +export const setViewerMode = createAction( + `${nameSpace} setViewerMode`, + props<{ + viewerMode: ViewerMode + }>() +) + +export const clearSelectedRegions = createAction( + `${nameSpace} clearSelectedRegions` +) + +export const selectATPById = createAction( + `${nameSpace} selectATPById`, + props<{ + atlasId?: string, + templateId?: string + parcellationId?: string + }>() +) + +export const clearNonBaseParcLayer = createAction( + `${nameSpace} clearNonBaseParcLayer` +) + +export const clearStandAloneVolumes = createAction( + `${nameSpace} clearStandAloneVolumes` +) + +export const navigateTo = createAction( + `${nameSpace} navigateTo`, + props<{ + navigation: Partial<{ + position: number[] + orientation: number[] + zoom: number + perspectiveOrientation: number[] + perspectiveZoom: number + }> + physical?: boolean + animation?: boolean + }>() +) + +export const navigateToRegion = createAction( + `${nameSpace} navigateToRegion`, + props<{ + region: SapiRegionModel + }>() +) + +export const clearViewerMode = createAction( + `${nameSpace} clearViewerMode`, +) + +export const toggleRegionSelect = createAction( + `${nameSpace} toggleRegionSelect`, + props<{ + region: SapiRegionModel + }>() +) + +export const toggleRegionSelectById = createAction( + `${nameSpace} toggleRegionSelectById`, + props<{ + id: string + }>() +) + +export const viewSelRegionInNewSpace = createAction( + `${nameSpace} viewSelRegionInNewSpace`, + props<{ + region: SapiRegionModel + template: SapiSpaceModel + }>() +) \ No newline at end of file diff --git a/src/state/atlasSelection/const.ts b/src/state/atlasSelection/const.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e26794f3173ebf6b97ce8ab614c963e32685548 --- /dev/null +++ b/src/state/atlasSelection/const.ts @@ -0,0 +1,2 @@ +export const nameSpace = `[state.atlasSelection]` +export type ViewerMode = 'annotating' | 'key frame' \ No newline at end of file diff --git a/src/state/atlasSelection/effects.spec.ts b/src/state/atlasSelection/effects.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..511cf1b532b84590b26bae80e43a470fc7d60377 --- /dev/null +++ b/src/state/atlasSelection/effects.spec.ts @@ -0,0 +1,141 @@ +describe("> effects.ts", () => { + describe("> Effect", () => { + + describe('> selectTemplate$', () => { + + describe('> when transiting from template A to template B', () => { + describe('> if the current navigation is correctly formed', () => { + it('> uses current navigation param', () => { + + }) + }) + + describe('> if current navigation is malformed', () => { + it('> if current navigation is undefined, use nehubaConfig of last template', () => { + }) + + it('> if current navigation is empty object, use nehubaConfig of last template', () => { + }) + }) + + }) + + it('> if coordXform returns error', () => { + + }) + + it('> if coordXform complete', () => { + + }) + + }) + + describe('> if selected atlas has no matching tmpl space', () => { + + it('> should emit gernal error', () => { + + }) + }) + + describe('> if selected atlas has matching tmpl', () => { + + describe('> if parc is empty array', () => { + it('> should emit with falsy as payload', () => { + + }) + }) + describe('> if no parc has eligible @id', () => { + + it('> should emit with falsy as payload', () => { + + }) + }) + + describe('> if some parc has eligible @id', () => { + describe('> if no @version is available', () => { + + it('> selects the first parc', () => { + + }) + }) + + describe('> if @version is available', () => { + + describe('> if there exist an entry without @next attribute', () => { + + it('> selects the first one without @next attribute', () => { + }) + }) + describe('> if there exist no entry without @next attribute', () => { + + it('> selects the first one without @next attribute', () => { + + }) + }) + }) + }) + }) + + describe('> onNavigateToRegion', () => { + + describe('> if atlas, template, parc is not set', () => { + + describe('> if atlas is unset', () => { + it('> returns general error', () => { + }) + }) + describe('> if template is unset', () => { + it('> returns general error', () => { + }) + }) + describe('> if parc is unset', () => { + it('> returns general error', () => { + }) + }) + }) + describe('> if atlas, template, parc is set, but region unset', () => { + it('> returns general error', () => { + }) + }) + + describe('> if inputs are fine', () => { + it('> getRegionDetailSpy is called', () => { + }) + + describe('> mal formed return', () => { + describe('> returns null', () => { + it('> generalactionerror', () => { + }) + }) + describe('> general throw', () => { + + it('> generalactionerror', () => { + }) + + }) + describe('> does not contain props attr', () => { + it('> generalactionerror', () => { + }) + }) + + describe('> does not contain props.length === 0', () => { + it('> generalactionerror', () => { + }) + }) + }) + + describe('> wellformed response', () => { + beforeEach(() => { + + beforeEach(() => { + }) + + it('> emits navigateTo', () => { + + }) + }) + }) + }) + }) + }) +}) \ No newline at end of file diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..f8be02508d3355868350ea07cd4991983f78f7c5 --- /dev/null +++ b/src/state/atlasSelection/effects.ts @@ -0,0 +1,169 @@ +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 { SAPI } from "src/atlasComponents/sapi"; +import * as actions from "./actions" +import { actions as generalAction } from "../actions" +import { select, Store } from "@ngrx/store"; +import { selectors } from '.' + +@Injectable() +export class Effect { + + onAtlasSelectionSelectTmplParc = createEffect(() => this.action.pipe( + ofType(actions.selectAtlas), + filter(action => !!action.atlas), + switchMap(action => + this.sapiSvc.getParcDetail(action.atlas["@id"], action.atlas.parcellations[0]["@id"], 100).then( + parcellation => ({ + parcellation, + atlas: action.atlas + }) + ) + ), + switchMap(({ atlas, parcellation }) => { + const spacdIds = parcellation.brainAtlasVersions.map(bas => bas.coordinateSpace) as { "@id": string }[] + return forkJoin( + spacdIds.filter( + spaceId => atlas.spaces.map(spc => spc["@id"]).indexOf(spaceId["@id"]) >= 0 + ).map(spaceId => + this.sapiSvc.getSpaceDetail(atlas["@id"], spaceId["@id"]) + ) + ).pipe( + switchMap(spaces => { + const selectSpaceId = spaces[2]["@id"] + const selectedSpace = spaces.find(s => s["@id"] === selectSpaceId) + if (!selectedSpace) { + return of( + generalAction.generalActionError({ + message: `space with id ${selectSpaceId} not found!` + }) + ) + } + + return of( + actions.selectTemplate({ + template: selectedSpace + }), + actions.selectParcellation({ + parcellation + }) + ) + }) + ) + }), + )) + + onAtlasSelClearStandAloneVolumes = createEffect(() => this.action.pipe( + ofType(actions.selectAtlas), + mapTo(actions.setStandAloneVolumes({ + standAloneVolumes: [] + })) + )) + + onClearRegion = createEffect(() => this.action.pipe( + ofType(actions.clearSelectedRegions), + mapTo(actions.selectRegions({ + regions: [] + })) + )) + + onNonBaseLayerRemoval = createEffect(() => this.action.pipe( + ofType(actions.clearNonBaseParcLayer), + mapTo(generalAction.generalActionError({ + message: `NYI` + })) + )) + + onClearStandAloneVolumes = createEffect(() => this.action.pipe( + ofType(actions.clearStandAloneVolumes), + mapTo(actions.setStandAloneVolumes({ + standAloneVolumes: [] + })) + )) + + /** + * nb for template selection + * navigation should be transformed + * see selectTemplate$ in spec.ts + */ + 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` + })) + )) + + onClearViewerMode = createEffect(() => this.action.pipe( + ofType(actions.clearViewerMode), + mapTo(actions.setViewerMode({ viewerMode: null })) + )) + + onToggleRegionSelectById = createEffect(() => this.action.pipe( + ofType(actions.toggleRegionSelectById), + mapTo(generalAction.generalActionError({ + message: `NYI` + })) + )) + + onNavigateToRegion = createEffect(() => this.action.pipe( + ofType(actions.navigateToRegion), + mapTo(generalAction.generalActionError({ + message: `NYI` + })) + )) + + onSelAtlasTmplParcClearRegion = createEffect(() => merge( + this.action.pipe( + ofType(actions.selectAtlas) + ), + this.action.pipe( + ofType(actions.selectTemplate) + ), + this.action.pipe( + ofType(actions.selectParcellation) + ) + ).pipe( + mapTo(actions.selectRegions({ + regions: [] + })) + )) + + onRegionToggleSelect = createEffect(() => this.action.pipe( + ofType(actions.toggleRegionSelect), + withLatestFrom( + this.store.pipe( + select(selectors.selectedRegions) + ) + ), + map(([ { region }, regions ]) => { + const selectedRegionsIndicies = regions.map(r => r["@id"]) + const roiIndex = selectedRegionsIndicies.indexOf(region["@id"]) + return actions.selectRegions({ + regions: roiIndex >= 0 + ? [...regions.slice(0, roiIndex), ...regions.slice(roiIndex + 1)] + : [...regions, region] + }) + }) + )) + + constructor( + private action: Actions, + private sapiSvc: SAPI, + private store: Store, + ){ + + } +} \ No newline at end of file diff --git a/src/state/atlasSelection/index.ts b/src/state/atlasSelection/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..603220cde6013640c42cb4f86d9fdeac342b7140 --- /dev/null +++ b/src/state/atlasSelection/index.ts @@ -0,0 +1,6 @@ +export * as selectors from "./selectors" +export { fromRootStore } from "./util" +export { nameSpace } from "./const" +export { reducer } from "./store" +export * as actions from "./actions" +export { Effect } from "./effects" \ No newline at end of file diff --git a/src/state/atlasSelection/selectors.ts b/src/state/atlasSelection/selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..a60c20225d382e20691a955a7f3010c4bb1a0dc0 --- /dev/null +++ b/src/state/atlasSelection/selectors.ts @@ -0,0 +1,49 @@ +import { createSelector } from "@ngrx/store" +import { nameSpace } from "./const" +import { AtlasSelectionState } from "./store" + +export const viewerStateHelperStoreName = 'viewerStateHelper' + +const selectStore = (state: any) => state[nameSpace] as AtlasSelectionState + +export const selectedAtlas = createSelector( + selectStore, + state => state.selectedAtlas +) + +export const selectedTemplate = createSelector( + selectStore, + state => state.selectedTemplate +) + +export const selectedParcellation = createSelector( + selectStore, + state => state.selectedParcellation +) + +export const selectedRegions = createSelector( + selectStore, + state => state.selectedRegions +) + +export const selectedATP = createSelector( + selectedAtlas, + selectedTemplate, + selectedParcellation, + (atlas, template, parcellation) => ({ atlas, template, parcellation }) +) + +export const standaloneVolumes = createSelector( + selectStore, + state => state.standAloneVolumes +) + +export const navigation = createSelector( + selectStore, + state => state.navigation +) + +export const viewerMode = createSelector( + selectStore, + state => state.viewerMode +) diff --git a/src/state/atlasSelection/store.ts b/src/state/atlasSelection/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..2f1caaf5419913b6991055248a485a316336ec90 --- /dev/null +++ b/src/state/atlasSelection/store.ts @@ -0,0 +1,107 @@ +import { createReducer, on } from "@ngrx/store"; +import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; +import * as actions from "./actions" +import { ViewerMode } from "./const" + +export type AtlasSelectionState = { + selectedAtlas: SapiAtlasModel + selectedTemplate: SapiSpaceModel + selectedParcellation: SapiParcellationModel + selectedRegions: SapiRegionModel[] + standAloneVolumes: string[] + + /** + * the navigation may mean something very different + * depending on if the user is using threesurfer/nehuba view + */ + navigation: { + position: number[] + orientation: number[] + zoom: number + perspectiveOrientation: number[] + perspectiveZoom: number + } + + viewerMode: ViewerMode +} + +const defaultState: AtlasSelectionState = { + selectedAtlas: null, + selectedParcellation: null, + selectedRegions: [], + selectedTemplate: null, + standAloneVolumes: [], + navigation: null, + viewerMode: null +} + +const reducer = createReducer( + defaultState, + on( + actions.selectAtlas, + (state, { atlas }) => { + return { + ...state, + selectedAtlas: atlas + } + } + ), + on( + actions.selectTemplate, + (state, { template }) => { + return { + ...state, + selectedTemplate: template + } + } + ), + on( + actions.selectParcellation, + (state, { parcellation }) => { + return { + ...state, + selectedParcellation: parcellation + } + } + ), + on( + actions.selectRegions, + (state, { regions }) => { + return { + ...state, + selectedRegions: regions + } + } + ), + on( + actions.setStandAloneVolumes, + (state, { standAloneVolumes }) => { + return { + ...state, + standAloneVolumes + } + } + ), + on( + actions.setNavigation, + (state, { navigation }) => { + return { + ...state, + navigation + } + } + ), + on( + actions.setViewerMode, + (state, { viewerMode }) => { + return { + ...state, + viewerMode + } + } + ) +) + +export { + reducer +} diff --git a/src/state/atlasSelection/util.ts b/src/state/atlasSelection/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..241a4a1a211ba1a9bfaccb3ac030ce249c1b79df --- /dev/null +++ b/src/state/atlasSelection/util.ts @@ -0,0 +1,36 @@ +import { select } from "@ngrx/store"; +import { forkJoin, pipe } from "rxjs"; +import { switchMap } from "rxjs/operators"; +import { SAPI } from "src/atlasComponents/sapi"; +import * as selectors from "./selectors" + +const allAvailSpaces = (sapi: SAPI) => pipe( + select(selectors.selectedAtlas), + switchMap(atlas => forkJoin( + atlas.spaces.map(spcWId => sapi.getSpaceDetail(atlas["@id"], spcWId["@id"]))) + ) +) + +const allAvailParcs = (sapi: SAPI) => pipe( + select(selectors.selectedAtlas), + switchMap(atlas => + forkJoin( + atlas.parcellations.map(parcWId => sapi.getParcDetail(atlas["@id"], parcWId["@id"])) + ) + ) +) +const allAvailSpacesParcs = (sapi: SAPI) => pipe( + select(selectors.selectedAtlas), + switchMap(atlas => + forkJoin({ + spaces: atlas.spaces.map(spcWId => sapi.getSpaceDetail(atlas["@id"], spcWId["@id"])), + parcellation: atlas.parcellations.map(parcWId => sapi.getParcDetail(atlas["@id"], parcWId["@id"])), + }) + ) +) + +export const fromRootStore = { + allAvailSpaces, + allAvailParcs, + allAvailSpacesParcs, +} diff --git a/src/state/const.ts b/src/state/const.ts new file mode 100644 index 0000000000000000000000000000000000000000..60f4c38b8b2f0d9f9dd4f8db61c0bc42e37a6ac4 --- /dev/null +++ b/src/state/const.ts @@ -0,0 +1 @@ +export const nameSpace = `[state]` \ No newline at end of file diff --git a/src/state/effects/viewerState.useEffect.spec.ts b/src/state/effects/viewerState.useEffect.spec.ts index 37c7b0ee9f18297b0268a6459b74863e6f07d2b9..180223fc10dd36bd9e9cdffdfcc04d3850f4b5b8 100644 --- a/src/state/effects/viewerState.useEffect.spec.ts +++ b/src/state/effects/viewerState.useEffect.spec.ts @@ -3,22 +3,18 @@ import { Observable, of, throwError } from 'rxjs' import { TestBed } from '@angular/core/testing' import { provideMockActions } from '@ngrx/effects/testing' import { MockStore, provideMockStore } from '@ngrx/store/testing' -import { defaultRootState, generalActionError } from 'src/services/stateStore.service' +import { defaultRootState } from 'src/services/stateStore.service' import { Injectable } from '@angular/core' import { TemplateCoordinatesTransformation, ITemplateCoordXformResp } from 'src/services/templateCoordinatesTransformation.service' -import { hot } from 'jasmine-marbles' import { AngularMaterialModule } from 'src/sharedModules' import { HttpClientModule } from '@angular/common/http' -import { viewerStateFetchedTemplatesSelector, viewerStateNavigateToRegion, viewerStateNavigationStateSelector, viewerStateNewViewer, viewerStateSelectAtlas, viewerStateSelectTemplateWithName } from 'src/services/state/viewerState.store.helper' -import { viewerStateFetchedAtlasesSelector, viewerStateGetSelectedAtlas, viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector } from 'src/services/state/viewerState/selectors' -import { CONST } from 'common/constants' import { PureContantService } from 'src/util' -import { viewerStateChangeNavigation } from 'src/services/state/viewerState/actions' + let returnPosition = null const dummyParc1 = { name: 'dummyParc1' -} +} as any const dummyTmpl1 = { '@id': 'dummyTmpl1-id', name: 'dummyTmpl1', @@ -30,7 +26,7 @@ const dummyTmpl1 = { } } } -} +} as any const dummyParc2 = { name: 'dummyParc2' @@ -117,392 +113,6 @@ describe('> viewerState.useEffect.ts', () => { mockStore = TestBed.inject(MockStore) }) - describe('> selectTemplate$', () => { - beforeEach(() => { - mockStore.overrideSelector(viewerStateFetchedTemplatesSelector, mockFetchedTemplates) - mockStore.overrideSelector(viewerStateSelectedParcellationSelector, dummyParc1) - actions$ = hot( - 'a', - { - a: viewerStateSelectTemplateWithName({ - payload: { - name: dummyTmpl1.name - } - }) - } - ) - }) - describe('> when transiting from template A to template B', () => { - describe('> if the current navigation is correctly formed', () => { - it('> uses current navigation param', () => { - - const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - expect( - viewerStateCtrlEffect.selectTemplate$ - ).toBeObservable( - hot( - 'a', - { - a: viewerStateNewViewer({ - selectTemplate: dummyTmpl1, - selectParcellation: dummyTmpl1.parcellations[0], - }) - } - ) - ) - expect(spy).toHaveBeenCalledWith( - dummyTmpl2.name, - dummyTmpl1.name, - initialState.viewerState.navigation.position - ) - }) - }) - - describe('> if current navigation is malformed', () => { - it('> if current navigation is undefined, use nehubaConfig of last template', () => { - - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateNavigationStateSelector, null) - const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - - expect( - viewerStateCtrlEffect.selectTemplate$ - ).toBeObservable( - hot( - 'a', - { - a: viewerStateNewViewer({ - selectTemplate: dummyTmpl1, - selectParcellation: dummyTmpl1.parcellations[0], - }) - } - ) - ) - const { position } = cvtNehubaConfigToNavigationObj(dummyTmpl2.nehubaConfig.dataset.initialNgState) - - expect(spy).toHaveBeenCalledWith( - dummyTmpl2.name, - dummyTmpl1.name, - position - ) - }) - - it('> if current navigation is empty object, use nehubaConfig of last template', () => { - - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(viewerStateNavigationStateSelector, {}) - const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - - expect( - viewerStateCtrlEffect.selectTemplate$ - ).toBeObservable( - hot( - 'a', - { - a: viewerStateNewViewer({ - selectTemplate: dummyTmpl1, - selectParcellation: dummyTmpl1.parcellations[0], - }) - } - ) - ) - const { position } = cvtNehubaConfigToNavigationObj(dummyTmpl2.nehubaConfig.dataset.initialNgState) - - expect(spy).toHaveBeenCalledWith( - dummyTmpl2.name, - dummyTmpl1.name, - position - ) - }) - }) - - }) - - it('> if coordXform returns error', () => { - const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - expect( - viewerStateCtrlEffect.selectTemplate$ - ).toBeObservable( - hot( - 'a', - { - a: viewerStateNewViewer({ - selectTemplate: dummyTmpl1, - selectParcellation: dummyTmpl1.parcellations[0], - }) - } - ) - ) - }) - - it('> if coordXform complete', () => { - returnPosition = [ 1.11e6, 2.22e6, 3.33e6 ] - - const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - const updatedColin = JSON.parse( JSON.stringify( dummyTmpl1 ) ) - const initialNgState = updatedColin.nehubaConfig.dataset.initialNgState - const updatedColinNavigation = updatedColin.nehubaConfig.dataset.initialNgState.navigation - - const { zoom, orientation, perspectiveOrientation, position, perspectiveZoom } = currentNavigation - - for (const idx of [0, 1, 2]) { - updatedColinNavigation.pose.position.voxelCoordinates[idx] = returnPosition[idx] / updatedColinNavigation.pose.position.voxelSize[idx] - } - updatedColinNavigation.zoomFactor = zoom - updatedColinNavigation.pose.orientation = orientation - initialNgState.perspectiveOrientation = perspectiveOrientation - initialNgState.perspectiveZoom = perspectiveZoom - - expect( - viewerStateCtrlEffect.selectTemplate$ - ).toBeObservable( - hot( - 'a', - { - a: viewerStateNewViewer({ - selectTemplate: updatedColin, - selectParcellation: updatedColin.parcellations[0], - }) - } - ) - ) - }) - - }) - - describe('> navigateToRegion$', () => { - const setAction = region => { - actions$ = hot( - 'a', - { - a: viewerStateNavigateToRegion({ - payload: { region } - }) - } - ) - } - let mockStore: MockStore - beforeEach(() => { - - mockStore = TestBed.inject(MockStore) - - mockStore.overrideSelector(viewerStateGetSelectedAtlas, { '@id': 'foo-bar-atlas'}) - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, { '@id': 'foo-bar-template'}) - mockStore.overrideSelector(viewerStateSelectedParcellationSelector, { '@id': 'foo-bar-parcellation'}) - }) - describe('> if atlas, template, parc is not set', () => { - beforeEach(() => { - const region = { - name: 'foo bar' - } - setAction(region) - }) - describe('> if atlas is unset', () => { - beforeEach(() => { - mockStore.overrideSelector(viewerStateGetSelectedAtlas, null) - }) - it('> returns general error', () => { - const effect = TestBed.inject(ViewerStateControllerUseEffect) - expect(effect.navigateToRegion$).toBeObservable( - hot('a', { - a: generalActionError({ - message: 'Go to region: region / atlas / template / parcellation not defined.' - }) - }) - ) - }) - }) - describe('> if template is unset', () => { - beforeEach(() => { - mockStore.overrideSelector(viewerStateSelectedTemplateSelector, null) - }) - it('> returns general error', () => { - const effect = TestBed.inject(ViewerStateControllerUseEffect) - expect(effect.navigateToRegion$).toBeObservable( - hot('a', { - a: generalActionError({ - message: 'Go to region: region / atlas / template / parcellation not defined.' - }) - }) - ) - }) - }) - describe('> if parc is unset', () => { - beforeEach(() => { - mockStore.overrideSelector(viewerStateSelectedParcellationSelector, null) - }) - it('> returns general error', () => { - const effect = TestBed.inject(ViewerStateControllerUseEffect) - expect(effect.navigateToRegion$).toBeObservable( - hot('a', { - a: generalActionError({ - message: 'Go to region: region / atlas / template / parcellation not defined.' - }) - }) - ) - }) - }) - }) - describe('> if atlas, template, parc is set, but region unset', () => { - beforeEach(() => { - setAction(null) - }) - it('> returns general error', () => { - const effect = TestBed.inject(ViewerStateControllerUseEffect) - expect(effect.navigateToRegion$).toBeObservable( - hot('a', { - a: generalActionError({ - message: 'Go to region: region / atlas / template / parcellation not defined.' - }) - }) - ) - }) - }) - - describe('> if inputs are fine', () => { - let getRegionDetailSpy: jasmine.Spy - const region = { - name: 'foo bar' - } - beforeEach(() => { - getRegionDetailSpy = spyOn(mockPureConstantService, 'getRegionDetail').and.callThrough() - setAction(region) - }) - afterEach(() => { - getRegionDetailSpy.calls.reset() - }) - - it('> getRegionDetailSpy is called', () => { - const ctrl = TestBed.inject(ViewerStateControllerUseEffect) - - // necessary to trigger the emit - expect( - ctrl.navigateToRegion$ - ).toBeObservable( - hot('a', { - a: generalActionError({ - message: 'Fetching region detail error: Error: region does not have props defined!' - }) - }) - ) - - expect(getRegionDetailSpy).toHaveBeenCalled() - }) - - describe('> mal formed return', () => { - describe('> returns null', () => { - it('> generalactionerror', () => { - const ctrl = TestBed.inject(ViewerStateControllerUseEffect) - expect( - ctrl.navigateToRegion$ - ).toBeObservable( - hot('a', { - a: generalActionError({ - message: 'Fetching region detail error: Error: region does not have props defined!' - }) - }) - ) - }) - }) - describe('> general throw', () => { - const msg = 'oh no!' - beforeEach(() => { - getRegionDetailSpy.and.callFake(() => throwError(msg)) - }) - - it('> generalactionerror', () => { - const ctrl = TestBed.inject(ViewerStateControllerUseEffect) - expect( - ctrl.navigateToRegion$ - ).toBeObservable( - hot('a', { - a: generalActionError({ - message: `Fetching region detail error: ${msg}` - }) - }) - ) - }) - - }) - describe('> does not contain props attr', () => { - - beforeEach(() => { - getRegionDetailSpy.and.callFake(() => of({ - name: 'foo-bar' - })) - }) - - it('> generalactionerror', () => { - const ctrl = TestBed.inject(ViewerStateControllerUseEffect) - expect( - ctrl.navigateToRegion$ - ).toBeObservable( - hot('a', { - a: generalActionError({ - message: `Fetching region detail error: Error: region does not have props defined!` - }) - }) - ) - }) - }) - - describe('> does not contain props.length === 0', () => { - - beforeEach(() => { - getRegionDetailSpy.and.callFake(() => of({ - name: 'foo-bar', - props: {} - })) - }) - - it('> generalactionerror', () => { - const ctrl = TestBed.inject(ViewerStateControllerUseEffect) - expect( - ctrl.navigateToRegion$ - ).toBeObservable( - hot('a', { - a: generalActionError({ - message: `Fetching region detail error: Error: region does not have props defined!` - }) - }) - ) - }) - }) - }) - - describe('> wellformed response', () => { - beforeEach(() => { - - beforeEach(() => { - getRegionDetailSpy.and.callFake(() => of({ - name: 'foo-bar', - props: { - components: { - centroid: [1,2,3] - } - } - })) - }) - - it('> emits viewerStateChangeNavigation', () => { - const ctrl = TestBed.inject(ViewerStateControllerUseEffect) - expect( - ctrl.navigateToRegion$ - ).toBeObservable( - hot('a', { - a: viewerStateChangeNavigation({ - navigation: { - position: [1e6,2e6,3e6], - animation: {} - } - }) - }) - ) - }) - }) - }) - }) - }) describe('> onSelectAtlasSelectTmplParc$', () => { let mockStore: MockStore @@ -511,26 +121,7 @@ describe('> viewerState.useEffect.ts', () => { }) it('> if atlas not found, return general error', () => { - mockStore.overrideSelector(viewerStateFetchedTemplatesSelector, []) - mockStore.overrideSelector(viewerStateFetchedAtlasesSelector, []) - actions$ = hot('a', { - a: viewerStateSelectAtlas({ - atlas: { - ['@id']: 'foo-bar', - } - }) - }) - - const viewerSTateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - expect( - viewerSTateCtrlEffect.onSelectAtlasSelectTmplParc$ - ).toBeObservable( - hot('a', { - a: generalActionError({ - message: CONST.ATLAS_NOT_FOUND - }) - }) - ) + }) describe('> if atlas found', () => { @@ -560,66 +151,10 @@ describe('> viewerState.useEffect.ts', () => { it('> if fails, will return general error', () => { - mockStore.overrideSelector(viewerStateFetchedTemplatesSelector, [ - mockTmplSpc1 - ]) - mockStore.overrideSelector(viewerStateFetchedAtlasesSelector, [{ - ['@id']: 'foo-bar', - templateSpaces: [ mockTmplSpc ], - parcellations: [ mockParc0 ] - }]) - actions$ = hot('a', { - a: viewerStateSelectAtlas({ - atlas: { - ['@id']: 'foo-bar', - } - }) - }) - - const viewerSTateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - expect( - viewerSTateCtrlEffect.onSelectAtlasSelectTmplParc$ - ).toBeObservable( - hot('a', { - a: generalActionError({ - message: CONST.TEMPLATE_NOT_FOUND - }) - }) - ) }) it('> if succeeds, will dispatch new viewer', () => { - const completeMocktmpl = { - ...mockTmplSpc1, - parcellations: [ mockParc1 ] - } - mockStore.overrideSelector(viewerStateFetchedTemplatesSelector, [ - completeMocktmpl - ]) - mockStore.overrideSelector(viewerStateFetchedAtlasesSelector, [{ - ['@id']: 'foo-bar', - templateSpaces: [ mockTmplSpc1 ], - parcellations: [ mockParc1 ] - }]) - actions$ = hot('a', { - a: viewerStateSelectAtlas({ - atlas: { - ['@id']: 'foo-bar', - } - }) - }) - - const viewerSTateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - expect( - viewerSTateCtrlEffect.onSelectAtlasSelectTmplParc$ - ).toBeObservable( - hot('a', { - a: viewerStateNewViewer({ - selectTemplate: completeMocktmpl, - selectParcellation: mockParc1 - }) - }) - ) + }) }) @@ -637,68 +172,13 @@ describe('> viewerState.useEffect.ts', () => { } beforeEach(() => { - mockStore.overrideSelector(viewerStateFetchedTemplatesSelector, [ - completeMockTmpl, - completeMocktmpl1, - ]) - mockStore.overrideSelector(viewerStateFetchedAtlasesSelector, [{ - ['@id']: 'foo-bar', - templateSpaces: [ mockTmplSpc, mockTmplSpc1 ], - parcellations: [ mockParc0, mockParc1 ] - }]) }) it('> will select template.@id', () => { - actions$ = hot('a', { - a: viewerStateSelectAtlas({ - atlas: { - ['@id']: 'foo-bar', - template: { - ['@id']: mockTmplSpc1['@id'] - } - } - }) - }) - - const viewerSTateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - expect( - viewerSTateCtrlEffect.onSelectAtlasSelectTmplParc$ - ).toBeObservable( - hot('a', { - a: viewerStateNewViewer({ - selectTemplate: completeMocktmpl1, - selectParcellation: mockParc1 - }) - }) - ) - }) it('> if template.@id is not defined, will fallback to first template', () => { - actions$ = hot('a', { - a: viewerStateSelectAtlas({ - atlas: { - ['@id']: 'foo-bar', - template: { - - } as any - } - }) - }) - - const viewerSTateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - expect( - viewerSTateCtrlEffect.onSelectAtlasSelectTmplParc$ - ).toBeObservable( - hot('a', { - a: viewerStateNewViewer({ - selectTemplate: completeMockTmpl, - selectParcellation: mockParc0 - }) - }) - ) - }) }) }) diff --git a/src/state/effects/viewerState.useEffect.ts b/src/state/effects/viewerState.useEffect.ts deleted file mode 100644 index 7c590f0c46922146b5c1b8471d219b24e5a335e5..0000000000000000000000000000000000000000 --- a/src/state/effects/viewerState.useEffect.ts +++ /dev/null @@ -1,466 +0,0 @@ -import { Injectable, OnDestroy } from "@angular/core"; -import { Actions, Effect, ofType } from "@ngrx/effects"; -import { Action, select, Store } from "@ngrx/store"; -import { Observable, Subscription, of, merge } from "rxjs"; -import { distinctUntilChanged, filter, map, shareReplay, withLatestFrom, switchMap, mapTo, startWith, catchError } from "rxjs/operators"; -import { FETCHED_TEMPLATE, IavRootStoreInterface, SELECT_PARCELLATION, SELECT_REGIONS, generalActionError } from "src/services/stateStore.service"; -import { TemplateCoordinatesTransformation } from "src/services/templateCoordinatesTransformation.service"; -import { CLEAR_STANDALONE_VOLUMES } from "src/services/state/viewerState.store"; -import { viewerStateToggleRegionSelect, viewerStateHelperSelectParcellationWithId, viewerStateSelectTemplateWithId, viewerStateNavigateToRegion, viewerStateSelectedTemplateSelector, viewerStateFetchedTemplatesSelector, viewerStateNewViewer, viewerStateSelectedParcellationSelector, viewerStateNavigationStateSelector, viewerStateSelectTemplateWithName, viewerStateSelectedRegionsSelector, viewerStateSelectAtlas } from "src/services/state/viewerState.store.helper"; -import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState/selectors"; -import { ngViewerActionClearView } from "src/services/state/ngViewerState/actions"; -import { PureContantService } from "src/util"; -import { CONST } from 'common/constants' -import { viewerStateFetchedAtlasesSelector, viewerStateGetSelectedAtlas } from "src/services/state/viewerState/selectors"; -import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; -import { cvtNavigationObjToNehubaConfig } from 'src/viewerModule/nehuba/util' -import { getPosFromRegion } from "src/util/siibraApiConstants/fn"; - -const defaultPerspectiveZoom = 1e6 -const defaultZoom = 1e6 - -export const defaultNavigationObject = { - orientation: [0, 0, 0, 1], - perspectiveOrientation: [0.5, -0.5, -0.5, 0.5], - perspectiveZoom: defaultPerspectiveZoom, - zoom: defaultZoom, - position: [0, 0, 0], - positionReal: true -} - -export const defaultNehubaConfigObject = { - perspectiveOrientation: [0.5, -0.5, -0.5, 0.5], - perspectiveZoom: 1e6, - navigation: { - pose: { - position: { - voxelCoordinates: [0, 0, 0], - voxelSize: [1,1,1] - }, - orientation: [0, 0, 0, 1], - }, - zoomFactor: defaultZoom - } -} - -export function cvtNehubaConfigToNavigationObj(nehubaConfig?){ - const { - navigation, - perspectiveOrientation = defaultNavigationObject.perspectiveOrientation, - perspectiveZoom = defaultNavigationObject.perspectiveZoom - } = nehubaConfig || {} - const { pose, zoomFactor = 1e6 } = navigation || {} - const { position, orientation = [0, 0, 0, 1] } = pose || {} - const { voxelSize = [1, 1, 1], voxelCoordinates = [0, 0, 0] } = position || {} - - return { - orientation, - perspectiveOrientation: perspectiveOrientation, - perspectiveZoom: perspectiveZoom, - zoom: zoomFactor, - position: [0, 1, 2].map(idx => voxelSize[idx] * voxelCoordinates[idx]), - positionReal: true - } -} - -@Injectable({ - providedIn: 'root', -}) - -export class ViewerStateControllerUseEffect implements OnDestroy { - - private subscriptions: Subscription[] = [] - - private selectedRegions$: Observable<any[]> - - @Effect() - public init$ = this.pureService.initFetchTemplate$.pipe( - map(fetchedTemplate => { - return { - type: FETCHED_TEMPLATE, - fetchedTemplate, - } - }), - ) - - @Effect() - public onSelectAtlasSelectTmplParc$ = this.actions$.pipe( - ofType(viewerStateSelectAtlas.type), - switchMap(action => this.pureService.allFetchingReady$.pipe( - filter(v => !!v), - mapTo(action) - )), - withLatestFrom( - this.store$.pipe( - select(viewerStateFetchedTemplatesSelector), - startWith([]) - ), - this.store$.pipe( - select(viewerStateFetchedAtlasesSelector), - startWith([]) - ) - ), - map(([action, fetchedTemplates, fetchedAtlases ])=> { - - const { atlas: atlasObj } = action as any - const atlas = fetchedAtlases.find(a => a['@id'] === atlasObj['@id']) - if (!atlas) { - return generalActionError({ - message: CONST.ATLAS_NOT_FOUND - }) - } - /** - * selecting atlas means selecting the first available templateSpace - */ - const targetTmplSpcId = atlasObj['template']?.['@id'] - const templateTobeSelected = ( - targetTmplSpcId - && atlas.templateSpaces.find(t => t['@id'] === targetTmplSpcId) - ) || atlas.templateSpaces[0] - - const templateSpaceId = templateTobeSelected['@id'] - const atlasTmpl = atlas.templateSpaces.find(t => t['@id'] === templateSpaceId) - - const templateSelected = fetchedTemplates.find(t => templateSpaceId === t['@id']) - if (!templateSelected) { - return generalActionError({ - message: CONST.TEMPLATE_NOT_FOUND - }) - } - - const atlasParcs = atlasTmpl.availableIn - .map(availP => atlas.parcellations.find(p => availP['@id'] === p['@id'])) - .filter(fullP => !!fullP) - const atlasParc = atlasParcs.find(p => { - if (!p.baseLayer) return false - if (p['@version']) { - return !p['@version']['@next'] - } - return true - }) || templateSelected.parcellations[0] - const parcellationId = atlasParc && atlasParc['@id'] - const parcellationSelected = parcellationId && templateSelected.parcellations.find(p => p['@id'] === parcellationId) - return viewerStateNewViewer({ - selectTemplate: templateSelected, - selectParcellation: parcellationSelected - }) - }) - ) - - - @Effect() - public selectParcellation$: Observable<any> - - private selectTemplateIntent$: Observable<any> = merge( - this.actions$.pipe( - ofType(viewerStateSelectTemplateWithId.type), - map(({ payload, config }) => { - return { - templateId: payload['@id'], - parcellationId: config && config['selectParcellation'] && config['selectParcellation']['@id'] - } - }) - ), - this.actions$.pipe( - ofType(viewerStateSelectTemplateWithName), - withLatestFrom(this.store$.pipe( - select(viewerStateFetchedTemplatesSelector) - )), - map(([ action, fetchedTemplates ]) => { - const templateName = (action as any).payload.name - const foundTemplate = fetchedTemplates.find(t => t.name === templateName) - return foundTemplate && foundTemplate['@id'] - }), - filter(v => !!v), - map(templateId => { - return { templateId, parcellationId: null } - }) - ) - ) - - @Effect() - public selectTemplate$: Observable<any> = this.selectTemplateIntent$.pipe( - withLatestFrom( - this.store$.pipe( - select(viewerStateFetchedTemplatesSelector) - ), - this.store$.pipe( - select(viewerStateSelectedParcellationSelector) - ) - ), - map(([ { templateId, parcellationId }, fetchedTemplates, parcellationSelected ]) => { - /** - * find the correct template & parcellation from their IDs - */ - - /** - * for template, just look for the new id in fetched templates - */ - const newTemplateTobeSelected = fetchedTemplates.find(t => t['@id'] === templateId) - if (!newTemplateTobeSelected) { - return { - selectTemplate: null, - selectParcellation: null, - errorMessage: `Selected templateId ${templateId} not found.` - } - } - - /** - * for parcellation, - * if new parc id is defined, try to find the corresponding parcellation in the new template - * if above fails, try to find the corresponding parcellation of the currently selected parcellation - * if the above fails, select the first parcellation in the new template - */ - const selectParcellationWithTemplate = (parcellationId && newTemplateTobeSelected['parcellations'].find(p => p['@id'] === parcellationId)) - || (parcellationSelected && parcellationSelected['@id'] && newTemplateTobeSelected['parcellations'].find(p => p['@id'] === parcellationSelected['@id'])) - || newTemplateTobeSelected.parcellations[0] - - return { - selectTemplate: newTemplateTobeSelected, - selectParcellation: selectParcellationWithTemplate - } - }), - withLatestFrom( - this.store$.pipe( - select(viewerStateSelectedTemplateSelector), - startWith(null as any), - ), - this.store$.pipe( - select(viewerStateNavigationStateSelector), - startWith(null as any), - ) - ), - switchMap(([{ selectTemplate, selectParcellation, errorMessage }, lastSelectedTemplate, navigation]) => { - /** - * if selectTemplate is undefined (cannot find template with id) - */ - if (errorMessage) { - return of(generalActionError({ - message: errorMessage || 'Switching template error.', - })) - } - /** - * if there were no template selected last - * simply return selectTemplate object - */ - if (!lastSelectedTemplate) { - return of(viewerStateNewViewer({ - selectParcellation, - selectTemplate, - })) - } - - /** - * if there were template selected last, extract navigation info - */ - const previousNavigation = (navigation && Object.keys(navigation).length > 0 && navigation) || cvtNehubaConfigToNavigationObj(lastSelectedTemplate.nehubaConfig?.dataset?.initialNgState) - return this.coordinatesTransformation.getPointCoordinatesForTemplate(lastSelectedTemplate.name, selectTemplate.name, previousNavigation.position).pipe( - map(({ status, result }) => { - - /** - * if getPointCoordinatesForTemplate returns error, simply load the temp/parc - */ - if (status === 'error') { - return viewerStateNewViewer({ - selectParcellation, - selectTemplate, - }) - } - - /** - * otherwise, copy the nav state to templateSelected - * deepclone of json object is required, or it will mutate the fetchedTemplate - * setting navigation sometimes creates a race con, as creating nehubaViewer is not sync - */ - const deepCopiedState = JSON.parse(JSON.stringify(selectTemplate)) - const initialNgState = deepCopiedState.nehubaConfig.dataset.initialNgState - - const newInitialNgState = cvtNavigationObjToNehubaConfig({ - ...previousNavigation, - position: result - }, initialNgState) - - /** - * mutation of initialNgState is expected here - */ - deepCopiedState.nehubaConfig.dataset.initialNgState = { - ...initialNgState, - ...newInitialNgState - } - - return viewerStateNewViewer({ - selectTemplate: deepCopiedState, - selectParcellation, - }) - }) - ) - }) - ) - - @Effect() - public toggleRegionSelection$: Observable<any> - - @Effect() - public navigateToRegion$: Observable<any> - - @Effect() - public onTemplateSelectClearStandAloneVolumes$: Observable<any> - - @Effect() - public onTemplateSelectUnsetAllClearQueues$: Observable<any> = this.store$.pipe( - select(viewerStateSelectedTemplateSelector), - withLatestFrom(this.store$.pipe( - select(ngViewerSelectorClearViewEntries) - )), - map(([_, clearViewQueue]) => { - const newVal = {} - for (const key of clearViewQueue) { - newVal[key] = false - } - return ngViewerActionClearView({ - payload: newVal - }) - }) - ) - - constructor( - private actions$: Actions, - private store$: Store<IavRootStoreInterface>, - private pureService: PureContantService, - private coordinatesTransformation: TemplateCoordinatesTransformation - ) { - const viewerState$ = this.store$.pipe( - select('viewerState'), - shareReplay(1), - ) - - this.selectedRegions$ = viewerState$.pipe( - select('regionsSelected'), - distinctUntilChanged(), - ) - - this.onTemplateSelectClearStandAloneVolumes$ = this.store$.pipe( - select(viewerStateSelectedTemplateSelector), - distinctUntilChanged(), - mapTo({ type: CLEAR_STANDALONE_VOLUMES }) - ) - - /** - * merge all sources of selecting parcellation into parcellation id - */ - this.selectParcellation$ = merge( - - /** - * listening on action - */ - - this.actions$.pipe( - ofType(viewerStateHelperSelectParcellationWithId.type), - map(({ payload }) => payload['@id']) - ), - - ).pipe( - withLatestFrom(viewerState$.pipe( - select('templateSelected'), - )), - map(([id, templateSelected]) => { - const { parcellations: availableParcellations } = templateSelected - const newParcellation = availableParcellations.find(t => t['@id'] === id) - if (!newParcellation) { - return generalActionError({ - message: 'Selected parcellation not found.' - }) - } - return { - type: SELECT_PARCELLATION, - selectParcellation: newParcellation, - } - }) - ) - - this.navigateToRegion$ = this.actions$.pipe( - ofType(viewerStateNavigateToRegion), - map(action => action.payload?.region), - withLatestFrom( - this.store$.pipe( - select(viewerStateGetSelectedAtlas) - ), - this.store$.pipe( - select(viewerStateSelectedTemplateSelector) - ), - this.store$.pipe( - select(viewerStateSelectedParcellationSelector) - ) - ), - switchMap(([ region, selectedAtlas, selectedTemplate, selectedParcellation ]) => { - if (!region || !selectedAtlas || !selectedTemplate || !selectedParcellation) { - return of( - generalActionError({ - message: `Go to region: region / atlas / template / parcellation not defined.` - }) - ) - } - return this.pureService.getRegionDetail(selectedAtlas['@id'], selectedParcellation['@id'], selectedTemplate['@id'], region).pipe( - map(regDetail => { - const position = getPosFromRegion(regDetail) - if (!position) throw new Error(`region does not have props defined!`) - - return viewerStateChangeNavigation({ - navigation: { - position, - animation: {}, - } - }) - }), - catchError((err) => of( - generalActionError({ - message: `Fetching region detail error: ${err}` - }) - )) - ) - }), - ) - - this.toggleRegionSelection$ = this.actions$.pipe( - ofType(viewerStateToggleRegionSelect.type), - withLatestFrom(this.selectedRegions$), - map(([action, regionsSelected]) => { - - const { payload = {} } = action as ViewerStateAction - const { region } = payload - - /** - * if region does not have labelIndex (not tree leaf), for now, return error - */ - if (!region.labelIndex) { - return generalActionError({ - message: 'Currently, only regions at the lowest hierarchy can be selected.' - }) - } - - /** - * if the region is already selected, deselect it - * if the region is not yet selected, deselect any existing region, and select this region - */ - const roiIsSelected = !!regionsSelected.find(r => r.name === region.name) - return { - type: SELECT_REGIONS, - selectRegions: roiIsSelected - ? [] - : [ region ] - } - }), - ) - } - - public ngOnDestroy() { - while (this.subscriptions.length > 0) { - this.subscriptions.pop().unsubscribe() - } - } -} - -interface ViewerStateAction extends Action { - payload: any - config: any -} diff --git a/src/state/index.ts b/src/state/index.ts index 778709a1c7cdd5a85c71824a18b8c2f1aff1df07..8567985487ab7f6278d38d5d4253b7edc985b2e7 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,5 +1,8 @@ export { StateModule } from "./state.module" -export { - ViewerStateControllerUseEffect, - cvtNehubaConfigToNavigationObj, -} from "./effects/viewerState.useEffect" \ No newline at end of file + +export * as atlasSelection from "./atlasSelection" +export * as annotation from "./annotations" +export * as userInterface from "./userInterface" +export * as atlasAppearance from "./atlasAppearance" +export * as plugins from "./plugins" +export * as userInteraction from "./userInteraction" diff --git a/src/state/plugins/actions.ts b/src/state/plugins/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..5fe4fcd15b162f4bf26f5947d124e925dca43552 --- /dev/null +++ b/src/state/plugins/actions.ts @@ -0,0 +1,18 @@ +import { createAction, props } from "@ngrx/store"; +import { nameSpace } from "./const" + +export const clearInitManifests = createAction( + `${nameSpace} clearInitManifests`, + props<{ + nameSpace: string + }>() +) + +export const setInitMan = createAction( + `${nameSpace} setInitMan`, + props<{ + nameSpace: string + url: string + internal?: boolean + }>() +) diff --git a/src/state/plugins/const.ts b/src/state/plugins/const.ts new file mode 100644 index 0000000000000000000000000000000000000000..7b004e658ddc8e803a00c892e32a82a6a57499be --- /dev/null +++ b/src/state/plugins/const.ts @@ -0,0 +1,2 @@ +export const nameSpace = `[state.plugins]` +export const INIT_MANIFEST_SRC = `__INIT_MANFEST_SRC__` diff --git a/src/services/effect/pluginUseEffect.spec.ts b/src/state/plugins/effects.spec.ts similarity index 80% rename from src/services/effect/pluginUseEffect.spec.ts rename to src/state/plugins/effects.spec.ts index 978b28ad0485761025ad70b263e0440e26929326..0acf2f75a00630c5797aab39d59c2c957484fd67 100644 --- a/src/services/effect/pluginUseEffect.spec.ts +++ b/src/state/plugins/effects.spec.ts @@ -1,18 +1,18 @@ import { TestBed } from "@angular/core/testing"; import { HttpClientModule, HTTP_INTERCEPTORS, HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpHeaders } from "@angular/common/http"; -import { PluginServiceUseEffect } from "./pluginUseEffect"; +import { Effects } from "./effects"; import { Observable, of } from "rxjs"; import { Action } from "@ngrx/store"; import { provideMockActions } from "@ngrx/effects/testing"; import { provideMockStore } from "@ngrx/store/testing"; -import { defaultRootState } from "../stateStore.service"; -import { PLUGINSTORE_CONSTANTS, PLUGINSTORE_ACTION_TYPES } from '../state/pluginState.helper' import { Injectable } from "@angular/core"; import { getRandomHex } from 'common/util' import { PluginServices } from "src/plugin"; import { AngularMaterialModule } from "src/sharedModules"; import { hot } from "jasmine-marbles"; import { BS_ENDPOINT } from "src/util/constants"; +import * as actions from "./actions" +import { INIT_MANIFEST_SRC } from "./const" const actions$: Observable<Action> = of({type: 'TEST'}) @@ -83,18 +83,9 @@ describe('pluginUseEffect.ts', () => { AngularMaterialModule ], providers: [ - PluginServiceUseEffect, + Effects, provideMockActions(() => actions$), - provideMockStore({ - initialState: { - ...defaultRootState, - pluginState:{ - initManifests: [ - [ PLUGINSTORE_CONSTANTS.INIT_MANIFEST_SRC, 'http://localhost:12345/manifest.json' ] - ] - } - } - }), + provideMockStore(), { provide: HTTP_INTERCEPTORS, useClass: HTTPInterceptorClass, @@ -115,13 +106,13 @@ describe('pluginUseEffect.ts', () => { }) it('initManifests should fetch manifest.json', () => { - const effect = TestBed.get(PluginServiceUseEffect) as PluginServiceUseEffect + const effect = TestBed.inject(Effects) expect( - effect.initManifests$ + effect.initManClear ).toBeObservable( - hot('a', { - a: {type: PLUGINSTORE_ACTION_TYPES.CLEAR_INIT_PLUGIN} - }) + hot('a', actions.clearInitManifests({ + nameSpace: INIT_MANIFEST_SRC + })) ) expect(spy).toHaveBeenCalledWith(manifest) }) diff --git a/src/state/plugins/effects.ts b/src/state/plugins/effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d810950bedd003ef237e8fee4c38da08de360e3 --- /dev/null +++ b/src/state/plugins/effects.ts @@ -0,0 +1,67 @@ +import { Injectable } from "@angular/core"; +import { createEffect } from "@ngrx/effects"; +import { select, Store } from "@ngrx/store"; +import { catchError, filter, map, mapTo, switchMap } from "rxjs/operators"; +import { PluginServices } from "src/plugin"; +import { WidgetServices } from "src/widget"; +import { atlasSelection } from ".." +import * as constants from "./const" +import * as selectors from "./selectors" +import * as actions from "./actions" +import { DialogService } from "src/services/dialogService.service"; +import { of } from "rxjs"; +import { HttpClient } from "@angular/common/http"; +import { getHttpHeader } from "src/util/constants" + +@Injectable() +export class Effects{ + onATPUpdateClearWidgets = createEffect(() => this.store.pipe( + select(atlasSelection.selectors.selectedATP), + map(() => { + this.widgetSvc.clearAllWidgets() + }) + ), { dispatch: false }) + + initMan = this.store.pipe( + select(selectors.initManfests), + map(initMan => initMan[constants.INIT_MANIFEST_SRC]), + filter(val => !!val), + ) + + initManLaunch = createEffect(() => this.initMan.pipe( + switchMap(val => + this.dialogSvc + .getUserConfirm({ + message: `This URL is trying to open a plugin from ${val}. Proceed?` + }) + .then(() => + this.http.get(val, { + headers: getHttpHeader(), + responseType: 'json' + }).toPromise() + ) + .then(json => + this.pluginSvc.launchNewWidget(json) + ) + ), + catchError(() => of(null)) + ), { dispatch: false }) + + initManClear = createEffect(() => this.initMan.pipe( + mapTo( + actions.clearInitManifests({ + nameSpace: constants.INIT_MANIFEST_SRC + }) + ) + )) + + constructor( + private store: Store, + private widgetSvc: WidgetServices, + private pluginSvc: PluginServices, + private dialogSvc: DialogService, + private http: HttpClient, + ){ + + } +} \ No newline at end of file diff --git a/src/state/plugins/index.ts b/src/state/plugins/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..bdca8a18cd994e4c61ac6588fe2052efd79e4634 --- /dev/null +++ b/src/state/plugins/index.ts @@ -0,0 +1,5 @@ +export * as selectors from "./selectors" +export * as actions from "./actions" +export { reducer } from "./store" +export { Effects } from "./effects" +export { nameSpace, INIT_MANIFEST_SRC } from "./const" \ No newline at end of file diff --git a/src/state/plugins/selectors.ts b/src/state/plugins/selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b70d2e7d5dcc500739a02e53abcf938f2ccc094 --- /dev/null +++ b/src/state/plugins/selectors.ts @@ -0,0 +1,10 @@ +import { createSelector } from "@ngrx/store"; +import { PluginStore } from "./store" +import { nameSpace } from "./const" + +const storeSelector = state => state[nameSpace] as PluginStore + +export const initManfests = createSelector( + storeSelector, + state => state.initManifests +) diff --git a/src/state/plugins/store.ts b/src/state/plugins/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..0648d1fa68c2dd6e631ef975984c8f005966f810 --- /dev/null +++ b/src/state/plugins/store.ts @@ -0,0 +1,47 @@ +import { createReducer, on } from "@ngrx/store"; +import * as actions from "./actions" +import { INIT_MANIFEST_SRC } from "./const" + +export type PluginStore = { + initManifests: Record<string, string> +} + +const defaultState: PluginStore = { + initManifests: {} +} + +export const reducer = createReducer( + defaultState, + on( + actions.clearInitManifests, + (state, { nameSpace }) => { + if (!state[nameSpace]) return state + const newMan: Record<string, string> = {} + const { initManifests } = state + for (const key in initManifests) { + if (key === nameSpace) continue + newMan[key] = initManifests[key] + } + return { + ...state, + initManifests: newMan + } + } + ), + on( + actions.setInitMan, + (state, { nameSpace, url, internal }) => { + if (!internal) { + if (nameSpace === INIT_MANIFEST_SRC) return state + } + const { initManifests } = state + return { + ...state, + initManifests: { + ...initManifests, + [nameSpace]: url + } + } + } + ) +) diff --git a/src/state/userInteraction/actions.ts b/src/state/userInteraction/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7d30d2de689972864d6fede14753ee376525600 --- /dev/null +++ b/src/state/userInteraction/actions.ts @@ -0,0 +1,33 @@ +import { createAction, props } from "@ngrx/store" +import { nameSpace } from "./const" +import * as atlasSelection from "../atlasSelection" +import { SapiRegionModel } from "src/atlasComponents/sapi" +import * as userInterface from "../userInterface" + +export const { + clearSelectedRegions, + clearStandAloneVolumes, + clearNonBaseParcLayer, +} = atlasSelection.actions + +export const { + openSidePanel, + closeSidePanel, + expandSidePanelDetailView, +} = userInterface.actions + +export const mouseOverAnnotations = createAction( + `${nameSpace} mouseOverAnnotations`, + props<{ + annotations: { + "@id": string + }[] + }>() +) + +export const mouseoverRegions = createAction( + `${nameSpace} mouseoverRegions`, + props<{ + regions: SapiRegionModel[] + }>() +) diff --git a/src/state/userInteraction/const.ts b/src/state/userInteraction/const.ts new file mode 100644 index 0000000000000000000000000000000000000000..322454b4575be2ad44f6f7a251a46fc087db1e0c --- /dev/null +++ b/src/state/userInteraction/const.ts @@ -0,0 +1 @@ +export const nameSpace = `[state.userInteraction]` \ No newline at end of file diff --git a/src/state/userInteraction/effects.ts b/src/state/userInteraction/effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee8baa706e67df5746b163fa59f11cb01855bc33 --- /dev/null +++ b/src/state/userInteraction/effects.ts @@ -0,0 +1,17 @@ +import { Injectable } from "@angular/core"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import * as actions from "./actions" +import * as atlasSelectionActions from "../atlasSelection/actions" +import { mapTo } from "rxjs/operators"; + +@Injectable() +export class Effect { + onStandAloneVolumesExistCloseMatDrawer = createEffect(() => this.action.pipe( + ofType(atlasSelectionActions.clearStandAloneVolumes), + mapTo(actions.closeSidePanel()) + )) + + constructor(private action: Actions){ + + } +} \ No newline at end of file diff --git a/src/state/userInteraction/index.ts b/src/state/userInteraction/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2bad8f6b313c2a4a4f8c7afff8f5def60db6d43c --- /dev/null +++ b/src/state/userInteraction/index.ts @@ -0,0 +1,5 @@ +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 diff --git a/src/state/userInteraction/selectors.ts b/src/state/userInteraction/selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b9b0bd79c821d3d21e31b6482894340775974b4 --- /dev/null +++ b/src/state/userInteraction/selectors.ts @@ -0,0 +1,11 @@ +import { createAction, createSelector, props } from "@ngrx/store"; +import { SapiRegionModel } from "src/atlasComponents/sapi"; +import { nameSpace } from "./const" +import { UserInteraction } from "./store"; + +const selectStore = state => state[nameSpace] as UserInteraction + +export const mousingOverRegions = createSelector( + selectStore, + state => state.mouseoverRegions +) diff --git a/src/state/userInteraction/store.ts b/src/state/userInteraction/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..83bf75863f3c1c76733851f74a0930dea37afc5a --- /dev/null +++ b/src/state/userInteraction/store.ts @@ -0,0 +1,24 @@ +import { createReducer, on } from "@ngrx/store"; +import { SapiRegionModel } from "src/atlasComponents/sapi"; +import * as actions from "./actions" + +export type UserInteraction = { + mouseoverRegions: SapiRegionModel[] +} + +const defaultState: UserInteraction = { + mouseoverRegions: [] +} + +export const reducer = createReducer( + defaultState, + on( + actions.mouseoverRegions, + (state, { regions }) => { + return { + ...state, + mouseoverRegions: regions + } + } + ) +) \ No newline at end of file diff --git a/src/state/userInterface/actions.ts b/src/state/userInterface/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..f229006bc473dc280521befb7e4cf1ae8fa78c6a --- /dev/null +++ b/src/state/userInterface/actions.ts @@ -0,0 +1,46 @@ +import { TemplateRef } from "@angular/core"; +import { MatBottomSheetConfig } from "@angular/material/bottom-sheet"; +import { MatSnackBarConfig } from "@angular/material/snack-bar"; +import { createAction, props } from "@ngrx/store"; +import { nameSpace } from "./const" + +export const showFeature = createAction( + `${nameSpace} showFeature`, + props<{ + feature: { + "@id": string + } + }>() +) + +export const clearShownFeature = createAction( + `${nameSpace} clearShownFeature`, +) + +export const openSidePanel = createAction( + `${nameSpace} openSidePanel` +) + +export const closeSidePanel = createAction( + `${nameSpace} closeSidePanel` +) + +export const expandSidePanelDetailView = createAction( + `${nameSpace} expandDetailView` +) + +export const showBottomSheet = createAction( + `${nameSpace} showBottomSheet`, + props<{ + template: TemplateRef<any> + config?: MatBottomSheetConfig + }>() +) + +export const snackBarMessage = createAction( + `${nameSpace} snackBarMessage`, + props<{ + message: string + config?: MatSnackBarConfig + }>() +) diff --git a/src/state/userInterface/const.ts b/src/state/userInterface/const.ts new file mode 100644 index 0000000000000000000000000000000000000000..1033ee3687f1571cc6f6125f949cb7660d0673b2 --- /dev/null +++ b/src/state/userInterface/const.ts @@ -0,0 +1 @@ +export const nameSpace = `[state.ui]` \ No newline at end of file diff --git a/src/state/userInterface/effects.ts b/src/state/userInterface/effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..254cf22a40b32c9cd910e624175c85bb6f64622f --- /dev/null +++ b/src/state/userInterface/effects.ts @@ -0,0 +1,56 @@ +import { Injectable } from "@angular/core"; +import { MatBottomSheet, MatBottomSheetRef } from "@angular/material/bottom-sheet"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { select, Store } from "@ngrx/store"; +import { filter, map, mapTo, pairwise, startWith } from "rxjs/operators"; +import { selectors } from "../atlasSelection" +import * as actions from "./actions" + +@Injectable() +export class Effects{ + + freshRegionSelect = this.store.pipe( + select(selectors.selectedRegions), + map(selReg => selReg.length), + startWith(0), + pairwise(), + filter(([prev, curr]) => prev === 0 && curr > 0), + ) + + onFreshRegionSelectSidePanelOpen = createEffect(() => this.freshRegionSelect.pipe( + mapTo(actions.openSidePanel()), + )) + + onFreshRegionSelectSidePanelDetailExpand = createEffect(() => this.freshRegionSelect.pipe( + mapTo(actions.expandSidePanelDetailView()) + )) + + private bottomSheetRef: MatBottomSheetRef + constructor( + private store: Store, + private action: Actions, + bottomsheet: MatBottomSheet, + snackbar: MatSnackBar, + ){ + this.action.pipe( + ofType(actions.showBottomSheet) + ).subscribe(({ template, config }) => { + if (this.bottomSheetRef) { + this.bottomSheetRef.dismiss() + } + this.bottomSheetRef = bottomsheet.open( + template, + config + ) + this.bottomSheetRef.afterDismissed().subscribe(() => this.bottomSheetRef = null) + }) + + this.action.pipe( + ofType(actions.snackBarMessage) + ).subscribe(({ message, config }) => { + const _config = config || { duration: 5000 } + snackbar.open(message, "Dismiss", _config) + }) + } +} diff --git a/src/state/userInterface/index.ts b/src/state/userInterface/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0b98013604df6b9bb5923a4a3a84f80d39c7b52 --- /dev/null +++ b/src/state/userInterface/index.ts @@ -0,0 +1,4 @@ +export * as actions from "./actions" +export * as selectors from "./selectors" +export { nameSpace } from "./const" +export { reducer } from "./store" \ No newline at end of file diff --git a/src/state/userInterface/selectors.ts b/src/state/userInterface/selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..fd36b44ec95d78fb394e3c17ad785661e02d7fa8 --- /dev/null +++ b/src/state/userInterface/selectors.ts @@ -0,0 +1,10 @@ +import { createSelector } from "@ngrx/store"; +import { nameSpace } from "./const" +import { UiStore } from "./store" + +const selectStore = state => state[nameSpace] as UiStore + +export const selectedFeature = createSelector( + selectStore, + state => state.selectedFeature +) diff --git a/src/state/userInterface/store.ts b/src/state/userInterface/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..400e7983b59269a4c47983d42797d3b63e2ea788 --- /dev/null +++ b/src/state/userInterface/store.ts @@ -0,0 +1,33 @@ +import { createReducer, on } from "@ngrx/store"; +import { SapiVolumeModel } from "src/atlasComponents/sapi"; +import * as actions from "./actions" + +export type UiStore = { + selectedFeature: SapiVolumeModel +} + +const defaultStore: UiStore = { + selectedFeature: null +} + +export const reducer = createReducer( + defaultStore, + on( + actions.showFeature, + (state, { feature }) => { + return { + ...state, + feature + } + } + ), + on( + actions.clearShownFeature, + state => { + return { + ...state, + feature: null + } + } + ) +) diff --git a/src/state/userInterface/ui.ts b/src/state/userInterface/ui.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/ui/config/configCmp/config.component.ts b/src/ui/config/configCmp/config.component.ts index 15f92277c707e22aeeb25cb64dec3e562f907030..f34ca0eec31149a10ab3b96a425b2cf8ebfd5bee 100644 --- a/src/ui/config/configCmp/config.component.ts +++ b/src/ui/config/configCmp/config.component.ts @@ -12,7 +12,7 @@ import {MatSliderChange} from "@angular/material/slider"; import { PureContantService } from 'src/util'; import { ngViewerActionSwitchPanelMode } from 'src/services/state/ngViewerState/actions'; import { ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from 'src/services/state/ngViewerState/selectors'; -import { viewerStateSelectorNavigation } from 'src/services/state/viewerState/selectors'; +import { atlasSelection } from 'src/state'; const GPU_TOOLTIP = `Higher GPU usage can cause crashes on lower end machines` const ANIMATION_TOOLTIP = `Animation can cause slowdowns in lower end machines` @@ -84,7 +84,7 @@ export class ConfigComponent implements OnInit, OnDestroy { ) this.viewerObliqueRotated$ = this.store.pipe( - select(viewerStateSelectorNavigation), + select(atlasSelection.selectors.navigation), map(navigation => (navigation && navigation.orientation) || [0, 0, 0, 1]), debounceTime(100), map(isIdentityQuat), diff --git a/src/ui/dialogInfo/const.ts b/src/ui/dialogInfo/const.ts new file mode 100644 index 0000000000000000000000000000000000000000..c787856708abba3e57f01fe1779cac6da3fa4c2b --- /dev/null +++ b/src/ui/dialogInfo/const.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from "@angular/core"; + +export const IAV_DATASET_SHOW_DATASET_DIALOG_CMP = new InjectionToken('IAV_DATASET_SHOW_DATASET_DIALOG_CMP') \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.ts b/src/ui/dialogInfo/dialog.directive.ts similarity index 73% rename from src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.ts rename to src/ui/dialogInfo/dialog.directive.ts index d4b02e120d85776b02b0db606da5aa059cd46113..e05ef5fdec865b152600285cee13675ba1467f18 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.ts +++ b/src/ui/dialogInfo/dialog.directive.ts @@ -2,17 +2,14 @@ import { Directive, HostListener, Inject, Input, Optional } from "@angular/core" import { MatDialog, MatDialogConfig } from "@angular/material/dialog"; import { MatSnackBar } from "@angular/material/snack-bar"; import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, TOverwriteShowDatasetDialog } from "src/util/interfaces"; -import { TRegionDetail as TSiibraRegion } from "src/util/siibraApiConstants/types"; -import { TRegion as TContextRegion } from 'src/atlasComponents/regionalFeatures/bsFeatures/type' - -export const IAV_DATASET_SHOW_DATASET_DIALOG_CMP = 'IAV_DATASET_SHOW_DATASET_DIALOG_CMP' -export const IAV_DATASET_SHOW_DATASET_DIALOG_CONFIG = `IAV_DATASET_SHOW_DATASET_DIALOG_CONFIG` +import { IAV_DATASET_SHOW_DATASET_DIALOG_CMP } from "./const"; @Directive({ - selector: '[iav-dataset-show-dataset-dialog]', - exportAs: 'iavDatasetShowDatasetDialog' + selector: `[iav-dataset-show-dataset-dialog]`, + exportAs: 'iavDatasetShowDatasetDialog', }) -export class ShowDatasetDialogDirective{ + +export class DialogDirective{ static defaultDialogConfig: MatDialogConfig = { autoFocus: false @@ -25,13 +22,20 @@ export class ShowDatasetDialogDirective{ description: string @Input('iav-dataset-show-dataset-dialog-kgschema') - kgSchema: string = 'minds/core/dataset/v1.0.0' + set kgSchema(val) { + throw new Error(`setting kgschema & kgid has been deprecated`) + } @Input('iav-dataset-show-dataset-dialog-kgid') - kgId: string + set kgId(val) { + throw new Error(`setting kgschema & kgid has been deprecated`) + } @Input('iav-dataset-show-dataset-dialog-fullid') - fullId: string + set fullId(val) { + throw new Error(`setting fullid has been deprecated`) + } + @Input('iav-dataset-show-dataset-dialog-urls') urls: { @@ -42,8 +46,6 @@ export class ShowDatasetDialogDirective{ @Input('iav-dataset-show-dataset-dialog-ignore-overwrite') ignoreOverwrite = false - @Input('iav-dataset-show-dataset-dialog-contexted-region') - region: TSiibraRegion & TContextRegion constructor( private matDialog: MatDialog, @@ -77,9 +79,9 @@ export class ShowDatasetDialogDirective{ if (!this.dialogCmp) throw new Error(`IAV_DATASET_SHOW_DATASET_DIALOG_CMP not provided!`) const { useClassicUi } = data this.matDialog.open(this.dialogCmp, { - ...ShowDatasetDialogDirective.defaultDialogConfig, + ...DialogDirective.defaultDialogConfig, data, ...(useClassicUi ? {} : { panelClass: ['no-padding-dialog'] }) }) } -} +} \ No newline at end of file diff --git a/src/ui/dialogInfo/dialog/dialog.component.ts b/src/ui/dialogInfo/dialog/dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e478f1cc88ad9e4b9798dd6d8bc5e8ea4bfe2f42 --- /dev/null +++ b/src/ui/dialogInfo/dialog/dialog.component.ts @@ -0,0 +1,22 @@ +import { Component, Inject, Optional } from "@angular/core" +import { MAT_DIALOG_DATA } from "@angular/material/dialog" +import { CONST, ARIA_LABELS } from "common/constants" + +@Component({ + templateUrl: './dialog.template.html', + styleUrls: [ + './dialog.style.css' + ] +}) + +export class DialogCmp{ + public useClassicUi = false + public CONST = CONST + public ARIA_LABELS = ARIA_LABELS + constructor( + @Optional() @Inject(MAT_DIALOG_DATA) public data: any + ){ + const { dataType, description, name, urls, useClassicUi, view, region, summary, isGdprProtected } = data + this.useClassicUi = data.useClassicUi + } +} \ No newline at end of file diff --git a/src/ui/dialogInfo/dialog/dialog.style.css b/src/ui/dialogInfo/dialog/dialog.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.template.html b/src/ui/dialogInfo/dialog/dialog.template.html similarity index 83% rename from src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.template.html rename to src/ui/dialogInfo/dialog/dialog.template.html index 2bce44e578033cd6aa517f3dae798aafb6944bc4..324616d965dc23ea9950d71718ab393696669c93 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.template.html +++ b/src/ui/dialogInfo/dialog/dialog.template.html @@ -1,6 +1,6 @@ <!-- classic UI --> -<ng-template [ngIf]="useClassicUi" [ngIfElse]="modernUi"> +<ng-template [ngIf]="data.useClassicUi" [ngIfElse]="modernUi"> <mat-card-subtitle> <ng-container *ngTemplateOutlet="nameTmpl"> </ng-container> @@ -27,7 +27,7 @@ <!-- explore --> <ng-container> - <a *ngFor="let kgRef of (doiUrls || [])" + <a *ngFor="let kgRef of (data.urls || [])" [href]="kgRef.doi | doiParserPipe" class="color-inherit" [matTooltip]="ARIA_LABELS.EXPLORE_DATASET_IN_KG" @@ -61,10 +61,10 @@ <mat-card-subtitle class="d-inline-flex align-items-center"> <mat-icon fontSet="fas" fontIcon="fa-database"></mat-icon> <span> - {{ dataType }} + {{ data?.dataType || 'Dataset' }} </span> - <button *ngIf="isGdprProtected" + <button *ngIf="data?.isGdprProtected" [matTooltip]="CONST.GDPR_TEXT" mat-icon-button color="warn"> <i class="fas fa-exclamation-triangle"></i> @@ -73,7 +73,7 @@ <mat-divider [vertical]="true" class="ml-2 h-2rem"></mat-divider> <!-- explore btn --> - <a *ngFor="let kgRef of (urls || [])" + <a *ngFor="let kgRef of (data.urls || [])" [href]="kgRef.doi | doiParserPipe" class="color-inherit" mat-icon-button @@ -88,8 +88,7 @@ </mat-card> <!-- description --> - <div class="text-muted d-block mat-body m-4" - *ngIf="!loadingFlag"> + <div class="text-muted d-block mat-body m-4"> <ng-container *ngTemplateOutlet="descTmpl"> </ng-container> </div> @@ -101,15 +100,13 @@ </ng-template> <ng-template #nameTmpl> - <span *ngIf="!loadingFlag; else isLoadingTmpl"> - {{ name || nameFallback }} - </span> + {{ data?.name || 'Unknown Name' }} </ng-template> <!-- desc --> <ng-template #descTmpl> <markdown-dom - [markdown]="description || descriptionFallback"> + [markdown]="data?.description || 'Unknown Desc'"> </markdown-dom> </ng-template> @@ -119,8 +116,3 @@ </ng-template> </ng-template> - -<!-- is loading tmpl --> -<ng-template #isLoadingTmpl> - <spinner-cmp></spinner-cmp> -</ng-template> diff --git a/src/ui/dialogInfo/index.ts b/src/ui/dialogInfo/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e78523f63c467b78ed512627163806b9e46ca510 --- /dev/null +++ b/src/ui/dialogInfo/index.ts @@ -0,0 +1,5 @@ +export { + DialogInfoModule, + DialogDirective, + DialogCmp, +} from "./module" diff --git a/src/ui/dialogInfo/module.ts b/src/ui/dialogInfo/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa02156c5245ab5bdc44c7d79b9279b637d699e4 --- /dev/null +++ b/src/ui/dialogInfo/module.ts @@ -0,0 +1,38 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { ComponentsModule } from "src/components"; +import { AngularMaterialModule } from "src/sharedModules"; +import { UtilModule } from "src/util"; +import { IAV_DATASET_SHOW_DATASET_DIALOG_CMP } from "./const"; +import { DialogDirective } from "./dialog.directive" +import { DialogCmp } from "./dialog/dialog.component" + +@NgModule({ + imports: [ + CommonModule, + AngularMaterialModule, + UtilModule, + ComponentsModule, + ], + declarations: [ + DialogDirective, + DialogCmp, + ], + exports: [ + DialogDirective, + DialogCmp, + ], + providers: [ + { + provide: IAV_DATASET_SHOW_DATASET_DIALOG_CMP, + useValue: DialogCmp + } + ] +}) + +export class DialogInfoModule{} + +export { + DialogDirective, + DialogCmp, +} \ No newline at end of file diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index c33043639f957bbe8640db5e3c5104d682c14958..92ecdd79f20feb0060cc996e7329549eb2de5a33 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -26,11 +26,11 @@ import { ActionDialog } from "./actionDialog/actionDialog.component"; import { APPEND_SCRIPT_TOKEN, appendScriptFactory } from "src/util/constants"; import { DOCUMENT } from "@angular/common"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { RegionalFeaturesModule } from "../atlasComponents/regionalFeatures"; import { Landmark2DModule } from "./nehubaContainer/2dLandmarks/module"; import { HANDLE_SCREENSHOT_PROMISE, TypeHandleScrnShotPromise } from "../screenshot"; import { ParcellationRegionModule } from "src/atlasComponents/parcellationRegion"; import { AtlasCmpParcellationModule } from "src/atlasComponents/parcellation"; +import { DialogInfoModule } from "./dialogInfo" @NgModule({ imports : [ @@ -45,10 +45,10 @@ import { AtlasCmpParcellationModule } from "src/atlasComponents/parcellation"; AngularMaterialModule, ShareModule, AuthModule, - RegionalFeaturesModule, Landmark2DModule, ParcellationRegionModule, AtlasCmpParcellationModule, + DialogInfoModule, ], declarations : [ diff --git a/src/util/constants.ts b/src/util/constants.ts index 84ee596993c519223fc8f28487af1a0cb8372f4d..ee179b2d40b4eeb7bb1bc533000034f6ba6b77e9 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -4,7 +4,6 @@ import { environment } from 'src/environments/environment' export const LOCAL_STORAGE_CONST = { GPU_LIMIT: 'fzj.xg.iv.GPU_LIMIT', ANIMATION: 'fzj.xg.iv.ANIMATION_FLAG', - SAVED_REGION_SELECTIONS: 'fzj.xg.iv.SAVED_REGION_SELECTIONS', MOBILE_UI: 'fzj.xg.iv.MOBILE_UI', AGREE_COOKIE: 'fzj.xg.iv.AGREE_COOKIE', AGREE_KG_TOS: 'fzj.xg.iv.AGREE_KG_TOS', diff --git a/src/util/fn.spec.ts b/src/util/fn.spec.ts index 61bb2fd8cb23665bc85aacb76b298f026d2f89aa..efd5e418d29f6d46df2442bb8d02cb1a63b14840 100644 --- a/src/util/fn.spec.ts +++ b/src/util/fn.spec.ts @@ -3,78 +3,10 @@ import {} from 'jasmine' import { hot } from 'jasmine-marbles' import { Observable, of } from 'rxjs' import { switchMap } from 'rxjs/operators' -import { isSame, getGetRegionFromLabelIndexId, switchMapWaitFor, bufferUntil } from './fn' +import { switchMapWaitFor, bufferUntil } from './fn' describe(`> util/fn.ts`, () => { - describe('> #getGetRegionFromLabelIndexId', () => { - - const COLIN_JULICHBRAIN_LAYER_NAME = `COLIN_V25_LEFT_NG_SPLIT_HEMISPHERE` - const LABEL_INDEX = 12 - const dummyParc = { - regions: [ - { - name: 'foo-bar', - children: [ - { - name: 'foo-bar-region-ba', - ngId: `${COLIN_JULICHBRAIN_LAYER_NAME}-ba`, - labelIndex: LABEL_INDEX - }, - { - name: 'foo-bar-region+1', - ngId: COLIN_JULICHBRAIN_LAYER_NAME, - labelIndex: LABEL_INDEX + 1 - }, - { - name: 'foo-bar-region', - ngId: COLIN_JULICHBRAIN_LAYER_NAME, - labelIndex: LABEL_INDEX - } - ] - } - ] - } - it('translateds hoc1 from labelIndex to region', () => { - - const getRegionFromlabelIndexId = getGetRegionFromLabelIndexId({ - parcellation: { - ...dummyParc, - updated: true, - }, - }) - const fetchedRegion = getRegionFromlabelIndexId({ labelIndexId: `${COLIN_JULICHBRAIN_LAYER_NAME}#${LABEL_INDEX}` }) - expect(fetchedRegion).toBeTruthy() - expect(fetchedRegion.name).toEqual('foo-bar-region') - - }) - }) - describe(`> #isSame`, () => { - it('should return true with null, null', () => { - expect(isSame(null, null)).toBe(true) - }) - - it('should return true with string', () => { - expect(isSame('test', 'test')).toBe(true) - }) - - it(`should return true with numbers`, () => { - expect(isSame(12, 12)).toBe(true) - }) - - it('should return true with obj with name attribute', () => { - - const obj = { - name: 'hello', - } - const obj2 = { - name: 'hello', - world: 'world', - } - expect(isSame(obj, obj2)).toBe(true) - expect(obj).not.toEqual(obj2) - }) - }) describe('> #switchMapWaitFor', () => { const val = 'hello world' diff --git a/src/util/fn.ts b/src/util/fn.ts index 479958c477576d2e34b7cc8f1be6243048024992..e5ef8ac6bde4b72b67c2fb76f7b94edde8123476 100644 --- a/src/util/fn.ts +++ b/src/util/fn.ts @@ -1,12 +1,6 @@ -import { deserialiseParcRegionId } from 'common/util' import { interval, Observable, of } from 'rxjs' import { filter, mapTo, take } from 'rxjs/operators' -export function isSame(o, n) { - if (!o) { return !n } - return o === n || (o && n && o.name === n.name) -} - export function getViewer() { return (window as any).viewer } @@ -45,26 +39,10 @@ const recursiveFlatten = (region, {ngId}) => { ) } -export function recursiveFindRegionWithLabelIndexId({ regions, labelIndexId, inheritedNgId = 'root' }: {regions: any[], labelIndexId: string, inheritedNgId: string}) { - const { ngId, labelIndex } = deserialiseParcRegionId( labelIndexId ) - const fr1 = regions.map(r => recursiveFlatten(r, { ngId: inheritedNgId })) - const fr2 = fr1.reduce((acc, curr) => acc.concat(...curr), []) - const found = fr2.find(r => r.ngId === ngId && Number(r.labelIndex) === Number(labelIndex)) - if (found) { return found } - return null -} - export function getUuid(){ return crypto.getRandomValues(new Uint32Array(1))[0].toString(16) } -export const getGetRegionFromLabelIndexId = ({ parcellation }) => { - const { ngId: defaultNgId, regions } = parcellation - // if (!updated) throw new Error(`parcellation not yet updated`) - return ({ labelIndexId }) => - recursiveFindRegionWithLabelIndexId({ regions, labelIndexId, inheritedNgId: defaultNgId }) -} - type TPrimitive = string | number const include = <T extends TPrimitive>(el: T, arr: T[]) => arr.indexOf(el) >= 0 @@ -131,8 +109,7 @@ export class QuickHash { if (opts?.length) this.length = opts.length } - @CachedFunction() - getHash(str: string){ + static GetHash(str: string) { let hash = 0 for (const char of str) { const charCode = char.charCodeAt(0) @@ -141,6 +118,11 @@ export class QuickHash { } return hash.toString(16).slice(1) } + + @CachedFunction() + getHash(str: string){ + return QuickHash.GetHash(str) + } } // fsaverage uses threesurfer, which, whilst do not use ngId, uses 'left' and 'right' as keys diff --git a/src/util/interfaces.ts b/src/util/interfaces.ts index e5c2fbd9686a3f1036a5e34959c2e697e9f8d1a4..21de2b34d51872fb0ead9e8c2f6f2299a3f278f2 100644 --- a/src/util/interfaces.ts +++ b/src/util/interfaces.ts @@ -20,7 +20,6 @@ export const OVERWRITE_SHOW_DATASET_DIALOG_TOKEN = new InjectionToken<TOverwrite export type TRegionOfInterest = { ['fullId']: string } -export const REGION_OF_INTEREST = new InjectionToken<Observable<TRegionOfInterest>>('RegionOfInterest') export const CANCELLABLE_DIALOG = new InjectionToken('CANCELLABLE_DIALOG') export type TTemplateImage = { diff --git a/src/util/priority.ts b/src/util/priority.ts new file mode 100644 index 0000000000000000000000000000000000000000..441ed5e0a5270854eeada43ea220ab80054392ef --- /dev/null +++ b/src/util/priority.ts @@ -0,0 +1,92 @@ +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http" +import { Injectable } from "@angular/core" +import { interval, merge, Observable, Subject, timer } from "rxjs" +import { filter, finalize, switchMap, switchMapTo, take } from "rxjs/operators" + +export const PRIORITY_HEADER = 'x-sxplr-http-priority' + +type PriorityReq = { + urlWithParams: string + priority: number + req: HttpRequest<any> + next: HttpHandler +} + +@Injectable() +export class PriorityHttpInterceptor implements HttpInterceptor{ + + private priorityQueue: PriorityReq[] = [] + + private priority$: Subject<PriorityReq> = new Subject() + + private forceCheck$ = new Subject() + + private counter = 0 + private max = 6 + + private shouldRun(){ + return this.counter <= this.max + } + + constructor(){ + this.forceCheck$.pipe( + switchMapTo( + merge( + timer(0), + interval(16) + ).pipe( + filter(() => this.shouldRun()) + ) + ) + ).subscribe(() => { + this.priority$.next( + this.priorityQueue.pop() + ) + }) + } + + updatePriority(urlWithParams: string, newPriority: number) { + const foundIdx = this.priorityQueue.findIndex(v => v.urlWithParams === urlWithParams) + if (foundIdx < 0) return false + const [ item ] = this.priorityQueue.splice(foundIdx, 1) + item.priority = newPriority + + this.insert(item) + this.forceCheck$.next(true) + return true + } + + private insert(obj: PriorityReq) { + const { priority } = obj + const foundIdx = this.priorityQueue.findIndex(q => q.priority <= priority) + const useIndex = foundIdx >= 0 ? foundIdx : this.priorityQueue.length + this.priorityQueue.splice(useIndex, 0, obj) + } + + intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { + const { urlWithParams } = req + + const priority = Number(req.headers.get(PRIORITY_HEADER) || 0) + const objToInsert: PriorityReq = { + priority, + req, + next, + urlWithParams + } + + this.insert(objToInsert) + this.forceCheck$.next(true) + + return this.priority$.pipe( + filter(v => v.urlWithParams === urlWithParams), + take(1), + switchMap(({ next, req }) => { + this.counter ++ + return next.handle(req) + }), + finalize(() => { + this.counter -- + }), + ) + } +} \ No newline at end of file diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts index 98eff755d9e3ea11d4f03b6d3a89a098e4c2da94..9ffc4b9e992880421d931a88083f73bed093dee5 100644 --- a/src/util/pureConstant.service.ts +++ b/src/util/pureConstant.service.ts @@ -4,20 +4,14 @@ import { Observable, Subscription, of, forkJoin, combineLatest, from } from "rxj import { viewerConfigSelectorUseMobileUi } from "src/services/state/viewerConfig.store.helper"; import { shareReplay, tap, scan, catchError, filter, switchMap, map, distinctUntilChanged, mapTo } from "rxjs/operators"; import { HttpClient } from "@angular/common/http"; -import { viewerStateFetchedTemplatesSelector, viewerStateSetFetchedAtlases } from "src/services/state/viewerState.store.helper"; import { LoggingService } from "src/logging"; -import { viewerStateFetchedAtlasesSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; import { BS_ENDPOINT, BACKENDURL } from "src/util/constants"; -import { flattenReducer } from 'common/util' -import { IVolumeTypeDetail, TAtlas, TId, TParc, TRegionDetail, TRegionSummary, TSpaceFull, TSpaceSummary, TVolumeSrc } from "./siibraApiConstants/types"; +import { TAtlas, TId, TParc, TRegionDetail, TRegionSummary, TSpaceFull, TSpaceSummary, TVolumeSrc } from "./siibraApiConstants/types"; import { MultiDimMap, recursiveMutate, mutateDeepMerge } from "./fn"; import { patchRegions } from './patchPureConstants' import { environment } from "src/environments/environment"; import { MatSnackBar } from "@angular/material/snack-bar"; -import { TTemplateImage } from "./interfaces"; - -export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version' -export const SIIBRA_API_VERSION = '0.1.11' +import { atlasSelection } from "src/state"; const validVolumeType = new Set([ 'neuroglancer/precomputed', @@ -93,93 +87,6 @@ export const spaceMiscInfoMap = new Map([ }], ]) -function getNehubaConfig(space: TSpaceFull) { - - const darkTheme = space.src_volume_type === 'mri' - const { scale } = spaceMiscInfoMap.get(space.id) || { scale: 1 } - const backgrd = darkTheme - ? [0,0,0,1] - : [1,1,1,1] - - const rmPsp = darkTheme - ? {"mode":"<","color":[0.1,0.1,0.1,1]} - :{"color":[1,1,1,1],"mode":"=="} - const drawSubstrates = darkTheme - ? {"color":[0.5,0.5,1,0.2]} - : {"color":[0,0,0.5,0.15]} - const drawZoomLevels = darkTheme - ? {"cutOff":150000 * scale } - : {"cutOff":200000 * scale,"color":[0.5,0,0,0.15] } - - // enable surface parcellation - // otherwise, on segmentation selection, the unselected meshes will also be invisible - const surfaceParcellation = space.id === 'minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992' - return { - "configName": "", - "globals": { - "hideNullImageValues": true, - "useNehubaLayout": { - "keepDefaultLayouts": false - }, - "useNehubaMeshLayer": true, - "rightClickWithCtrlGlobal": false, - "zoomWithoutCtrlGlobal": false, - "useCustomSegmentColors": true - }, - "zoomWithoutCtrl": true, - "hideNeuroglancerUI": true, - "rightClickWithCtrl": true, - "rotateAtViewCentre": true, - "enableMeshLoadingControl": true, - "zoomAtViewCentre": true, - // "restrictUserNavigation": true, - "dataset": { - "imageBackground": backgrd, - "initialNgState": { - "showDefaultAnnotations": false, - "layers": {}, - "navigation": { - "zoomFactor": 350000 * scale, - }, - "perspectiveOrientation": [ - 0.3140767216682434, - -0.7418519854545593, - 0.4988985061645508, - -0.3195493221282959 - ], - "perspectiveZoom": 1922235.5293810747 * scale - } - }, - "layout": { - "useNehubaPerspective": { - "perspectiveSlicesBackground": backgrd, - "removePerspectiveSlicesBackground": rmPsp, - "perspectiveBackground": backgrd, - "fixedZoomPerspectiveSlices": { - "sliceViewportWidth": 300, - "sliceViewportHeight": 300, - "sliceZoom": 563818.3562426177 * scale, - "sliceViewportSizeMultiplier": 2 - }, - "mesh": { - "backFaceColor": backgrd, - "removeBasedOnNavigation": true, - "flipRemovedOctant": true, - surfaceParcellation - }, - "centerToOrigin": true, - "drawSubstrates": drawSubstrates, - "drawZoomLevels": drawZoomLevels, - "restrictZoomLevel": { - "minZoom": 1200000 * scale, - "maxZoom": 3500000 * scale - } - } - } - } - -} - @Injectable({ providedIn: 'root' }) @@ -202,10 +109,6 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" public totalAtlasesLength: number - public allFetchingReady$: Observable<boolean> - - private atlasParcSpcRegionMap = new MultiDimMap() - private _backendUrl = (BACKENDURL && `${BACKENDURL}/`.replace(/\/\/$/, '/')) || `${window.location.origin}${window.location.pathname}` get backendUrl() { console.warn(`something is using backendUrl`) @@ -396,9 +299,11 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" private snackbar: MatSnackBar, @Inject(BS_ENDPOINT) private bsEndpoint: string, ){ + + // TODO how do we find out which theme to use now? this.darktheme$ = this.store.pipe( - select(viewerStateSelectedTemplateSelector), - map(tmpl => tmpl?.useTheme === 'dark') + select(atlasSelection.selectors.selectedTemplate), + map(tmpl => !!(tmpl && tmpl["@id"] !== 'minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588')) ) this.useTouchUI$ = this.store.pipe( @@ -406,127 +311,95 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" shareReplay(1) ) - this.subscriptions.push( - this.fetchedAtlases$.subscribe(fetchedAtlases => - this.store.dispatch( - viewerStateSetFetchedAtlases({ fetchedAtlases }) - ) - ) - ) - - this.allFetchingReady$ = combineLatest([ - this.initFetchTemplate$.pipe( - filter(v => !!v), - map(arr => arr.length), - ), - this.store.pipe( - select(viewerStateFetchedTemplatesSelector), - map(arr => arr.length), - ), - this.store.pipe( - select(viewerStateFetchedAtlasesSelector), - map(arr => arr.length), - ) - ]).pipe( - map(([ expNumTmpl, actNumTmpl, actNumAtlas ]) => { - return expNumTmpl === actNumTmpl && actNumAtlas === this.totalAtlasesLength - }), - distinctUntilChanged(), - shareReplay(1), - ) + // this.allFetchingReady$ = combineLatest([ + // this.initFetchTemplate$.pipe( + // filter(v => !!v), + // map(arr => arr.length), + // ), + // this.store.pipe( + // select(viewerStateFetchedTemplatesSelector), + // map(arr => arr.length), + // ), + // this.store.pipe( + // select(viewerStateFetchedAtlasesSelector), + // map(arr => arr.length), + // ) + // ]).pipe( + // map(([ expNumTmpl, actNumTmpl, actNumAtlas ]) => { + // return expNumTmpl === actNumTmpl && actNumAtlas === this.totalAtlasesLength + // }), + // distinctUntilChanged(), + // shareReplay(1), + // ) } - private getAtlases$ = this.http.get<TAtlas[]>( - `${this.bsEndpoint}/atlases`, - { - observe: 'response' - } - ).pipe( - tap(resp => { - const respVersion = resp.headers.get(SIIBRA_API_VERSION_HEADER_KEY) - if (respVersion !== SIIBRA_API_VERSION) { - this.snackbar.open(`Expecting ${SIIBRA_API_VERSION}, got ${respVersion}. Some functionalities may not work as expected.`, 'Dismiss', { - duration: 5000 - }) - } - console.log(`siibra-api::version::${respVersion}, expecting::${SIIBRA_API_VERSION}`) - }), - map(resp => { - const arr = resp.body - const { EXPERIMENTAL_FEATURE_FLAG } = environment - if (EXPERIMENTAL_FEATURE_FLAG) return arr - return arr - }), - shareReplay(1), - ) - - public fetchedAtlases$: Observable<TIAVAtlas[]> = this.getAtlases$.pipe( - switchMap(atlases => { - return forkJoin( - atlases.map( - atlas => this.getSpacesAndParc(atlas.id).pipe( - map(({ templateSpaces, parcellations }) => { - return { - '@id': atlas.id, - name: atlas.name, - templateSpaces: templateSpaces.map(tmpl => { - return { - '@id': tmpl.id, - name: tmpl.name, - availableIn: tmpl.availableParcellations.map(parc => { - return { - '@id': parc.id, - name: parc.name - } - }), - originDatainfos: (tmpl._dataset_specs || []).filter(spec => spec["@type"] === 'fzj/tmp/simpleOriginInfo/v0.0.1') - } - }), - parcellations: parcellations.filter(p => { - if (p.version?.deprecated) return false - return true - }).map(parc => { - return { - '@id': parseId(parc.id), - name: parc.name, - baseLayer: parc.modality === 'cytoarchitecture', - '@version': { - '@next': parc.version?.next, - '@previous': parc.version?.prev, - 'name': parc.version?.name, - '@this': parseId(parc.id) - }, - groupName: parc.modality || null, - availableIn: parc.availableSpaces.map(space => { - return { - '@id': space.id, - name: space.name, - /** - * TODO need original data format - */ - // originalDatasetFormats: [{ - // name: "probability map" - // }] - } - }), - originDatainfos: [...(parc.infos || []), ...(parc._dataset_specs || []).filter(spec => spec["@type"] === 'fzj/tmp/simpleOriginInfo/v0.0.1')] - } - }) - } - }), - catchError((err, obs) => { - console.error(err) - return of(null) - }) - ) - ) - ) - }), - catchError((err, obs) => of([])), - tap((arr: any[]) => this.totalAtlasesLength = arr.length), - scan((acc, curr) => acc.concat(curr).sort((a, b) => (a.order || 0) - (b.order || 0)), []), - shareReplay(1) - ) + // public fetchedAtlases$: Observable<TIAVAtlas[]> = this.getAtlases$.pipe( + // switchMap(atlases => { + // return forkJoin( + // atlases.map( + // atlas => this.getSpacesAndParc(atlas.id).pipe( + // map(({ templateSpaces, parcellations }) => { + // return { + // '@id': atlas.id, + // name: atlas.name, + // templateSpaces: templateSpaces.map(tmpl => { + // return { + // '@id': tmpl.id, + // name: tmpl.name, + // availableIn: tmpl.availableParcellations.map(parc => { + // return { + // '@id': parc.id, + // name: parc.name + // } + // }), + // originDatainfos: (tmpl._dataset_specs || []).filter(spec => spec["@type"] === 'fzj/tmp/simpleOriginInfo/v0.0.1') + // } + // }), + // parcellations: parcellations.filter(p => { + // if (p.version?.deprecated) return false + // return true + // }).map(parc => { + // return { + // '@id': parseId(parc.id), + // name: parc.name, + // baseLayer: parc.modality === 'cytoarchitecture', + // '@version': { + // '@next': parc.version?.next, + // '@previous': parc.version?.prev, + // 'name': parc.version?.name, + // '@this': parseId(parc.id) + // }, + // groupName: parc.modality || null, + // availableIn: parc.availableSpaces.map(space => { + // return { + // '@id': space.id, + // name: space.name, + // /** + // * TODO need original data format + // */ + // // originalDatasetFormats: [{ + // // name: "probability map" + // // }] + // } + // }), + // originDatainfos: [...(parc.infos || []), ...(parc._dataset_specs || []).filter(spec => spec["@type"] === 'fzj/tmp/simpleOriginInfo/v0.0.1')] + // } + // }) + // } + // }), + // catchError((err, obs) => { + // console.error(err) + // return of(null) + // }) + // ) + // ) + // ) + // }), + // catchError((err, obs) => of([])), + // tap((arr: any[]) => this.totalAtlasesLength = arr.length), + // scan((acc, curr) => acc.concat(curr).sort((a, b) => (a.order || 0) - (b.order || 0)), []), + // shareReplay(1) + // ) private atlasTmplConfig: TAtlasTmplViewerConfig = {} @@ -536,316 +409,316 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" return templateLayers || {} } - public initFetchTemplate$ = this.fetchedAtlases$.pipe( - switchMap(atlases => { - return forkJoin( - atlases.map(atlas => this.getSpacesAndParc(atlas['@id']).pipe( - switchMap(({ templateSpaces, parcellations }) => { - this.atlasTmplConfig[atlas["@id"]] = {} - return forkJoin( - templateSpaces.map( - tmpl => { - // hardcode - // see https://github.com/FZJ-INM1-BDA/siibra-python/issues/98 - if ( - tmpl.id === 'minds/core/referencespace/v1.0.0/tmp-fsaverage' - && !tmpl.availableParcellations.find(p => p.id === 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290') - ) { - tmpl.availableParcellations.push({ - id: 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290', - name: 'Julich-Brain Probabilistic Cytoarchitectonic Maps (v2.9)' - }) - } - this.atlasTmplConfig[atlas["@id"]][tmpl.id] = {} - return tmpl.availableParcellations.map( - parc => this.getRegions(atlas['@id'], parc.id, tmpl.id).pipe( - tap(regions => { - recursiveMutate( - regions, - region => region.children, - region => { - /** - * individual map(s) - * this should work for both fully mapped and interpolated - * in the case of interpolated, it sucks that the ngLayerObj will be set multiple times - */ - - const dedicatedMap = region._dataset_specs.filter( - spec => spec["@type"] === 'fzj/tmp/volume_type/v0.0.1' - && spec.space_id === tmpl.id - && spec['volume_type'] === 'neuroglancer/precomputed' - ) as TVolumeSrc<'neuroglancer/precomputed'>[] - if (dedicatedMap.length === 1) { - const ngId = getNgId(atlas['@id'], tmpl.id, parc.id, dedicatedMap[0]['@id']) - region['ngId'] = ngId - region['labelIndex'] = dedicatedMap[0].detail['neuroglancer/precomputed'].labelIndex - this.atlasTmplConfig[atlas["@id"]][tmpl.id][ngId] = { - source: `precomputed://${dedicatedMap[0].url}`, - type: "segmentation", - transform: dedicatedMap[0].detail['neuroglancer/precomputed'].transform - } - } + // public initFetchTemplate$ = this.fetchedAtlases$.pipe( + // switchMap(atlases => { + // return forkJoin( + // atlases.map(atlas => this.getSpacesAndParc(atlas['@id']).pipe( + // switchMap(({ templateSpaces, parcellations }) => { + // this.atlasTmplConfig[atlas["@id"]] = {} + // return forkJoin( + // templateSpaces.map( + // tmpl => { + // // hardcode + // // see https://github.com/FZJ-INM1-BDA/siibra-python/issues/98 + // if ( + // tmpl.id === 'minds/core/referencespace/v1.0.0/tmp-fsaverage' + // && !tmpl.availableParcellations.find(p => p.id === 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290') + // ) { + // tmpl.availableParcellations.push({ + // id: 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290', + // name: 'Julich-Brain Probabilistic Cytoarchitectonic Maps (v2.9)' + // }) + // } + // this.atlasTmplConfig[atlas["@id"]][tmpl.id] = {} + // return tmpl.availableParcellations.map( + // parc => this.getRegions(atlas['@id'], parc.id, tmpl.id).pipe( + // tap(regions => { + // recursiveMutate( + // regions, + // region => region.children, + // region => { + // /** + // * individual map(s) + // * this should work for both fully mapped and interpolated + // * in the case of interpolated, it sucks that the ngLayerObj will be set multiple times + // */ + + // const dedicatedMap = region._dataset_specs.filter( + // spec => spec["@type"] === 'fzj/tmp/volume_type/v0.0.1' + // && spec.space_id === tmpl.id + // && spec['volume_type'] === 'neuroglancer/precomputed' + // ) as TVolumeSrc<'neuroglancer/precomputed'>[] + // if (dedicatedMap.length === 1) { + // const ngId = getNgId(atlas['@id'], tmpl.id, parc.id, dedicatedMap[0]['@id']) + // region['ngId'] = ngId + // region['labelIndex'] = dedicatedMap[0].detail['neuroglancer/precomputed'].labelIndex + // this.atlasTmplConfig[atlas["@id"]][tmpl.id][ngId] = { + // source: `precomputed://${dedicatedMap[0].url}`, + // type: "segmentation", + // transform: dedicatedMap[0].detail['neuroglancer/precomputed'].transform + // } + // } - /** - * if label index is defined - */ - if (!!region.labelIndex) { - const hemisphereKey = /left hemisphere|left/.test(region.name) - // these two keys are, unfortunately, more or less hardcoded - // which is less than ideal - ? 'left hemisphere' - : /right hemisphere|right/.test(region.name) - ? 'right hemisphere' - : 'whole brain' - - if (!region['ngId']) { - const hemispheredNgId = getNgId(atlas['@id'], tmpl.id, parc.id, hemisphereKey) - region['ngId'] = hemispheredNgId - } - } - } - ) - this.atlasParcSpcRegionMap.set( - atlas['@id'], tmpl.id, parc.id, regions - ) + // /** + // * if label index is defined + // */ + // if (!!region.labelIndex) { + // const hemisphereKey = /left hemisphere|left/.test(region.name) + // // these two keys are, unfortunately, more or less hardcoded + // // which is less than ideal + // ? 'left hemisphere' + // : /right hemisphere|right/.test(region.name) + // ? 'right hemisphere' + // : 'whole brain' + + // if (!region['ngId']) { + // const hemispheredNgId = getNgId(atlas['@id'], tmpl.id, parc.id, hemisphereKey) + // region['ngId'] = hemispheredNgId + // } + // } + // } + // ) + // this.atlasParcSpcRegionMap.set( + // atlas['@id'], tmpl.id, parc.id, regions + // ) - /** - * populate maps for parc - */ - for (const parc of parcellations) { - const precomputedVols = parc._dataset_specs.filter( - spec => spec["@type"] === 'fzj/tmp/volume_type/v0.0.1' - && spec.volume_type === 'neuroglancer/precomputed' - && spec.space_id === tmpl.id - ) as TVolumeSrc<'neuroglancer/precomputed'>[] - - if (precomputedVols.length === 1) { - const vol = precomputedVols[0] - const key = 'whole brain' - - const ngIdKey = getNgId(atlas['@id'], tmpl.id, parseId(parc.id), key) - this.atlasTmplConfig[atlas["@id"]][tmpl.id][ngIdKey] = { - source: `precomputed://${vol.url}`, - type: "segmentation", - transform: vol.detail['neuroglancer/precomputed'].transform - } - } - - if (precomputedVols.length === 2) { - const mapIndexKey = [{ - mapIndex: 0, - key: 'left hemisphere' - }, { - mapIndex: 1, - key: 'right hemisphere' - }] - for (const { key, mapIndex } of mapIndexKey) { - const ngIdKey = getNgId(atlas['@id'], tmpl.id, parseId(parc.id), key) - this.atlasTmplConfig[atlas["@id"]][tmpl.id][ngIdKey] = { - source: `precomputed://${precomputedVols[mapIndex].url}`, - type: "segmentation", - transform: precomputedVols[mapIndex].detail['neuroglancer/precomputed'].transform - } - } - } - - if (precomputedVols.length > 2) { - console.error(`precomputedVols.length > 0, most likely an error`) - } - } - }), - catchError((err, obs) => { - return of(null) - }) - ) - ) - } - ).reduce(flattenReducer, []) - ).pipe( - mapTo({ templateSpaces, parcellations, ngLayerObj: this.atlasTmplConfig }) - ) - }), - map(({ templateSpaces, parcellations, ngLayerObj }) => { - return templateSpaces.map(tmpl => { - - // configuring three-surfer - let threeSurferConfig = {} - const volumes = tmpl._dataset_specs.filter(v => v["@type"] === 'fzj/tmp/volume_type/v0.0.1') as TVolumeSrc<keyof IVolumeTypeDetail>[] - const threeSurferVolSrc = volumes.find(v => v.volume_type === 'threesurfer/gii') - if (threeSurferVolSrc) { - const foundP = parcellations.find(p => { - return p._dataset_specs.some(spec => spec["@type"] === 'fzj/tmp/volume_type/v0.0.1' && spec.space_id === tmpl.id) - }) - const url = threeSurferVolSrc.url - const { surfaces } = threeSurferVolSrc.detail['threesurfer/gii'] as { surfaces: {mode: string, hemisphere: 'left' | 'right', url: string}[] } - const modObj = {} - for (const surface of surfaces) { + // /** + // * populate maps for parc + // */ + // for (const parc of parcellations) { + // const precomputedVols = parc._dataset_specs.filter( + // spec => spec["@type"] === 'fzj/tmp/volume_type/v0.0.1' + // && spec.volume_type === 'neuroglancer/precomputed' + // && spec.space_id === tmpl.id + // ) as TVolumeSrc<'neuroglancer/precomputed'>[] + + // if (precomputedVols.length === 1) { + // const vol = precomputedVols[0] + // const key = 'whole brain' + + // const ngIdKey = getNgId(atlas['@id'], tmpl.id, parseId(parc.id), key) + // this.atlasTmplConfig[atlas["@id"]][tmpl.id][ngIdKey] = { + // source: `precomputed://${vol.url}`, + // type: "segmentation", + // transform: vol.detail['neuroglancer/precomputed'].transform + // } + // } + + // if (precomputedVols.length === 2) { + // const mapIndexKey = [{ + // mapIndex: 0, + // key: 'left hemisphere' + // }, { + // mapIndex: 1, + // key: 'right hemisphere' + // }] + // for (const { key, mapIndex } of mapIndexKey) { + // const ngIdKey = getNgId(atlas['@id'], tmpl.id, parseId(parc.id), key) + // this.atlasTmplConfig[atlas["@id"]][tmpl.id][ngIdKey] = { + // source: `precomputed://${precomputedVols[mapIndex].url}`, + // type: "segmentation", + // transform: precomputedVols[mapIndex].detail['neuroglancer/precomputed'].transform + // } + // } + // } + + // if (precomputedVols.length > 2) { + // console.error(`precomputedVols.length > 0, most likely an error`) + // } + // } + // }), + // catchError((err, obs) => { + // return of(null) + // }) + // ) + // ) + // } + // ).reduce(flattenReducer, []) + // ).pipe( + // mapTo({ templateSpaces, parcellations, ngLayerObj: this.atlasTmplConfig }) + // ) + // }), + // map(({ templateSpaces, parcellations, ngLayerObj }) => { + // return templateSpaces.map(tmpl => { + + // // configuring three-surfer + // let threeSurferConfig = {} + // const volumes = tmpl._dataset_specs.filter(v => v["@type"] === 'fzj/tmp/volume_type/v0.0.1') as TVolumeSrc<keyof IVolumeTypeDetail>[] + // const threeSurferVolSrc = volumes.find(v => v.volume_type === 'threesurfer/gii') + // if (threeSurferVolSrc) { + // const foundP = parcellations.find(p => { + // return p._dataset_specs.some(spec => spec["@type"] === 'fzj/tmp/volume_type/v0.0.1' && spec.space_id === tmpl.id) + // }) + // const url = threeSurferVolSrc.url + // const { surfaces } = threeSurferVolSrc.detail['threesurfer/gii'] as { surfaces: {mode: string, hemisphere: 'left' | 'right', url: string}[] } + // const modObj = {} + // for (const surface of surfaces) { - const hemisphereKey = surface.hemisphere === 'left' - ? 'left hemisphere' - : 'right hemisphere' - - - /** - * concating all available gii maps - */ - // const allFreesurferLabels = foundP.volumeSrc[tmpl.id][hemisphereKey].filter(v => v.volume_type === 'threesurfer/gii-label') - // for (const lbl of allFreesurferLabels) { - // const modeToConcat = { - // mesh: surface.url, - // hemisphere: surface.hemisphere, - // colormap: lbl.url - // } - - // const key = `${surface.mode} - ${lbl.name}` - // if (!modObj[key]) { - // modObj[key] = [] - // } - // modObj[key].push(modeToConcat) - // } - - /** - * only concat first matching gii map - */ - const mapIndex = hemisphereKey === 'left hemisphere' - ? 0 - : 1 - const labelMaps = foundP._dataset_specs.filter(spec => spec["@type"] === 'fzj/tmp/volume_type/v0.0.1' && spec.volume_type === 'threesurfer/gii-label') as TVolumeSrc<'threesurfer/gii-label'>[] - const key = surface.mode - const modeToConcat = { - mesh: surface.url, - hemisphere: surface.hemisphere, - colormap: (() => { - const lbl = labelMaps[mapIndex] - return lbl?.url - })() - } - if (!modObj[key]) { - modObj[key] = [] - } - modObj[key].push(modeToConcat) - - } - foundP[tmpl.id] - threeSurferConfig = { - "three-surfer": { - '@context': { - root: url - }, - modes: Object.keys(modObj).map(name => { - return { - name, - meshes: modObj[name] - } - }) - }, - nehubaConfig: null, - nehubaConfigURL: null, - useTheme: 'dark' - } - } - const darkTheme = tmpl.src_volume_type === 'mri' - const nehubaConfig = getNehubaConfig(tmpl) - const initialLayers = nehubaConfig.dataset.initialNgState.layers + // const hemisphereKey = surface.hemisphere === 'left' + // ? 'left hemisphere' + // : 'right hemisphere' + + + // /** + // * concating all available gii maps + // */ + // // const allFreesurferLabels = foundP.volumeSrc[tmpl.id][hemisphereKey].filter(v => v.volume_type === 'threesurfer/gii-label') + // // for (const lbl of allFreesurferLabels) { + // // const modeToConcat = { + // // mesh: surface.url, + // // hemisphere: surface.hemisphere, + // // colormap: lbl.url + // // } + + // // const key = `${surface.mode} - ${lbl.name}` + // // if (!modObj[key]) { + // // modObj[key] = [] + // // } + // // modObj[key].push(modeToConcat) + // // } + + // /** + // * only concat first matching gii map + // */ + // const mapIndex = hemisphereKey === 'left hemisphere' + // ? 0 + // : 1 + // const labelMaps = foundP._dataset_specs.filter(spec => spec["@type"] === 'fzj/tmp/volume_type/v0.0.1' && spec.volume_type === 'threesurfer/gii-label') as TVolumeSrc<'threesurfer/gii-label'>[] + // const key = surface.mode + // const modeToConcat = { + // mesh: surface.url, + // hemisphere: surface.hemisphere, + // colormap: (() => { + // const lbl = labelMaps[mapIndex] + // return lbl?.url + // })() + // } + // if (!modObj[key]) { + // modObj[key] = [] + // } + // modObj[key].push(modeToConcat) + + // } + // foundP[tmpl.id] + // threeSurferConfig = { + // "three-surfer": { + // '@context': { + // root: url + // }, + // modes: Object.keys(modObj).map(name => { + // return { + // name, + // meshes: modObj[name] + // } + // }) + // }, + // nehubaConfig: null, + // nehubaConfigURL: null, + // useTheme: 'dark' + // } + // } + // const darkTheme = tmpl.src_volume_type === 'mri' + // const nehubaConfig = getNehubaConfig(tmpl) + // const initialLayers = nehubaConfig.dataset.initialNgState.layers - const tmplAuxMesh = `${tmpl.name} auxmesh` - - const precomputedArr = tmpl._dataset_specs.filter(src => src['@type'] === 'fzj/tmp/volume_type/v0.0.1' && src.volume_type === 'neuroglancer/precomputed') as TVolumeSrc<'neuroglancer/precomputed'>[] - let visible = true - let tmplNgId: string - const templateImages: TTemplateImage[] = [] - for (const precomputedItem of precomputedArr) { - const ngIdKey = MultiDimMap.GetKey(precomputedItem["@id"]) - const precomputedUrl = 'https://neuroglancer.humanbrainproject.eu/precomputed/data-repo-ng-bot/20211001-mebrain/precomputed/images/MEBRAINS_T1.masked' === precomputedItem.url - ? 'https://neuroglancer.humanbrainproject.eu/precomputed/data-repo-ng-bot/20211018-mebrains-masked-templates/precomputed/images/MEBRAINS_T1_masked' - : precomputedItem.url - initialLayers[ngIdKey] = { - type: "image", - source: `precomputed://${precomputedUrl}`, - transform: precomputedItem.detail['neuroglancer/precomputed'].transform, - visible - } - templateImages.push({ - "@id": precomputedItem['@id'], - name: precomputedItem.name, - ngId: ngIdKey, - visible - }) - if (visible) { - tmplNgId = ngIdKey - } - visible = false - } - - // TODO - // siibra-python accidentally left out volume type of precompmesh - // https://github.com/FZJ-INM1-BDA/siibra-python/pull/55 - // use url to determine for now - // const precompmesh = tmpl.volume_src.find(src => src.volume_type === 'neuroglancer/precompmesh') - const precompmesh = tmpl._dataset_specs.find(src => src["@type"] === 'fzj/tmp/volume_type/v0.0.1' && !!src.detail?.['neuroglancer/precompmesh']) as TVolumeSrc<'neuroglancer/precompmesh'> - const auxMeshes = [] - if (precompmesh){ - initialLayers[tmplAuxMesh] = { - source: `precompmesh://${precompmesh.url}`, - type: "segmentation", - transform: precompmesh.detail['neuroglancer/precompmesh'].transform - } - for (const auxMesh of precompmesh.detail['neuroglancer/precompmesh'].auxMeshes) { - - auxMeshes.push({ - ...auxMesh, - ngId: tmplAuxMesh, - '@id': `${tmplAuxMesh} ${auxMesh.name}`, - visible: true - }) - } - } - - for (const key in (ngLayerObj[atlas["@id"]][tmpl.id] || {})) { - initialLayers[key] = ngLayerObj[atlas["@id"]][tmpl.id][key] - } - - return { - name: tmpl.name, - '@id': tmpl.id, - fullId: tmpl.id, - useTheme: darkTheme ? 'dark' : 'light', - ngId: tmplNgId, - nehubaConfig, - templateImages, - auxMeshes, - /** - * only populate the parcelltions made available - */ - parcellations: tmpl.availableParcellations.filter( - p => parcellations.some(p2 => parseId(p2.id) === p.id) - ).map(parc => { - const fullParcInfo = parcellations.find(p => parseId(p.id) === parc.id) - const regions = this.atlasParcSpcRegionMap.get(atlas['@id'], tmpl.id, parc.id) || [] - return { - fullId: parc.id, - '@id': parc.id, - name: parc.name, - regions, - originDatainfos: [...fullParcInfo.infos, ...(fullParcInfo?._dataset_specs || []).filter(spec => spec["@type"] === 'fzj/tmp/simpleOriginInfo/v0.0.1')] - } - }), - ...threeSurferConfig - } - }) - }) - )) - ) - }), - map(arr => { - return arr.reduce(flattenReducer, []) - }), - catchError((err) => { - this.log.warn(`fetching templates error`, err) - return of(null) - }), - shareReplay(1), - ) + // const tmplAuxMesh = `${tmpl.name} auxmesh` + + // const precomputedArr = tmpl._dataset_specs.filter(src => src['@type'] === 'fzj/tmp/volume_type/v0.0.1' && src.volume_type === 'neuroglancer/precomputed') as TVolumeSrc<'neuroglancer/precomputed'>[] + // let visible = true + // let tmplNgId: string + // const templateImages: TTemplateImage[] = [] + // for (const precomputedItem of precomputedArr) { + // const ngIdKey = MultiDimMap.GetKey(precomputedItem["@id"]) + // const precomputedUrl = 'https://neuroglancer.humanbrainproject.eu/precomputed/data-repo-ng-bot/20211001-mebrain/precomputed/images/MEBRAINS_T1.masked' === precomputedItem.url + // ? 'https://neuroglancer.humanbrainproject.eu/precomputed/data-repo-ng-bot/20211018-mebrains-masked-templates/precomputed/images/MEBRAINS_T1_masked' + // : precomputedItem.url + // initialLayers[ngIdKey] = { + // type: "image", + // source: `precomputed://${precomputedUrl}`, + // transform: precomputedItem.detail['neuroglancer/precomputed'].transform, + // visible + // } + // templateImages.push({ + // "@id": precomputedItem['@id'], + // name: precomputedItem.name, + // ngId: ngIdKey, + // visible + // }) + // if (visible) { + // tmplNgId = ngIdKey + // } + // visible = false + // } + + // // TODO + // // siibra-python accidentally left out volume type of precompmesh + // // https://github.com/FZJ-INM1-BDA/siibra-python/pull/55 + // // use url to determine for now + // // const precompmesh = tmpl.volume_src.find(src => src.volume_type === 'neuroglancer/precompmesh') + // const precompmesh = tmpl._dataset_specs.find(src => src["@type"] === 'fzj/tmp/volume_type/v0.0.1' && !!src.detail?.['neuroglancer/precompmesh']) as TVolumeSrc<'neuroglancer/precompmesh'> + // const auxMeshes = [] + // if (precompmesh){ + // initialLayers[tmplAuxMesh] = { + // source: `precompmesh://${precompmesh.url}`, + // type: "segmentation", + // transform: precompmesh.detail['neuroglancer/precompmesh'].transform + // } + // for (const auxMesh of precompmesh.detail['neuroglancer/precompmesh'].auxMeshes) { + + // auxMeshes.push({ + // ...auxMesh, + // ngId: tmplAuxMesh, + // '@id': `${tmplAuxMesh} ${auxMesh.name}`, + // visible: true + // }) + // } + // } + + // for (const key in (ngLayerObj[atlas["@id"]][tmpl.id] || {})) { + // initialLayers[key] = ngLayerObj[atlas["@id"]][tmpl.id][key] + // } + + // return { + // name: tmpl.name, + // '@id': tmpl.id, + // fullId: tmpl.id, + // useTheme: darkTheme ? 'dark' : 'light', + // ngId: tmplNgId, + // nehubaConfig, + // templateImages, + // auxMeshes, + // /** + // * only populate the parcelltions made available + // */ + // parcellations: tmpl.availableParcellations.filter( + // p => parcellations.some(p2 => parseId(p2.id) === p.id) + // ).map(parc => { + // const fullParcInfo = parcellations.find(p => parseId(p.id) === parc.id) + // const regions = this.atlasParcSpcRegionMap.get(atlas['@id'], tmpl.id, parc.id) || [] + // return { + // fullId: parc.id, + // '@id': parc.id, + // name: parc.name, + // regions, + // originDatainfos: [...fullParcInfo.infos, ...(fullParcInfo?._dataset_specs || []).filter(spec => spec["@type"] === 'fzj/tmp/simpleOriginInfo/v0.0.1')] + // } + // }), + // ...threeSurferConfig + // } + // }) + // }) + // )) + // ) + // }), + // map(arr => { + // return arr.reduce(flattenReducer, []) + // }), + // catchError((err) => { + // this.log.warn(`fetching templates error`, err) + // return of(null) + // }), + // shareReplay(1), + // ) ngOnDestroy(){ while(this.subscriptions.length > 0) this.subscriptions.pop().unsubscribe() diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 8ec584071f6b3b2b2142e3d1be818f8a31f588e5..2f3d89f79a5999a2059a454b289c35da97e5d68d 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -3,7 +3,6 @@ import { NgModule } from "@angular/core"; import { Observable } from "rxjs"; import { AtlasCmpParcellationModule } from "src/atlasComponents/parcellation"; import { ParcellationRegionModule } from "src/atlasComponents/parcellationRegion"; -import { BSFeatureModule, BS_DARKTHEME, } from "src/atlasComponents/regionalFeatures/bsFeatures"; import { SplashUiModule } from "src/atlasComponents/splashScreen"; import { AtlasCmpUiSelectorsModule } from "src/atlasComponents/uiSelectors"; import { ComponentsModule } from "src/components"; @@ -12,18 +11,16 @@ import { LayoutModule } from "src/layouts/layout.module"; import { AngularMaterialModule } from "src/sharedModules"; import { TopMenuModule } from "src/ui/topMenu/module"; import { CONTEXT_MENU_ITEM_INJECTOR, TContextMenu, UtilModule } from "src/util"; -import { VIEWERMODULE_DARKTHEME } from "./constants"; import { NehubaModule, NehubaViewerUnit } from "./nehuba"; import { ThreeSurferModule } from "./threeSurfer"; import { ViewerCmp } from "./viewerCmp/viewerCmp.component"; -import {UserAnnotationsModule} from "src/atlasComponents/userAnnotations"; -import {QuickTourModule} from "src/ui/quickTour/module"; +import { UserAnnotationsModule } from "src/atlasComponents/userAnnotations"; +import { QuickTourModule } from "src/ui/quickTour/module"; 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 { KgRegionalFeatureModule } from "src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature"; 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"; @@ -46,12 +43,10 @@ import { NehubaVCtxToBbox } from "./pipes/nehubaVCtxToBbox.pipe"; UtilModule, AtlasCmpParcellationModule, ComponentsModule, - BSFeatureModule, UserAnnotationsModule, QuickTourModule, ContextMenuModule, ViewerStateBreadCrumbModule, - KgRegionalFeatureModule, KeyFrameModule, LayerBrowserModule, SAPIModule, @@ -61,13 +56,6 @@ import { NehubaVCtxToBbox } from "./pipes/nehubaVCtxToBbox.pipe"; NehubaVCtxToBbox, ], providers: [ - { - provide: BS_DARKTHEME, - useFactory: (obs$: Observable<boolean>) => obs$, - deps: [ - VIEWERMODULE_DARKTHEME - ] - }, { provide: INJ_ANNOT_TARGET, useFactory: (obs$: Observable<NehubaViewerUnit>) => { diff --git a/src/viewerModule/nehuba/config.service/config.service.spec.ts b/src/viewerModule/nehuba/config.service/config.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/viewerModule/nehuba/config.service/index.ts b/src/viewerModule/nehuba/config.service/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e92b3a002d5971a9616ed6899316506f7b615c0 --- /dev/null +++ b/src/viewerModule/nehuba/config.service/index.ts @@ -0,0 +1,19 @@ +export { + NehubaConfig, + NgConfig, + NgConfigViewerState, + NgLayerSpec, + NgPrecompMeshSpec, + NgSegLayerSpec, +} from "./type" +export { + getParcNgLayers, + getTmplAuxNgLayer, + getTmplNgLayer, + getNgLayersFromVolumesATP, + getParcNgId, + fromRootStore, + getRegionLabelIndex, + getNehubaConfig, + defaultNehubaConfig, +} from "./util" diff --git a/src/viewerModule/nehuba/config.service/type.ts b/src/viewerModule/nehuba/config.service/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc79b98557e80a05e49ae1c5f526a5763520c18d --- /dev/null +++ b/src/viewerModule/nehuba/config.service/type.ts @@ -0,0 +1,115 @@ + +export type RecursivePartial<T> = { + [K in keyof T]?: RecursivePartial<T[K]> +} + +type Vec4 = number[] +type Vec3 = number[] + +export type NgConfigViewerState = { + perspectiveOrientation: Vec4 + perspectiveZoom: number + navigation: { + pose: { + position: { + voxelSize: Vec3 + voxelCoordinates: Vec3 + } + orientation: Vec4 + } + zoomFactor: number + } +} + +export type NgConfig = { + showDefaultAnnotations: boolean + layers: Record<string, NgLayerSpec> + gpuMemoryLimit: number, +} & NgConfigViewerState + +interface _NehubaConfig { + configName: string + globals: { + hideNullImageValues: boolean + useNehubaLayout: { + keepDefaultLayouts: boolean + } + useNehubaMeshLayer: boolean + rightClickWithCtrlGlobal: boolean + zoomWithoutCtrlGlobal: boolean + useCustomSegmentColors: boolean + } + zoomWithoutCtrl: boolean + hideNeuroglancerUI: boolean + rightClickWithCtrl: boolean + rotateAtViewCentre: boolean + enableMeshLoadingControl: boolean + zoomAtViewCentre: boolean + restrictUserNavigation: boolean + disableSegmentSelection: boolean + dataset: { + imageBackground: Vec4 + initialNgState: NgConfig + }, + layout: { + views: string, + planarSlicesBackground: Vec4 + useNehubaPerspective: { + enableShiftDrag: boolean + doNotRestrictUserNavigation: boolean + removePerspectiveSlicesBackground: { + mode: string + color: Vec4 + } + perspectiveSlicesBackground: Vec4 + perspectiveBackground: Vec4 + fixedZoomPerspectiveSlices: { + sliceViewportWidth: number + sliceViewportHeight: number + sliceZoom: number + sliceViewportSizeMultiplier: number + } + mesh: { + backFaceColor: Vec4 + removeBasedOnNavigation: boolean + flipRemovedOctant: boolean + surfaceParcellation: boolean + }, + centerToOrigin: boolean + drawSubstrates: { + color: Vec4 + } + drawZoomLevels: { + cutOff: number + color: Vec4 + } + restrictZoomLevel: { + minZoom: number + maxZoom: number + } + hideImages: boolean + waitForMesh: boolean + } + } +} + +export type NehubaConfig = RecursivePartial<_NehubaConfig> + +export type NgLayerSpec = { + source: string + transform: number[][] + opacity?: number + visible?: boolean +} + +export type NgPrecompMeshSpec = { + auxMeshes: { + name: string + labelIndicies: number[] + }[] +} & NgLayerSpec + +export type NgSegLayerSpec = { + labelIndicies: number[] + laterality: 'left hemisphere' | 'right hemisphere' | 'whole brain' +} & NgLayerSpec diff --git a/src/viewerModule/nehuba/config.service/util.spec.ts b/src/viewerModule/nehuba/config.service/util.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3e87a6c2ec5aa8ad9cde9d2eac4a9c9300194d2 --- /dev/null +++ b/src/viewerModule/nehuba/config.service/util.spec.ts @@ -0,0 +1,141 @@ +import { cvtNavigationObjToNehubaConfig } from './util' + +const currentNavigation = { + position: [4, 5, 6], + orientation: [0, 0, 0, 1], + perspectiveOrientation: [ 0, 0, 0, 1], + perspectiveZoom: 2e5, + zoom: 1e5 +} + +const defaultPerspectiveZoom = 1e6 +const defaultZoom = 1e6 + +const defaultNavigationObject = { + orientation: [0, 0, 0, 1], + perspectiveOrientation: [0 , 0, 0, 1], + perspectiveZoom: defaultPerspectiveZoom, + zoom: defaultZoom, + position: [0, 0, 0], + positionReal: true +} + +const defaultNehubaConfigObject = { + perspectiveOrientation: [0, 0, 0, 1], + perspectiveZoom: 1e6, + navigation: { + pose: { + position: { + voxelCoordinates: [0, 0, 0], + voxelSize: [1,1,1] + }, + orientation: [0, 0, 0, 1], + }, + zoomFactor: defaultZoom + } +} + +const bigbrainNehubaConfig = { + "showDefaultAnnotations": false, + "layers": { + }, + "navigation": { + "pose": { + "position": { + "voxelSize": [ + 21166.666015625, + 20000, + 21166.666015625 + ], + "voxelCoordinates": [ + -21.8844051361084, + 16.288618087768555, + 28.418994903564453 + ] + } + }, + "zoomFactor": 350000 + }, + "perspectiveOrientation": [ + 0.3140767216682434, + -0.7418519854545593, + 0.4988985061645508, + -0.3195493221282959 + ], + "perspectiveZoom": 1922235.5293810747 +} + +describe('> util.ts', () => { + + describe('> cvtNavigationObjToNehubaConfig', () => { + const validNavigationObj = currentNavigation + describe('> if inputs are malformed', () => { + describe('> if navigation object is malformed, uses navigation default object', () => { + it('> if navigation object is null', () => { + const v1 = cvtNavigationObjToNehubaConfig(null, bigbrainNehubaConfig) + const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, bigbrainNehubaConfig) + expect(v1).toEqual(v2) + }) + it('> if navigation object is undefined', () => { + const v1 = cvtNavigationObjToNehubaConfig(undefined, bigbrainNehubaConfig) + const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, bigbrainNehubaConfig) + expect(v1).toEqual(v2) + }) + + it('> if navigation object is otherwise malformed', () => { + const v1 = cvtNavigationObjToNehubaConfig({foo: 'bar'}, bigbrainNehubaConfig) + const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, bigbrainNehubaConfig) + expect(v1).toEqual(v2) + + const v3 = cvtNavigationObjToNehubaConfig({}, bigbrainNehubaConfig) + const v4 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, bigbrainNehubaConfig) + expect(v3).toEqual(v4) + }) + }) + + describe('> if nehubaConfig object is malformed, use default nehubaConfig obj', () => { + it('> if nehubaConfig is null', () => { + const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, null) + const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) + expect(v1).toEqual(v2) + }) + + it('> if nehubaConfig is undefined', () => { + const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, undefined) + const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) + expect(v1).toEqual(v2) + }) + + it('> if nehubaConfig is otherwise malformed', () => { + const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, {}) + const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) + expect(v1).toEqual(v2) + + const v3 = cvtNavigationObjToNehubaConfig(validNavigationObj, {foo: 'bar'} as any) + const v4 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) + expect(v3).toEqual(v4) + }) + }) + }) + it('> converts navigation object and reference nehuba config object to navigation object', () => { + const convertedVal = cvtNavigationObjToNehubaConfig(validNavigationObj, bigbrainNehubaConfig) + const { perspectiveOrientation, orientation, zoom, perspectiveZoom, position } = validNavigationObj + + expect(convertedVal).toEqual({ + navigation: { + pose: { + position: { + voxelSize: bigbrainNehubaConfig.navigation.pose.position.voxelSize, + voxelCoordinates: [0, 1, 2].map(idx => position[idx] / bigbrainNehubaConfig.navigation.pose.position.voxelSize[idx]) + }, + orientation + }, + zoomFactor: zoom + }, + perspectiveOrientation: perspectiveOrientation, + perspectiveZoom: perspectiveZoom + }) + }) + }) + +}) diff --git a/src/viewerModule/nehuba/config.service/util.ts b/src/viewerModule/nehuba/config.service/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..b81660ed18b776d67be8eb629d42c8d201baafbf --- /dev/null +++ b/src/viewerModule/nehuba/config.service/util.ts @@ -0,0 +1,484 @@ +import { select, Store } from '@ngrx/store' +import { forkJoin, pipe } from 'rxjs' +import { SapiParcellationModel, SapiSpaceModel, SapiAtlasModel, SapiRegionModel, SAPI } from 'src/atlasComponents/sapi' +import { SapiVolumeModel } from 'src/atlasComponents/sapi/type' +import { MultiDimMap } from 'src/util/fn' +import { ParcVolumeSpec } from "../store/util" +import { fromRootStore as nehubaStoreFromRootStore } from "../store" +import { map, switchMap } from 'rxjs/operators' +import { + NehubaConfig, + NgConfig, + RecursivePartial, + NgLayerSpec, + NgPrecompMeshSpec, + NgSegLayerSpec, +} from "./type" +import { atlasSelection } from 'src/state' + +// fsaverage uses threesurfer, which, whilst do not use ngId, uses 'left' and 'right' as keys +const fsAverageKeyVal = { + "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290": { + "left hemisphere": "left", + "right hemisphere": "right" + } +} + +/** + * in order to maintain backwards compat with url encoding of selected regions + * TODO setup a sentry to catch if these are ever used. if not, retire the hard coding + */ + const BACKCOMAP_KEY_DICT = { + + // human multi level + 'juelich/iav/atlas/v1.0.0/1': { + // icbm152 + 'minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2': { + // julich brain v2.6 + 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26': { + 'left hemisphere': 'MNI152_V25_LEFT_NG_SPLIT_HEMISPHERE', + 'right hemisphere': 'MNI152_V25_RIGHT_NG_SPLIT_HEMISPHERE' + }, + // bundle hcp + // even though hcp, long/short bundle, and difumo has no hemisphere distinctions, the way siibra-python parses the region, + // and thus attributes left/right hemisphere, still results in some regions being parsed as left/right hemisphere + "juelich/iav/atlas/v1.0.0/79cbeaa4ee96d5d3dfe2876e9f74b3dc3d3ffb84304fb9b965b1776563a1069c": { + "whole brain": "superficial-white-bundle-HCP", + "left hemisphere": "superficial-white-bundle-HCP", + "right hemisphere": "superficial-white-bundle-HCP" + }, + // julich brain v1.18 + "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579": { + "left hemisphere": "jubrain mni152 v18 left", + "right hemisphere": "jubrain mni152 v18 right", + }, + // long bundle + "juelich/iav/atlas/v1.0.0/5": { + "whole brain": "fibre bundle long", + "left hemisphere": "fibre bundle long", + "right hemisphere": "fibre bundle long", + }, + // bundle short + "juelich/iav/atlas/v1.0.0/6": { + "whole brain": "fibre bundle short", + "left hemisphere": "fibre bundle short", + "right hemisphere": "fibre bundle short", + }, + // difumo 64 + "minds/core/parcellationatlas/v1.0.0/d80fbab2-ce7f-4901-a3a2-3c8ef8a3b721": { + "whole brain": "DiFuMo Atlas (64 dimensions)", + "left hemisphere": "DiFuMo Atlas (64 dimensions)", + "right hemisphere": "DiFuMo Atlas (64 dimensions)", + }, + "minds/core/parcellationatlas/v1.0.0/73f41e04-b7ee-4301-a828-4b298ad05ab8": { + "whole brain": "DiFuMo Atlas (128 dimensions)", + "left hemisphere": "DiFuMo Atlas (128 dimensions)", + "right hemisphere": "DiFuMo Atlas (128 dimensions)", + }, + "minds/core/parcellationatlas/v1.0.0/141d510f-0342-4f94-ace7-c97d5f160235": { + "whole brain": "DiFuMo Atlas (256 dimensions)", + "left hemisphere": "DiFuMo Atlas (256 dimensions)", + "right hemisphere": "DiFuMo Atlas (256 dimensions)", + }, + "minds/core/parcellationatlas/v1.0.0/63b5794f-79a4-4464-8dc1-b32e170f3d16": { + "whole brain": "DiFuMo Atlas (512 dimensions)", + "left hemisphere": "DiFuMo Atlas (512 dimensions)", + "right hemisphere": "DiFuMo Atlas (512 dimensions)", + }, + "minds/core/parcellationatlas/v1.0.0/12fca5c5-b02c-46ce-ab9f-f12babf4c7e1": { + "whole brain": "DiFuMo Atlas (1024 dimensions)", + "left hemisphere": "DiFuMo Atlas (1024 dimensions)", + "right hemisphere": "DiFuMo Atlas (1024 dimensions)", + }, + }, + // colin 27 + "minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992": { + "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26": { + "left hemisphere": "COLIN_V25_LEFT_NG_SPLIT_HEMISPHERE", + "right hemisphere": "COLIN_V25_RIGHT_NG_SPLIT_HEMISPHERE", + }, + "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579": { + "left hemisphere": "jubrain colin v18 left", + "right hemisphere": "jubrain colin v18 right", + } + }, + // big brain + "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588": { + "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26": { + + }, + // isocortex + "juelich/iav/atlas/v1.0.0/4": { + "whole brain": " tissue type: " + }, + // cortical layers + "juelich/iav/atlas/v1.0.0/3": { + "whole brain": "cortical layers" + }, + }, + + // fsaverage + "minds/core/referencespace/v1.0.0/tmp-fsaverage": fsAverageKeyVal, + }, + // allen mouse + 'juelich/iav/atlas/v1.0.0/2': { + // ccf v3 + "minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9": { + // ccf v3 2017 + "minds/core/parcellationatlas/v1.0.0/05655b58-3b6f-49db-b285-64b5a0276f83": { + "whole brain": "v3_2017", + "left hemisphere": "v3_2017", + "right hemisphere": "v3_2017" + }, + // ccf v3 2015, + "minds/core/parcellationatlas/v1.0.0/39a1384b-8413-4d27-af8d-22432225401f": { + "whole brain": "atlas", + "left hemisphere": "atlas", + "right hemisphere": "atlas" + } + } + }, + // waxholm + "minds/core/parcellationatlas/v1.0.0/522b368e-49a3-49fa-88d3-0870a307974a": { + "minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8": { + // v1.01 + "minds/core/parcellationatlas/v1.0.0/11017b35-7056-4593-baad-3934d211daba": { + "whole brain": "v1_01", + "left hemisphere": "v1_01", + "right hemisphere": "v1_01" + }, + // v2 + "minds/core/parcellationatlas/v1.0.0/2449a7f0-6dd0-4b5a-8f1e-aec0db03679d": { + "whole brain": "v2", + "left hemisphere": "v2", + "right hemisphere": "v2" + }, + // v3 + "minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe": { + "whole brain": "v3", + "left hemisphere": "v3", + "right hemisphere": "v3" + } + } + } +} + + +export function getTmplNgLayer(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, spaceVolumes: SapiVolumeModel[]): Record<string, NgLayerSpec>{ + const ngId = `_${MultiDimMap.GetKey(atlas["@id"], tmpl["@id"], "tmplImage")}` + const tmplImage = spaceVolumes.find(v => "neuroglancer/precomputed" in v.data.detail) + return { + [ngId]: { + source: `precomputed://${tmplImage.data.url.replace(/^precomputed:\/\//, '')}`, + ...tmplImage.data.detail["neuroglancer/precomputed"] as NgLayerSpec + } + } +} + +export function getTmplAuxNgLayer(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, spaceVolumes: SapiVolumeModel[]): Record<string, NgPrecompMeshSpec>{ + const ngId = `_${MultiDimMap.GetKey(atlas["@id"], tmpl["@id"], "auxLayer")}` + const tmplImage = spaceVolumes.find(v => "neuroglancer/precompmesh" in v.data.detail) + if (!tmplImage) return {} + return { + [ngId]: { + source: `precompmesh://${tmplImage.data.url.replace(/^precompmesh:\/\//, '')}`, + ...tmplImage.data.detail["neuroglancer/precompmesh"] as NgPrecompMeshSpec + } + } +} + +export function getParcNgId(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, parc: SapiParcellationModel, _laterality: string | SapiRegionModel) { + let laterality: string + if (typeof _laterality === "string") { + laterality = _laterality + } else { + laterality = "whole brain" + if (_laterality.name.indexOf("left") >= 0) laterality = "left hemisphere" + if (_laterality.name.indexOf("right") >= 0) laterality = "right hemisphere" + } + let ngId = BACKCOMAP_KEY_DICT[atlas["@id"]]?.[tmpl["@id"]]?.[parc["@id"]]?.[laterality] + if (!ngId) { + ngId = `_${MultiDimMap.GetKey(atlas["@id"], tmpl["@id"], parc["@id"], laterality)}` + } + return ngId +} + +export function getParcNgLayers(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, parc: SapiParcellationModel, parcVolumes: { volume: SapiVolumeModel, volumeMetadata: ParcVolumeSpec }[]){ + const returnVal: Record<string, NgSegLayerSpec> = {} + for (const parcVol of parcVolumes) { + const { volume, volumeMetadata } = parcVol + const { laterality, labelIndicies } = volumeMetadata + const ngId = getParcNgId(atlas, tmpl, parc, laterality) + + returnVal[ngId] = { + source: `precomputed://${volume.data.url.replace(/^precomputed:\/\//, '')}`, + labelIndicies, + laterality, + transform: (volume.data.detail["neuroglancer/precomputed"] as any).transform + } + } + return returnVal +} + +type CongregatedVolume = { + tmplVolumes: SapiVolumeModel[] + tmplAuxMeshVolumes: SapiVolumeModel[] + parcVolumes: { volume: SapiVolumeModel, volumeMetadata: ParcVolumeSpec}[] +} + +export const getNgLayersFromVolumesATP = (volumes: CongregatedVolume, ATP: { atlas: SapiAtlasModel, template: SapiSpaceModel, parcellation: SapiParcellationModel }) => { + + const { tmplVolumes, tmplAuxMeshVolumes, parcVolumes } = volumes + const { atlas, template, parcellation } = ATP + return { + tmplNgLayers: getTmplNgLayer(atlas, template, tmplVolumes), + tmplAuxNgLayers: getTmplAuxNgLayer(atlas, template, tmplAuxMeshVolumes), + parcNgLayers: getParcNgLayers(atlas, template, parcellation, parcVolumes) + } +} + +export const fromRootStore = { + getNgLayers: (store: Store, sapiSvc: SAPI) => pipe( + select(atlasSelection.selectors.selectedATP), + switchMap(ATP => + forkJoin({ + tmplVolumes: store.pipe( + nehubaStoreFromRootStore.getTmplVolumes(sapiSvc), + ), + tmplAuxMeshVolumes: store.pipe( + nehubaStoreFromRootStore.getAuxMeshVolumes(sapiSvc), + ), + parcVolumes: store.pipe( + nehubaStoreFromRootStore.getParcVolumes(sapiSvc), + ) + }).pipe( + map(volumes => getNgLayersFromVolumesATP(volumes, ATP)) + ) + ) + ) +} + +export function getRegionLabelIndex(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, parc: SapiParcellationModel, region: SapiRegionModel) { + const lblIdx = Number(region?.hasAnnotation?.internalIdentifier) + if (lblIdx === NaN) return null + return lblIdx +} + +export const defaultNehubaConfig: NehubaConfig = { + "configName": "", + "globals": { + "hideNullImageValues": true, + "useNehubaLayout": { + "keepDefaultLayouts": false + }, + "useNehubaMeshLayer": true, + "rightClickWithCtrlGlobal": false, + "zoomWithoutCtrlGlobal": false, + "useCustomSegmentColors": true + }, + "zoomWithoutCtrl": true, + "hideNeuroglancerUI": true, + "rightClickWithCtrl": true, + "rotateAtViewCentre": true, + "enableMeshLoadingControl": true, + "zoomAtViewCentre": true, + "restrictUserNavigation": true, + "disableSegmentSelection": false, + "dataset": { + "imageBackground": [ + 1, + 1, + 1, + 1 + ], + "initialNgState": { + "showDefaultAnnotations": false, + "layers": {}, + } + }, + "layout": { + "views": "hbp-neuro", + "planarSlicesBackground": [ + 1, + 1, + 1, + 1 + ], + "useNehubaPerspective": { + "enableShiftDrag": false, + "doNotRestrictUserNavigation": false, + "perspectiveSlicesBackground": [ + 1, + 1, + 1, + 1 + ], + "perspectiveBackground": [ + 1, + 1, + 1, + 1 + ], + "mesh": { + "backFaceColor": [ + 1, + 1, + 1, + 1 + ], + "removeBasedOnNavigation": true, + "flipRemovedOctant": true + }, + "hideImages": false, + "waitForMesh": false, + } + } +} + +export const spaceMiscInfoMap = new Map([ + ['minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588', { + name: 'bigbrain', + scale: 1, + }], + ['minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2', { + name: 'icbm2009c', + scale: 1, + }], + ['minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992', { + name: 'colin27', + scale: 1, + }], + ['minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9', { + name: 'allen-mouse', + scale: 0.1, + }], + ['minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8', { + name: 'waxholm', + scale: 0.1, + }], +]) + +export function getNehubaConfig(space: SapiSpaceModel): NehubaConfig { + + const darkTheme = space["@id"] !== "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588" + const { scale } = spaceMiscInfoMap.get(space["@id"]) || { scale: 1 } + const backgrd = darkTheme + ? [0,0,0,1] + : [1,1,1,1] + + const rmPsp = darkTheme + ? {"mode":"<","color":[0.1,0.1,0.1,1]} + :{"color":[1,1,1,1],"mode":"=="} + const drawSubstrates = darkTheme + ? {"color":[0.5,0.5,1,0.2]} + : {"color":[0,0,0.5,0.15]} + const drawZoomLevels = darkTheme + ? {"cutOff":150000 * scale } + : {"cutOff":200000 * scale,"color":[0.5,0,0,0.15] } + + // enable surface parcellation + // otherwise, on segmentation selection, the unselected meshes will also be invisible + const surfaceParcellation = space["@id"] === 'minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992' + return { + "configName": "", + "globals": { + "hideNullImageValues": true, + "useNehubaLayout": { + "keepDefaultLayouts": false + }, + "useNehubaMeshLayer": true, + "rightClickWithCtrlGlobal": false, + "zoomWithoutCtrlGlobal": false, + "useCustomSegmentColors": true + }, + "zoomWithoutCtrl": true, + "hideNeuroglancerUI": true, + "rightClickWithCtrl": true, + "rotateAtViewCentre": true, + "enableMeshLoadingControl": true, + "zoomAtViewCentre": true, + // "restrictUserNavigation": true, + "dataset": { + "imageBackground": backgrd, + "initialNgState": { + "showDefaultAnnotations": false, + "layers": {}, + "navigation": { + "zoomFactor": 350000 * scale, + }, + "perspectiveOrientation": [ + 0.3140767216682434, + -0.7418519854545593, + 0.4988985061645508, + -0.3195493221282959 + ], + "perspectiveZoom": 1922235.5293810747 * scale + } + }, + "layout": { + "useNehubaPerspective": { + "perspectiveSlicesBackground": backgrd, + "removePerspectiveSlicesBackground": rmPsp, + "perspectiveBackground": backgrd, + "fixedZoomPerspectiveSlices": { + "sliceViewportWidth": 300, + "sliceViewportHeight": 300, + "sliceZoom": 563818.3562426177 * scale, + "sliceViewportSizeMultiplier": 2 + }, + "mesh": { + "backFaceColor": backgrd, + "removeBasedOnNavigation": true, + "flipRemovedOctant": true, + surfaceParcellation + }, + "centerToOrigin": true, + "drawSubstrates": drawSubstrates, + "drawZoomLevels": drawZoomLevels, + "restrictZoomLevel": { + "minZoom": 1200000 * scale, + "maxZoom": 3500000 * scale + } + } + } + } +} + + +export function cvtNavigationObjToNehubaConfig(navigationObj, nehubaConfigObj: RecursivePartial<NgConfig>): RecursivePartial<NgConfig>{ + const { + orientation = [0, 0, 0, 1], + perspectiveOrientation = [0, 0, 0, 1], + perspectiveZoom = 1e6, + zoom = 1e6, + position = [0, 0, 0], + positionReal = true, + } = navigationObj || {} + + const voxelSize = (() => { + const { + navigation = {} + } = nehubaConfigObj || {} + const { pose = {} } = navigation + const { position = {} } = pose + const { voxelSize = [1, 1, 1] } = position + return voxelSize + })() + + return { + perspectiveOrientation, + perspectiveZoom, + navigation: { + pose: { + position: { + voxelCoordinates: positionReal + ? [0, 1, 2].map(idx => position[idx] / voxelSize[idx]) + : position, + voxelSize + }, + orientation, + }, + zoomFactor: zoom + } + } +} diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index d3232e3e098d260c3771b95858fb428ea7f1caf0..156410ee6a7c12efb549b6a72e6928700a315974 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -1,18 +1,18 @@ -import { Inject, Injectable, OnDestroy, Optional } from "@angular/core"; +import { Injectable, OnDestroy } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { BehaviorSubject, combineLatest, from, merge, NEVER, Observable, of, Subject, Subscription } from "rxjs"; -import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators"; -import { viewerStateCustomLandmarkSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; -import { getRgb, IColorMap, INgLayerCtrl, INgLayerInterface, TNgLayerCtrl } from "./layerCtrl.util"; -import { getMultiNgIdsRegionsLabelIndexMap } from "../constants"; +import { BehaviorSubject, combineLatest, from, 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 { IAuxMesh } from '../store' -import { REGION_OF_INTEREST } from "src/util/interfaces"; -import { TRegionDetail } from "src/util/siibraApiConstants/types"; -import { EnumColorMapName } from "src/util/colorMaps"; -import { getShader, PMAP_DEFAULT_CONFIG } from "src/util/constants"; +import { IVolumeTypeDetail } from "src/util/siibraApiConstants/types"; import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerSelectorClearView, ngViewerSelectorLayers } from "src/services/state/ngViewerState.store.helper"; -import { serialiseParcellationRegion } from 'common/util' -import { _PLI_VOLUME_INJ_TOKEN, _TPLIVal } from "src/glue"; +import { hexToRgb } from 'common/util' +import { SAPI, SapiParcellationModel } from "src/atlasComponents/sapi"; +import { SAPISpace } from "src/atlasComponents/sapi/core"; +import { getParcNgId, fromRootStore as nehubaConfigSvcFromRootStore } from "../config.service" +import { getRegionLabelIndex } from "../config.service/util"; +import { annotation, atlasSelection } from "src/state"; +import { serializeSegment } from "../util"; export const BACKUP_COLOR = { red: 255, @@ -20,6 +20,10 @@ export const BACKUP_COLOR = { blue: 255 } +export function getNgLayerName(parc: SapiParcellationModel){ + return parc["@id"] +} + export function getAuxMeshesAndReturnIColor(auxMeshes: IAuxMesh[]): IColorMap{ const returnVal: IColorMap = {} for (const auxMesh of auxMeshes as IAuxMesh[]) { @@ -45,100 +49,103 @@ export class NehubaLayerControlService implements OnDestroy{ static PMAP_LAYER_NAME = 'regional-pmap' private selectedRegion$ = this.store$.pipe( - select(viewerStateSelectedRegionsSelector), + select(atlasSelection.selectors.selectedRegions), shareReplay(1), ) - private selectedParcellation$ = this.store$.pipe( - select(viewerStateSelectedParcellationSelector) - ) - private selectedTemplateSelector$ = this.store$.pipe( - select(viewerStateSelectedTemplateSelector) + private defaultNgLayers$ = this.store$.pipe( + nehubaConfigSvcFromRootStore.getNgLayers(this.store$, this.sapiSvc) ) - private selParcNgIdMap$ = this.selectedParcellation$.pipe( - map(parc => getMultiNgIdsRegionsLabelIndexMap(parc)), + private selectedATP$ = this.store$.pipe( + select(atlasSelection.selectors.selectedATP), shareReplay(1), ) - - private activeColorMap$: Observable<IColorMap> = combineLatest([ - this.selParcNgIdMap$.pipe( - map(map => { + + public selectedATPR$ = this.selectedATP$.pipe( + switchMap(({ atlas, template, parcellation }) => + from(this.sapiSvc.getParcRegions(atlas["@id"], parcellation["@id"], template["@id"])).pipe( + map(regions => ({ + atlas, template, parcellation, regions + })), + shareReplay(1) + ) + ) + ) + + private activeColorMap$ = combineLatest([ + this.selectedATPR$.pipe( + map(({ atlas, parcellation, regions, template }) => { + const returnVal: IColorMap = {} - for (const [ key, val ] of map.entries()) { - returnVal[key] = {} - for (const [ lblIdx, region ] of val.entries()) { - const rgb = getRgb(lblIdx, region) - returnVal[key][lblIdx] = rgb + for (const r of regions) { + + if (!r.hasAnnotation) continue + 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 labelIndex = getRegionLabelIndex(atlas, template, parcellation, r) + if (!labelIndex) continue + + if (!returnVal[ngId]) { + returnVal[ngId] = {} } + returnVal[ngId][labelIndex] = { red, green, blue } } return returnVal }) ), - this.selectedRegion$, - this.selectedTemplateSelector$.pipe( - map(template => { - const { auxMeshes = [] } = template || {} - return getAuxMeshesAndReturnIColor(auxMeshes) - }) - ), - this.selectedParcellation$.pipe( - map(parc => { - const { auxMeshes = [] } = parc || {} - return getAuxMeshesAndReturnIColor(auxMeshes) - }) - ), - ]).pipe( - map(([ regions, selReg, ...auxMeshesArr ]) => { - - const returnVal: IColorMap = {} - if (selReg.length === 0) { - for (const key in regions) { - returnVal[key] = regions[key] - } - } else { - /** - * if selected regions are non empty - * set the selected regions to show color, - * but the rest to show white - */ - for (const key in regions) { - const colorMap = {} - returnVal[key] = colorMap - for (const lblIdx in regions[key]) { - if (selReg.some(r => r.ngId === key && r.labelIndex === Number(lblIdx))) { - colorMap[lblIdx] = regions[key][lblIdx] - } else { - colorMap[lblIdx] = BACKUP_COLOR + this.defaultNgLayers$.pipe( + map(({ tmplAuxNgLayers }) => { + const returnVal: IColorMap = {} + for (const ngId in tmplAuxNgLayers) { + returnVal[ngId] = {} + const { auxMeshes } = tmplAuxNgLayers[ngId] + for (const auxMesh of auxMeshes) { + const { labelIndicies } = auxMesh + for (const lblIdx of labelIndicies) { + returnVal[ngId][lblIdx] = BACKUP_COLOR } } } - } - - for (const auxMeshes of auxMeshesArr) { - for (const key in auxMeshes) { - const existingObj = returnVal[key] || {} - returnVal[key] = { - ...existingObj, - ...auxMeshes[key], - } - } - } - this.activeColorMap = returnVal - return returnVal - }) - ) - - private auxMeshes$: Observable<IAuxMesh[]> = combineLatest([ - this.selectedTemplateSelector$, - this.selectedParcellation$, + return returnVal + }) + ) ]).pipe( - map(([ tmpl, parc ]) => { - const { auxMeshes: tmplAuxMeshes = [] as IAuxMesh[] } = tmpl || {} - const { auxMeshes: parclAuxMeshes = [] as IAuxMesh[] } = parc || {} - return [...tmplAuxMeshes, ...parclAuxMeshes] - }) + map(([cmParc, cmAux]) => ({ + ...cmParc, + ...cmAux + })) + ) + + private auxMeshes$: Observable<IAuxMesh[]> = this.selectedATP$.pipe( + map(({ template }) => template), + switchMap(tmpl => { + return this.sapiSvc.registry.get<SAPISpace>(tmpl["@id"]) + .getVolumes() + .then(tmplVolumes => { + const auxMeshArr: IAuxMesh[] = [] + for (const vol of tmplVolumes) { + if (vol.data.detail["neuroglancer/precompmesh"]) { + const detail = vol.data.detail as IVolumeTypeDetail["neuroglancer/precompmesh"] + for (const auxMesh of detail["neuroglancer/precompmesh"].auxMeshes) { + auxMeshArr.push({ + "@id": `auxmesh-${tmpl["@id"]}-${auxMesh.name}`, + labelIndicies: auxMesh.labelIndicies, + name: auxMesh.name, + ngId: '', + rgb: [255, 255, 255], + visible: auxMesh.name !== "Sulci" + }) + } + } + } + return auxMeshArr + }) + } + ) ) private sub: Subscription[] = [] @@ -147,90 +154,92 @@ export class NehubaLayerControlService implements OnDestroy{ while (this.sub.length > 0) this.sub.pop().unsubscribe() } - private pliVol$: Observable<string[]> = this._pliVol$ - ? this._pliVol$.pipe( - map(arr => { - const output = [] - for (const item of arr) { - for (const volume of item.data["iav-registered-volumes"].volumes) { - output.push(volume.name) - } - } - return output - }) - ) - : NEVER constructor( private store$: Store<any>, - @Optional() @Inject(_PLI_VOLUME_INJ_TOKEN) private _pliVol$: Observable<_TPLIVal[]>, - @Optional() @Inject(REGION_OF_INTEREST) roi$: Observable<TRegionDetail> + private sapiSvc: SAPI, ){ - if (roi$) { + this.sub.push( + 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 + */ - 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 } ) - } + // if (roi$) { - const layer = { - name: NehubaLayerControlService.PMAP_LAYER_NAME, - source : `nifti://${regionalMapUrl}`, - mixability : 'nonmixable', - shader : getShader(shaderObj), - } + // 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 - this.store$.dispatch( - ngViewerActionAddNgLayer({ layer }) - ) + // const shaderObj = { + // ...PMAP_DEFAULT_CONFIG, + // ...{ colormap }, + // ...( typeof min !== 'undefined' ? { lowThreshold: min } : {} ), + // ...( max ? { highThreshold: max } : { highThreshold: 1 } ) + // } - // this.layersService.highThresholdMap.set(layerName, highThreshold) - // this.layersService.lowThresholdMap.set(layerName, lowThreshold) - // this.layersService.colorMapMap.set(layerName, cmap) - // this.layersService.removeBgMap.set(layerName, removeBg) - }) - ) - } + // 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 }) => { @@ -262,7 +271,7 @@ export class NehubaLayerControlService implements OnDestroy{ */ this.sub.push( this.store$.pipe( - select(viewerStateCustomLandmarkSelector), + select(annotation.selectors.annotations), withLatestFrom(this.auxMeshes$) ).subscribe(([landmarks, auxMeshes]) => { @@ -303,34 +312,19 @@ export class NehubaLayerControlService implements OnDestroy{ shareReplay(1) ) - public expectedLayerNames$ = combineLatest([ - this.selectedTemplateSelector$, - this.auxMeshes$, - this.selParcNgIdMap$, - ]).pipe( - map(([ tmpl, auxMeshes, parcNgIdMap ]) => { - const ngIdSet = new Set<string>() - const { ngId } = tmpl - ngIdSet.add(ngId) - for (const auxMesh of auxMeshes) { - const { ngId } = auxMesh - ngIdSet.add(ngId as string) - } - for (const ngId of parcNgIdMap.keys()) { - ngIdSet.add(ngId) - } - return Array.from(ngIdSet) + public expectedLayerNames$ = this.defaultNgLayers$.pipe( + map(({ parcNgLayers, tmplAuxNgLayers, tmplNgLayers }) => { + return [ + ...Object.keys(parcNgLayers), + ...Object.keys(tmplAuxNgLayers), + ...Object.keys(tmplNgLayers), + ] }) ) - public visibleLayer$: Observable<string[]> = combineLatest([ - this.expectedLayerNames$, - this.pliVol$.pipe( - startWith([]) - ), - ]).pipe( - map(([ expectedLayerNames, layerNames ]) => { - const ngIdSet = new Set<string>([...layerNames, ...expectedLayerNames]) + public visibleLayer$: Observable<string[]> = this.expectedLayerNames$.pipe( + map(expectedLayerNames => { + const ngIdSet = new Set<string>([...expectedLayerNames]) return Array.from(ngIdSet) }) ) @@ -338,6 +332,13 @@ export class NehubaLayerControlService implements OnDestroy{ /** * define when shown segments should be updated */ + public _segmentVis$: Observable<string[]> = combineLatest([ + this.selectedATP$, + this.selectedRegion$ + ]).pipe( + map(() => ['']) + ) + public segmentVis$: Observable<string[]> = combineLatest([ /** * selectedRegions @@ -359,15 +360,20 @@ export class NehubaLayerControlService implements OnDestroy{ distinctUntilChanged() ) ]).pipe( - withLatestFrom(this.selectedParcellation$), - map(([[ regions, nonmixableLayerExists, clearViewFlag ], selParc]) => { + withLatestFrom(this.selectedATP$), + map(([[ regions, nonmixableLayerExists, clearViewFlag ], { atlas, parcellation, template }]) => { if (nonmixableLayerExists && !clearViewFlag) { return null } - const { ngId: defaultNgId } = selParc || {} /* selectedregionindexset needs to be updated regardless of forceshowsegment */ - const selectedRegionIndexSet = new Set<string>(regions.map(({ngId = defaultNgId, labelIndex}) => serialiseParcellationRegion({ ngId, labelIndex }))) + const selectedRegionIndexSet = new Set<string>( + regions.map(r => { + const ngId = getParcNgId(atlas, template, parcellation, r) + const label = getRegionLabelIndex(atlas, template, parcellation, r) + return serializeSegment(ngId, label) + }) + ) if (selectedRegionIndexSet.size > 0 && !clearViewFlag) { return [...selectedRegionIndexSet] } else { diff --git a/src/viewerModule/nehuba/mesh.effects/mesh.effects.ts b/src/viewerModule/nehuba/mesh.effects/mesh.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d6fe28a1dc844f2c4e94340a7d51a0835fb0cf1 --- /dev/null +++ b/src/viewerModule/nehuba/mesh.effects/mesh.effects.ts @@ -0,0 +1,23 @@ +import { Injectable } from "@angular/core"; +import { createEffect } from "@ngrx/effects"; +import { select, Store } from "@ngrx/store"; +import { filter } from "jszip"; +import { combineLatest } from "rxjs"; +import { map } from "rxjs/operators"; +import { actionSetAuxMeshes } from "../store"; + +@Injectable() +export class MeshEffects{ + constructor(private store: Store<any>){ + + } + // auxMeshEffect$ = createEffect(() => this.store.pipe( + + // map(() => { + // console.log("TODO need to fix") + // return actionSetAuxMeshes({ + // payload: [] + // }) + // }) + // )) +} \ No newline at end of file diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts index 967762e46acb61182a31dfc625772c14da8a2dca..b5a1e065ecddaf750fdc8007bb6f02ffd7c165b1 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts @@ -2,8 +2,7 @@ import { TestBed } from "@angular/core/testing" import { MockStore, provideMockStore } from "@ngrx/store/testing" import { hot } from "jasmine-marbles" import { viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors" -import { selectorAuxMeshes } from "../store" -import { getLayerNameIndiciesFromParcRs, collateLayerNameIndicies, findFirstChildrenWithLabelIndex, NehubaMeshService } from "./mesh.service" +import { NehubaMeshService } from "./mesh.service" const fits1 = { @@ -41,82 +40,6 @@ const nofit2 = { } describe('> mesh.server.ts', () => { - describe('> findFirstChildrenWithLabelIndex', () => { - it('> if root fits, return root', () => { - const result = findFirstChildrenWithLabelIndex({ - ...fits1, - children: [fits2] - }) - - expect(result).toEqual([{ - ...fits1, - children: [fits2] - }]) - }) - - it('> if root doesnt fit, will try to find the next node, until one fits', () => { - const result = findFirstChildrenWithLabelIndex({ - ...nofit1, - children: [fits1, fits2] - }) - expect(result).toEqual([fits1, fits2]) - }) - - it('> if notthings fits, will return empty array', () => { - const result = findFirstChildrenWithLabelIndex({ - ...nofit1, - children: [nofit1, nofit2] - }) - expect(result).toEqual([]) - }) - }) - - describe('> collateLayerNameIndicies', () => { - it('> collates same ngIds', () => { - const result = collateLayerNameIndicies([ - fits1_1, fits1, fits2, fits2_1 - ]) - expect(result).toEqual({ - [fits1.ngId]: [fits1_1.labelIndex, fits1.labelIndex], - [fits2.ngId]: [fits2.labelIndex, fits2_1.labelIndex] - }) - }) - }) - - describe('> getLayerNameIndiciesFromParcRs', () => { - const root = { - ...fits1, - children: [ - { - ...nofit1, - children: [ - { - ...fits1_1, - children: [ - fits2, fits2_1 - ] - } - ] - } - ] - } - const parc = { - regions: [ root ] - } - it('> if selectedRegion.length === 0, selects top most regions with labelIndex', () => { - const result = getLayerNameIndiciesFromParcRs(parc, []) - expect(result).toEqual({ - [root.ngId]: [root.labelIndex] - }) - }) - - it('> if selReg.length !== 0, select region ngId & labelIndex', () => { - const result = getLayerNameIndiciesFromParcRs(parc, [ fits1_1 ]) - expect(result).toEqual({ - [fits1_1.ngId]: [fits1_1.labelIndex] - }) - }) - }) describe('> NehubaMeshService', () => { beforeEach(() => { @@ -140,16 +63,6 @@ describe('> mesh.server.ts', () => { const mockStore = TestBed.inject(MockStore) mockStore.overrideSelector(viewerStateSelectedRegionsSelector, [ fits1 ]) - mockStore.overrideSelector(selectorAuxMeshes, [{ - ngId: fits2.ngId, - labelIndicies: [11, 22], - "@id": '', - name: '', - rgb: [100, 100, 100], - visible: true, - displayName: '' - }]) - const service = TestBed.inject(NehubaMeshService) expect( service.loadMeshes$ diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.ts index dd17226af19ad4c3ec2dfc8f10dbc79a96e53635..552af17c944f40a27e8b6ec1427e7b29ad6f88ac 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.ts @@ -1,11 +1,12 @@ import { Injectable, OnDestroy } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { combineLatest, Observable, of } from "rxjs"; -import { switchMap } from "rxjs/operators"; -import { viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; +import { Observable, of } from "rxjs"; +import { map, switchMap } from "rxjs/operators"; import { IMeshesToLoad } from '../constants' -import { flattenReducer } from 'common/util' -import { IAuxMesh, selectorAuxMeshes, actionSetAuxMeshes } from "../store"; +import { selectorAuxMeshes } from "../store"; +import { SAPI } from "src/atlasComponents/sapi"; +import { fromRootStore as nehubaConfigSvcFromRootStore } from "../config.service"; +import { atlasSelection } from "src/state"; interface IRegion { ngId?: string @@ -13,44 +14,6 @@ interface IRegion { children: IRegion[] } -interface IParc { - ngId?: string - regions: IRegion[] -} - -type TCollatedLayerNameIdx = { - [key: string]: number[] -} - -export function findFirstChildrenWithLabelIndex(region: IRegion): IRegion[]{ - if (region.ngId && region.labelIndex) { - return [ region ] - } - return region.children - .map(findFirstChildrenWithLabelIndex) - .reduce(flattenReducer, []) -} - -export function collateLayerNameIndicies(regions: IRegion[]){ - const returnObj: TCollatedLayerNameIdx = {} - for (const r of regions) { - if (returnObj[r.ngId]) { - returnObj[r.ngId].push(r.labelIndex) - } else { - returnObj[r.ngId] = [r.labelIndex] - } - } - return returnObj -} - -export function getLayerNameIndiciesFromParcRs(parc: IParc, rs: IRegion[]): TCollatedLayerNameIdx { - - const arrOfRegions = (rs.length === 0 ? parc.regions : rs) - .map(findFirstChildrenWithLabelIndex) - .reduce(flattenReducer, []) as IRegion[] - - return collateLayerNameIndicies(arrOfRegions) -} /** * control mesh loading etc @@ -62,98 +25,57 @@ export class NehubaMeshService implements OnDestroy { private onDestroyCb: (() => void)[] = [] constructor( - private store$: Store<any> + private store$: Store<any>, + private sapiSvc: SAPI ){ - const auxMeshSub = combineLatest([ - this.selectedTemplate$, - this.selectedParc$ - ]).subscribe(([ tmpl, parc ]) => { - const { auxMeshes: tmplAuxMeshes = [] as IAuxMesh[] } = tmpl || {} - const { auxMeshes: parcAuxMeshes = [] as IAuxMesh[]} = parc || {} - this.store$.dispatch( - actionSetAuxMeshes({ - payload: [...tmplAuxMeshes, ...parcAuxMeshes] - }) - ) - }) - this.onDestroyCb.push(() => auxMeshSub.unsubscribe()) } ngOnDestroy(){ while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() } - private selectedTemplate$ = this.store$.pipe( - select(viewerStateSelectedTemplateSelector) + private allRegions$ = this.store$.pipe( + select(atlasSelection.selectors.selectedATP), + switchMap(({ atlas, template, parcellation }) => this.sapiSvc.getParcRegions(atlas["@id"], parcellation["@id"], template["@id"])) ) private selectedRegions$ = this.store$.pipe( - select(viewerStateSelectedRegionsSelector) + select(atlasSelection.selectors.selectedRegions) ) - private selectedParc$ = this.store$.pipe( - select(viewerStateSelectedParcellationSelector) - ) private auxMeshes$ = this.store$.pipe( select(selectorAuxMeshes), ) - public loadMeshes$: Observable<IMeshesToLoad> = combineLatest([ - this.auxMeshes$, - this.selectedTemplate$, - this.selectedParc$, - this.selectedRegions$, - ]).pipe( - switchMap(([auxMeshes, template, parc, selRegions]) => { - - /** - * if colin 27 and julich brain 2.9.0, select all regions - */ - let overrideSelRegion = null - if ( - template['@id'] === 'minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992' && - parc['@id'] === 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290' - ) { - overrideSelRegion = [] - } + private ngLayers$ = this.store$.pipe( + nehubaConfigSvcFromRootStore.getNgLayers(this.store$, this.sapiSvc) + ) - const obj = getLayerNameIndiciesFromParcRs(parc, overrideSelRegion || selRegions) - const { auxillaryMeshIndices = [] } = parc + public loadMeshes$: Observable<IMeshesToLoad> = this.ngLayers$.pipe( + switchMap(ngLayers => { const arr: IMeshesToLoad[] = [] - for (const key in obj) { - const labelIndicies = Array.from(new Set([...obj[key], ...auxillaryMeshIndices])) - arr.push({ - layer: { - name: key - }, - labelIndicies - }) - } - - const auxLayers: { - [key: string]: number[] - } = {} - - for (const auxMesh of auxMeshes) { - const { name, ngId, labelIndicies } = auxMesh - if (!auxLayers[ngId]) { - auxLayers[ngId] = [] + const { parcNgLayers, tmplAuxNgLayers, tmplNgLayers } = ngLayers + for (const ngId in tmplAuxNgLayers) { + const meshToLoad: IMeshesToLoad = { + labelIndicies: [], + layer: { name: ngId } } - if (auxMesh.visible) { - auxLayers[ngId].push(...labelIndicies) + for (const auxMesh of tmplAuxNgLayers[ngId].auxMeshes) { + meshToLoad.labelIndicies.push(...auxMesh.labelIndicies) } + arr.push(meshToLoad) } - for (const key in auxLayers) { + + for (const ngId in parcNgLayers) { + const {labelIndicies} = parcNgLayers[ngId] arr.push({ - layer: { - name: key - }, - labelIndicies: auxLayers[key] + labelIndicies, + layer: { name: ngId } }) } return of(...arr) - }), + }) ) } diff --git a/src/viewerModule/nehuba/module.ts b/src/viewerModule/nehuba/module.ts index 75dd30a18861417639d86bc543c4e368f00fc74d..96b57ff45f539d69a229efd2041f604a0be31543 100644 --- a/src/viewerModule/nehuba/module.ts +++ b/src/viewerModule/nehuba/module.ts @@ -28,6 +28,8 @@ import { WindowResizeModule } from "src/util/windowResize"; import { ViewerCtrlModule } from "./viewerCtrl"; import { DragDropFileModule } from "src/dragDropFile/module"; import { NgLayerCtrlCmp } from "./ngLayerCtl/ngLayerCtrl.component"; +import { EffectsModule } from "@ngrx/effects"; +import { MeshEffects } from "./mesh.effects/mesh.effects"; @NgModule({ imports: [ @@ -55,6 +57,9 @@ import { NgLayerCtrlCmp } from "./ngLayerCtl/ngLayerCtrl.component"; NEHUBA_VIEWER_FEATURE_KEY, reducer ), + EffectsModule.forFeature([ + MeshEffects + ]), QuickTourModule ], declarations: [ @@ -75,6 +80,7 @@ import { NgLayerCtrlCmp } from "./ngLayerCtl/ngLayerCtrl.component"; NgLayerCtrlCmp, ], providers: [ + { provide: IMPORT_NEHUBA_INJECT_TOKEN, useFactory: importNehubaFactory, diff --git a/src/viewerModule/nehuba/navigation.service/navigation.service.ts b/src/viewerModule/nehuba/navigation.service/navigation.service.ts index ed751e8b85149abb089fbd5cf5adf0ec888abdee..7836d27972f0bb75906f7768e0c1d4e4c76d6f16 100644 --- a/src/viewerModule/nehuba/navigation.service/navigation.service.ts +++ b/src/viewerModule/nehuba/navigation.service/navigation.service.ts @@ -3,12 +3,12 @@ import { select, Store } from "@ngrx/store"; import { Observable, ReplaySubject, Subscription } from "rxjs"; import { debounceTime } from "rxjs/operators"; import { selectViewerConfigAnimationFlag } from "src/services/state/viewerConfig/selectors"; -import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; -import { viewerStateSelectorNavigation } from "src/services/state/viewerState/selectors"; import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; import { NEHUBA_INSTANCE_INJTKN } from "../util"; import { timedValues } from 'src/util/generator' import { INavObj, navAdd, navMul, navObjEqual } from './navigation.util' +import { actions } from "src/state/atlasSelection"; +import { atlasSelection } from "src/state"; @Injectable() export class NehubaNavigationService implements OnDestroy{ @@ -52,56 +52,25 @@ export class NehubaNavigationService implements OnDestroy{ this.subscriptions.push( // realtime state nav state this.store$.pipe( - select(viewerStateSelectorNavigation) + select(atlasSelection.selectors.navigation) ).subscribe(v => { this.storeNav = v // if stored nav differs from viewerNav if (!this.viewerNavLock && this.nehubaViewerInstance) { const navEql = navObjEqual(this.storeNav, this.viewerNav) if (!navEql) { - this.navigateViewer({ - ...this.storeNav, - positionReal: true - }) + this.navigateViewer(this.storeNav) } } }) ) } - navigateViewer(navigation: INavObj & { positionReal?: boolean, animation?: any }){ + navigateViewer(navigation: INavObj){ if (!navigation) return - const { animation, ...rest } = navigation - if (animation && this.globalAnimationFlag) { - - const gen = timedValues() - const src = this.viewerNav - - const dest = { - ...src, - ...navigation - } - - const delta = navAdd(dest, navMul(src, -1)) - - const animate = () => { - const next = gen.next() - const d = next.value - - const n = navAdd(src, navMul(delta, d)) - this.nehubaViewerInstance.setNavigationState({ - ...n, - positionReal: true - }) - - if ( !next.done ) { - this.rafRef = requestAnimationFrame(() => animate()) - } - } - this.rafRef = requestAnimationFrame(() => animate()) - } else { - this.nehubaViewerInstance.setNavigationState(rest) - } + // TODO + // readd consider how to do animation + this.nehubaViewerInstance.setNavigationState(navigation) } setupViewerSub(){ @@ -134,7 +103,7 @@ export class NehubaNavigationService implements OnDestroy{ if (!navEql) { this.store$.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: roundedNav }) ) diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 8281c6565f4f6c717e8586877285781f631a1069..c3b14f7b92e2d95e9b1bd2df9c0ed88119715bae 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -5,8 +5,8 @@ import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.se import { StateInterface as ViewerConfiguration } from "src/services/state/viewerConfig.store"; import { LoggingService } from "src/logging"; import { bufferUntil, getExportNehuba, getViewer, setNehubaViewer, switchMapWaitFor } from "src/util/fn"; -import { NEHUBA_INSTANCE_INJTKN, scanSliceViewRenderFn } from "../util"; -import { deserialiseParcRegionId, arrayOrderedEql } from 'common/util' +import { deserializeSegment, NEHUBA_INSTANCE_INJTKN, scanSliceViewRenderFn } from "../util"; +import { arrayOrderedEql } from 'common/util' import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants"; import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service"; @@ -68,6 +68,9 @@ export const scanFn = (acc: LayerLabelIndex[], curr: LayerLabelIndex) => { export class NehubaViewerUnit implements OnInit, OnDestroy { + + public ngIdSegmentsMap: Record<string, number[]> = {} + private sliceviewLoading$: Observable<boolean> public overrideShowLayers: string[] = [] @@ -90,7 +93,6 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { @Output() public mouseoverSegmentEmitter: EventEmitter<{ segmentId: number | null - segment: string | null layer: { name?: string url?: string @@ -98,10 +100,14 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { }> = new EventEmitter() @Output() public mouseoverLandmarkEmitter: EventEmitter<string> = new EventEmitter() @Output() public mouseoverUserlandmarkEmitter: EventEmitter<string> = new EventEmitter() - @Output() public regionSelectionEmitter: EventEmitter<{segment: number, layer: {name?: string, url?: string}}> = new EventEmitter() + @Output() public regionSelectionEmitter: EventEmitter<{ + segment: number, + layer: { + name?: string, + url?: string + }}> = new EventEmitter() @Output() public errorEmitter: EventEmitter<any> = new EventEmitter() - public auxilaryMeshIndices: number[] = [] /* only used to set initial navigation state */ public initNav: any @@ -185,45 +191,6 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { }) .catch(e => this.errorEmitter.emit(e)) - /** - * TODO move to layerCtrl.service - */ - this.ondestroySubscriptions.push( - fromEvent(this.workerService.worker, 'message').pipe( - filter((message: any) => { - - if (!message) { - // this.log.error('worker response message is undefined', message) - return false - } - if (!message.data) { - // this.log.error('worker response message.data is undefined', message.data) - return false - } - if (message.data.type !== 'ASSEMBLED_LANDMARKS_VTK') { - /* worker responded with not assembled landmark, no need to act */ - return false - } - if (!message.data.url) { - /* file url needs to be defined */ - return false - } - return true - }), - debounceTime(100), - filter(e => this.templateId === e.data.template), - map(e => e.data.url), - ).subscribe(url => { - this.removeSpatialSearch3DLandmarks() - const _ = {} - _[NG_LANDMARK_LAYER_NAME] = { - type : 'mesh', - source : `vtk://${url}`, - shader : FRAGMENT_MAIN_WHITE, - } - this.loadLayer(_) - }), - ) /** * TODO move to layerCtrl.service @@ -338,7 +305,15 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { if (this.segVis$) { this.ondestroySubscriptions.push( - this.segVis$.pipe().subscribe(val => { + this.segVis$.pipe( + switchMap( + switchMapWaitFor({ + condition: () => this.nehubaViewer?.ngviewer, + leading: true, + }) + ) + ).subscribe(val => { + console.log(val) // null === hide all seg if (val === null) { this.hideAllSeg() @@ -404,15 +379,6 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { } } - /* required to check if correct landmarks are loaded */ - private _templateId: string - get templateId() { - return this._templateId - } - set templateId(id: string) { - this._templateId = id - } - public spatialLandmarkSelectionChanged(labels: number[]) { const getCondition = (label: number) => `if(label > ${label - 0.1} && label < ${label + 0.1} ){${FRAGMENT_EMIT_RED}}` const newShader = `void main(){ ${labels.map(getCondition).join('else ')}else {${FRAGMENT_EMIT_WHITE}} }` @@ -432,10 +398,6 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { } } - // TODO move region management to another service - - public multiNgIdsLabelIndexMap: Map<string, Map<number, any>> = new Map() - public navPosReal: [number, number, number] = [0, 0, 0] public navPosVoxel: [number, number, number] = [0, 0, 0] @@ -579,10 +541,12 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { * The emitted value does not affect the region selection * the region selection is taken care of in nehubaContainer */ - const map = this.multiNgIdsLabelIndexMap.get(this.mouseOverLayer.name) - const region = map && map.get(this.mouseOverSegment) + if (arg === 'select') { - this.regionSelectionEmitter.emit({ segment: region, layer: this.mouseOverLayer }) + this.regionSelectionEmitter.emit({ + segment: this.mouseOverSegment, + layer: this.mouseOverLayer + }) } } @@ -662,23 +626,6 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { }) } - // pos in mm - public addSpatialSearch3DLandmarks(geometries: any[], scale?: number, type?: 'icosahedron') { - this.workerService.worker.postMessage({ - type : 'GET_LANDMARKS_VTK', - template : this.templateId, - scale: Math.min(...this.dim.map(v => v * NG_LANDMARK_CONSTANT)), - landmarks : geometries.map(geometry => - geometry === null - ? null - // gemoetry : [number,number,number] | [ [number,number,number][], [number,number,number][] ] - : isNaN(geometry[0]) - ? [geometry[0].map(triplets => triplets.map(coord => coord * 1e6)), geometry[1]] - : geometry.map(coord => coord * 1e6), - ), - }) - } - public setLayerVisibility(condition: {name: string|RegExp}, visible: boolean) { if (!this.nehubaViewer) { return false @@ -728,28 +675,34 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { } public hideAllSeg() { - if (!this.nehubaViewer) { return } - Array.from(this.multiNgIdsLabelIndexMap.keys()).forEach(ngId => { - - Array.from(this.multiNgIdsLabelIndexMap.get(ngId).keys()).forEach(idx => { + if (!this.nehubaViewer) return + for (const ngId in this.ngIdSegmentsMap) { + for (const idx of this.ngIdSegmentsMap[ngId]) { this.nehubaViewer.hideSegment(idx, { name: ngId, }) - }) + } + this.nehubaViewer.showSegment(0, { name: ngId, }) - }) + } } public showAllSeg() { if (!this.nehubaViewer) { return } - this.hideAllSeg() - Array.from(this.multiNgIdsLabelIndexMap.keys()).forEach(ngId => { + for (const ngId in this.ngIdSegmentsMap) { + console.log(ngId) + for (const idx of this.ngIdSegmentsMap[ngId]) { + this.nehubaViewer.showSegment(idx, { + name: ngId, + }) + } + this.nehubaViewer.hideSegment(0, { name: ngId, }) - }) + } } public showSegs(array: (number|string)[]) { @@ -772,7 +725,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { const reduceFn: (acc: Map<string, number[]>, curr: string) => Map<string, number[]> = (acc, curr) => { const newMap = new Map(acc) - const { ngId, labelIndex } = deserialiseParcRegionId(curr) + const { ngId, label: labelIndex } = deserializeSegment(curr) const exist = newMap.get(ngId) if (!exist) { newMap.set(ngId, [Number(labelIndex)]) @@ -804,7 +757,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { }) } - private vec3(pos: [number, number, number]) { + private vec3(pos: number[]) { return this.exportNehuba.vec3.fromValues(...pos) } @@ -883,7 +836,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { /** * remove transparency from meshes in current layer(s) */ - for (const layerKey of this.multiNgIdsLabelIndexMap.keys()) { + for (const layerKey in this.ngIdSegmentsMap) { const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerKey) if (layer) { layer.layer.displayState.objectAlpha.restoreState(flag ? 0.2 : 1.0) @@ -917,13 +870,8 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { } this._s8$ = this.nehubaViewer.mouseOver.segment.subscribe(({segment: segmentId, layer }) => { - - const {name = 'unnamed'} = layer - const map = this.multiNgIdsLabelIndexMap.get(name) - const region = map && map.get(segmentId) this.mouseoverSegmentEmitter.emit({ layer, - segment: region, segmentId, }) }) @@ -1053,10 +1001,10 @@ const patchSliceViewPanel = (sliceViewPanel: any) => { } export interface ViewerState { - orientation: [number, number, number, number] - perspectiveOrientation: [number, number, number, number] + orientation: number[] + perspectiveOrientation: number[] perspectiveZoom: number - position: [number, number, number] + position: number[] positionReal: boolean zoom: number } diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts index 8aa04373cf3c7d7a8c145d221d30c3d4f1b15a3e..faa78cb3d0e330b2516c0d8c8bafe45deb94ec5e 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts @@ -9,8 +9,6 @@ import { ClickInterceptorService } from "src/glue" import { LayoutModule } from "src/layouts/layout.module" import { PANELS } from "src/services/state/ngViewerState/constants" import { ngViewerSelectorOctantRemoval, ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from "src/services/state/ngViewerState/selectors" -import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors" -import { viewerStateSetSelectedRegions } from "src/services/state/viewerState.store.helper" import { viewerStateCustomLandmarkSelector, viewerStateNavigationStateSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors" import { Landmark2DModule } from "src/ui/nehubaContainer/2dLandmarks/module" import { QuickTourModule } from "src/ui/quickTour" @@ -139,7 +137,6 @@ describe('> nehubaViewerGlue.component.ts', () => { mockStore.overrideSelector(ngViewerSelectorOctantRemoval, true) mockStore.overrideSelector(viewerStateCustomLandmarkSelector, []) mockStore.overrideSelector(viewerStateSelectedRegionsSelector, []) - mockStore.overrideSelector(uiStateMouseOverSegmentsSelector, []) mockStore.overrideSelector(viewerStateNavigationStateSelector, null) mockStore.overrideSelector(selectorAuxMeshes, []) @@ -186,7 +183,6 @@ describe('> nehubaViewerGlue.component.ts', () => { const testObj1 = 'hello world' beforeEach(() => { fallbackSpy = spyOn(clickIntServ, 'fallback') - mockStore.overrideSelector(uiStateMouseOverSegmentsSelector, [testObj1, testObj0] as any) TestBed.createComponent(NehubaGlueCmp) clickIntServ.callRegFns(null) }) @@ -215,7 +211,6 @@ describe('> nehubaViewerGlue.component.ts', () => { } beforeEach(() => { fallbackSpy = spyOn(clickIntServ, 'fallback') - mockStore.overrideSelector(uiStateMouseOverSegmentsSelector, [testObj0, testObj1, testObj2] as any) }) afterEach(() => { @@ -225,11 +220,6 @@ describe('> nehubaViewerGlue.component.ts', () => { TestBed.createComponent(NehubaGlueCmp) clickIntServ.callRegFns(null) const { segment } = testObj1 - expect(dispatchSpy).toHaveBeenCalledWith( - viewerStateSetSelectedRegions({ - selectRegions: [segment] - } as any) - ) }) it('> fallback called (does not intercept)', () => { TestBed.createComponent(NehubaGlueCmp) diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index b2e1faf29433c748c5b54d163a98b13a72359729..55ae60bb353cf0b48e9ce03bdfcb2d76a703cac4 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -1,19 +1,14 @@ -import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges, TemplateRef, ViewChild } from "@angular/core"; +import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnDestroy, Optional, Output, TemplateRef, ViewChild } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { asyncScheduler, combineLatest, fromEvent, merge, NEVER, Observable, of, Subject, Subscription } from "rxjs"; +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 { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors"; import { debounceTime, distinctUntilChanged, filter, map, mapTo, scan, shareReplay, startWith, switchMap, switchMapTo, take, tap, throttleTime } from "rxjs/operators"; -import { viewerStateAddUserLandmarks, viewerStateChangeNavigation, viewerStateMouseOverCustomLandmark, viewerStateSelectRegionWithIdDeprecated, viewerStateSetSelectedRegions, viewreStateRemoveUserLandmarks } from "src/services/state/viewerState/actions"; +import { viewerStateAddUserLandmarks, viewerStateMouseOverCustomLandmark } from "src/services/state/viewerState/actions"; import { ngViewerSelectorPanelOrder, ngViewerSelectorPanelMode } from "src/services/state/ngViewerState/selectors"; -import { viewerStateCustomLandmarkSelector, viewerStateNavigationStateSelector } from "src/services/state/viewerState/selectors"; -import { serialiseParcellationRegion } from 'common/util' import { ARIA_LABELS, IDS, QUICKTOUR_DESC } from 'common/constants' import { PANELS } from "src/services/state/ngViewerState/constants"; import { LoggingService } from "src/logging"; - -import { getMultiNgIdsRegionsLabelIndexMap, SET_MESHES_TO_LOAD } from "../constants"; import { EnumViewerEvt, IViewer, TViewerEvent } from "../../viewer.interface"; import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; import { NehubaViewerContainerDirective, TMouseoverEvent } from "../nehubaViewerInterface/nehubaViewerInterface.directive"; @@ -31,6 +26,12 @@ import { getShader } from "src/util/constants"; import { EnumColorMapName } from "src/util/colorMaps"; 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 } from "src/state"; export const INVALID_FILE_INPUT = `Exactly one (1) nifti file is required!` @@ -72,7 +73,7 @@ export const INVALID_FILE_INPUT = `Exactly one (1) nifti file is required!` ] }) -export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, AfterViewInit { +export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewInit { @ViewChild('layerCtrlTmpl', { read: TemplateRef }) layerCtrlTmpl: TemplateRef<any> @@ -89,19 +90,49 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A public viewerLoaded: boolean = false - private onhoverSegments = [] + private onhoverSegments: SapiRegionModel[] = [] private onDestroyCb: (() => void)[] = [] private viewerUnit: NehubaViewerUnit - private multiNgIdsRegionsLabelIndexMap: Map<string, Map<number, any>> + private multiNgIdsRegionsLabelIndexMap = new Map<string, Map<number, SapiRegionModel>>() + private selectedParcellation$ = new BehaviorSubject<SapiParcellationModel>(null) + private _selectedParcellation: SapiParcellationModel + get selectedParcellation(){ + return this._selectedParcellation + } @Input() - public selectedParcellation: any + set selectedParcellation(val: SapiParcellationModel) { + this._selectedParcellation = val + this.selectedParcellation$.next(val) + } + + private selectedTemplate$ = new BehaviorSubject<SapiSpaceModel>(null) + private _selectedTemplate: SapiSpaceModel + get selectedTemplate(){ + return this._selectedTemplate + } @Input() - public selectedTemplate: any + set selectedTemplate(val: SapiSpaceModel) { + this._selectedTemplate = val + this.selectedTemplate$.next(val) + } - private navigation: any + private selectedAtlas$ = new BehaviorSubject<SapiAtlasModel>(null) + private _selectedAtlas: SapiAtlasModel + get selectedAtlas(){ + return this._selectedAtlas + } + @Input() + set selectedAtlas(val: SapiAtlasModel) { + this._selectedAtlas = val + this.selectedAtlas$.next(val) + } + + + public nehubaConfig: NehubaConfig + private navigation: any private newViewer$ = new Subject() public showPerpsectiveScreen$: Observable<string> @@ -129,14 +160,8 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A description: QUICKTOUR_DESC.VIEW_ICONS, } - public customLandmarks$: Observable<any> = this.store$.pipe( - select(viewerStateCustomLandmarkSelector), - map(lms => lms.map(lm => ({ - ...lm, - geometry: { - position: lm.position - } - }))), + public customLandmarks$ = this.store$.pipe( + select(annotation.selectors.annotations), ) public filterCustomLandmark(lm: any){ @@ -149,26 +174,6 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A shareReplay(1), ) - ngOnChanges(sc: SimpleChanges){ - const { - selectedParcellation, - selectedTemplate - } = sc - if (selectedTemplate) { - if (selectedTemplate?.currentValue?.['@id'] !== selectedTemplate?.previousValue?.['@id']) { - - if (selectedTemplate?.previousValue) { - this.unloadTmpl(selectedTemplate?.previousValue) - } - if (selectedTemplate?.currentValue?.['@id']) { - this.loadTmpl(selectedTemplate.currentValue, selectedParcellation.currentValue) - } - } - }else if (selectedParcellation && selectedParcellation.currentValue !== selectedParcellation.previousValue) { - this.loadParc(selectedParcellation.currentValue) - } - } - private nehubaContainerSub: Subscription private setupNehubaEvRelay() { if (this.nehubaContainerSub) this.nehubaContainerSub.unsubscribe() @@ -201,10 +206,15 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A payload: { nav, mouse, - nehuba: seg.map(v => { + nehuba: seg && seg.map(v => { return { layerName: v.layer.name, - labelIndices: [ Number(v.segmentId) ] + labelIndices: [ Number(v.segmentId) ], + regions: (() => { + const map = this.multiNgIdsRegionsLabelIndexMap.get(v.layer.name) + if (!map) return [] + return [map.get(Number(v.segmentId))] + })() } }) } @@ -230,22 +240,25 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A while (this.onDestroyCb.length) this.onDestroyCb.pop()() } - private loadParc(parcellation: any) { + private async loadParc(atlas: SapiAtlasModel, parcellation: SapiParcellationModel, space: SapiSpaceModel, ngLayers: Record<string, NgLayerSpec | NgPrecompMeshSpec | NgSegLayerSpec>) { /** - * parcellaiton may be undefined + * parcellation may be undefined */ - if ( !(parcellation && parcellation.regions)) { + if ( !parcellation) { return } + const pevs = await this.sapiSvc.getParcRegions(atlas["@id"], parcellation["@id"], space["@id"]) - this.multiNgIdsRegionsLabelIndexMap = getMultiNgIdsRegionsLabelIndexMap(parcellation) - - this.viewerUnit.multiNgIdsLabelIndexMap = this.multiNgIdsRegionsLabelIndexMap - this.viewerUnit.auxilaryMeshIndices = parcellation.auxillaryMeshIndices || [] - + const ngIdSegmentsMap: Record<string, number[]> = {} + for (const key in ngLayers) { + if ((ngLayers[key] as NgSegLayerSpec).labelIndicies) { + ngIdSegmentsMap[key] = (ngLayers[key] as NgSegLayerSpec).labelIndicies + } + } + this.viewerUnit.ngIdSegmentsMap = ngIdSegmentsMap } - private unloadTmpl(tmpl: any) { + private unloadTmpl() { /** * clear existing container */ @@ -253,36 +266,45 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A this.nehubaContainerDirective.clear() /* on selecting of new template, remove additional nglayers */ - const baseLayerNames = Object.keys(tmpl.nehubaConfig.dataset.initialNgState.layers) - this.layerCtrlService.removeNgLayers(baseLayerNames) + if (this.nehubaConfig) { + const baseLayerNames = Object.keys(this.nehubaConfig.dataset.initialNgState.layers) + this.layerCtrlService.removeNgLayers(baseLayerNames) + } } - private async loadTmpl(_template: any, parcellation: any) { + private async loadTmpl(atlas: SapiAtlasModel, _template: SapiSpaceModel, parcellation: SapiParcellationModel, ngLayers: Record<string, NgLayerSpec | NgPrecompMeshSpec | NgSegLayerSpec>) { if (!_template) return /** * recalcuate zoom */ - const template = (() => { - - const deepCopiedState = JSON.parse(JSON.stringify(_template)) - const initialNgState = deepCopiedState.nehubaConfig.dataset.initialNgState - - if (!initialNgState || !this.navigation) { - return deepCopiedState - } - const overwritingInitState = this.navigation - ? cvtNavigationObjToNehubaConfig(this.navigation, initialNgState) - : {} + const validSpaceIds = parcellation.brainAtlasVersions.map(bas => bas.coordinateSpace["@id"] as string) + let template: SapiSpaceModel + if (validSpaceIds.indexOf(_template["@id"]) >= 0) { + template = _template + } else { + /** + * 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}` + })) + template = await this.sapiSvc.getSpaceDetail(this.selectedAtlas["@id"], parcellation.brainAtlasVersions[0].coordinateSpace["@id"] as string) + } + const config = getNehubaConfig(template) + config.dataset.initialNgState.layers = ngLayers + const overwritingInitState = this.navigation + ? cvtNavigationObjToNehubaConfig(this.navigation, config.dataset.initialNgState) + : {} + + config.dataset.initialNgState = { + ...config.dataset.initialNgState, + ...overwritingInitState, + } - deepCopiedState.nehubaConfig.dataset.initialNgState = { - ...initialNgState, - ...overwritingInitState, - } - return deepCopiedState - })() + this.nehubaConfig = config - this.nehubaContainerDirective.createNehubaInstance(template) + this.nehubaContainerDirective.createNehubaInstance(config) this.viewerUnit = this.nehubaContainerDirective.nehubaViewerInstance this.sliceRenderEvent$.pipe( takeOnePipe() @@ -296,19 +318,17 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A this.nanometersToOffsetPixelsFn[idx] = e.detail.nanometersToOffsetPixels } }) - const foundParcellation = parcellation - && template?.parcellations?.find(p => parcellation.name === p.name) - this.loadParc(foundParcellation || template.parcellations[0]) - const nehubaConfig = template.nehubaConfig - const initialSpec = nehubaConfig.dataset.initialNgState + await this.loadParc(atlas, parcellation, template, ngLayers) + + const initialSpec = config.dataset.initialNgState const {layers} = initialSpec - const dispatchLayers = Object.keys(layers).map(key => { + const dispatchLayers = Object.keys(layers).map((key, idx) => { const layer = { name : key, source : layers[key].source, - mixability : layers[key].type === 'image' + mixability : idx === 0 ? 'base' : 'mixable', visible : typeof layers[key].visible === 'undefined' @@ -338,6 +358,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, @Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) setViewerHandle: TSetViewerHandle, @Optional() private layerCtrlService: NehubaLayerControlService, + private sapiSvc: SAPI, ){ /** @@ -350,6 +371,22 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A this.onDestroyCb.push(() => deregister(selOnhoverRegion)) } + /** + * subscribe to ngIdtolblIdxToRegion + */ + const ngIdSub = this.layerCtrlService.selectedATPR$.subscribe(({ atlas, parcellation, template, regions }) => { + this.multiNgIdsRegionsLabelIndexMap.clear() + for (const r of regions) { + const ngId = getParcNgId(atlas, template, parcellation, r) + const labelIndex = getRegionLabelIndex(atlas, template, parcellation, r) + if (!this.multiNgIdsRegionsLabelIndexMap.has(ngId)) { + this.multiNgIdsRegionsLabelIndexMap.set(ngId, new Map()) + } + this.multiNgIdsRegionsLabelIndexMap.get(ngId).set(labelIndex, r) + } + }) + this.onDestroyCb.push(() => ngIdSub.unsubscribe()) + /** * on layout change */ @@ -418,11 +455,10 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A * on hover segment */ const onhovSegSub = this.store$.pipe( - select(uiStateMouseOverSegmentsSelector), + select(userInteraction.selectors.mousingOverRegions), distinctUntilChanged(), ).subscribe(arr => { - const segments = arr.map(({ segment }) => segment).filter(v => !!v) - this.onhoverSegments = segments + this.onhoverSegments = arr }) this.onDestroyCb.push(() => onhovSegSub.unsubscribe()) @@ -508,6 +544,36 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A shareReplay(1), ) + const newTmplSub = this.store$.pipe( + select(atlasSelection.selectors.selectedATP), + distinctUntilChanged((o, n) => { + return o?.template?.["@id"] === n?.template?.["@id"] + }), + switchMap(ATP => { + return this.store$.pipe( + fromRootStore.getNgLayers(this.store$, this.sapiSvc), + map(ngLayers => ({ ATP, ngLayers })) + ) + } + ) + ).subscribe(({ ATP, ngLayers }) => { + const { template, parcellation, atlas } = ATP + const { tmplNgLayers, tmplAuxNgLayers, parcNgLayers } = ngLayers + + + // clean up previous tmpl + this.unloadTmpl() + + const layerObj = { + ...tmplNgLayers, + ...tmplAuxNgLayers, + ...parcNgLayers, + } + this.loadTmpl(atlas, template, parcellation, layerObj) + }) + + this.onDestroyCb.push(() => newTmplSub.unsubscribe()) + const setupViewerApiSub = this.newViewer$.pipe( tap(() => { setViewerHandle && setViewerHandle(null) @@ -519,14 +585,13 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A position : coord, positionReal : typeof realSpace !== 'undefined' ? realSpace : true, }), - /* TODO introduce animation */ moveToNavigationLoc : (coord, _realSpace?) => { this.store$.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { - position: coord, - animation: {}, - } + position: coord + }, + animation: true }) ) }, @@ -542,12 +607,6 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A * TODO reenable with updated select_regions api */ this.log.warn(`showSegment is temporarily disabled`) - - // if(!this.selectedRegionIndexSet.has(labelIndex)) - // this.store.dispatch({ - // type : SELECT_REGIONS, - // selectRegions : [labelIndex, ...this.selectedRegionIndexSet] - // }) }, add3DLandmarks : landmarks => { // TODO check uniqueness of ID @@ -566,9 +625,11 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A })) }, remove3DLandmarks : landmarkIds => { - this.store$.dispatch(viewreStateRemoveUserLandmarks({ - payload: { landmarkIds } - })) + this.store$.dispatch( + annotation.actions.rmAnnotations({ + annotations: landmarkIds.map(id => ({ "@id": id })) + }) + ) }, hideSegment : (_labelIndex) => { /** @@ -576,28 +637,10 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A */ this.log.warn(`hideSegment is temporarily disabled`) - // if(this.selectedRegionIndexSet.has(labelIndex)){ - // this.store.dispatch({ - // type :SELECT_REGIONS, - // selectRegions : [...this.selectedRegionIndexSet].filter(num=>num!==labelIndex) - // }) - // } }, showAllSegments : () => { - const selectRegionIds = [] - this.multiNgIdsRegionsLabelIndexMap.forEach((map, ngId) => { - Array.from(map.keys()).forEach(labelIndex => { - selectRegionIds.push(serialiseParcellationRegion({ ngId, labelIndex })) - }) - }) - this.store$.dispatch(viewerStateSelectRegionWithIdDeprecated({ - selectRegionIds - })) }, hideAllSegments : () => { - this.store$.dispatch(viewerStateSelectRegionWithIdDeprecated({ - selectRegionIds: [] - })) }, getLayersSegmentColourMap: () => { if (!this.layerCtrlService) { @@ -658,11 +701,8 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A mouseOverNehuba : of(null).pipe( tap(() => console.warn('mouseOverNehuba observable is becoming deprecated. use mouseOverNehubaLayers instead.')), ), - mouseOverNehubaLayers: this.mouseoverDirective.currentOnHoverObs$.pipe( - map(({ segments }) => segments) - ), mouseOverNehubaUI: this.mouseoverDirective.currentOnHoverObs$.pipe( - map(({annotation, landmark, segments, userLandmark: customLandmark }) => ({annotation, segments, landmark, customLandmark })), + map(({annotation, landmark, userLandmark: customLandmark }) => ({annotation, landmark, customLandmark })), shareReplay(1), ), getNgHash : this.nehubaContainerDirective.nehubaViewerInstance.getNgHash, @@ -672,8 +712,10 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A // listen to navigation change from store const navSub = this.store$.pipe( - select(viewerStateNavigationStateSelector) - ).subscribe(nav => this.navigation = nav) + select(atlasSelection.selectors.navigation) + ).subscribe(nav => { + this.navigation = nav + }) this.onDestroyCb.push(() => navSub.unsubscribe()) } @@ -699,8 +741,8 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A const trueOnhoverSegments = this.onhoverSegments && this.onhoverSegments.filter(v => typeof v === 'object') if (!trueOnhoverSegments || (trueOnhoverSegments.length === 0)) return true this.store$.dispatch( - viewerStateSetSelectedRegions({ - selectRegions: trueOnhoverSegments.slice(0, 1) + actions.selectRegions({ + regions: trueOnhoverSegments.slice(0, 1) }) ) return true diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html index 104c5add581a8bcb216d1afe8d15b42002657938..c9eb5773a04e34b8e2a43933136edf18f6a36fe2 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html @@ -5,7 +5,7 @@ [iav-viewer-touch-interface-v-panels]="viewPanels" [iav-viewer-touch-interface-vp-to-data]="iavContainer?.viewportToDatas" [iav-viewer-touch-interface-ngviewer]="iavContainer?.nehubaViewerInstance?.nehubaViewer?.ngviewer" - [iav-viewer-touch-interface-nehuba-config]="selectedTemplate?.nehubaConfig"> + [iav-viewer-touch-interface-nehuba-config]="nehubaConfig"> <div class="d-block" iav-nehuba-viewer-container diff --git a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts index 1b6fb599e73e9808098fe45872860672a26b5742..59062511f3bb3a13e626dac719b48da9e2e87f91 100644 --- a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts +++ b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts @@ -3,87 +3,17 @@ import { NehubaViewerUnit, INehubaLifecycleHook } from "../nehubaViewer/nehubaVi 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 { takeOnePipe } from "../util"; +import { serializeSegment, takeOnePipe } from "../util"; import { ngViewerActionNehubaReady } from "src/services/state/ngViewerState/actions"; -import { viewerStateMouseOverCustomLandmarkInPerspectiveView, viewerStateNehubaLayerchanged } from "src/services/state/viewerState/actions"; -import { viewerStateStandAloneVolumes } from "src/services/state/viewerState/selectors"; import { ngViewerSelectorOctantRemoval } from "src/services/state/ngViewerState/selectors"; import { LoggingService } from "src/logging"; import { uiActionMouseoverLandmark, uiActionMouseoverSegments } from "src/services/state/uiState/actions"; import { IViewerConfigState } from "src/services/state/viewerConfig.store.helper"; import { arrayOfPrimitiveEqual } from 'src/util/fn' import { INavObj, NehubaNavigationService } from "../navigation.service"; +import { NehubaConfig, defaultNehubaConfig } from "../config.service"; +import { atlasSelection } from "src/state"; -const defaultNehubaConfig = { - "configName": "", - "globals": { - "hideNullImageValues": true, - "useNehubaLayout": { - "keepDefaultLayouts": false - }, - "useNehubaMeshLayer": true, - "rightClickWithCtrlGlobal": false, - "zoomWithoutCtrlGlobal": false, - "useCustomSegmentColors": true - }, - "zoomWithoutCtrl": true, - "hideNeuroglancerUI": true, - "rightClickWithCtrl": true, - "rotateAtViewCentre": true, - "enableMeshLoadingControl": true, - "zoomAtViewCentre": true, - "restrictUserNavigation": true, - "disableSegmentSelection": false, - "dataset": { - "imageBackground": [ - 1, - 1, - 1, - 1 - ], - "initialNgState": { - "showDefaultAnnotations": false, - "layers": {}, - } - }, - "layout": { - "views": "hbp-neuro", - "planarSlicesBackground": [ - 1, - 1, - 1, - 1 - ], - "useNehubaPerspective": { - "enableShiftDrag": false, - "doNotRestrictUserNavigation": false, - "perspectiveSlicesBackground": [ - 1, - 1, - 1, - 1 - ], - "perspectiveBackground": [ - 1, - 1, - 1, - 1 - ], - "mesh": { - "backFaceColor": [ - 1, - 1, - 1, - 1 - ], - "removeBasedOnNavigation": true, - "flipRemovedOctant": true - }, - "hideImages": false, - "waitForMesh": false, - } - } -} const determineProtocol = (url: string) => { const re = /^([a-z0-9_-]{0,}):\/\//.exec(url) @@ -266,7 +196,7 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ this.subscriptions.push( this.store$.pipe( - select(viewerStateStandAloneVolumes), + select(atlasSelection.selectors.standaloneVolumes), filter(v => v && Array.isArray(v) && v.length > 0), distinctUntilChanged(arrayOfPrimitiveEqual) ).subscribe(async volumes => { @@ -285,7 +215,7 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ function onInit() { this.overrideShowLayers = forceShowLayerNames } - this.createNehubaInstance({ nehubaConfig: copiedNehubaConfig }, { onInit }) + this.createNehubaInstance(copiedNehubaConfig, { onInit }) }), this.viewerPerformanceConfig$.pipe( @@ -316,7 +246,7 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ this.nehubaViewerInstance.toggleOctantRemoval(flag) } - createNehubaInstance(template: any, lifeCycle: INehubaLifecycleHook = {}){ + createNehubaInstance(nehubaConfig: NehubaConfig, lifeCycle: INehubaLifecycleHook = {}){ this.clear() this.iavNehubaViewerContainerViewerLoading.emit(true) this.cr = this.el.createComponent(this.nehubaViewerFactory) @@ -328,8 +258,6 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ } } - const { nehubaConfig, name } = template - /** * apply viewer config such as gpu limit */ @@ -344,18 +272,13 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ initialNgState.gpuMemoryLimit = gpuLimit } - /* TODO replace with id from KG */ - this.nehubaViewerInstance.templateId = name - this.nehubaViewerSubscriptions.push( this.nehubaViewerInstance.errorEmitter.subscribe(e => { console.log(e) }), this.nehubaViewerInstance.layersChanged.subscribe(() => { - this.store$.dispatch( - viewerStateNehubaLayerchanged() - ) + }), this.nehubaViewerInstance.nehubaReady.subscribe(() => { @@ -387,11 +310,9 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ this.nehubaViewerInstance.mouseoverUserlandmarkEmitter.pipe( throttleTime(160, asyncScheduler, {trailing: true}), ).subscribe(label => { - this.store$.dispatch( - viewerStateMouseOverCustomLandmarkInPerspectiveView({ - payload: { label } - }) - ) + const idx = Number(label.replace('label=', '')) + // TODO + // this is exclusive for vtk layer }), this.nehubaViewerInstance.nehubaReady.pipe( @@ -444,15 +365,10 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ layer: { name: ngId, }, - segment: segment || `${ngId}#${segmentId}`, + segment: segment || serializeSegment(ngId, segmentId), segmentId } }) this.mouseOverSegments.emit(payload) - this.store$.dispatch( - uiActionMouseoverSegments({ - segments: payload - }) - ) } } diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts index 7be4aa437b59c3f92b75a3eb7607f32978d46de5..14919d73d3fd7ca4e5664b18f873ad002c333a1d 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts @@ -15,8 +15,8 @@ import { UtilModule } from "src/util" import { viewerConfigSelectorUseMobileUi } from "src/services/state/viewerConfig.store.helper" import { viewerStateNavigationStateSelector, viewerStateSelectedTemplatePureSelector } from "src/services/state/viewerState/selectors" import * as util from '../util' -import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions" import {QuickTourModule} from "src/ui/quickTour/module"; +import { actions } from "src/state/atlasSelection" @Directive({ selector: '[iav-auth-auth-state]', @@ -184,13 +184,13 @@ describe('> statusCard.component.ts', () => { if (method === 'position') overrideObj['position'] = mockNavState['position'] if (method === 'zoom') overrideObj['zoom'] = mockNavState['zoom'] expect(idspatchSpy).toHaveBeenCalledWith( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { ...mockCurrNavigation, ...overrideObj, - positionReal: false, - animation: {}, - } + }, + physical: true, + animation: true }) ) }) diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts index 6ac175a1d93dba044ed00e3a7d5e09bc6857dd31..324978a859dc62043085116ce746246397197ff8 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts @@ -16,11 +16,11 @@ import { MatBottomSheet } from "@angular/material/bottom-sheet"; import { MatDialog } from "@angular/material/dialog"; import { ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' import { FormControl } from "@angular/forms"; -import { viewerStateNavigationStateSelector, viewerStateSelectedTemplatePureSelector } from "src/services/state/viewerState/selectors"; -import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; import { getNavigationStateFromConfig, NEHUBA_INSTANCE_INJTKN } from '../util' import { IQuickTourData } from "src/ui/quickTour/constrants"; +import { actions } from "src/state/atlasSelection"; +import { atlasSelection } from "src/state"; @Component({ selector : 'iav-cmp-viewer-nehuba-status', @@ -90,13 +90,13 @@ export class StatusCardComponent implements OnInit, OnChanges{ this.subscriptions.push( this.store$.pipe( - select(viewerStateSelectedTemplatePureSelector) + select(atlasSelection.selectors.selectedTemplate) ).subscribe(n => this.selectedTemplatePure = n) ) this.subscriptions.push( this.store$.pipe( - select(viewerStateNavigationStateSelector) + select(atlasSelection.selectors.navigation) ).subscribe(nav => this.currentNavigation = nav) ) } @@ -184,15 +184,15 @@ export class StatusCardComponent implements OnInit, OnChanges{ } = getNavigationStateFromConfig(this.selectedTemplatePure.nehubaConfig) this.store$.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { ...this.currentNavigation, ...(rotationFlag ? { orientation: orientation } : {}), ...(positionFlag ? { position: position } : {}), ...(zoomFlag ? { zoom: zoom } : {}), - positionReal : false, - animation : {}, - } + }, + physical: false, + animation: true }) ) } diff --git a/src/viewerModule/nehuba/store/index.ts b/src/viewerModule/nehuba/store/index.ts index a384638d8bf82bd1b38296d9a061405b552cd7f1..bfaa5f424408a852f371a4abddc152bae202d81d 100644 --- a/src/viewerModule/nehuba/store/index.ts +++ b/src/viewerModule/nehuba/store/index.ts @@ -16,3 +16,5 @@ export { INehubaFeature, INgLayerInterface } from './type' + +export { fromRootStore } from './util' \ No newline at end of file diff --git a/src/viewerModule/nehuba/store/util.ts b/src/viewerModule/nehuba/store/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..c81635c543277325143bf69b1ac70d7fa04f01cf --- /dev/null +++ b/src/viewerModule/nehuba/store/util.ts @@ -0,0 +1,90 @@ +import { select } from "@ngrx/store"; +import { forkJoin, pipe } from "rxjs"; +import { switchMap, map, take } from "rxjs/operators"; +import { SAPI, SAPIParcellation, SapiParcellationModel, SAPISpace } from "src/atlasComponents/sapi"; +import { atlasSelection } from "src/state"; +import { getRegionLabelIndex } from "../config.service/util"; + +export type ParcVolumeSpec = { + volumeSrc: string + labelIndicies: number[] + parcellation: SapiParcellationModel + laterality: 'left hemisphere' | 'right hemisphere' | 'whole brain' +} + +type NehubaRegionIdentifier = { + source: string + labelIndex: number +} + +export const fromRootStore = { + getAuxMeshVolumes: (sapi: SAPI) => pipe( + select(atlasSelection.selectors.selectedTemplate), + switchMap(template => + sapi.registry.get<SAPISpace>(template["@id"]) + .getVolumes() + .then(volumes => volumes.filter(vol => "neuroglancer/precompmesh" in vol.data.detail)) + ), + take(1), + ), + getTmplVolumes: (sapi: SAPI) => pipe( + select(atlasSelection.selectors.selectedTemplate), + switchMap(template => + sapi.registry.get<SAPISpace>(template["@id"]) + .getVolumes() + .then(volumes => volumes.filter(vol => "neuroglancer/precomputed" in vol.data.detail)) + ), + take(1), + ), + getParcVolumes: (sapi: SAPI) => pipe( + select(atlasSelection.selectors.selectedATP), + switchMap(({ atlas, template, parcellation }) => + forkJoin([ + sapi.registry.get<SAPIParcellation>(parcellation["@id"]) + .getRegions(template["@id"]) + .then(regions => { + const returnArr: ParcVolumeSpec[] = [] + for (const r of regions) { + const source = r?.hasAnnotation?.visualizedIn?.["@id"] + if (!source) continue + if (source.indexOf("precomputed://") < 0) continue + const labelIndex = getRegionLabelIndex(atlas, template, parcellation, r) + if (!labelIndex) continue + + const found = returnArr.find(v => v.volumeSrc === source) + if (found) { + found.labelIndicies.push(labelIndex) + continue + } + + let laterality: "left hemisphere" | "right hemisphere" | "whole brain" = "whole brain" + if (r.name.indexOf("left") >= 0) laterality = "left hemisphere" + if (r.name.indexOf("right") >= 0) laterality = "right hemisphere" + returnArr.push({ + volumeSrc: source, + labelIndicies: [labelIndex], + parcellation, + laterality, + }) + } + return returnArr + }), + sapi.registry.get<SAPIParcellation>(parcellation["@id"]).getVolumes() + ]).pipe( + map(([ volumeSrcs, volumes ]) => { + return volumes.map( + v => { + const found = volumeSrcs.find(volSrc => volSrc.volumeSrc.indexOf(v.data.url) >= 0) + return { + volume: v, + volumeMetadata: found, + } + }).filter( + v => !!v.volumeMetadata?.labelIndicies + ) + }) + ) + ), + take(1), + ), +} diff --git a/src/viewerModule/nehuba/types.ts b/src/viewerModule/nehuba/types.ts index 35cca295acae535d68ba6bb98ce282650c830e45..6fd0dddab5fce72039cd81a92ac3b377c294f8fe 100644 --- a/src/viewerModule/nehuba/types.ts +++ b/src/viewerModule/nehuba/types.ts @@ -1,3 +1,4 @@ +import { SapiRegionModel } from "src/atlasComponents/sapi"; import { INavObj } from "./navigation.service"; export type TNehubaContextInfo = { @@ -9,5 +10,6 @@ export type TNehubaContextInfo = { nehuba: { layerName: string labelIndices: number[] + regions: SapiRegionModel[] }[] } diff --git a/src/viewerModule/nehuba/util.spec.ts b/src/viewerModule/nehuba/util.spec.ts index 0b022b7281e319318a0ae261ef8827b77334e7c1..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/src/viewerModule/nehuba/util.spec.ts +++ b/src/viewerModule/nehuba/util.spec.ts @@ -1,141 +0,0 @@ -import { cvtNavigationObjToNehubaConfig } from './util' - -const currentNavigation = { - position: [4, 5, 6], - orientation: [0, 0, 0, 1], - perspectiveOrientation: [ 0, 0, 0, 1], - perspectiveZoom: 2e5, - zoom: 1e5 -} - -const defaultPerspectiveZoom = 1e6 -const defaultZoom = 1e6 - -const defaultNavigationObject = { - orientation: [0, 0, 0, 1], - perspectiveOrientation: [0 , 0, 0, 1], - perspectiveZoom: defaultPerspectiveZoom, - zoom: defaultZoom, - position: [0, 0, 0], - positionReal: true -} - -const defaultNehubaConfigObject = { - perspectiveOrientation: [0, 0, 0, 1], - perspectiveZoom: 1e6, - navigation: { - pose: { - position: { - voxelCoordinates: [0, 0, 0], - voxelSize: [1,1,1] - }, - orientation: [0, 0, 0, 1], - }, - zoomFactor: defaultZoom - } -} - -const bigbrainNehubaConfig = { - "showDefaultAnnotations": false, - "layers": { - }, - "navigation": { - "pose": { - "position": { - "voxelSize": [ - 21166.666015625, - 20000, - 21166.666015625 - ], - "voxelCoordinates": [ - -21.8844051361084, - 16.288618087768555, - 28.418994903564453 - ] - } - }, - "zoomFactor": 350000 - }, - "perspectiveOrientation": [ - 0.3140767216682434, - -0.7418519854545593, - 0.4988985061645508, - -0.3195493221282959 - ], - "perspectiveZoom": 1922235.5293810747 -} - -describe('> util.ts', () => { - - describe('> cvtNavigationObjToNehubaConfig', () => { - const validNavigationObj = currentNavigation - describe('> if inputs are malformed', () => { - describe('> if navigation object is malformed, uses navigation default object', () => { - it('> if navigation object is null', () => { - const v1 = cvtNavigationObjToNehubaConfig(null, bigbrainNehubaConfig) - const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, bigbrainNehubaConfig) - expect(v1).toEqual(v2) - }) - it('> if navigation object is undefined', () => { - const v1 = cvtNavigationObjToNehubaConfig(undefined, bigbrainNehubaConfig) - const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, bigbrainNehubaConfig) - expect(v1).toEqual(v2) - }) - - it('> if navigation object is otherwise malformed', () => { - const v1 = cvtNavigationObjToNehubaConfig({foo: 'bar'}, bigbrainNehubaConfig) - const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, bigbrainNehubaConfig) - expect(v1).toEqual(v2) - - const v3 = cvtNavigationObjToNehubaConfig({}, bigbrainNehubaConfig) - const v4 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, bigbrainNehubaConfig) - expect(v3).toEqual(v4) - }) - }) - - describe('> if nehubaConfig object is malformed, use default nehubaConfig obj', () => { - it('> if nehubaConfig is null', () => { - const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, null) - const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) - expect(v1).toEqual(v2) - }) - - it('> if nehubaConfig is undefined', () => { - const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, undefined) - const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) - expect(v1).toEqual(v2) - }) - - it('> if nehubaConfig is otherwise malformed', () => { - const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, {}) - const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) - expect(v1).toEqual(v2) - - const v3 = cvtNavigationObjToNehubaConfig(validNavigationObj, {foo: 'bar'}) - const v4 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) - expect(v3).toEqual(v4) - }) - }) - }) - it('> converts navigation object and reference nehuba config object to navigation object', () => { - const convertedVal = cvtNavigationObjToNehubaConfig(validNavigationObj, bigbrainNehubaConfig) - const { perspectiveOrientation, orientation, zoom, perspectiveZoom, position } = validNavigationObj - - expect(convertedVal).toEqual({ - navigation: { - pose: { - position: { - voxelSize: bigbrainNehubaConfig.navigation.pose.position.voxelSize, - voxelCoordinates: [0, 1, 2].map(idx => position[idx] / bigbrainNehubaConfig.navigation.pose.position.voxelSize[idx]) - }, - orientation - }, - zoomFactor: zoom - }, - perspectiveOrientation: perspectiveOrientation, - perspectiveZoom: perspectiveZoom - }) - }) - }) - -}) diff --git a/src/viewerModule/nehuba/util.ts b/src/viewerModule/nehuba/util.ts index 398ad2d82aafbbdafc6c8b8c50d72239a91ded92..9c4baa3b039c4736e543ddb6e1c196e8ac2babc5 100644 --- a/src/viewerModule/nehuba/util.ts +++ b/src/viewerModule/nehuba/util.ts @@ -4,6 +4,8 @@ import { filter, scan, take } from 'rxjs/operators' import { PANELS } from 'src/services/state/ngViewerState.store.helper' import { getViewer } from 'src/util/fn' import { NehubaViewerUnit } from './nehubaViewer/nehubaViewer.component' +import { NgConfigViewerState } from "./config.service" +import { RecursivePartial } from './config.service/type' const flexContCmnCls = ['w-100', 'h-100', 'd-flex', 'justify-content-center', 'align-items-stretch'] @@ -290,7 +292,8 @@ export const takeOnePipe = () => { export const NEHUBA_INSTANCE_INJTKN = new InjectionToken<Observable<NehubaViewerUnit>>('NEHUBA_INSTANCE_INJTKN') -export function cvtNavigationObjToNehubaConfig(navigationObj, nehubaConfigObj){ + +export function cvtNavigationObjToNehubaConfig(navigationObj, ngNavigationObj: RecursivePartial<NgConfigViewerState>): Partial<NgConfigViewerState>{ const { orientation = [0, 0, 0, 1], perspectiveOrientation = [0, 0, 0, 1], @@ -300,15 +303,7 @@ export function cvtNavigationObjToNehubaConfig(navigationObj, nehubaConfigObj){ positionReal = true, } = navigationObj || {} - const voxelSize = (() => { - const { - navigation = {} - } = nehubaConfigObj || {} - const { pose = {} } = navigation - const { position = {} } = pose - const { voxelSize = [1, 1, 1] } = position - return voxelSize - })() + const voxelSize = ngNavigationObj?.navigation?.pose?.position?.voxelSize || [1,1,1] return { perspectiveOrientation, @@ -327,3 +322,21 @@ export function cvtNavigationObjToNehubaConfig(navigationObj, nehubaConfigObj){ } } } + +export function serializeSegment(ngId: string, label: number | string){ + return `${ngId}#${label}` +} + +export function deserializeSegment(id: string) { + const split = id.split('#') + if (split.length !== 2) { + throw new Error(`deserializeSegment error at ${id}. expecting splitting # to result in length 2, got ${split.length}`) + } + if (isNaN(Number(split[1]))) { + throw new Error(`deserializeSegment error at ${id}. expecting second element to be numberable. It was not.`) + } + return { + ngId: split[0], + label: Number(split[1]) + } +} diff --git a/src/viewerModule/nehuba/viewerCtrl/change-perspective-orientation/changePerspectiveOrientation.component.ts b/src/viewerModule/nehuba/viewerCtrl/change-perspective-orientation/changePerspectiveOrientation.component.ts index 0c4bdb1300fb0e7bc094a81d2dcac2d39f67c0d1..9f9d10f7b6467a186111d749e22b17dbebeb3129 100644 --- a/src/viewerModule/nehuba/viewerCtrl/change-perspective-orientation/changePerspectiveOrientation.component.ts +++ b/src/viewerModule/nehuba/viewerCtrl/change-perspective-orientation/changePerspectiveOrientation.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; -import {viewerStateChangeNavigation} from "src/services/state/viewerState/actions"; import {Store} from "@ngrx/store"; +import { actions } from 'src/state/atlasSelection'; @Component({ selector: 'app-change-perspective-orientation', @@ -22,10 +22,11 @@ export class ChangePerspectiveOrientationComponent { const orientation = this.viewOrientations[plane][view === 'first'? 0 : 1] this.store$.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { perspectiveOrientation: orientation, - } + }, + animation: true }) ) } diff --git a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.ts b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.ts index d46a84cfab7d129e94d0c542fd718fa7390e0b6c..cd43851399f9b528b2c2ad1234c8774fc071a551 100644 --- a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.ts +++ b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.ts @@ -4,13 +4,13 @@ import { combineLatest, merge, Observable, of, Subscription } from "rxjs"; import {filter, map, pairwise, withLatestFrom} from "rxjs/operators"; import { ngViewerActionSetPerspOctantRemoval } from "src/services/state/ngViewerState/actions"; import { ngViewerSelectorOctantRemoval } from "src/services/state/ngViewerState/selectors"; -import { viewerStateCustomLandmarkSelector, viewerStateGetSelectedAtlas, viewerStateSelectedTemplatePureSelector } from "src/services/state/viewerState/selectors"; import { NehubaViewerUnit } from "src/viewerModule/nehuba"; import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; import { ARIA_LABELS } from 'common/constants' import { actionSetAuxMeshes, selectorAuxMeshes } from "../../store"; import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; import {PureContantService} from "src/util"; +import { atlasSelection } from "src/state"; @Component({ selector: 'viewer-ctrl-component', @@ -57,16 +57,6 @@ export class ViewerCtrlCmp{ select(ngViewerSelectorOctantRemoval), ) - public customLandmarks$: Observable<any> = this.store$.pipe( - select(viewerStateCustomLandmarkSelector), - map(lms => lms.map(lm => ({ - ...lm, - geometry: { - position: lm.position - } - }))), - ) - public auxMeshFormGroup: FormGroup private auxMeshesNamesSet: Set<string> = new Set() public auxMeshes$ = this.store$.pipe( @@ -89,35 +79,34 @@ export class ViewerCtrlCmp{ this.auxMeshFormGroup = formBuilder.group({}) - if (this.nehubaInst$) { - this.sub.push( - combineLatest([ - this.customLandmarks$, - this.nehubaInst$, - ]).pipe( - filter(([_, nehubaInst]) => !!nehubaInst), - ).subscribe(([landmarks, nehubainst]) => { - this.setOctantRemoval(landmarks.length === 0) - nehubainst.updateUserLandmarks(landmarks) - }), - this.nehubaInst$.subscribe(nehubaInst => this.nehubaInst = nehubaInst) - ) - } else { - console.warn(`NEHUBA_INSTANCE_INJTKN not provided`) - } + // TODO move this to... nehubadirective? + // if (this.nehubaInst$) { + // this.sub.push( + // combineLatest([ + // this.customLandmarks$, + // this.nehubaInst$, + // ]).pipe( + // filter(([_, nehubaInst]) => !!nehubaInst), + // ).subscribe(([landmarks, nehubainst]) => { + // this.setOctantRemoval(landmarks.length === 0) + // nehubainst.updateUserLandmarks(landmarks) + // }), + // this.nehubaInst$.subscribe(nehubaInst => this.nehubaInst = nehubaInst) + // ) + // } else { + // console.warn(`NEHUBA_INSTANCE_INJTKN not provided`) + // } this.sub.push( - this.store$.select(viewerStateGetSelectedAtlas) - .pipe(filter(a => !!a)) - .subscribe(sa => this.selectedAtlasId = sa['@id']), this.store$.pipe( - select(viewerStateSelectedTemplatePureSelector) - ).subscribe(tmpl => { - this.selectedTemplateId = tmpl['@id'] - const { useTheme } = tmpl || {} - this.darktheme = useTheme === 'dark' + select(atlasSelection.selectors.selectedATP) + ).subscribe(({ atlas, parcellation, template }) => { + this.selectedAtlasId = atlas["@id"] + this.selectedTemplateId = template["@id"] }), + this.pureConstantService.darktheme$.subscribe(darktheme => this.darktheme = darktheme), + this.nehubaViewerPerspectiveOctantRemoval$.subscribe( flag => this.removeOctantFlag = flag ), diff --git a/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts b/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts index 623dc87ce9795ef650afe07a2fe621e59f9e4e5c..e3ceaad4673e198e11eba7ca257ed0a334bb1ac6 100644 --- a/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts +++ b/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts @@ -24,6 +24,9 @@ export class NehubaVCtxToBbox implements PipeTransform{ divisor = 1e6 } const { payload } = event as TContextArg<'nehuba'> + + if (!payload.nav) return null + const { position, zoom } = payload.nav // position is in nm // zoom can be directly applied as a multiple diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index a00b376604e92ed7e4328b4289c227813f475836..0a62b9a08053b43328afd1300ae638d2c3c90e17 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -7,15 +7,14 @@ import { Observable, Subject } from "rxjs"; import { debounceTime, filter, switchMap } from "rxjs/operators"; import { ComponentStore } from "src/viewerModule/componentStore"; import { select, Store } from "@ngrx/store"; -import { viewerStateChangeNavigation, viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; -import { viewerStateSelectorNavigation } from "src/services/state/viewerState/selectors"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; -import { REGION_OF_INTEREST } from "src/util/interfaces"; import { MatSnackBar } from "@angular/material/snack-bar"; import { CONST } from 'common/constants' import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service"; import { getUuid, switchMapWaitFor } from "src/util/fn"; import { AUTO_ROTATE, TInteralStatePayload, ViewerInternalStateSvc } from "src/viewerModule/viewerInternalState.service"; +import { actions } from "src/state/atlasSelection"; +import { atlasSelection } from "src/state"; const viewerType = 'ThreeSurfer' type TInternalState = { @@ -39,7 +38,7 @@ type THandlingCustomEv = { } type TCameraOrientation = { - perspectiveOrientation: [number, number, number, number] + perspectiveOrientation: number[] perspectiveZoom: number } @@ -112,7 +111,6 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af private navStateStoreRelay: ComponentStore<{ perspectiveOrientation: [number, number, number, number], perspectiveZoom: number }>, private snackbar: MatSnackBar, @Optional() intViewerStateSvc: ViewerInternalStateSvc, - @Optional() @Inject(REGION_OF_INTEREST) private roi$: Observable<any>, @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, @Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) setViewerHandle: TSetViewerHandle, ){ @@ -193,7 +191,6 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af hideSegment: nyi, mouseEvent: null, mouseOverNehuba: null, - mouseOverNehubaLayers: null, mouseOverNehubaUI: null, moveToNavigationLoc: null, moveToNavigationOri: null, @@ -211,40 +208,52 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af () => setViewerHandle(null) ) - if (this.roi$) { - const sub = this.roi$.pipe( - switchMap(switchMapWaitFor({ - condition: () => this.colormapLoaded - })) - ).subscribe(r => { - try { - if (!r) throw new Error(`No region selected.`) - const cmap = this.getColormapCopy() - const hemisphere = getHemisphereKey(r) - if (!hemisphere) { - this.snackbar.open(CONST.CANNOT_DECIPHER_HEMISPHERE, 'Dismiss', { - duration: 3000 - }) - throw new Error(CONST.CANNOT_DECIPHER_HEMISPHERE) - } - for (const [ hem, m ] of cmap.entries()) { - for (const lbl of m.keys()) { - if (hem !== hemisphere || lbl !== r.labelIndex) { - m.set(lbl, [1, 1, 1]) - } - } - } - this.internalHemisphLblColorMap = cmap - } catch (e) { - this.internalHemisphLblColorMap = null - } + const sub = this.store$.pipe( + select(atlasSelection.selectors.selectedRegions) + ).subscribe(() => { - this.applyColorMap() - }) - this.onDestroyCb.push( - () => sub.unsubscribe() - ) - } + /** + * TODO + * fix ... this? + */ + + // if (this.roi$) { + // const sub = this.roi$.pipe( + // switchMap(switchMapWaitFor({ + // condition: () => this.colormapLoaded + // })) + // ).subscribe(r => { + // try { + // if (!r) throw new Error(`No region selected.`) + // const cmap = this.getColormapCopy() + // const hemisphere = getHemisphereKey(r) + // if (!hemisphere) { + // this.snackbar.open(CONST.CANNOT_DECIPHER_HEMISPHERE, 'Dismiss', { + // duration: 3000 + // }) + // throw new Error(CONST.CANNOT_DECIPHER_HEMISPHERE) + // } + // for (const [ hem, m ] of cmap.entries()) { + // for (const lbl of m.keys()) { + // if (hem !== hemisphere || lbl !== r.labelIndex) { + // m.set(lbl, [1, 1, 1]) + // } + // } + // } + // this.internalHemisphLblColorMap = cmap + // } catch (e) { + // this.internalHemisphLblColorMap = null + // } + + // this.applyColorMap() + // }) + // this.onDestroyCb.push( + // () => sub.unsubscribe() + // ) + // } + + }) + this.onDestroyCb.push(() => sub.unsubscribe()) /** * intercept click and act @@ -264,10 +273,11 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af }) return true } + + // TODO check why typing is all messed up here + const regions = this.mouseoverRegions.slice(0, 1) as any[] this.store$.dispatch( - viewerStateSetSelectedRegions({ - selectRegions: this.mouseoverRegions - }) + actions.selectRegions({ regions }) ) return true } @@ -315,7 +325,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af */ const navStateSub = this.navStateStoreRelay.select(s => s).subscribe(v => { this.store$.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { position: [0, 0, 0], orientation: [0, 0, 0, 1], @@ -335,7 +345,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af * subscribe to main store and negotiate with relay to set camera */ const navSub = this.store$.pipe( - select(viewerStateSelectorNavigation) + select(atlasSelection.selectors.navigation) ).subscribe(nav => { const { perspectiveOrientation, perspectiveZoom } = nav this.mainStoreCameraNav = { diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.spec.ts b/src/viewerModule/viewerCmp/viewerCmp.component.spec.ts index c67f26f8dcb0be058be3f01c97138a0371dfc4d6..10bb20122d19b73aadd5f7f55b6f065320d055e9 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.spec.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.spec.ts @@ -1,9 +1,6 @@ import { TestBed } from "@angular/core/testing" import { MockStore, provideMockStore } from "@ngrx/store/testing" -import { hot } from "jasmine-marbles" -import { Observable, of, throwError } from "rxjs" -import { viewerStateContextedSelectedRegionsSelector } from "src/services/state/viewerState/selectors" -import { ROIFactory } from "./viewerCmp.component" + describe('> viewerCmp.component.ts', () => { let mockStore: MockStore @@ -15,111 +12,4 @@ describe('> viewerCmp.component.ts', () => { }) mockStore = TestBed.inject(MockStore) }) - describe('> ROIFactory', () => { - const mockDetail = { - foo: 'bar' - } - class MockPCSvc { - getRegionDetail(){ - return of(mockDetail) - } - } - const pcsvc = new MockPCSvc() - let getRegionDetailSpy: jasmine.Spy - - beforeEach(() => { - getRegionDetailSpy = spyOn(pcsvc, 'getRegionDetail') - mockStore.overrideSelector(viewerStateContextedSelectedRegionsSelector, []) - }) - - afterEach(() => { - getRegionDetailSpy.calls.reset() - }) - - describe('> if regoinselected is empty array', () => { - let returnVal: Observable<any> - beforeEach(() => { - getRegionDetailSpy.and.callThrough() - returnVal = ROIFactory(mockStore, pcsvc as any) - }) - it('> returns null', () => { - expect( - returnVal - ).toBeObservable(hot('a', { - a: null - })) - }) - - it('> regionDetail not called', () => { - expect(getRegionDetailSpy).not.toHaveBeenCalled() - }) - }) - - describe('> if regionselected is nonempty', () => { - const mockRegion = { - context: { - template: { - '@id': 'template-id' - }, - parcellation: { - '@id': 'parcellation-id' - }, - atlas: { - '@id': 'atlas-id' - } - }, - ngId: 'foo-bar', - labelIndex: 123 - } - const returnDetail = { - map: { - hello: 'world' - } - } - let returnVal: Observable<any> - beforeEach(() => { - getRegionDetailSpy.and.callFake(() => of(returnDetail)) - mockStore.overrideSelector(viewerStateContextedSelectedRegionsSelector, [mockRegion]) - returnVal = ROIFactory(mockStore, pcsvc as any) - }) - - // TODO check why marble is acting weird - // and that null is not emitted - it('> returns as expected', () => { - expect(returnVal).toBeObservable( - hot('(ab)', { - a: null, - b: { - ...mockRegion, - ...returnDetail - } - }) - ) - const { context } = mockRegion - expect(getRegionDetailSpy).toHaveBeenCalledWith( - context.atlas["@id"], - context.parcellation["@id"], - context.template["@id"], - mockRegion - ) - }) - - it('> if getRegionDetail throws, at least return original region', () => { - getRegionDetailSpy.and.callFake(() => throwError('blabla')) - expect(returnVal).toBeObservable( - hot('(ab)', { - a: null, - b: mockRegion - }) - ) - const { context } = mockRegion - expect(getRegionDetailSpy).toHaveBeenCalledWith( - context.atlas["@id"], - context.parcellation["@id"], - context.template["@id"], - mockRegion - ) - }) - }) - }) }) diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 7103e137e423fd320dd69a6ef7e42f6f988a0562..00ac6d7e2551046ff3c50838bec54c7da57b5a42 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -1,63 +1,24 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactory, ComponentFactoryResolver, Inject, Injector, Input, OnDestroy, Optional, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { combineLatest, merge, NEVER, Observable, of, Subscription } from "rxjs"; -import {catchError, debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, switchMap } from "rxjs/operators"; -import { actionViewerStateSelectFeature, viewerStateChangeNavigation, viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; -import { - viewerStateContextedSelectedRegionsSelector, - viewerStateGetSelectedAtlas, - viewerStateSelectedParcellationSelector, - viewerStateSelectedTemplateSelector, - viewerStateSelectorFeatureSelector, - viewerStateStandAloneVolumes, - viewerStateViewerModeSelector -} from "src/services/state/viewerState/selectors" +import { combineLatest, NEVER, Observable, of, Subscription } from "rxjs"; +import { debounceTime, map, shareReplay, startWith, switchMap } from "rxjs/operators"; import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' -import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, REGION_OF_INTEREST } from "src/util/interfaces"; +import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN } from "src/util/interfaces"; import { animate, state, style, transition, trigger } from "@angular/animations"; import { IQuickTourData } from "src/ui/quickTour"; -import { PureContantService } from "src/util"; import { EnumViewerEvt, TContextArg, TSupportedViewers, TViewerEvent } from "../viewer.interface"; -import { getGetRegionFromLabelIndexId, switchMapWaitFor } from "src/util/fn"; import { ContextMenuService, TContextMenuReg } from "src/contextMenuModule"; import { ComponentStore } from "../componentStore"; -import { MAT_DIALOG_DATA } from "@angular/material/dialog"; -import { GenericInfoCmp } from "src/atlasComponents/regionalFeatures/bsFeatures/genericInfo"; -import { _PLI_VOLUME_INJ_TOKEN, _TPLIVal } from "src/glue"; -import { uiActionSetPreviewingDatasetFiles } from "src/services/state/uiState.store.helper"; -import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper"; import { DialogService } from "src/services/dialogService.service"; -import { SapiVoiResponse } from "src/atlasComponents/sapi/type" +import { SAPI, SapiRegionModel } from "src/atlasComponents/sapi"; +import { actions } from "src/state/atlasSelection"; +import { atlasSelection, userInterface, userInteraction } from "src/state"; +import { SapiSpatialFeatureModel } from "src/atlasComponents/sapi/type"; type TCStoreViewerCmp = { overlaySideNav: any } -export function ROIFactory(store: Store<any>, svc: PureContantService){ - return store.pipe( - select(viewerStateContextedSelectedRegionsSelector), - switchMap(r => { - if (!r[0]) return of(null) - const { context } = r[0] - const { atlas, template, parcellation } = context || {} - return merge( - of(null), - svc.getRegionDetail(atlas['@id'], parcellation['@id'], template['@id'], r[0]).pipe( - map(det => { - return { - ...r[0], - ...det, - } - }), - // in case detailed requests fails - catchError((_err, _obs) => of(r[0])), - ) - ) - }), - shareReplay(1) - ) -} - @Component({ selector: 'iav-cmp-viewer-container', templateUrl: './viewerCmp.template.html', @@ -98,11 +59,6 @@ export function ROIFactory(store: Store<any>, svc: PureContantService){ ]), ], providers: [ - { - provide: REGION_OF_INTEREST, - useFactory: ROIFactory, - deps: [ Store, PureContantService ] - }, { provide: OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, useFactory: (cStore: ComponentStore<TCStoreViewerCmp>) => { @@ -121,12 +77,12 @@ export function ROIFactory(store: Store<any>, svc: PureContantService){ }) export class ViewerCmp implements OnDestroy { - public _pliTitle = "Fiber structures of a human hippocampus based on joint DMRI, 3D-PLI, and TPFM acquisitions" - public _pliDesc = "The collected datasets provide real multimodal, multiscale structural connectivity insights into the human hippocampus. One post mortem hippocampus was scanned with Anatomical and Diffusion MRI (dMRI) [1], 3D Polarized Light Imaging (3D-PLI) [2], and Two-Photon Fluorescence Microscopy (TPFM) [3] using protocols specifically developed during SGA1 and SGA2, rendering joint tissue imaging possible. MRI scanning was performed with a 11.7 T Preclinical MRI system (gradients: 760 mT/m, slew rate: 9500 T/m/s) yielding T1-w and T2-w maps at 200 µm and dMRI-based maps at 300 µm resolution. During tissue sectioning (60 µm thickness) blockface (en-face) images were acquired from the surface of the frozen brain block, serving as reference for data integration/co-alignment. 530 brain sections were scanned with 3D-PLI. HPC-based image analysis provided transmittance, retardation, and fiber orientation maps at 1.3 µm in-plane resolution. TPFM was finally applied to selected brain sections utilizing autofluorescence properties of the fibrous tissue which appears after PBS washing (MAGIC protocol). The TPFM measurements provide a resolution of 0.44 µm x 0.44 µm x 1 µm." - public _pliLink = "https://doi.org/10.25493/JQ30-E08" + public CONST = CONST public ARIA_LABELS = ARIA_LABELS + public overlaySidenav$ = NEVER + @ViewChild('genericInfoVCR', { read: ViewContainerRef }) genericInfoVCR: ViewContainerRef @@ -145,53 +101,72 @@ export class ViewerCmp implements OnDestroy { private onDestroyCb: (() => void)[] = [] public viewerLoaded: boolean = false - public templateSelected$ = this.store$.pipe( - select(viewerStateSelectedTemplateSelector), - distinctUntilChanged(), + private selectedATP = this.store$.pipe( + select(atlasSelection.selectors.selectedATP), + shareReplay(1) + ) + + public selectedAtlas$ = this.selectedATP.pipe( + map(({ atlas }) => atlas) ) - public parcellationSelected$ = this.store$.pipe( - select(viewerStateSelectedParcellationSelector), - distinctUntilChanged(), + public templateSelected$ = this.selectedATP.pipe( + map(({ template }) => template) + ) + public parcellationSelected$ = this.selectedATP.pipe( + map(({ parcellation }) => parcellation) ) public selectedRegions$ = this.store$.pipe( - select(viewerStateContextedSelectedRegionsSelector), - distinctUntilChanged(), + select(atlasSelection.selectors.selectedRegions), ) public isStandaloneVolumes$ = this.store$.pipe( - select(viewerStateStandAloneVolumes), + select(atlasSelection.selectors.standaloneVolumes), map(v => v.length > 0) ) public viewerMode$: Observable<string> = this.store$.pipe( - select(viewerStateViewerModeSelector), - shareReplay(1), - ) - - public overlaySidenav$ = this.cStore.select(s => s.overlaySideNav).pipe( + select(atlasSelection.selectors.viewerMode), shareReplay(1), ) public useViewer$: Observable<TSupportedViewers | 'notsupported'> = combineLatest([ - this.templateSelected$, + this.store$.pipe( + select(atlasSelection.selectors.selectedATP), + switchMap(({ atlas, template }) => atlas && template + ? this.sapi.getSpace(atlas["@id"], template["@id"]).getVolumes() + : of(null)), + map(vols => { + const flags = { + isNehuba: false, + isThreeSurfer: false + } + if (!vols) return null + if (vols.find(vol => vol.data.volume_type === "neuroglancer/precomputed")) { + flags.isNehuba = true + } + + if (vols.find(vol => vol.data.volume_type === "gii")) { + flags.isThreeSurfer = true + } + return flags + }) + ), this.isStandaloneVolumes$, ]).pipe( - map(([t, isSv]) => { + map(([flags, isSv]) => { if (isSv) return 'nehuba' - if (!t) return null - if (!!t['nehubaConfigURL'] || !!t['nehubaConfig']) return 'nehuba' - if (!!t['three-surfer']) return 'threeSurfer' + if (!flags) return null + if (flags.isNehuba) return 'nehuba' + if (flags.isThreeSurfer) return 'threeSurfer' return 'notsupported' }) ) - public viewerCtx$ = this.viewerModuleSvc.context$ - - public pliVol$ = this._pliVol$ || NEVER + public viewerCtx$ = this.ctxMenuSvc.context$ public selectedFeature$ = this.store$.pipe( - select(viewerStateSelectorFeatureSelector) + select(userInterface.selectors.selectedFeature) ) /** @@ -202,15 +177,12 @@ export class ViewerCmp implements OnDestroy { */ public onlyShowMiniTray$: Observable<boolean> = combineLatest([ this.selectedRegions$, - this.pliVol$.pipe( - startWith([]) - ), this.viewerMode$.pipe( startWith(null as string) ), this.selectedFeature$, ]).pipe( - map(([ regions, layers, viewerMode, selectedFeature ]) => regions.length === 0 && layers.length === 0 && !viewerMode && !selectedFeature) + map(([ regions, viewerMode, selectedFeature ]) => regions.length === 0 && !viewerMode && !selectedFeature) ) @ViewChild('viewerStatusCtxMenu', { read: TemplateRef }) @@ -223,37 +195,20 @@ export class ViewerCmp implements OnDestroy { private templateSelected: any private getRegionFromlabelIndexId: (arg: {labelIndexId: string}) => any - private genericInfoCF: ComponentFactory<GenericInfoCmp> - - public selectedAtlas$ = this.store$.pipe( - select(viewerStateGetSelectedAtlas), - ) - - public clearVoi(){ - this.store$.dispatch( - uiActionSetPreviewingDatasetFiles({ - previewingDatasetFiles: [] - }) - ) - } constructor( private store$: Store<any>, - private viewerModuleSvc: ContextMenuService<TContextArg<'threeSurfer' | 'nehuba'>>, + private ctxMenuSvc: ContextMenuService<TContextArg<'threeSurfer' | 'nehuba'>>, private cStore: ComponentStore<TCStoreViewerCmp>, - cfr: ComponentFactoryResolver, private dialogSvc: DialogService, private cdr: ChangeDetectorRef, - @Optional() @Inject(_PLI_VOLUME_INJ_TOKEN) private _pliVol$: Observable<_TPLIVal[]>, - @Optional() @Inject(REGION_OF_INTEREST) public regionOfInterest$: Observable<any> + private sapi: SAPI, ){ - this.genericInfoCF = cfr.resolveComponentFactory(GenericInfoCmp) - this.subscriptions.push( this.selectedRegions$.subscribe(() => { this.clearPreviewingDataset() }), - this.viewerModuleSvc.context$.subscribe( + this.ctxMenuSvc.context$.subscribe( (ctx: any) => this.context = ctx ), this.templateSelected$.subscribe( @@ -261,9 +216,7 @@ export class ViewerCmp implements OnDestroy { ), this.parcellationSelected$.subscribe( p => { - this.getRegionFromlabelIndexId = !!p - ? getGetRegionFromLabelIndexId({ parcellation: p }) - : null + this.getRegionFromlabelIndexId = null } ), combineLatest([ @@ -283,7 +236,7 @@ export class ViewerCmp implements OnDestroy { message.push(`- _${atlas.name}_`) } if (checkPrerelease(tmpl)) { - message.push(`- _${tmpl.name}_`) + message.push(`- _${tmpl.fullName}_`) } if (checkPrerelease(parc)) { message.push(`- _${parc.name}_`) @@ -328,19 +281,7 @@ export class ViewerCmp implements OnDestroy { let hoveredRegions = [] if (context.viewerType === 'nehuba') { hoveredRegions = (context as TContextArg<'nehuba'>).payload.nehuba.reduce( - (acc, curr) => acc.concat( - curr.labelIndices.map( - lblIdx => { - const labelIndexId = `${curr.layerName}#${lblIdx}` - if (!!this.getRegionFromlabelIndexId) { - return this.getRegionFromlabelIndexId({ - labelIndexId: `${curr.layerName}#${lblIdx}` - }) - } - return labelIndexId - } - ) - ), + (acc, curr) => acc.concat(...curr.regions), [] ) } @@ -348,6 +289,7 @@ 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({ @@ -362,31 +304,9 @@ export class ViewerCmp implements OnDestroy { return true } - this.viewerModuleSvc.register(cb) + this.ctxMenuSvc.register(cb) this.onDestroyCb.push( - () => this.viewerModuleSvc.deregister(cb) - ) - this.subscriptions.push( - this.overlaySidenav$.pipe( - switchMap(switchMapWaitFor({ - condition: () => !!this.genericInfoVCR - })) - ).subscribe(data => { - if (!this.genericInfoVCR) { - console.warn(`genericInfoVCR not defined!`) - return - } - const injector = Injector.create({ - providers: [{ - provide: MAT_DIALOG_DATA, - useValue: data - }] - }) - - this.genericInfoVCR.clear() - this.genericInfoVCR.createComponent(this.genericInfoCF, null, injector) - this.cdr.markForCheck() - }) + () => this.ctxMenuSvc.deregister(cb) ) } @@ -395,19 +315,17 @@ export class ViewerCmp implements OnDestroy { while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()() } - public selectRoi(roi: any) { + public selectRoi(roi: SapiRegionModel) { this.store$.dispatch( - viewerStateSetSelectedRegions({ - selectRegions: [ roi ] + actions.selectRegions({ + regions: [ roi ] }) ) } public exitSpecialViewMode(){ this.store$.dispatch( - viewerStateSetViewerMode({ - payload: null - }) + actions.clearViewerMode() ) } @@ -426,37 +344,46 @@ export class ViewerCmp implements OnDestroy { this.viewerLoaded = event.data break case EnumViewerEvt.VIEWER_CTX: - this.viewerModuleSvc.context$.next(event.data) + this.ctxMenuSvc.context$.next(event.data) + if (event.data.viewerType === "nehuba") { + const { nehuba } = (event.data as TContextArg<"nehuba">).payload + const mousingOverRegions = (nehuba || []).reduce((acc, { regions }) => acc.concat(...regions), []) + this.store$.dispatch( + userInteraction.actions.mouseoverRegions({ + regions: mousingOverRegions + }) + ) + } break default: } } public disposeCtxMenu(){ - this.viewerModuleSvc.dismissCtxMenu() + this.ctxMenuSvc.dismissCtxMenu() } - showSpatialDataset(feature: SapiVoiResponse) { + showSpatialDataset(feature: SapiSpatialFeatureModel) { this.store$.dispatch( - viewerStateChangeNavigation({ + actions.navigateTo({ navigation: { orientation: [0, 0, 0, 1], - position: feature.location.center.map(v => v * 1e6), - animation: {} - } + position: feature.location.center.coordinates.map(v => (v.unit as number) * 1e6) + }, + animation: true }) ) this.store$.dispatch( - actionViewerStateSelectFeature({ feature }) + userInterface.actions.showFeature({ + feature + }) ) } clearSelectedFeature(){ this.store$.dispatch( - actionViewerStateSelectFeature({ - feature: null - }) + userInterface.actions.clearShownFeature() ) } } diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index e557965486969e1cda62c84c719537bbdf84e3c0..ff6bd01addf7111c741f5c5b919c003bd2ee3ccb 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -164,11 +164,9 @@ <!-- if pli voi is visible, show pli template otherwise show region tmpl --> <ng-template - [ngTemplateOutlet]="(pliVol$ | async)?.[0] - ? voiTmpl - : (selectedFeature$ | async) - ? selectedFeatureTmpl - : sidenavRegionTmpl" + [ngTemplateOutlet]="(selectedFeature$ | async) + ? selectedFeatureTmpl + : sidenavRegionTmpl" [ngTemplateOutletContext]="{ drawer: drawer, showFullSidenavSwitch: showFullSidenavSwitch, @@ -390,6 +388,7 @@ (viewerEvent)="handleViewerEvent($event)" [selectedTemplate]="templateSelected$ | async" [selectedParcellation]="parcellationSelected$ | async" + [selectedAtlas]="selectedAtlas$ | async" #iavCmpViewerNehubaGlue="iavCmpViewerNehubaGlue"> </iav-cmp-viewer-nehuba-glue> @@ -534,32 +533,6 @@ </button> </ng-template> -<!-- VOI sidenav tmpl --> -<ng-template #voiTmpl> - <ng-container *ngTemplateOutlet="sapiBaseFeatureTmpl; context: { - backCb: clearVoi.bind(this), - title: _pliTitle, - subtitle: 'Dataset preview', - description: _pliDesc, - url: [{ - doi: _pliLink - }], - contentTmpl: pliDetailTmpl - }"> - </ng-container> - - <ng-template #pliDetailTmpl> - <mat-expansion-panel class="sidenav-cover-header-container"> - <mat-expansion-panel-header> - <mat-panel-title> - Registered Volumes - </mat-panel-title> - </mat-expansion-panel-header> - <layer-browser></layer-browser> - </mat-expansion-panel> - </ng-template> -</ng-template> - <!-- region sidenav tmpl --> <ng-template #sidenavRegionTmpl @@ -582,7 +555,7 @@ <ng-template [ngIf]="selectedRegions.length === 1" [ngIfElse]="multiRegionWrapperTmpl"> <!-- a series of bugs result in requiring this hacky --> <!-- see https://github.com/HumanBrainProject/interactive-viewer/issues/698 --> - <ng-container *ngTemplateOutlet="singleRegionTmpl; context: { region: (regionOfInterest$ | async) }"> + <ng-container *ngTemplateOutlet="singleRegionTmpl; context: { region: selectedRegions[0] }"> </ng-container> </ng-template> @@ -619,6 +592,9 @@ <ng-template #singleRegionTmpl let-region="region"> <!-- region detail --> <region-menu + [atlas]="selectedAtlas$ | async" + [template]="templateSelected$ | async" + [parcellation]="parcellationSelected$ | async" [region]="region" class="flex-grow-1 bs-border-box mat-elevation-z4"> </region-menu> @@ -780,6 +756,7 @@ </mat-card> </ng-template> + <!-- viewer status ctx menu --> <ng-template #viewerStatusCtxMenu let-data> <mat-list> @@ -826,31 +803,43 @@ </mat-list> </ng-template> + +<!-- viewer state hover ctx menu --> <ng-template #viewerStatusRegionCtxMenu let-data> <!-- hovered ROIs --> <mat-list> - <mat-list-item *ngFor="let hoveredR of data.metadata.hoveredRegions; let first = first"> + <mat-list-item *ngFor="let region of data.metadata.hoveredRegions; let first = first"> <mat-divider class="top-0" *ngIf="!first"></mat-divider> - <span mat-line> - {{ hoveredR.displayName || hoveredR.name }} - </span> - <span mat-line class="text-muted"> - <i class="fas fa-brain"></i> - <span> - Brain region - </span> - </span> - <!-- lookup region --> - <button mat-icon-button - (click)="selectRoi(hoveredR)" - ctx-menu-dismiss> - <i class="fas fa-search"></i> - </button> + <ng-container *ngTemplateOutlet="viewerStateSapiRegionTmpl; context: { $implicit: region }"> + </ng-container> + </mat-list-item> </mat-list> </ng-template> + +<!-- sapi region tmpl --> +<ng-template #viewerStateSapiRegionTmpl let-region> + <span mat-line> + {{ region.name }} + </span> + <span mat-line class="text-muted"> + <i class="fas fa-brain"></i> + <span> + Brain region + </span> + </span> + + <!-- lookup region --> + <button mat-icon-button + (click)="selectRoi(region)" + ctx-menu-dismiss> + <i class="fas fa-search"></i> + </button> +</ng-template> + + <!-- feature tmpls --> <ng-template #sapiBaseFeatureTmpl let-backCb="backCb" diff --git a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.component.ts b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.component.ts index 5a4bea443845687db10084cd253a18ec1c44bb17..a2b0b594b3ab0e2f002e2ab140aefe54f289d3da 100644 --- a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.component.ts +++ b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.component.ts @@ -1,13 +1,13 @@ -import { Component, EventEmitter, Output, Pipe, PipeTransform } from "@angular/core"; +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 { viewerStateContextedSelectedRegionsSelector, viewerStateGetOverlayingAdditionalParcellations, viewerStateParcVersionSelector, viewerStateSelectedParcellationSelector } from "src/services/state/viewerState/selectors"; import { distinctUntilChanged, map } from "rxjs/operators"; -import { viewerStateHelperSelectParcellationWithId, viewerStateRemoveAdditionalLayer, viewerStateSetSelectedRegions } from "src/services/state/viewerState.store.helper"; import { ngViewerActionClearView, ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState.store.helper"; import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN } from "src/util/interfaces"; -import { TDatainfosDetail, TSimpleInfo } from "src/util/siibraApiConstants/types"; +import { atlasSelection } from "src/state" +import { NEVER, of } from "rxjs"; +import { SapiParcellationModel } from "src/atlasComponents/sapi"; @Component({ selector: 'viewer-state-breadcrumb', @@ -40,33 +40,24 @@ export class ViewerStateBreadCrumb { select(ngViewerSelectorClearViewEntries) ) - public selectedAdditionalLayers$ = this.store$.pipe( - select(viewerStateGetOverlayingAdditionalParcellations), - ) + // TODO what is this observable anyway? + public selectedAdditionalLayers$ = of([]) public parcellationSelected$ = this.store$.pipe( - select(viewerStateSelectedParcellationSelector), + select(atlasSelection.selectors.selectedParcellation), distinctUntilChanged(), + ) public selectedRegions$ = this.store$.pipe( - select(viewerStateContextedSelectedRegionsSelector), + select(atlasSelection.selectors.selectedRegions), distinctUntilChanged(), ) - public selectedLayerVersions$ = this.store$.pipe( - select(viewerStateParcVersionSelector), - map(arr => arr.map(item => { - const overwrittenName = item['@version'] && item['@version']['name'] - return overwrittenName - ? { ...item, displayName: overwrittenName } - : item - })) - ) - + // TODO add version info in siibra-api/siibra-python + public selectedLayerVersions$ = NEVER constructor(private store$: Store<any>){ - } handleChipClick(){ @@ -75,9 +66,7 @@ export class ViewerStateBreadCrumb { public clearSelectedRegions(){ this.store$.dispatch( - viewerStateSetSelectedRegions({ - selectRegions: [] - }) + atlasSelection.actions.clearSelectedRegions() ) } @@ -91,16 +80,14 @@ export class ViewerStateBreadCrumb { public clearAdditionalLayer(layer: { ['@id']: string }){ this.store$.dispatch( - viewerStateRemoveAdditionalLayer({ - payload: layer - }) + atlasSelection.actions.clearNonBaseParcLayer() ) } - public selectParcellation(parc: any) { + public selectParcellationWithId(parcId: string) { this.store$.dispatch( - viewerStateHelperSelectParcellationWithId({ - payload: parc + atlasSelection.actions.selectATPById({ + parcellationId: parcId }) ) } @@ -114,28 +101,3 @@ export class ViewerStateBreadCrumb { } } - -@Pipe({ - name: 'originalDatainfoPriorityPipe' -}) - -export class OriginalDatainfoPipe implements PipeTransform{ - public transform(arr: (TSimpleInfo | TDatainfosDetail)[]): TDatainfosDetail[]{ - const detailedInfos = arr.filter(item => item['@type'] === 'minds/core/dataset/v1.0.0') as TDatainfosDetail[] - const simpleInfos = arr.filter(item => item['@type'] === 'fzj/tmp/simpleOriginInfo/v0.0.1') as TSimpleInfo[] - - if (detailedInfos.length > 0) return detailedInfos - if (simpleInfos.length > 0) { - return arr.map(d => { - return { - '@type': 'minds/core/dataset/v1.0.0', - name: d.name, - description: d.name, - urls: [], - useClassicUi: false - } - }) - } - return [] - } -} diff --git a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.template.html b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.template.html index a80037b10ee223444d42bfe1f144872b82fa0fa2..bb2b36c8cf56e8e59bea040460409e36c5c56051 100644 --- a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.template.html +++ b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.template.html @@ -27,7 +27,7 @@ [matMenuTriggerData]="{ layerVersionMenuTrigger: layerVersionMenuTrigger }" #layerVersionMenuTrigger="matMenuTrigger"> - <ng-template [ngIf]="addParc.length > 0" [ngIfElse]="defaultParcTmpl"> + <ng-template [ngIf]="addParc && addParc.length > 0" [ngIfElse]="defaultParcTmpl"> <ng-container *ngFor="let p of addParc"> <ng-container *ngTemplateOutlet="chipTmpl; context: { parcel: p, @@ -150,7 +150,7 @@ class: 'w-100', ariaLabel: parcVer.displayName || parcVer.name, onclick: bindFns([ - [ selectParcellation.bind(this), parcVer ], + [ selectParcellationWithId.bind(this), parcVer['@id'] ], [ layerVersionMenuTrigger.closeMenu.bind(layerVersionMenuTrigger) ] ]) }"> @@ -181,15 +181,15 @@ </span> <!-- info icon --> - <ng-container *ngFor="let originDatainfo of (parcel.originDatainfos | originalDatainfoPriorityPipe)"> + <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.name" - [iav-dataset-show-dataset-dialog-description]="originDatainfo.description" + [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> diff --git a/src/viewerModule/viewerStateBreadCrumb/module.ts b/src/viewerModule/viewerStateBreadCrumb/module.ts index ba53571ed934fe530330bd5b96fa2064484151e3..28de784c3e4c6e44fa6257f13604794066d9726a 100644 --- a/src/viewerModule/viewerStateBreadCrumb/module.ts +++ b/src/viewerModule/viewerStateBreadCrumb/module.ts @@ -1,11 +1,12 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { ParcellationRegionModule } from "src/atlasComponents/parcellationRegion"; -import { KgDatasetModule } from "src/atlasComponents/regionalFeatures/bsFeatures/kgDataset"; import { QuickTourModule } from "src/ui/quickTour"; import { AngularMaterialModule } from "src/sharedModules"; import { UtilModule } from "src/util"; -import { OriginalDatainfoPipe, ViewerStateBreadCrumb } from "./breadcrumb/breadcrumb.component"; +import { ViewerStateBreadCrumb } from "./breadcrumb/breadcrumb.component"; +import { OriginalDatainfoPipe } from "./pipes/originDataInfo.pipe" +import { DialogInfoModule } from "src/ui/dialogInfo"; @NgModule({ imports: [ @@ -13,8 +14,8 @@ import { OriginalDatainfoPipe, ViewerStateBreadCrumb } from "./breadcrumb/breadc AngularMaterialModule, QuickTourModule, ParcellationRegionModule, - KgDatasetModule, UtilModule, + DialogInfoModule, ], declarations: [ ViewerStateBreadCrumb, diff --git a/src/viewerModule/viewerStateBreadCrumb/pipes/originDataInfo.pipe.ts b/src/viewerModule/viewerStateBreadCrumb/pipes/originDataInfo.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c8676554dd7c1b766878d579b42b9a2bdbfc2ce --- /dev/null +++ b/src/viewerModule/viewerStateBreadCrumb/pipes/originDataInfo.pipe.ts @@ -0,0 +1,24 @@ +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 [] + } + } +} diff --git a/worker/worker.js b/worker/worker.js index 04a3db58bd8cbac500f2ab0dedc896ea2de7f979..8889abb92c88f400260cd4f391551e7ad7277c51 100644 --- a/worker/worker.js +++ b/worker/worker.js @@ -17,7 +17,6 @@ if (typeof self.importScripts === 'function') self.importScripts('./worker-type */ const validTypes = [ - 'GET_LANDMARKS_VTK', 'GET_USERLANDMARKS_VTK', 'PROPAGATE_PARC_REGION_ATTR' ] @@ -35,7 +34,6 @@ const VALID_METHODS = [ ] const validOutType = [ - 'ASSEMBLED_LANDMARKS_VTK', 'ASSEMBLED_USERLANDMARKS_VTK', ] @@ -170,32 +168,6 @@ const parseLmToVtk = (landmarks, scale) => { .concat(reduce.labelString.join('\n')) } -let landmarkVtkUrl - -const getLandmarksVtk = (action) => { - - // landmarks are array of triples in nm (array of array of numbers) - const landmarks = action.landmarks - const template = action.template - const scale = action.scale - ? action.scale - : 2.8 - - const vtk = parseLmToVtk(landmarks, scale) - - if(!vtk) return - - // when new set of landmarks are to be displayed, the old landmarks will be discarded - if(landmarkVtkUrl) URL.revokeObjectURL(landmarkVtkUrl) - - landmarkVtkUrl = URL.createObjectURL(new Blob( [encoder.encode(vtk)], {type : 'application/octet-stream'} )) - postMessage({ - type : 'ASSEMBLED_LANDMARKS_VTK', - template, - url : landmarkVtkUrl - }) -} - let userLandmarkVtkUrl const getuserLandmarksVtk = (action) => { @@ -327,9 +299,6 @@ onmessage = (message) => { if(validTypes.findIndex(type => type === message.data.type) >= 0){ switch(message.data.type){ - case 'GET_LANDMARKS_VTK': - getLandmarksVtk(message.data) - return case 'GET_USERLANDMARKS_VTK': getuserLandmarksVtk(message.data) return