diff --git a/src/atlasComponents/sapi/stories.base.ts b/src/atlasComponents/sapi/stories.base.ts index becc12536efb8391c236d905ab8cb10d6243a0e3..f4162b72d80ec585472dbe40e77423336f0ec630 100644 --- a/src/atlasComponents/sapi/stories.base.ts +++ b/src/atlasComponents/sapi/stories.base.ts @@ -46,7 +46,8 @@ export const atlasId = { export const spaceId = { human: { - mni152: 'minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2' + mni152: 'minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2', + bigbrain: 'minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588' }, rat: { waxholm: "minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8" @@ -56,7 +57,9 @@ export const spaceId = { export const parcId = { human: { jba29: "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290", - longBundle: "juelich/iav/atlas/v1.0.0/5" + longBundle: "juelich/iav/atlas/v1.0.0/5", + difumo256: "minds/core/parcellationatlas/v1.0.0/141d510f-0342-4f94-ace7-c97d5f160235", + corticalLayers: "juelich/iav/atlas/v1.0.0/3" }, rat: { v4: 'minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe-v4' diff --git a/src/atlasComponents/sapiViews/core/atlas/module.ts b/src/atlasComponents/sapiViews/core/atlas/module.ts index 770a8e9e1e91354fe5edb9d4b1376e2ee25e2c86..a41cdb25c60ea1255837923285522a32ca85dfcc 100644 --- a/src/atlasComponents/sapiViews/core/atlas/module.ts +++ b/src/atlasComponents/sapiViews/core/atlas/module.ts @@ -3,6 +3,7 @@ import { NgModule } from "@angular/core"; import { SpinnerModule } from "src/components/spinner"; import { AngularMaterialModule } from "src/sharedModules"; import { QuickTourModule } from "src/ui/quickTour"; +import { SapiViewsUtilModule } from "../../util"; import { SapiViewsCoreParcellationModule } from "../parcellation"; import { SapiViewsCoreSpaceModule } from "../space"; import { SapiViewsCoreAtlasAtlasDropdownSelector } from "./dropdownAtlasSelector/dropdownAtlasSelector.component"; @@ -16,6 +17,7 @@ import { SapiViewsCoreAtlasAtlasTmplParcSelector } from "./tmplParcSelector/tmpl SapiViewsCoreParcellationModule, QuickTourModule, SpinnerModule, + SapiViewsUtilModule, ], declarations: [ SapiViewsCoreAtlasAtlasDropdownSelector, diff --git a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts index 721f6508c8c0ad4249c943d66a1c09e5a0cc45a5..be8b5bfc581d88baf37a5957cea41c115f2fd659 100644 --- a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts +++ b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts @@ -1,6 +1,6 @@ import { animate, state, style, transition, trigger } from "@angular/animations"; import { ChangeDetectionStrategy, Component, ElementRef, HostBinding, QueryList, ViewChild, ViewChildren } from "@angular/core"; -import { select, Store } from "@ngrx/store"; +import { Store } from "@ngrx/store"; import { combineLatest, forkJoin, merge, Observable, Subject, Subscription } from "rxjs"; import { distinctUntilChanged, map, mapTo, shareReplay, switchMap, tap } from "rxjs/operators"; import { SAPI } from "src/atlasComponents/sapi"; @@ -170,11 +170,7 @@ export class SapiViewsCoreAtlasAtlasTmplParcSelector { } - trackbyAtId(t) { + trackTmpl(t:SapiSpaceModel) { return t['@id'] } - - trackKeyVal(obj: { key: string, value: any }) { - return obj.key - } } \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.stories.ts b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.stories.ts index 8121569be49e29e23fc3bb751f8d40f0bf284ad4..c5bae1bbdef9c9d0709271e63580043fcc406aee 100644 --- a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.stories.ts +++ b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.stories.ts @@ -4,7 +4,7 @@ import { provideMockStore } from "@ngrx/store/testing" import { action } from "@storybook/addon-actions" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI } from "src/atlasComponents/sapi" -import { spaceId, provideDarkTheme, getHumanAtlas, getMni152, getJba29 } from "src/atlasComponents/sapi/stories.base" +import { spaceId, provideDarkTheme, getHumanAtlas, getMni152, getJba29, getSpace, atlasId, getParc, parcId } from "src/atlasComponents/sapi/stories.base" import { AngularMaterialModule } from "src/sharedModules" import { atlasSelection } from "src/state" import { SapiViewsCoreAtlasModule } from "../module" @@ -67,29 +67,49 @@ Template.loaders = [ ] -const asyncLoader = async () => { - const atlas = await getHumanAtlas() - const template = await getMni152() - const parcellation = await getJba29() - return { - atlas, - template, - parcellation, - } +export const MNI152JBA29 = Template.bind({}) +MNI152JBA29.args = { + } +MNI152JBA29.loaders = [ + async () => { + const atlas = await getHumanAtlas() + const template = await getMni152() + const parcellation = await getJba29() + return { + atlas, + template, + parcellation, + } + } +] -export const Default = Template.bind({}) -Default.args = { - selected: spaceId.human.mni152 +export const BigBrainJBA29 = Template.bind({}) +BigBrainJBA29.args = { + } -Default.loaders = [ - +BigBrainJBA29.loaders = [ async () => { - const { + const atlas = await getHumanAtlas() + const template = await getSpace(atlasId.human, spaceId.human.bigbrain) + const parcellation = await getJba29() + return { atlas, template, parcellation, - } = await asyncLoader() + } + } +] + +export const BigBrainCorticalLayers = Template.bind({}) +BigBrainCorticalLayers.args = { + +} +BigBrainCorticalLayers.loaders = [ + async () => { + const atlas = await getHumanAtlas() + const template = await getSpace(atlasId.human, spaceId.human.bigbrain) + const parcellation = await getParc(atlasId.human, parcId.human.corticalLayers) return { atlas, template, @@ -97,3 +117,20 @@ Default.loaders = [ } } ] + +export const MNI152LongBundle = Template.bind({}) +MNI152LongBundle.args = { + +} +MNI152LongBundle.loaders = [ + async () => { + const atlas = await getHumanAtlas() + const template = await getMni152() + const parcellation = await getParc(atlasId.human, parcId.human.longBundle) + return { + atlas, + template, + parcellation, + } + } +] \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.template.html b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.template.html index 944f2bc950889db8e2e0a5d0cb58de32ada590ea..db9ba08d8bf273f3726bda3fe2ae8d19b94f5b0a 100644 --- a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.template.html +++ b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.template.html @@ -18,10 +18,13 @@ rowHeight="2:3" gutterSize="16"> - <mat-grid-tile *ngFor="let template of availableTemplates$ | async; trackBy: trackbyAtId" + <mat-grid-tile *ngFor="let template of availableTemplates$ | async; trackBy: trackTmpl" [attr.aria-checked]="(selectedTemplate$ | async)?.['@id'] === template['@id']"> <sxplr-sapiviews-core-space-tile + [ngClass]="{ + 'sxplr-extra-muted': !(template | spaceSupportedInCurrentParcellation | async) + }" [sxplr-sapiviews-core-space-tile-space]="template" [sxplr-sapiviews-core-space-tile-selected]="(selectedTemplate$ | async)?.['@id'] === template['@id']" (click)="selectTemplate(template)"> @@ -40,16 +43,29 @@ rowHeight="2:3" gutterSize="16"> - <mat-grid-tile *ngFor="let parc of availableParcellations$ | async | filterUnsupportedParc | filterGroupedParcs"> + <!-- using single parc template, since it's reused in non individual parcellation and tmpl for grp parcellation --> + <ng-template #singleParcTmpl let-parc> <sxplr-sapiviews-core-parcellation-tile + [ngClass]="{ + 'sxplr-extra-muted': !(parc | parcellationSupportedInCurrentSpace | async) + }" + [sxplr-sapiviews-core-parcellation-tile-selected]="(selectedParcellation$ | async)?.['@id'] === parc['@id']" [sxplr-sapiviews-core-parcellation-tile-parcellation]="parc" (sxplr-sapiviews-core-parcellation-tile-onclick-parc)="selectParcellation($event)"> </sxplr-sapiviews-core-parcellation-tile> + </ng-template> + + <mat-grid-tile *ngFor="let parc of availableParcellations$ | async | filterUnsupportedParc | filterGroupedParcs"> + <ng-template + [ngTemplateOutlet]="singleParcTmpl" + [ngTemplateOutletContext]="{ $implicit: parc }"> + </ng-template> </mat-grid-tile> <mat-grid-tile *ngFor="let group of availableParcellations$ | async | filterUnsupportedParc | filterGroupedParcs : true | filterUnsupportedParc"> <sxplr-sapiviews-core-parcellation-tile + [sxplr-sapiviews-core-parcellation-tile-groupmenu-parc-tmpl]="singleParcTmpl" [sxplr-sapiviews-core-parcellation-tile-parcellation]="group" (sxplr-sapiviews-core-parcellation-tile-onclick-parc)="selectParcellation($event)"> diff --git a/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.stories.ts b/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.stories.ts index 3210779ecbce9b9dbc0ecbe29ab3617e1cb3d285..5e3afe419fc640d609b2f6acbfee703a7e5ae69c 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.stories.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.stories.ts @@ -1,5 +1,6 @@ import { CommonModule } from "@angular/common" import { HttpClientModule } from "@angular/common/http" +import { provideMockStore } from "@ngrx/store/testing" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI, SapiParcellationModel } from "src/atlasComponents/sapi" import { atlasId, getAtlas, provideDarkTheme, getParc } from "src/atlasComponents/sapi/stories.base" @@ -19,6 +20,7 @@ export default { AngularMaterialModule, ], providers: [ + provideMockStore(), SAPI, ...provideDarkTheme, ], diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.stories.ts b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.stories.ts index 7f3338a6a61a755f450cb04413a777452281dcfd..beab85bcc397b881acbd639f4616969a19034d5d 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.stories.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.stories.ts @@ -7,6 +7,7 @@ import { SAPI, SapiParcellationModel } from "src/atlasComponents/sapi" import { atlasId, getAtlas, provideDarkTheme, getParc, getAtlases } from "src/atlasComponents/sapi/stories.base" import { AngularMaterialModule } from "src/sharedModules" import { SapiViewsCoreParcellationModule } from "../module" +import { provideMockStore } from "@ngrx/store/testing" @Component({ selector: `parc-smart-chip-wrapper`, @@ -54,6 +55,7 @@ export default { AngularMaterialModule, ], providers: [ + provideMockStore(), SAPI, ...provideDarkTheme, ], diff --git a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.component.ts b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.component.ts index e6052cd0ca1eeb0e7011c42b1fa53af6c60bbc71..839e8791ff2df8090de142deedf0e39046cfec32 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.component.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core"; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef } from "@angular/core"; import { SapiParcellationModel } from "src/atlasComponents/sapi"; import { GroupedParcellation } from "../groupedParcellation"; @@ -16,8 +16,18 @@ const lightthemeId = [ }) export class SapiViewsCoreParcellationParcellationTile implements OnChanges{ + @Input('sxplr-sapiviews-core-parcellation-tile-groupmenu-parc-tmpl') + singleParcTmpl: TemplateRef<any> + + private _parcellation: SapiParcellationModel | GroupedParcellation @Input('sxplr-sapiviews-core-parcellation-tile-parcellation') - parcellation: SapiParcellationModel | GroupedParcellation + set parcellation(val: SapiParcellationModel | GroupedParcellation) { + this._parcellation = val + this.ngOnChanges() + } + get parcellation(){ + return this._parcellation + } @Input('sxplr-sapiviews-core-parcellation-tile-selected') selected: boolean = false diff --git a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.stories.ts b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.stories.ts index fc7b39f5018544063d10e2247551ed249b0845f7..1fad5b1cd9332700829f662798829e919691b956 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.stories.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.stories.ts @@ -1,6 +1,7 @@ import { CommonModule } from "@angular/common" import { HttpClientModule } from "@angular/common/http" import { Component, Input, Output, EventEmitter } from "@angular/core" +import { provideMockStore } from "@ngrx/store/testing" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI, SapiParcellationModel } from "src/atlasComponents/sapi" import { atlasId, parcId, getAtlas, provideDarkTheme, getParc } from "src/atlasComponents/sapi/stories.base" @@ -10,6 +11,10 @@ import { SapiViewsCoreParcellationModule } from "../module" @Component({ selector: `parc-tile-wrapper`, template: ` + <ng-template #grpParcTmpl let-parc> + {{ parc.name }} + </ng-template> + <mat-accordion> <mat-expansion-panel *ngFor="let item of parcs | keyvalue"> @@ -47,6 +52,24 @@ import { SapiViewsCoreParcellationModule } from "../module" </ng-container> </ng-template> </mat-expansion-panel> + + <mat-expansion-panel> + <mat-expansion-panel-header> + > grouped tmpl + </mat-expansion-panel-header> + + <ng-template matExpansionPanelContent> + <ng-container *ngFor="let item of parcs | keyvalue"> + <sxplr-sapiviews-core-parcellation-tile + *ngFor="let parc of (item.value | filterGroupedParcs : true)" + [sxplr-sapiviews-core-parcellation-tile-groupmenu-parc-tmpl]="grpParcTmpl" + [sxplr-sapiviews-core-parcellation-tile-parcellation]="parc" + class="sxplr-m-2" + (sxplr-sapiviews-core-parcellation-tile-onclick-parc)="parcClicked.emit($event)"> + </sxplr-sapiviews-core-parcellation-tile> + </ng-container> + </ng-template> + </mat-expansion-panel> </mat-accordion> `, styles: [ @@ -76,6 +99,7 @@ export default { AngularMaterialModule, ], providers: [ + provideMockStore(), SAPI, ...provideDarkTheme, ], diff --git a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html index 324120aa0f23b0d2f7504da98e2668c776c4fed0..19cd424794e7bc343d775fa541979bf1019cf0ac 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html +++ b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html @@ -1,7 +1,8 @@ <mat-menu hasBackDrop="false" #matMenu="matMenu"> <ng-template matMenuContent let-subParcellations="subParcellations"> - <div class="sxplr-ml-2 sxplr-mr-2"> + + <div class="sxplr-custom-cmp sxplr-ml-2 sxplr-mr-2"> <mat-grid-list cols="1" [rowHeight]="rowHeight" @@ -10,15 +11,28 @@ <mat-grid-tile *ngFor="let parc of subParcellations"> - <tile-cmp *ngIf="parc" - class="sxplr-custom-cmp text" - [tile-text]="parc.name" - [tile-image-src]="parc | previewParcellationUrl" - [tile-selected]="selected" - [tile-image-darktheme]="darktheme" - (click)="clickOnParcellation(parc)"> - - </tile-cmp> + + <!-- if parent component injected template, use injected template --> + <ng-template + [ngIf]="singleParcTmpl" + [ngIfElse]="fallbackGrpParcTmpl" + [ngTemplateOutlet]="singleParcTmpl" + [ngTemplateOutletContext]="{ + $implicit: parc + }"> + </ng-template> + + <ng-template #fallbackGrpParcTmpl> + <tile-cmp *ngIf="parc" + class="sxplr-custom-cmp text" + [tile-text]="parc.name" + [tile-image-src]="parc | previewParcellationUrl" + [tile-selected]="selected" + [tile-image-darktheme]="darktheme" + (click)="clickOnParcellation(parc)"> + + </tile-cmp> + </ng-template> </mat-grid-tile> </mat-grid-list> diff --git a/src/atlasComponents/sapiViews/core/parcellation/tile/singleTile.stories.ts b/src/atlasComponents/sapiViews/core/parcellation/tile/singleTile.stories.ts index 410456e99f31401f91342f43c9190b60898777ec..920c1cfc82a26550a2710a584f6db9f45b0ea5af 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/tile/singleTile.stories.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/tile/singleTile.stories.ts @@ -1,9 +1,9 @@ import { CommonModule } from "@angular/common" import { HttpClientModule } from "@angular/common/http" -import { Component, Input, Output, EventEmitter } from "@angular/core" +import { provideMockStore } from "@ngrx/store/testing" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI, SapiParcellationModel } from "src/atlasComponents/sapi" -import { atlasId, parcId, getAtlas, provideDarkTheme, getParc, getHumanAtlas } from "src/atlasComponents/sapi/stories.base" +import { parcId, provideDarkTheme, getParc, getHumanAtlas } from "src/atlasComponents/sapi/stories.base" import { AngularMaterialModule } from "src/sharedModules" import { FilterGroupedParcellationPipe } from "../filterGroupedParcellations.pipe" import { GroupedParcellation } from "../groupedParcellation" @@ -21,13 +21,14 @@ export default { AngularMaterialModule, ], providers: [ + provideMockStore(), SAPI, ...provideDarkTheme, ], declarations: [ - ] - }) + ], + }), ], } as Meta @@ -45,7 +46,10 @@ const Template: Story<SapiViewsCoreParcellationParcellationTile> = (args: SapiVi gutterSize, rowHeight, parcellation: groups[1] - } + }, + styles: [ + `sxplr-sapiviews-core-parcellation-tile { display: inline-block; max-width: 8rem; }` + ] }) } Template.loaders = [ diff --git a/src/atlasComponents/sapiViews/util/module.ts b/src/atlasComponents/sapiViews/util/module.ts index 4d99281d6df7f3836fe278d8992475b2e90f5847..4a9eddaf8ef6cb42a5b0b5b1bec270fac766f82b 100644 --- a/src/atlasComponents/sapiViews/util/module.ts +++ b/src/atlasComponents/sapiViews/util/module.ts @@ -2,7 +2,10 @@ import { NgModule } from "@angular/core"; import { AddUnitAndJoin } from "./addUnitAndJoin.pipe"; import { IncludesPipe } from "./includes.pipe"; import { NumbersPipe } from "./numbers.pipe"; +import { ParcellationSupportedInCurrentSpace } from "./parcellationSupportedInCurrentSpace.pipe"; +import { ParcellationSupportedInSpacePipe } from "./parcellationSupportedInSpace.pipe"; import { ParseDoiPipe } from "./parseDoi.pipe"; +import { SpaceSupportedInCurrentParcellationPipe } from "./spaceSupportedInCurrentParcellation.pipe"; @NgModule({ declarations: [ @@ -10,12 +13,18 @@ import { ParseDoiPipe } from "./parseDoi.pipe"; NumbersPipe, AddUnitAndJoin, IncludesPipe, + ParcellationSupportedInSpacePipe, + ParcellationSupportedInCurrentSpace, + SpaceSupportedInCurrentParcellationPipe, ], exports: [ ParseDoiPipe, NumbersPipe, AddUnitAndJoin, IncludesPipe, + ParcellationSupportedInSpacePipe, + ParcellationSupportedInCurrentSpace, + SpaceSupportedInCurrentParcellationPipe, ] }) diff --git a/src/atlasComponents/sapiViews/util/parcellationSupportedInCurrentSpace.pipe.ts b/src/atlasComponents/sapiViews/util/parcellationSupportedInCurrentSpace.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..3fd1069f0b13c69822d115aba804ff93e81c6c27 --- /dev/null +++ b/src/atlasComponents/sapiViews/util/parcellationSupportedInCurrentSpace.pipe.ts @@ -0,0 +1,37 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { select, Store } from "@ngrx/store"; +import { Observable } from "rxjs"; +import { switchMap } from "rxjs/operators"; +import { SAPI } from "src/atlasComponents/sapi/sapi.service"; +import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; +import { atlasSelection } from "src/state"; +import { ParcellationSupportedInSpacePipe } from "./parcellationSupportedInSpace.pipe" + +@Pipe({ + name: 'parcellationSupportedInCurrentSpace', + /** + * the pipe is not exactly pure, since it makes http call + * but for the sake of angular change detection, this is suitable + * since the result should only change on input change + */ + pure: true +}) + +export class ParcellationSupportedInCurrentSpace implements PipeTransform{ + + private transformPipe = new ParcellationSupportedInSpacePipe(this.sapi) + + private selectedTemplate$ = this.store.pipe( + select(atlasSelection.selectors.selectedTemplate) + ) + constructor( + private store: Store, + private sapi: SAPI, + ){} + + public transform(parcellation: SapiParcellationModel): Observable<boolean> { + return this.selectedTemplate$.pipe( + switchMap(tmpl => this.transformPipe.transform(parcellation, tmpl)) + ) + } +} diff --git a/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts b/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..aaafaf90f56744650f7ec17341eb91cc497f5c1d --- /dev/null +++ b/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts @@ -0,0 +1,42 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; +import { SAPIParcellation } from "src/atlasComponents/sapi/core"; +import { SAPI } from "src/atlasComponents/sapi/sapi.service"; +import { SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type"; + +export const knownExceptions = { + supported: { + /** + * jba29 + */ + 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290': [ + /** + * big brain + */ + 'minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588' + ] + } +} + +@Pipe({ + name: 'parcellationSuppportedInSpace', + pure: false, +}) + +export class ParcellationSupportedInSpacePipe implements PipeTransform{ + + constructor(private sapi: SAPI){} + + public transform(parc: SapiParcellationModel|string, tmpl: SapiSpaceModel|string): Observable<boolean> { + const parcId = typeof parc === "string" + ? parc + : parc["@id"] + const tmplId = typeof tmpl === "string" + ? tmpl + : tmpl["@id"] + return this.sapi.registry.get<SAPIParcellation>(parcId).getVolumes().pipe( + map(volumes => volumes.some(v => v.data.space["@id"] === tmplId)) + ) + } +} diff --git a/src/atlasComponents/sapiViews/util/spaceSupportedInCurrentParcellation.pipe.ts b/src/atlasComponents/sapiViews/util/spaceSupportedInCurrentParcellation.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..e462fcc6bf41fc412852a9ef8d3404023eff96d6 --- /dev/null +++ b/src/atlasComponents/sapiViews/util/spaceSupportedInCurrentParcellation.pipe.ts @@ -0,0 +1,38 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { select, Store } from "@ngrx/store"; +import { Observable } from "rxjs"; +import { switchMap } from "rxjs/operators"; +import { SAPI } from "src/atlasComponents/sapi/sapi.service"; +import { SapiSpaceModel } from "src/atlasComponents/sapi/type"; +import { atlasSelection } from "src/state"; +import { ParcellationSupportedInSpacePipe } from "./parcellationSupportedInSpace.pipe" + +@Pipe({ + name: "spaceSupportedInCurrentParcellation", + /** + * the pipe is not exactly pure, since it makes http call + * but for the sake of angular change detection, this is suitable + * since the result should only change on input change + */ + pure: true +}) + +export class SpaceSupportedInCurrentParcellationPipe implements PipeTransform{ + private supportedPipe = new ParcellationSupportedInSpacePipe(this.sapi) + private selectedParcellation$ = this.store.pipe( + select(atlasSelection.selectors.selectedParcellation) + ) + constructor( + private store: Store, + private sapi: SAPI + ){ + + } + public transform(space: SapiSpaceModel): Observable<boolean> { + return this.selectedParcellation$.pipe( + switchMap(parc => + this.supportedPipe.transform(parc, space) + ) + ) + } +} diff --git a/src/components/tile/tile.style.css b/src/components/tile/tile.style.css index 0fa6bb8a8a8834d9fd378d10f331b9092323337c..40d1830348c3e18559a2d2c75668e4b03cbcd3dc 100644 --- a/src/components/tile/tile.style.css +++ b/src/components/tile/tile.style.css @@ -9,6 +9,7 @@ .tile-selected { border: 2px solid #FED363; + box-sizing: border-box; } :host, diff --git a/src/overwrite.scss b/src/overwrite.scss index 2a2e2468ea041d89ff6f1c2ef81d8de29c55adb6..a6b3e9f55255d3080d58a94e7be6aedce47fc890 100644 --- a/src/overwrite.scss +++ b/src/overwrite.scss @@ -191,6 +191,14 @@ $justify-content-vars: end, center, space-between; opacity: 0.75; } +.#{$nsp}-very-muted { + opacity: 0.5; +} + +.#{$nsp}-extra-muted { + opacity: 0.25; +} + $position-vars: relative, absolute; @each $position-var in $position-vars { .#{$nsp}-position-#{$position-var} { diff --git a/src/state/atlasSelection/actions.ts b/src/state/atlasSelection/actions.ts index 8e8ecf2e80f0d6724a4a13a3fe2c2ff9045cbcc0..d51f0838327fa67fd8ab8e2f6ff5e0dbc2f31b96 100644 --- a/src/state/atlasSelection/actions.ts +++ b/src/state/atlasSelection/actions.ts @@ -1,6 +1,6 @@ import { createAction, props } from "@ngrx/store"; import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; -import { BreadCrumb, nameSpace, ViewerMode } from "./const" +import { BreadCrumb, nameSpace, ViewerMode, AtlasSelectionState } from "./const" export const selectAtlas = createAction( `${nameSpace} selectAtlas`, @@ -23,6 +23,28 @@ export const selectParcellation = createAction( }>() ) +/** + * setATP is called as a final step to (potentially) set: + * - selectedAtlas + * - selectedTemplate + * - selectedParcellation + * + * It is up to the dispatcher to ensure that the selection makes sense. + * If any field is unset, it will take the default value from the store. + * It is **specifically** not setup to do **anymore** than atlas, template and parcellation + * + * We may setup post hook for navigation adjustments/etc. + * Probably easier is simply subscribe to store and react to selectedTemplate selector + */ +export const setATP = createAction( + `${nameSpace} setATP`, + props<{ + atlas?: SapiAtlasModel, + template?: SapiSpaceModel, + parcellation?: SapiParcellationModel, + }>() +) + export const setSelectedParcellationAllRegions = createAction( `${nameSpace} setSelectedParcellationAllRegions`, props<{ diff --git a/src/state/atlasSelection/const.ts b/src/state/atlasSelection/const.ts index 5ae1942f7be037d174612e9dbbb9b229065bbad9..1499e2472a896352406389a50a6eacaa14752539 100644 --- a/src/state/atlasSelection/const.ts +++ b/src/state/atlasSelection/const.ts @@ -1,6 +1,33 @@ +import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi" + export const nameSpace = `[state.atlasSelection]` export type ViewerMode = 'annotating' | 'key frame' export type BreadCrumb = { id: string name: string } + +export type AtlasSelectionState = { + selectedAtlas: SapiAtlasModel + selectedTemplate: SapiSpaceModel + selectedParcellation: SapiParcellationModel + selectedParcellationAllRegions: SapiRegionModel[] + + 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 + breadcrumbs: BreadCrumb[] +} diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts index 5f64cef83853f9be92156a7b516b9338fc33642e..686c1f25ebe1713b99eacd0c27e71744c8c73d96 100644 --- a/src/state/atlasSelection/effects.ts +++ b/src/state/atlasSelection/effects.ts @@ -1,8 +1,8 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; -import { forkJoin, merge, of } from "rxjs"; -import { filter, map, mapTo, switchMap, switchMapTo, withLatestFrom } from "rxjs/operators"; -import { SAPI, SAPIRegion, SapiRegionModel } from "src/atlasComponents/sapi"; +import { concat, forkJoin, merge, of } from "rxjs"; +import { filter, map, mapTo, switchMap, switchMapTo, take, tap, withLatestFrom } from "rxjs/operators"; +import { SAPI, SapiParcellationModel, SAPIRegion, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; import * as mainActions from "../actions" import { select, Store } from "@ngrx/store"; import { selectors, actions } from '.' @@ -10,10 +10,103 @@ import { fromRootStore } from "./util"; import { ParcellationIsBaseLayer } from "src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe"; import { OrderParcellationByVersionPipe } from "src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe"; import { atlasAppearance, atlasSelection } from ".."; +import { ParcellationSupportedInSpacePipe } from "src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe"; @Injectable() export class Effect { + parcSupportedInSpacePipe = new ParcellationSupportedInSpacePipe(this.sapiSvc) + + onTemplateParcSelection = createEffect(() => merge<{ template: SapiSpaceModel, parcellation: SapiParcellationModel }>( + this.action.pipe( + ofType(actions.selectTemplate), + map(({ template }) => { + return { + template, + parcellation: null + } + }) + ), + this.action.pipe( + ofType(actions.selectParcellation), + map(({ parcellation }) => { + return { + template: null, + parcellation + } + }) + ) + ).pipe( + withLatestFrom(this.store), + switchMap(([ { template, parcellation }, store ]) => { + const currTmpl = selectors.selectedTemplate(store) + const currParc = selectors.selectedParcellation(store) + const currAtlas = selectors.selectedAtlas(store) + return this.parcSupportedInSpacePipe.transform( + parcellation || currParc, + template || currTmpl + ).pipe( + switchMap(flag => { + /** + * if desired parc is supported in tmpl, emit them + */ + if (flag) { + return of({ + template: template || currTmpl, + parcellation: parcellation || currParc, + }) + } + /** + * if template is defined, find the first parcellation that is supported + */ + if (!!template) { + return concat( + ...currAtlas.parcellations.map( + p => this.parcSupportedInSpacePipe.transform(p["@id"], template).pipe( + filter(flag => flag), + switchMap(() => this.sapiSvc.getParcDetail(currAtlas["@id"], p['@id'])), + ) + ) + ).pipe( + map(parcellation => { + return { + template, + parcellation + } + }) + ) + } + if (!!parcellation) { + return concat( + ...currAtlas.spaces.map( + sp => this.parcSupportedInSpacePipe.transform(parcellation["@id"], sp["@id"]).pipe( + filter(flag => flag), + switchMap(() => this.sapiSvc.getSpaceDetail(currAtlas["@id"], sp['@id'])), + ) + ) + ).pipe( + take(1), + map(template => { + return { + template, + parcellation + } + }) + ) + } + throw new Error(`neither template nor parcellation has been defined!`) + }), + map(({ parcellation, template }) => + actions.setATP({ + parcellation, + template + }) + ) + ) + }), + + )) + onAtlasSelectionSelectTmplParc = createEffect(() => this.action.pipe( ofType(actions.selectAtlas), filter(action => !!action.atlas), @@ -37,16 +130,13 @@ export class Effect { this.sapiSvc.getSpaceDetail(atlas["@id"], spaceId["@id"]) ) ).pipe( - switchMap(spaces => { + map(spaces => { const selectedSpace = spaces.find(s => /152/.test(s.fullName)) || spaces[0] - return of( - actions.selectTemplate({ - template: selectedSpace - }), - actions.selectParcellation({ - parcellation - }) - ) + return actions.setATP({ + atlas, + template: selectedSpace, + parcellation + }) }) ) }), diff --git a/src/state/atlasSelection/index.ts b/src/state/atlasSelection/index.ts index d058f9d5049d3864c10e66aaa0e82f95c2b1d6c5..5ef795456578636c52a28ce08189b4feec31bdb5 100644 --- a/src/state/atlasSelection/index.ts +++ b/src/state/atlasSelection/index.ts @@ -1,6 +1,6 @@ export * as selectors from "./selectors" export { fromRootStore } from "./util" -export { nameSpace } from "./const" -export { reducer, AtlasSelectionState, defaultState } from "./store" +export { nameSpace, AtlasSelectionState } from "./const" +export { reducer, defaultState } 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 index c0a53adc5a17c388f44f583d1265fef2f5d27df3..93f075350ac3d1ef496d3f6dd766b6d4f493bbda 100644 --- a/src/state/atlasSelection/selectors.ts +++ b/src/state/atlasSelection/selectors.ts @@ -1,6 +1,5 @@ import { createSelector } from "@ngrx/store" -import { nameSpace } from "./const" -import { AtlasSelectionState } from "./store" +import { nameSpace, AtlasSelectionState } from "./const" export const viewerStateHelperStoreName = 'viewerStateHelper' diff --git a/src/state/atlasSelection/store.ts b/src/state/atlasSelection/store.ts index 11ac3f534822446d832b86567a669b9c86f5d34d..45bc4f669ead58ed58c6af97e3cb45e2c601f14c 100644 --- a/src/state/atlasSelection/store.ts +++ b/src/state/atlasSelection/store.ts @@ -1,7 +1,7 @@ import { createReducer, on } from "@ngrx/store"; import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; import * as actions from "./actions" -import { ViewerMode, BreadCrumb } from "./const" +import { AtlasSelectionState } from "./const" function getRegionLabelIndex(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, parc: SapiParcellationModel, region: SapiRegionModel) { const lblIdx = Number(region?.hasAnnotation?.internalIdentifier) @@ -9,31 +9,6 @@ function getRegionLabelIndex(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, parc: return lblIdx } -export type AtlasSelectionState = { - selectedAtlas: SapiAtlasModel - selectedTemplate: SapiSpaceModel - selectedParcellation: SapiParcellationModel - selectedParcellationAllRegions: SapiRegionModel[] - - 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 - breadcrumbs: BreadCrumb[] -} - export const defaultState: AtlasSelectionState = { selectedAtlas: null, selectedParcellation: null, @@ -49,29 +24,13 @@ export const defaultState: AtlasSelectionState = { 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 }) => { + actions.setATP, + (state, { atlas, parcellation, template }) => { return { ...state, - selectedParcellation: parcellation + selectedAtlas: atlas || state.selectedAtlas, + selectedTemplate: template || state.selectedTemplate, + selectedParcellation: parcellation || state.selectedParcellation, } } ), diff --git a/src/util/windowResize/windowResize.directive.ts b/src/util/windowResize/windowResize.directive.ts index ce8de31804fcaec53296925c5eef5235979719b0..97c6534b5a270f93979a8c86d57a23da1256c8c3 100644 --- a/src/util/windowResize/windowResize.directive.ts +++ b/src/util/windowResize/windowResize.directive.ts @@ -1,4 +1,4 @@ -import { Directive, EventEmitter, Input, OnChanges, OnInit, Output } from "@angular/core"; +import { Directive, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from "@angular/core"; import { Subscription } from "rxjs"; import { ResizeObserverService } from "./windowResize.service"; @@ -7,7 +7,7 @@ import { ResizeObserverService } from "./windowResize.service"; exportAs: 'iavWindowResize' }) -export class ResizeObserverDirective implements OnChanges, OnInit { +export class ResizeObserverDirective implements OnChanges, OnInit, OnDestroy { @Input('iav-window-resize-type') type: 'debounce' | 'throttle' = 'throttle' @@ -34,6 +34,10 @@ export class ResizeObserverDirective implements OnChanges, OnInit { this.configure() } + ngOnDestroy(): void { + while(this.sub.length > 0) this.sub.pop().unsubscribe() + } + configure(){ while(this.sub.length > 0) this.sub.pop().unsubscribe()