diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index f63b14cd95658b20dc7795ad8804b6e322020b50..1095b2dd6ede917c1a36f2768a39d6198d500716 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -8,6 +8,11 @@ { overflow: scroll!important; } + #root + { + width: 100%; + height: 100%; + } </style> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous"> <script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.5/dist/connectivity-component/connectivity-component.js" defer></script> diff --git a/deploy/quickstart/index.js b/deploy/quickstart/index.js index ceef857a7d4db225eac74ae82d47a354f66e9d39..07b5f1bb7eb3bf5cca7e84bf465ea1fee1cb02a1 100644 --- a/deploy/quickstart/index.js +++ b/deploy/quickstart/index.js @@ -27,8 +27,11 @@ const getQuickStartMdPr = (async () => { <script src="https://unpkg.com/dompurify@latest/dist/purify.min.js"></script> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Atlas Viewer Quickstart</title> + <style> + .padded { padding: 1.5rem; } + </style> </head> -<body class="sxplr-p-4"> +<body class="padded"> </body> <script> diff --git a/docs/releases/v2.8.0.md b/docs/releases/v2.8.0.md index c8f58a7317679a8c7e8999b4de246e95417e1b7a..73da5cc5b096a90ea888949ce2787cce5c5491ca 100644 --- a/docs/releases/v2.8.0.md +++ b/docs/releases/v2.8.0.md @@ -2,11 +2,19 @@ ## Features -- Added detail filter for connectivity +- More detailed filter for connectivity (#1158) +- Supports arbitrary layer based on URL parameter (#1252) +- Mini region search now shows prompt ... and {n} more (#1249) +- Minor update of mini region search UI +- Reworked altas, template, parcellation selector (#1230) - Added minimap picture-in-picture in single panel mode -- Supports arbitrary layer based on URL parameter ## Behind the scenes - upgraded to Angular v14 - update some dependencies + +## Bugfix + +- fixed open cheatsheet/quickstart in new window functionality +- fixed region hierarchy, on hit enter resetting the application (#1240) diff --git a/src/assets/images/atlas-selection/allen-mouse-2015.png b/src/assets/images/atlas-selection/allen-mouse-2015.png deleted file mode 100644 index 36b493023e63d3b33c2527191165174713ff7701..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/allen-mouse-2015.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/allen-mouse-2017.png b/src/assets/images/atlas-selection/allen-mouse-2017.png deleted file mode 100644 index 4a8b4cad740bf6e0a5b7ee3f27661fe4941f13e3..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/allen-mouse-2017.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/allen-mouse.png b/src/assets/images/atlas-selection/allen-mouse.png deleted file mode 100644 index 1d38cfce2b17f7f973d1fab5469dfba4f1b63537..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/allen-mouse.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/bigbrain.png b/src/assets/images/atlas-selection/bigbrain.png deleted file mode 100644 index 416dbc79b489db22ed82868237e5a68c40ecf23a..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/bigbrain.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/colin27.png b/src/assets/images/atlas-selection/colin27.png deleted file mode 100644 index d49773ece71854f93d8dcaed9ca3a710e11decb0..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/colin27.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/cortical-layers.png b/src/assets/images/atlas-selection/cortical-layers.png deleted file mode 100644 index 38a9e902a8691dbb3e2d9732e42a12cda8cae2a2..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/cortical-layers.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/cytoarchitectonic-maps.png b/src/assets/images/atlas-selection/cytoarchitectonic-maps.png deleted file mode 100644 index c9645ea83f8299e1ce64e185df17d75de1032da5..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/cytoarchitectonic-maps.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/difumo-1024.png b/src/assets/images/atlas-selection/difumo-1024.png deleted file mode 100644 index 7ab4bf4ac91ac9be0eb6c96af1c45ab17cc9bddd..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/difumo-1024.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/difumo-128.png b/src/assets/images/atlas-selection/difumo-128.png deleted file mode 100644 index 47de2e12571b69f192f9117f41b45e1200c7f7e2..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/difumo-128.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/difumo-256.png b/src/assets/images/atlas-selection/difumo-256.png deleted file mode 100644 index 08e508a6faa65bcbbc9799ff37025915992b4199..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/difumo-256.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/difumo-512.png b/src/assets/images/atlas-selection/difumo-512.png deleted file mode 100644 index b1c064664a6836b2d41c60a4407def4bfbc2e4dc..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/difumo-512.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/difumo-64.png b/src/assets/images/atlas-selection/difumo-64.png deleted file mode 100644 index bf14836310113c9cb2688cb5f298a2aa87c010e3..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/difumo-64.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/fibre-long.png b/src/assets/images/atlas-selection/fibre-long.png deleted file mode 100644 index 040d96a8dc7c575e1076072e800fedf08a413cb1..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/fibre-long.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/fibre-short.png b/src/assets/images/atlas-selection/fibre-short.png deleted file mode 100644 index 2c52194bc5fa263114e6d4be9a2e6422ff5fa79e..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/fibre-short.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/freesurfer.png b/src/assets/images/atlas-selection/freesurfer.png deleted file mode 100644 index c4b2819bc44292bb5fde7a86241846b33a5c50b2..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/freesurfer.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/grey-white-matter.png b/src/assets/images/atlas-selection/grey-white-matter.png deleted file mode 100644 index 352a7de6500d4d82a2952f911815726d7bae474b..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/grey-white-matter.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/icbm2009c.png b/src/assets/images/atlas-selection/icbm2009c.png deleted file mode 100644 index 1dfc2c47e84d60c7a5b4d69eba182cf003f1bf62..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/icbm2009c.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/primate-parc.png b/src/assets/images/atlas-selection/primate-parc.png deleted file mode 100644 index 0bc2641af423732f7abe60d5914bc4ab63e09ed7..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/primate-parc.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/primate.png b/src/assets/images/atlas-selection/primate.png deleted file mode 100644 index 2f5458b28a49fde61dbcc97c04df3d0da8f219f4..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/primate.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/short-bundle-hcp.png b/src/assets/images/atlas-selection/short-bundle-hcp.png deleted file mode 100644 index 0204752305a93284af053ff9210266e30f288001..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/short-bundle-hcp.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/waxholm-v1.png b/src/assets/images/atlas-selection/waxholm-v1.png deleted file mode 100644 index 0e24e005e975938cbe6f3f4c658b6a306d9cce8e..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/waxholm-v1.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/waxholm-v2.png b/src/assets/images/atlas-selection/waxholm-v2.png deleted file mode 100644 index e18fe6d5876c02b5ab34297c048219d6f622f0f8..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/waxholm-v2.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/waxholm-v3.png b/src/assets/images/atlas-selection/waxholm-v3.png deleted file mode 100644 index 1b072a165fded37525ab849afe69ea61c3503f3b..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/waxholm-v3.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/waxholm-v4.png b/src/assets/images/atlas-selection/waxholm-v4.png deleted file mode 100644 index d0af0a5be2b2cf40fd814dce032fbd0801da30a8..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/waxholm-v4.png and /dev/null differ diff --git a/src/assets/images/atlas-selection/waxholm.png b/src/assets/images/atlas-selection/waxholm.png deleted file mode 100644 index fea10fee5272e99bc6cfd969a42307c38fa8b6d7..0000000000000000000000000000000000000000 Binary files a/src/assets/images/atlas-selection/waxholm.png and /dev/null differ diff --git a/src/atlasComponents/sapi/stories.base.ts b/src/atlasComponents/sapi/stories.base.ts index 57e28f20da83acf96a247117f70fb921998c3bf0..9b9037ba626ccfe7b05bcdd6750d6788a7fd6486 100644 --- a/src/atlasComponents/sapi/stories.base.ts +++ b/src/atlasComponents/sapi/stories.base.ts @@ -76,6 +76,11 @@ export async function getAtlas(id: string): Promise<SapiAtlasModel>{ return await (await fetch(`${endPt}/atlases/${id}`)).json() } +export async function getParcellations(atlasId: string): Promise<SapiParcellationModel[]> { + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations`)).json() +} + export async function getParc(atlasId: string, id: string): Promise<SapiParcellationModel>{ const endPt = await SAPI.BsEndpoint$.toPromise() return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}`)).json() @@ -85,6 +90,11 @@ export async function getParcRegions(atlasId: string, id: string, spaceId: strin return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json() } +export async function getSpaces(atlasId: string): Promise<SapiSpaceModel[]> { + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId}/spaces`)).json() +} + export async function getSpace(atlasId: string, id: string): Promise<SapiSpaceModel> { const endPt = await SAPI.BsEndpoint$.toPromise() return await (await fetch(`${endPt}/atlases/${atlasId}/spaces/${id}`)).json() diff --git a/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.component.ts b/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.component.ts deleted file mode 100644 index 815a4d45ed29cad974c1465e8a8cca5a0af6bcee..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Component, OnDestroy } from "@angular/core"; -import { Store, select } from "@ngrx/store"; -import { Observable, Subscription } from "rxjs"; -import { ARIA_LABELS } from 'common/constants' -import { atlasSelection, generalActions } from "src/state" -import { SAPI, SapiAtlasModel } from "src/atlasComponents/sapi"; - -@Component({ - selector: 'sxplr-sapiviews-core-atlas-dropdown-selector', - templateUrl: './dropdownAtlasSelector.template.html', - styleUrls: [ - './dropdownAtlasSelector.style.css' - ] -}) - -export class SapiViewsCoreAtlasAtlasDropdownSelector implements OnDestroy{ - - private subs: Subscription[] = [] - private fetchedAtlases: SapiAtlasModel[] = [] - public fetchedAtlases$: Observable<SapiAtlasModel[]> = this.sapi.atlases$ - public selectedAtlas$: Observable<SapiAtlasModel> = this.store$.pipe( - select(atlasSelection.selectors.selectedAtlas) - ) - - public SELECT_ATLAS_ARIA_LABEL = ARIA_LABELS.SELECT_ATLAS - - constructor( - private store$: Store<any>, - private sapi: SAPI, - ){ - this.subs.push( - this.fetchedAtlases$.subscribe(val => this.fetchedAtlases = val) - ) - } - - ngOnDestroy(): void { - this.subs.pop().unsubscribe() - } - - handleChangeAtlas({ value }) { - const found = this.fetchedAtlases.find(atlas => atlas["@id"] === value) - if (found) { - this.store$.dispatch( - atlasSelection.actions.selectAtlas({ - atlas: found - }) - ) - } else { - this.store$.dispatch( - generalActions.generalActionError({ - message: `Atlas with id ${value} not found.` - }) - ) - } - } -} diff --git a/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.stories.ts b/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.stories.ts deleted file mode 100644 index 027fa024eb8bf497fa6600558b59098bab837d54..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.stories.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { CommonModule } from "@angular/common" -import { HttpClientModule } from "@angular/common/http" -import { Meta, moduleMetadata, Story } from "@storybook/angular" -import { SAPI } from "src/atlasComponents/sapi" -import { provideDarkTheme } from "src/atlasComponents/sapi/stories.base" -import { SapiViewsCoreAtlasAtlasDropdownSelector } from "./dropdownAtlasSelector.component" -import { SapiViewsCoreAtlasModule } from "../module" -import { atlasSelection } from "src/state" -import { StoreModule } from "@ngrx/store" - -export default { - component: SapiViewsCoreAtlasAtlasDropdownSelector, - decorators: [ - moduleMetadata({ - imports: [ - CommonModule, - HttpClientModule, - SapiViewsCoreAtlasModule, - - StoreModule.forRoot({ - [atlasSelection.nameSpace]: atlasSelection.reducer - }), - ], - providers: [ - SAPI, - ...provideDarkTheme, - ], - declarations: [] - }) - ], -} as Meta - -const Template: Story<SapiViewsCoreAtlasAtlasDropdownSelector> = (args: SapiViewsCoreAtlasAtlasDropdownSelector, { parameters }) => { - /** - * TODO can't seem to hook in handlechangeAtlas action - * always results in maximum call stack reached - * perhaps related: https://github.com/storybookjs/storybook/issues/13238 - */ - return ({ - props: { - }, - }) -} - - -export const Default = Template.bind({}) -Default.args = { - -} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.template.html b/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.template.html deleted file mode 100644 index f3922fea6c44412fc83137f6c3ed71db7efd26e7..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.template.html +++ /dev/null @@ -1,15 +0,0 @@ -<mat-form-field> - <mat-label> - {{ SELECT_ATLAS_ARIA_LABEL }} - </mat-label> - <mat-select - [aria-label]="SELECT_ATLAS_ARIA_LABEL" - [value]="(selectedAtlas$ | async)?.['@id']" - (selectionChange)="handleChangeAtlas($event)"> - <mat-option - *ngFor="let atlas of (fetchedAtlases$ | async)" - [value]="atlas['@id']"> - {{ atlas.name }} - </mat-option> - </mat-select> -</mat-form-field> diff --git a/src/atlasComponents/sapiViews/core/atlas/module.ts b/src/atlasComponents/sapiViews/core/atlas/module.ts index 691d41303c80c52229423586c9243d27fc419479..174ba5d3945d86de16c184062fadbdebc038782a 100644 --- a/src/atlasComponents/sapiViews/core/atlas/module.ts +++ b/src/atlasComponents/sapiViews/core/atlas/module.ts @@ -1,35 +1,25 @@ import { CommonModule } from "@angular/common"; 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"; import { SapiViewsCoreAtlasSplashScreen } from "./splashScreen/splashScreen.component"; -import { SapiViewsCoreAtlasAtlasTmplParcSelector } from "./tmplParcSelector/tmplParcSelector.component"; +import { MatCardModule } from "@angular/material/card"; +import { SAPIModule } from "src/atlasComponents/sapi/module"; +import { MatRippleModule } from "@angular/material/core"; @NgModule({ imports: [ CommonModule, - AngularMaterialModule, - SapiViewsCoreSpaceModule, - SapiViewsCoreParcellationModule, - QuickTourModule, + MatCardModule, + MatRippleModule, SpinnerModule, - SapiViewsUtilModule, + SAPIModule, ], declarations: [ - SapiViewsCoreAtlasAtlasDropdownSelector, - SapiViewsCoreAtlasAtlasTmplParcSelector, SapiViewsCoreAtlasSplashScreen, ], exports: [ - SapiViewsCoreAtlasAtlasDropdownSelector, - SapiViewsCoreAtlasAtlasTmplParcSelector, SapiViewsCoreAtlasSplashScreen, ] }) -export class SapiViewsCoreAtlasModule{} \ No newline at end of file +export class SapiViewsCoreAtlasModule{} diff --git a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts deleted file mode 100644 index 58a2151732937cd390d516ace5a2bfab2de87f7b..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts +++ /dev/null @@ -1,217 +0,0 @@ -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 { combineLatest, forkJoin, merge, Observable, Subject, Subscription } from "rxjs"; -import { distinctUntilChanged, map, mapTo, shareReplay, switchMap, take } from "rxjs/operators"; -import { SAPI } from "src/atlasComponents/sapi"; -import { atlasSelection } from "src/state"; -import { fromRootStore } from "src/state/atlasSelection"; -import { IQuickTourData } from "src/ui/quickTour"; -import { ARIA_LABELS, CONST, QUICKTOUR_DESC } from 'common/constants' -import { MatMenuTrigger } from "@angular/material/menu"; -import { SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type"; -import { InterSpaceCoordXformSvc } from "src/atlasComponents/sapi/core/space/interSpaceCoordXform.service" - -@Component({ - selector: `sxplr-sapiviews-core-atlas-tmplparcselector`, - templateUrl: './tmplParcSelector.template.html', - styleUrls: [ - `./tmplParcSelector.style.css` - ], - exportAs: 'sxplrSapiViewsCoreAtlasTmplparcselector', - animations: [ - trigger('toggleAtlasLayerSelector', [ - state('false', style({ - transform: 'scale(0)', - opacity: 0, - transformOrigin: '0% 100%' - })), - state('true', style({ - transform: 'scale(1)', - opacity: 1, - transformOrigin: '0% 100%' - })), - transition('false => true', [ - animate('200ms cubic-bezier(0.35, 0, 0.25, 1)') - ]), - transition('true => false', [ - animate('200ms cubic-bezier(0.35, 0, 0.25, 1)') - ]) - ]) - ], - changeDetection: ChangeDetectionStrategy.OnPush -}) - -export class SapiViewsCoreAtlasAtlasTmplParcSelector { - - public ARIA_LABELS = ARIA_LABELS - public CONST = CONST - - @ViewChildren(MatMenuTrigger) - matMenuTriggers: QueryList<MatMenuTrigger> - - @ViewChild('selectorPanelTmpl', { read: ElementRef }) - selectorPanelTemplateRef: ElementRef - - private atp$ = this.store$.pipe( - fromRootStore.distinctATP() - ) - - public availableParcellations$ = this.store$.pipe( - fromRootStore.allAvailParcs(this.sapi), - shareReplay(1), - ) - - public availableTemplates$ = this.store$.pipe( - fromRootStore.allAvailSpaces(this.sapi), - ) - - public selectedTemplate$ = this.atp$.pipe( - map(({ template }) => template) - ) - - public selectedParcellation$ = this.atp$.pipe( - map(({ parcellation }) => parcellation) - ) - - public parcsAvailableInCurrentTmpl$: Observable<SapiParcellationModel[]> = combineLatest([ - this.atp$, - this.availableParcellations$, - ]).pipe( - switchMap(([{ atlas, template }, parcs]) => - forkJoin( - parcs.map( - parc => this.sapi.getParcellation(atlas["@id"], parc["@id"]).getVolumes().pipe( - map( - volumes => { - return { - parcellation: parc, - volumes - } - } - ) - ) - ) - ).pipe( - map(arr => - arr.filter( - item => item.volumes.find(vol => vol.data.space["@id"] === template["@id"]) - ).map( - ({ parcellation }) => parcellation - ) - ) - ) - ) - ) - - private showOverlayIntentByTemplate$ = new Subject() - private showOverlayIntentByParcellation$ = new Subject() - public showLoadingOverlay$ = merge( - this.showOverlayIntentByTemplate$.pipe( - mapTo(true) - ), - this.selectedTemplate$.pipe( - mapTo(false) - ), - this.showOverlayIntentByParcellation$.pipe( - mapTo(true) - ), - this.selectedParcellation$.pipe( - mapTo(false) - ) - ).pipe( - distinctUntilChanged(), - ) - - private subscriptions: Subscription[] = [] - - @HostBinding('attr.data-opened') - public selectorExpanded: boolean = false - - public quickTourData: IQuickTourData = { - order: 4, - description: QUICKTOUR_DESC.LAYER_SELECTOR, - } - - - constructor( - private store$: Store, - private sapi: SAPI, - private interSpaceCoordXformSvc: InterSpaceCoordXformSvc, - ) { - - } - ngOnDestroy() { - while (this.subscriptions.length) this.subscriptions.pop().unsubscribe() - } - - - toggleSelector() { - this.selectorExpanded = !this.selectorExpanded - /** - * on selector open, call transform end point - * this caches the result, and will not bottleneck when the user selects a new space - */ - if (this.selectorExpanded) { - forkJoin({ - availableTemplates: this.availableTemplates$.pipe( - take(1) - ), - selectedTemplate: this.selectedTemplate$.pipe( - take(1) - ), - navigation: this.store$.pipe( - select(atlasSelection.selectors.navigation), - take(1) - ) - }).pipe( - - ).subscribe(({ availableTemplates, selectedTemplate, navigation }) => { - for (const avail of availableTemplates) { - this.interSpaceCoordXformSvc.transform( - InterSpaceCoordXformSvc.TmplIdToValidSpaceName(selectedTemplate["@id"]), - InterSpaceCoordXformSvc.TmplIdToValidSpaceName(avail["@id"]), - navigation.position as [number, number, number] - ).subscribe() - } - }) - } - } - - closeSelector() { - this.selectorExpanded = false - } - - openSelector() { - this.selectorExpanded = true - } - - selectTemplate(tmpl: SapiSpaceModel) { - this.showOverlayIntentByTemplate$.next(true) - - this.store$.dispatch( - atlasSelection.actions.selectTemplate({ - template: tmpl - }) - ) - } - - selectParcellation(parc: SapiParcellationModel) { - this.showOverlayIntentByParcellation$.next(true) - - this.store$.dispatch( - atlasSelection.actions.selectParcellation({ - parcellation: parc - }) - ) - } - - collapseExpandedGroup() { - this.matMenuTriggers.forEach(t => t.menuOpen && t.closeMenu()) - } - - - trackTmpl(t:SapiSpaceModel) { - return t['@id'] - } -} \ 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 deleted file mode 100644 index 3f18f737ec57e70b77c688c95f7fdbdc84c9e95b..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.stories.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { CommonModule } from "@angular/common" -import { HttpClientModule } from "@angular/common/http" -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 { InterSpaceCoordXformSvc } from "src/atlasComponents/sapi/core/space/interSpaceCoordXform.service" -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" -import { SapiViewsCoreAtlasAtlasTmplParcSelector } from "./tmplParcSelector.component" - -const actionsData = { - selectTemplate: action('selectTemplate'), - selectParcellation: action('selectParcellation') -} - -export default { - component: SapiViewsCoreAtlasAtlasTmplParcSelector, - decorators: [ - moduleMetadata({ - imports: [ - CommonModule, - HttpClientModule, - SapiViewsCoreAtlasModule, - AngularMaterialModule, - ], - providers: [ - SAPI, - InterSpaceCoordXformSvc, - ...provideDarkTheme, - ], - declarations: [ - ] - }) - ], -} as Meta - -const Template: Story<SapiViewsCoreAtlasAtlasTmplParcSelector> = (args: SapiViewsCoreAtlasAtlasTmplParcSelector, { loaded }) => { - const { - atlas, - template, - parcellation, - } = loaded - - return ({ - props: { - selectTemplate: actionsData.selectTemplate, - selectParcellation: actionsData.selectParcellation - }, - moduleMetadata: { - providers: [ - provideMockStore({ - initialState: { - [atlasSelection.nameSpace]: { - ...atlasSelection.defaultState, - selectedAtlas: atlas, - selectedTemplate: template, - selectedParcellation: parcellation, - } - } - }) - ] - } - }) -} -Template.loaders = [ - -] - -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 BigBrainJBA29 = Template.bind({}) -BigBrainJBA29.args = { - -} -BigBrainJBA29.loaders = [ - async () => { - const atlas = await getHumanAtlas() - const template = await getSpace(atlasId.human, spaceId.human.bigbrain) - const parcellation = await getJba29() - return { - atlas, - template, - parcellation, - } - } -] - -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, - parcellation, - } - } -] - -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.style.css b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.style.css deleted file mode 100644 index 49568298b447009881affb039d95cdd967cd675f..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.style.css +++ /dev/null @@ -1,41 +0,0 @@ -.selector-container -{ - overflow-y:scroll; - max-height: 80vh; - width: 21rem; - bottom: 4rem; -} - -.loading-overlay -{ - background-color: rgba(250, 250, 250, 0.8); -} - -.loading-overlay -{ - position: fixed; - width: 100%; - height: 100%; - top: 0; - left: 0; - font-size: 200%; - - display: grid; - grid-template-columns: auto; - grid-template-rows: 1fr auto 1fr; - grid-template-columns: 1fr auto 1fr; - grid-template-areas: "." "vertical-center" "."; -} - -.loading-overlay > .spinner -{ - grid-column: 2; - grid-row: 2; -} - -/* necessary to align the tiles to the start of grid tile */ -sxplr-sapiviews-core-space-tile, -sxplr-sapiviews-core-parcellation-tile -{ - height: 100%; -} \ 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 deleted file mode 100644 index db9ba08d8bf273f3726bda3fe2ae8d19b94f5b0a..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.template.html +++ /dev/null @@ -1,103 +0,0 @@ -<!-- selector panel when expanded --> - -<mat-card class="selector-container m-2 position-absolute" - [ngClass]="{'pe-all': selectorExpanded}" - [@toggleAtlasLayerSelector]="selectorExpanded" - (@toggleAtlasLayerSelector.done)="atlasSelectorTour?.attachTo(selectorExpanded ? selectorPanelTemplateRef : null)" - #selectorPanelTmpl> - - <mat-card-content> - - <!-- templates --> - <mat-card-subtitle> - {{ CONST.ATLAS_SELECTOR_LABEL_SPACES }} - </mat-card-subtitle> - - <!-- template grid and tiles --> - <mat-grid-list cols="3" - rowHeight="2:3" - gutterSize="16"> - - <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)"> - </sxplr-sapiviews-core-space-tile> - </mat-grid-tile> - </mat-grid-list> - - <mat-divider></mat-divider> - - <!-- parcellations --> - <mat-card-subtitle class="mt-2"> - {{ CONST.ATLAS_SELECTOR_LABEL_PARC_MAPS }} - </mat-card-subtitle> - - <mat-grid-list cols="3" - rowHeight="2:3" - gutterSize="16"> - - <!-- 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)"> - - </sxplr-sapiviews-core-parcellation-tile> - </mat-grid-tile> - </mat-grid-list> - - </mat-card-content> - - <div [ngClass]="{ - 'sxplr-d-none': !(showLoadingOverlay$ | async) - }" - class="loading-overlay"> - <spinner-cmp class="spinner"></spinner-cmp> - </div> - - -</mat-card> - -<!-- place holder when not expanded --> -<div class="position-relative m-2 cursor-pointer scale-up-bl pe-all sxplr-d-inline-block" - quick-tour - [quick-tour-description]="quickTourData.description" - [quick-tour-order]="quickTourData.order" - #atlasSelectorTour="quickTour"> - <!-- TODO check when do we disable atlas selector --> - <button color="primary" - *ngIf="true" - matTooltip="Select layer" - mat-mini-fab - [attr.aria-label]="ARIA_LABELS.TOGGLE_ATLAS_LAYER_SELECTOR" - (click)="toggleSelector()"> - <i class="fas fa-layer-group"></i> - </button> -</div> diff --git a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.stories.ts b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.stories.ts index acec7a744768e383bdb511d15b4a8d537b9e0633..641e4fb6f7008cfa7128efd019ea7e5871e2b09b 100644 --- a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.stories.ts +++ b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.stories.ts @@ -36,7 +36,7 @@ const Template: Story<DatasetView> = (args: DatasetView, { loaded }) => { const loadFeat = async () => { const features = await getHoc1RightFeatures() - const receptorfeat = features.find(f => f['@type'] === "siibra/core/dataset") + const receptorfeat = features.find(f => f['@type'] === "siibra/features/receptor") const feature = await getHoc1RightFeatureDetail(receptorfeat["@id"]) return { feature diff --git a/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.component.ts b/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.component.ts deleted file mode 100644 index 13358ff46b1a3eadc2b2e5ddb9fe417675ad94e3..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Component, Input, Output, EventEmitter } from "@angular/core"; -import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; - -@Component({ - selector: `sxplr-sapiviews-core-parcellation-chip`, - templateUrl: './parcellation.chip.template.html', - styleUrls: [ - `./parcellation.chip.style.css` - ], -}) - -export class SapiViewsCoreParcellationParcellationChip { - - @Input('sxplr-sapiviews-core-parcellation-chip-parcellation') - parcellation: SapiParcellationModel - - @Input('sxplr-sapiviews-core-parcellation-chip-color') - color: 'default' | 'primary' | 'accent' | 'warn' = "default" - - @Output('sxplr-sapiviews-core-parcellation-chip-onclick') - onClick = new EventEmitter<MouseEvent>() - - click(event: MouseEvent) { - this.onClick.emit(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 deleted file mode 100644 index 5e3afe419fc640d609b2f6acbfee703a7e5ae69c..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.stories.ts +++ /dev/null @@ -1,120 +0,0 @@ -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" -import { AngularMaterialModule } from "src/sharedModules" -import { SapiViewsCoreParcellationModule } from "../module" -import { SapiViewsCoreParcellationParcellationChip } from "./parcellation.chip.component" - - -export default { - component: SapiViewsCoreParcellationParcellationChip, - decorators: [ - moduleMetadata({ - imports: [ - CommonModule, - HttpClientModule, - SapiViewsCoreParcellationModule, - AngularMaterialModule, - ], - providers: [ - provideMockStore(), - SAPI, - ...provideDarkTheme, - ], - declarations: [] - }) - ], -} as Meta - -const Template: Story<SapiViewsCoreParcellationParcellationChip> = (args: SapiViewsCoreParcellationParcellationChip, { loaded, parameters }) => { - const { - parcellation - } = loaded - const { - contentProjection - } = parameters - - return ({ - props: { - ...args, - parcellation - }, - template: ` - <sxplr-sapiviews-core-parcellation-chip> - ${contentProjection || ''} - </sxplr-sapiviews-core-parcellation-chip> - ` - }) -} -Template.loaders = [] - -const asyncLoader = async (_atlasId: string) => { - const parcs: SapiParcellationModel[] = [] - const atlasDetail = await getAtlas(_atlasId) - - for (const parc of atlasDetail.parcellations) { - const parcDetail = await getParc(atlasDetail['@id'], parc['@id']) - parcs.push(parcDetail) - } - return { - parcs - } -} - -const getContentProjection = ({ prefix = null, suffix = null }) => { - let returnVal = `` - if (prefix) { - returnVal += `<div prefix>${prefix}</div>` - } - if (suffix) { - returnVal += `<div suffix>${suffix}</div>` - } - return returnVal -} - -export const Default = Template.bind({}) -Default.loaders = [ - async () => { - const { - parcs - } = await asyncLoader(atlasId.human) - return { - parcellation: parcs[0] - } - } -] - -export const Prefix = Template.bind({}) -Prefix.loaders = [ - ...Default.loaders -] -Prefix.parameters = { - contentProjection: getContentProjection({ - prefix: `PREFIX`, - }) -} - -export const Suffix = Template.bind({}) -Suffix.loaders = [ - ...Default.loaders -] -Suffix.parameters = { - contentProjection: getContentProjection({ - suffix: `SUFFIX`, - }) -} - - -export const PrefixSuffix = Template.bind({}) -PrefixSuffix.loaders = [ - ...Default.loaders -] -PrefixSuffix.parameters = { - contentProjection: getContentProjection({ - prefix: `PREFIX`, - suffix: `SUFFIX`, - }) -} diff --git a/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.style.css b/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.style.css deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.template.html b/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.template.html deleted file mode 100644 index 417575bf67914c3886697a435134eb17852b1676..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/chip/parcellation.chip.template.html +++ /dev/null @@ -1,16 +0,0 @@ -<mat-chip-list *ngIf="parcellation"> - <mat-chip [selected]="color !== 'default'" - (click)="click($event)" - [color]="color"> - - <ng-content select="[prefix]"> - </ng-content> - - <span class="mat-body sxplr-white-space-nowrap"> - {{ parcellation.name }} - </span> - - <ng-content select="[suffix]"> - </ng-content> - </mat-chip> -</mat-chip-list> \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/parcellation/filterUnsupportedParc.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/filterUnsupportedParc.pipe.ts index 3a9d1a30ec5a9bc9ed281cca3e44051cd7b5b163..222020d7296acd3555b08c2b69c05fd49ceba80c 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/filterUnsupportedParc.pipe.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/filterUnsupportedParc.pipe.ts @@ -10,7 +10,6 @@ const unsupportedIds = [ ] const hideGroup = [ - "cytoarchitecture" ] @Pipe({ diff --git a/src/atlasComponents/sapiViews/core/parcellation/index.ts b/src/atlasComponents/sapiViews/core/parcellation/index.ts index 801b1f3e381a6a806c94cd6458e3187a620755c1..78616fb9ba32c12462740f59fcc315a93e0da45e 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/index.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/index.ts @@ -1,11 +1,6 @@ -export { - SapiViewsCoreParcellationModule -} from "./module" - -export { - FilterGroupedParcellationPipe -} from "./filterGroupedParcellations.pipe" - -export { - FilterUnsupportedParcPipe -} from "./filterUnsupportedParc.pipe" \ No newline at end of file +export { SapiViewsCoreParcellationModule } from "./module" +export { FilterGroupedParcellationPipe } from "./filterGroupedParcellations.pipe" +export { FilterUnsupportedParcPipe } from "./filterUnsupportedParc.pipe" +export { GroupedParcellation } from "./groupedParcellation" +export { ParcellationDoiPipe } from "./parcellationDoi.pipe" +export { ParcellationGroupSelectedPipe } from "./parcellationGroupSelected.pipe" diff --git a/src/atlasComponents/sapiViews/core/parcellation/module.ts b/src/atlasComponents/sapiViews/core/parcellation/module.ts index 1fe2e70c09d9a1bda3e3fd3a991d76eac75be09b..d3074f6e8e56b7ddabbf65bd74336f7d098ae42a 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/module.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/module.ts @@ -8,15 +8,11 @@ import { StrictLocalModule } from "src/strictLocal"; import { DialogModule } from "src/ui/dialogInfo/module"; import { UtilModule } from "src/util"; import { SapiViewsUtilModule } from "../../util"; -import { SapiViewsCoreParcellationParcellationChip } from "./chip/parcellation.chip.component"; import { FilterGroupedParcellationPipe } from "./filterGroupedParcellations.pipe"; import { FilterUnsupportedParcPipe } from "./filterUnsupportedParc.pipe"; import { ParcellationDoiPipe } from "./parcellationDoi.pipe"; -import { ParcellationIsBaseLayer } from "./parcellationIsBaseLayer.pipe"; import { ParcellationVisibilityService } from "./parcellationVis.service"; -import { PreviewParcellationUrlPipe } from "./previewParcellationUrl.pipe"; -import { SapiViewsCoreParcellationParcellationSmartChip } from "./smartChip/parcellation.smartChip.component"; -import { SapiViewsCoreParcellationParcellationTile } from "./tile/parcellation.tile.component"; +import { ParcellationGroupSelectedPipe } from "./parcellationGroupSelected.pipe"; @NgModule({ imports: [ @@ -29,21 +25,16 @@ import { SapiViewsCoreParcellationParcellationTile } from "./tile/parcellation.t StrictLocalModule ], declarations: [ - SapiViewsCoreParcellationParcellationTile, - SapiViewsCoreParcellationParcellationChip, - SapiViewsCoreParcellationParcellationSmartChip, - PreviewParcellationUrlPipe, FilterGroupedParcellationPipe, FilterUnsupportedParcPipe, - ParcellationIsBaseLayer, ParcellationDoiPipe, + ParcellationGroupSelectedPipe, ], exports: [ - SapiViewsCoreParcellationParcellationTile, - SapiViewsCoreParcellationParcellationChip, - SapiViewsCoreParcellationParcellationSmartChip, FilterGroupedParcellationPipe, FilterUnsupportedParcPipe, + ParcellationGroupSelectedPipe, + ParcellationDoiPipe, ], providers: [ ParcellationVisibilityService, diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationGroupSelected.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationGroupSelected.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd31e439157a8e34182c03427fb70c35031241b9 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationGroupSelected.pipe.ts @@ -0,0 +1,20 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; +import { GroupedParcellation } from "./groupedParcellation"; + +function isGroupedParc(parc: GroupedParcellation|unknown): parc is GroupedParcellation { + if (!parc['parcellations']) return false + return (parc['parcellations'] as SapiParcellationModel[]).every(p => p["@type"] === "minds/core/parcellationatlas/v1.0.0") +} + +@Pipe({ + name: 'parcellationGroupSelected', + pure: true +}) + +export class ParcellationGroupSelectedPipe implements PipeTransform { + public transform(parc: GroupedParcellation|unknown, selectedParcellation: SapiParcellationModel): boolean { + if (!isGroupedParc(parc)) return false + return parc.parcellations.some(p => p["@id"] === selectedParcellation["@id"]) + } +} diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts deleted file mode 100644 index 2c3c09a9d9b7307a7a1f8b18c193565137e9bd2f..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; - -const baseLayerIds = [ - /** - * julich brain - */ - "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290", - "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", - "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579", - - /** - * allen mouse - */ - "minds/core/parcellationatlas/v1.0.0/05655b58-3b6f-49db-b285-64b5a0276f83", - "minds/core/parcellationatlas/v1.0.0/39a1384b-8413-4d27-af8d-22432225401f", - - /** - * waxholm - */ - "minds/core/parcellationatlas/v1.0.0/11017b35-7056-4593-baad-3934d211daba", - "minds/core/parcellationatlas/v1.0.0/2449a7f0-6dd0-4b5a-8f1e-aec0db03679d", - "minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe", - "minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe-v4", - - /** - * monkey - */ - "minds/core/parcellationatlas/v1.0.0/mebrains-tmp-id", -] - -@Pipe({ - name: 'parcellationIsBaseLayer', - pure: true -}) - -export class ParcellationIsBaseLayer implements PipeTransform{ - public transform(parc: SapiParcellationModel): boolean { - /** - * currently, the only base layer is cyto maps - */ - return baseLayerIds.includes(parc["@id"]) - } -} diff --git a/src/atlasComponents/sapiViews/core/parcellation/previewParcellationUrl.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/previewParcellationUrl.pipe.ts deleted file mode 100644 index 7c1a543002a3f533271b778e209c9b39f241b0d8..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/previewParcellationUrl.pipe.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { SapiParcellationModel } from "src/atlasComponents/sapi"; -import { GroupedParcellation } from "./groupedParcellation"; - -const previewImgMap = new Map([ - - ['minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579', 'cytoarchitectonic-maps.png'], - ['juelich/iav/atlas/v1.0.0/3', 'cortical-layers.png'], - ['juelich/iav/atlas/v1.0.0/4', 'grey-white-matter.png'], - ['juelich/iav/atlas/v1.0.0/5', 'fibre-long.png'], - ['juelich/iav/atlas/v1.0.0/6', 'fibre-short.png'], - ['minds/core/parcellationatlas/v1.0.0/d80fbab2-ce7f-4901-a3a2-3c8ef8a3b721', 'difumo-64.png'], - ['minds/core/parcellationatlas/v1.0.0/73f41e04-b7ee-4301-a828-4b298ad05ab8', 'difumo-128.png'], - ['minds/core/parcellationatlas/v1.0.0/141d510f-0342-4f94-ace7-c97d5f160235', 'difumo-256.png'], - ['minds/core/parcellationatlas/v1.0.0/63b5794f-79a4-4464-8dc1-b32e170f3d16', 'difumo-512.png'], - ['minds/core/parcellationatlas/v1.0.0/12fca5c5-b02c-46ce-ab9f-f12babf4c7e1', 'difumo-1024.png'], - - - ['minds/core/parcellationatlas/v1.0.0/05655b58-3b6f-49db-b285-64b5a0276f83', 'allen-mouse-2017.png'], - ['minds/core/parcellationatlas/v1.0.0/39a1384b-8413-4d27-af8d-22432225401f', 'allen-mouse-2015.png'], - - - ['minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe', 'waxholm-v3.png'], - ['minds/core/parcellationatlas/v1.0.0/2449a7f0-6dd0-4b5a-8f1e-aec0db03679d', 'waxholm-v2.png'], - ['minds/core/parcellationatlas/v1.0.0/11017b35-7056-4593-baad-3934d211daba', 'waxholm-v1.png'], - ['juelich/iav/atlas/v1.0.0/79cbeaa4ee96d5d3dfe2876e9f74b3dc3d3ffb84304fb9b965b1776563a1069c', 'short-bundle-hcp.png'], - - ['minds/core/parcellationatlas/v1.0.0/mebrains-tmp-id', 'primate-parc.png'], -]) - -/** - * used for directories - */ -const previewNameToPngMap = new Map([ - ['fibre architecture', 'fibre-long.png'], - ['functional modes', 'difumo-128.png'] -]) - -@Pipe({ - name: 'previewParcellationUrl', - pure: true -}) - -export class PreviewParcellationUrlPipe implements PipeTransform{ - public transform(tile: SapiParcellationModel | GroupedParcellation): string { - if (tile instanceof GroupedParcellation) { - const filename = previewNameToPngMap.get(tile.name) - return filename && `assets/images/atlas-selection/${filename}` - } - const filename = previewImgMap.get(tile['@id']) - return filename && `assets/images/atlas-selection/${filename}` - } -} diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts deleted file mode 100644 index 27d4a53cc941a95164407b86f61825c01df1527f..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges } from "@angular/core"; -import { BehaviorSubject, concat, Observable, of, timer } from "rxjs"; -import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; -import { ParcellationVisibilityService } from "../parcellationVis.service"; -import { ARIA_LABELS } from "common/constants" -import { getTraverseFunctions } from "../parcellationVersion.pipe"; -import { mapTo, shareReplay, switchMap } from "rxjs/operators"; - -@Component({ - selector: `sxplr-sapiviews-core-parcellation-smartchip`, - templateUrl: `./parcellation.smartChip.template.html`, - styleUrls: [ - `./parcellation.smartChip.style.css` - ] -}) - -export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges{ - - public ARIA_LABELS = ARIA_LABELS - - @Input('sxplr-sapiviews-core-parcellation-smartchip-parcellation') - parcellation: SapiParcellationModel - - @Input('sxplr-sapiviews-core-parcellation-smartchip-all-parcellations') - parcellations: SapiParcellationModel[] - - @Output('sxplr-sapiviews-core-parcellation-smartchip-dismiss-nonbase-layer') - onDismiss = new EventEmitter<SapiParcellationModel>() - - @Output('sxplr-sapiviews-core-parcellation-smartchip-select-parcellation') - onSelectParcellation = new EventEmitter<SapiParcellationModel>() - - constructor( - private svc: ParcellationVisibilityService - ){ - - } - - otherVersions: SapiParcellationModel[] - - ngOnChanges(changes: SimpleChanges) { - const { parcellation } = changes - if (parcellation) { - this.onDismissClicked$.next(false) - } - this.otherVersions = [] - if (!this.parcellation) { - return - } - this.otherVersions = [ this.parcellation ] - if (!this.parcellations || this.parcellations.length === 0) { - return - } - if (!this.parcellation.version) { - return - } - - this.otherVersions = [] - - const { - findNewest, - findOlder - } = getTraverseFunctions(this.parcellations) - - let cursor: SapiParcellationModel = findNewest() - while (cursor) { - this.otherVersions.push(cursor) - cursor = findOlder(cursor) - } - } - - loadingParc$: Observable<SapiParcellationModel> = this.onSelectParcellation.pipe( - switchMap(parc => concat( - of(parc), - timer(5000).pipe( - mapTo(null) - ), - )), - shareReplay(1), - ) - - parcellationVisibility$: Observable<boolean> = this.svc.visibility$ - - toggleParcellationVisibility(){ - this.svc.toggleVisibility() - } - - dismiss(){ - if (this.onDismissClicked$.value) return - this.onDismissClicked$.next(true) - this.onDismiss.emit(this.parcellation) - } - - selectParcellation(parc: SapiParcellationModel){ - if (this.trackByFn(parc) === this.trackByFn(this.parcellation)) return - this.onSelectParcellation.emit(parc) - } - - trackByFn(parc: SapiParcellationModel){ - return parc["@id"] - } - - onDismissClicked$ = new BehaviorSubject<boolean>(false) -} diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.stories.ts b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.stories.ts deleted file mode 100644 index 054c0c70eacd03943a5db1465bd412f4a8353c04..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.stories.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { CommonModule } from "@angular/common" -import { HttpClientModule } from "@angular/common/http" -import { Component, Input } from "@angular/core" -import { Meta, moduleMetadata, Story } from "@storybook/angular" -import { action } from "@storybook/addon-actions" -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" -import { within, userEvent } from '@storybook/testing-library'; -import { ARIA_LABELS } from "common/constants" -import { ParcellationVisibilityService } from "../parcellationVis.service" -import { of } from "rxjs" - -@Component({ - selector: `parc-smart-chip-wrapper`, - template: ` - <mat-accordion> - <mat-expansion-panel *ngFor="let item of parcRecords | keyvalue"> - <mat-expansion-panel-header> - {{ item.key }} - </mat-expansion-panel-header> - - <div class="sxplr-of-x-scroll sxplr-white-space-nowrap"> - <sxplr-sapiviews-core-parcellation-smartchip *ngFor="let parc of item.value | filterUnsupportedParc" - [sxplr-sapiviews-core-parcellation-smartchip-parcellation]="parc" - [sxplr-sapiviews-core-parcellation-smartchip-all-parcellations]="item.value" - (sxplr-sapiviews-core-parcellation-smartchip-select-parcellation)="selectParcellation($event)"> - </sxplr-sapiviews-core-parcellation-smartchip> - </div> - - </mat-expansion-panel> - </mat-accordion> - `, - styles: [ - `sxplr-sapiviews-core-parcellation-chip { display: block; }` - ] -}) - -class ParcSmartChipWrapper{ - @Input() - parcRecords: Record<string, SapiParcellationModel[]> = {} - - selectParcellation(parc: SapiParcellationModel){ - - } -} - - -export default { - component: ParcSmartChipWrapper, - decorators: [ - moduleMetadata({ - imports: [ - CommonModule, - HttpClientModule, - SapiViewsCoreParcellationModule, - AngularMaterialModule, - ], - providers: [ - provideMockStore(), - SAPI, - ...provideDarkTheme, - ], - declarations: [] - }) - ], -} as Meta - -const Template: Story<ParcSmartChipWrapper> = (args: ParcSmartChipWrapper, { loaded, parameters }) => { - const { - parcRecords - } = loaded - const { providers = [] } = parameters - - return ({ - props: { - ...args, - selectParcellation: action("selectParcellation"), - parcRecords - }, - moduleMetadata: { - providers - } - }) -} -Template.loaders = [] - -const asyncLoader = async () => { - const parcRecords: Record<string, SapiParcellationModel[]> = {} - - for (const species in atlasId) { - - const atlasDetail = await getAtlas(atlasId[species]) - parcRecords[species] = [] - for (const parc of atlasDetail.parcellations) { - const parcDetail = await getParc(atlasDetail['@id'], parc['@id']) - parcRecords[species].push(parcDetail) - } - } - - return { - parcRecords - } -} - -export const Default = Template.bind({}) -Default.loaders = [ - async () => { - const { - parcRecords - } = await asyncLoader() - return { - parcRecords - } - } -] - -export const TestInteraction = Template.bind({}) -TestInteraction.loaders = [ - ...Default.loaders -] -TestInteraction.parameters = { - providers: [ - { - provide: ParcellationVisibilityService, - useValue: { - setVisibility: action('setVisibility'), - toggleVisibility: action('toggleVisibility'), - visibility$: of(true) - } - } - ] -} -TestInteraction.play = async ({ args, canvasElement }) => { - const canvas = within(canvasElement) - - await userEvent.click(canvas.getByText("human")) - const allEye = canvas.getAllByText(ARIA_LABELS.TOGGLE_DELINEATION) - await userEvent.hover(allEye[0]) - await userEvent.click(allEye[0]) -} diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css deleted file mode 100644 index f83fb1f49984f554b1a3fbcea0852d7f8005949b..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css +++ /dev/null @@ -1,34 +0,0 @@ -.otherversion-wrapper -{ - position: relative; - overflow: hidden; - margin: 0.5rem; -} - -.otherversion-wrapper.loading > .spinner-container -{ - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - - display: flex; - align-items: center; -} - -.otherversion-wrapper.loading > .spinner-container > spinner-cmp -{ - margin: 0.5rem; -} - -.icons-container -{ - transform: scale(0.7); - margin-right: -1.5rem; -} - -.icons-container > * -{ - margin: auto 0.2rem; -} diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html deleted file mode 100644 index 4dac117899e6a081d5d4309053c67d51ee9e714f..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html +++ /dev/null @@ -1,136 +0,0 @@ -<mat-menu #otherParcMenu="matMenu" - [hasBackdrop]="false" - class="parc-smart-chip-menu-panel sxplr-bg-none sxplr-of-x-hidden sxplr-box-shadow-none sxplr-mxw-80vw"> - <div (iav-outsideClick)="menuTrigger.closeMenu()"> - - <div *ngFor="let parc of otherVersions" - class="otherversion-wrapper" - [ngClass]="{ - 'loading': (loadingParc$ | async) === parc - }"> - - - <sxplr-sapiviews-core-parcellation-chip - [ngClass]="{ - 'sxplr-blink': (loadingParc$ | async) === parc - }" - [sxplr-sapiviews-core-parcellation-chip-parcellation]="parc" - [sxplr-sapiviews-core-parcellation-chip-color]="(parcellation | equality : parc : trackByFn) ? 'primary' : 'default'" - (sxplr-sapiviews-core-parcellation-chip-onclick)="selectParcellation(parc)"> - - <div class="sxplr-scale-70" - suffix - iav-stop="mousedown click"> - - <ng-template #otherParcDesc> - <ng-template [ngTemplateOutlet]="parcDescTmpl" - [ngTemplateOutletContext]="{ parcellation: parc }"> - </ng-template> - </ng-template> - - <button mat-mini-fab color="default" - [sxplr-dialog]="otherParcDesc" - [sxplr-dialog-size]="null"> - <i class="fas fa-info"></i> - </button> - </div> - </sxplr-sapiviews-core-parcellation-chip> - - <div class="spinner-container" *ngIf="(loadingParc$ | async) === parc"> - <spinner-cmp> - </spinner-cmp> - </div> - </div> - </div> - -</mat-menu> - -<sxplr-sapiviews-core-parcellation-chip - [ngClass]="{ - 'sxplr-muted': !(parcellationVisibility$ | async), - 'sxplr-blink': onDismissClicked$ | async - }" - class="sxplr-d-inline-block" - [sxplr-sapiviews-core-parcellation-chip-parcellation]="parcellation" - [sxplr-sapiviews-core-parcellation-chip-color]="(parcellation | parcellationIsBaseLayer) ? 'default' : 'primary'" - (sxplr-sapiviews-core-parcellation-chip-onclick)="menuTrigger.toggleMenu()" - [matMenuTriggerFor]="otherParcMenu" - #menuTrigger="matMenuTrigger" - > - - <div class="icons-container" - suffix - iav-stop="mousedown click"> - - <ng-template #mainParcDesc> - <ng-template [ngTemplateOutlet]="parcDescTmpl" - [ngTemplateOutletContext]="{ parcellation: parcellation }"> - </ng-template> - </ng-template> - - <button mat-icon-button - color="default" - [sxplr-dialog]="mainParcDesc" - [sxplr-dialog-size]="null"> - <i class="fas fa-info"></i> - </button> - - <button mat-icon-button - color="default" - [matTooltip]="ARIA_LABELS.TOGGLE_DELINEATION" - iav-stop="mousedown click" - [iav-key-listener]="[{'type': 'keydown', 'key': 'q', 'capture': true, 'target': 'document' }]" - (iav-key-event)="toggleParcellationVisibility()" - (click)="toggleParcellationVisibility()"> - <i class="fas" - [ngClass]="(parcellationVisibility$ | async) ? 'fa-eye': 'fa-eye-slash'" - aria-hidden="true"> - </i> - <span class="sr-only"> - {{ ARIA_LABELS.TOGGLE_DELINEATION }} - </span> - </button> - - <button mat-mini-fab - *ngIf="!(parcellation | parcellationIsBaseLayer)" - color="default" - (click)="dismiss()"> - - <spinner-cmp class="sxplr-w-100 sxplr-h-100" *ngIf="onDismissClicked$ | async; else defaultDismissIcon"></spinner-cmp> - <ng-template #defaultDismissIcon> - <i class="fas fa-times"></i> - </ng-template> - - </button> - </div> -</sxplr-sapiviews-core-parcellation-chip> - -<!-- parcellation description template --> - -<ng-template #parcDescTmpl let-parc="parcellation"> - <h1 mat-dialog-title> - {{ parc.name }} - </h1> - <div mat-dialog-content> - <markdown-dom - *ngIf="parc.brainAtlasVersions.length > 0 && parc.brainAtlasVersions[0].versionInnovation" - [markdown]="parc.brainAtlasVersions[0].versionInnovation"> - </markdown-dom> - </div> - - <mat-dialog-actions align="start"> - <a *ngFor="let url of parc | parcellationDoiPipe" - [href]="url" - sxplr-hide-when-local - target="_blank" - mat-raised-button - color="primary"> - <div class="fas fa-external-link-alt"></div> - <span> - Dataset Detail - </span> - </a> - - <button mat-button mat-dialog-close>Close</button> - </mat-dialog-actions> -</ng-template> diff --git a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.component.ts b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.component.ts deleted file mode 100644 index 839e8791ff2df8090de142deedf0e39046cfec32..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.component.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef } from "@angular/core"; -import { SapiParcellationModel } from "src/atlasComponents/sapi"; -import { GroupedParcellation } from "../groupedParcellation"; - -const lightthemeId = [ - 'juelich/iav/atlas/v1.0.0/3', - 'juelich/iav/atlas/v1.0.0/4', -] - -@Component({ - selector: `sxplr-sapiviews-core-parcellation-tile`, - templateUrl: './parcellation.tile.template.html', - styleUrls: [ - `./parcellation.tile.style.css` - ], -}) - -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') - 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 - - @Output('sxplr-sapiviews-core-parcellation-tile-onclick-parc') - onClickOnParcellation = new EventEmitter<SapiParcellationModel>() - - public gutterSize = "2" - public rowHeight = "6:11" - - public darktheme = false - public pureParc: SapiParcellationModel - public dirParc: GroupedParcellation - - ngOnChanges(): void { - if (this.parcellation instanceof GroupedParcellation) { - this.dirParc = this.parcellation - } else { - this.pureParc = this.parcellation - } - this.darktheme = !!this.dirParc || lightthemeId.indexOf(this.parcellation['@id']) < 0 - } - - clickOnParcellation(parc: SapiParcellationModel){ - this.onClickOnParcellation.emit(parc) - } -} diff --git a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.stories.ts b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.stories.ts deleted file mode 100644 index 1fad5b1cd9332700829f662798829e919691b956..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.stories.ts +++ /dev/null @@ -1,160 +0,0 @@ -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" -import { AngularMaterialModule } from "src/sharedModules" -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"> - - <mat-expansion-panel-header> - {{ item.key }} - </mat-expansion-panel-header> - - <ng-template matExpansionPanelContent> - <div class="sxplr-d-inline-flex align-items-start"> - <sxplr-sapiviews-core-parcellation-tile - *ngFor="let parc of item.value" - [sxplr-sapiviews-core-parcellation-tile-parcellation]="parc" - [sxplr-sapiviews-core-parcellation-tile-selected]="parc['@id'] === selected" - class="sxplr-m-2" - (sxplr-sapiviews-core-parcellation-tile-onclick-parc)="parcClicked.emit($event)"> - </sxplr-sapiviews-core-parcellation-tile> - </div> - </ng-template> - - </mat-expansion-panel> - - <mat-expansion-panel> - <mat-expansion-panel-header> - > grouped - </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-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-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: [ - `sxplr-sapiviews-core-parcellation-tile { display: inline-block; max-width: 8rem; }` - ] -}) - -class ParcTileWrapper{ - @Input() - parcs: Record<string, SapiParcellationModel[]> = {} - - @Input() - selected: string = parcId.human.longBundle - - @Output() - parcClicked = new EventEmitter() -} - -export default { - component: ParcTileWrapper, - decorators: [ - moduleMetadata({ - imports: [ - CommonModule, - HttpClientModule, - SapiViewsCoreParcellationModule, - AngularMaterialModule, - ], - providers: [ - provideMockStore(), - SAPI, - ...provideDarkTheme, - ], - declarations: [ - ParcTileWrapper - ] - }) - ], -} as Meta - -const Template: Story<ParcTileWrapper> = (args: ParcTileWrapper, { loaded }) => { - const { - parcs - } = loaded - - return ({ - props: { - ...args, - parcs - } - }) -} -Template.loaders = [ - -] - -const asyncLoader = async () => { - const parcs: Record<string, SapiParcellationModel[]> = {} - for (const species in atlasId) { - const atlasDetail = await getAtlas(atlasId[species]) - parcs[species] = [] - - for (const parc of atlasDetail.parcellations) { - const parcDetail = await getParc(atlasDetail['@id'], parc['@id']) - parcs[species].push(parcDetail) - } - } - - return { - parcs - } -} - -export const Default = Template.bind({}) -Default.args = { - selected: parcId.human.longBundle -} -Default.loaders = [ - - async () => { - const { - parcs - } = await asyncLoader() - return { - parcs - } - } -] diff --git a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.style.css b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.style.css deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html deleted file mode 100644 index 19cd424794e7bc343d775fa541979bf1019cf0ac..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html +++ /dev/null @@ -1,69 +0,0 @@ -<mat-menu hasBackDrop="false" #matMenu="matMenu"> - - <ng-template matMenuContent let-subParcellations="subParcellations"> - - <div class="sxplr-custom-cmp sxplr-ml-2 sxplr-mr-2"> - <mat-grid-list - cols="1" - [rowHeight]="rowHeight" - [gutterSize]="gutterSize"> - - <mat-grid-tile - *ngFor="let parc of subParcellations"> - - - <!-- 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> - </div> - </ng-template> -</mat-menu> - -<ng-template [ngIf]="parcellation"> - - <tile-cmp *ngIf="pureParc" - [tile-text]="pureParc.name" - [tile-image-src]="pureParc | previewParcellationUrl" - [tile-selected]="selected" - [tile-image-darktheme]="darktheme" - (click)="clickOnParcellation(pureParc)" - > - - </tile-cmp> - - - <tile-cmp *ngIf="dirParc" - [tile-text]="dirParc.name" - [tile-image-src]="dirParc | previewParcellationUrl" - [tile-selected]="selected" - [tile-image-darktheme]="darktheme" - tile-is-dir="true" - [matMenuTriggerFor]="matMenu" - [matMenuTriggerData]="{ - subParcellations: dirParc.parcellations || [] - }" - > - </tile-cmp> - -</ng-template> \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/parcellation/tile/singleTile.stories.ts b/src/atlasComponents/sapiViews/core/parcellation/tile/singleTile.stories.ts deleted file mode 100644 index 920c1cfc82a26550a2710a584f6db9f45b0ea5af..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/parcellation/tile/singleTile.stories.ts +++ /dev/null @@ -1,90 +0,0 @@ -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 { parcId, provideDarkTheme, getParc, getHumanAtlas } from "src/atlasComponents/sapi/stories.base" -import { AngularMaterialModule } from "src/sharedModules" -import { FilterGroupedParcellationPipe } from "../filterGroupedParcellations.pipe" -import { GroupedParcellation } from "../groupedParcellation" -import { SapiViewsCoreParcellationModule } from "../module" -import { SapiViewsCoreParcellationParcellationTile } from "./parcellation.tile.component" - -export default { - component: SapiViewsCoreParcellationParcellationTile, - decorators: [ - moduleMetadata({ - imports: [ - CommonModule, - HttpClientModule, - SapiViewsCoreParcellationModule, - AngularMaterialModule, - ], - providers: [ - provideMockStore(), - SAPI, - ...provideDarkTheme, - ], - declarations: [ - - ], - }), - ], -} as Meta - -const Template: Story<SapiViewsCoreParcellationParcellationTile> = (args: SapiViewsCoreParcellationParcellationTile, { loaded }) => { - const { - groups - } = loaded - - const { - gutterSize, - rowHeight - } = args - return ({ - props: { - gutterSize, - rowHeight, - parcellation: groups[1] - }, - styles: [ - `sxplr-sapiviews-core-parcellation-tile { display: inline-block; max-width: 8rem; }` - ] - }) -} -Template.loaders = [ - -] - -const asyncLoader = async () => { - const parcs: SapiParcellationModel[] = [] - - const atlasDetail = await getHumanAtlas() - - for (const parc of atlasDetail.parcellations) { - const parcDetail = await getParc(atlasDetail['@id'], parc['@id']) - parcs.push(parcDetail) - } - - const pipe = new FilterGroupedParcellationPipe() - const groups = pipe.transform(parcs, true) as GroupedParcellation[] - return { - groups - } -} - -export const Default = Template.bind({}) -Default.args = { - selected: parcId.human.longBundle -} -Default.loaders = [ - - async () => { - const { - groups - } = await asyncLoader() - return { - groups - } - } -] diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/index.ts b/src/atlasComponents/sapiViews/core/rich/ATPSelector/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a8a2025ad72387c5dcb03972b800f9801f9500c --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/index.ts @@ -0,0 +1,3 @@ +export { + ATPSelectorModule +} from "./module" \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/module.ts b/src/atlasComponents/sapiViews/core/rich/ATPSelector/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..188a5487140e5da124b84bebab05c831796b54c1 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/module.ts @@ -0,0 +1,38 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { MatButtonModule } from "@angular/material/button"; +import { MatRippleModule } from "@angular/material/core"; +import { MatIconModule } from "@angular/material/icon"; +import { MarkdownModule } from "src/components/markdown"; +import { SmartChipModule } from "src/components/smartChip"; +import { DialogModule } from "src/ui/dialogInfo"; +import { UtilModule } from "src/util"; +import { SapiViewsCoreParcellationModule } from "src/atlasComponents/sapiViews/core/parcellation"; +import { PureATPSelector } from "./pureDumb/pureATPSelector.components"; +import { WrapperATPSelector } from "./wrapper/wrapper.component"; +import { SAPIModule } from "src/atlasComponents/sapi/module"; + +@NgModule({ + imports: [ + CommonModule, + SmartChipModule, + UtilModule, + MarkdownModule, + MatRippleModule, + MatIconModule, + MatButtonModule, + DialogModule, + SAPIModule, + SapiViewsCoreParcellationModule, + ], + declarations: [ + PureATPSelector, + WrapperATPSelector, + ], + exports: [ + PureATPSelector, + WrapperATPSelector, + ] +}) + +export class ATPSelectorModule{} diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.components.ts b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.components.ts new file mode 100644 index 0000000000000000000000000000000000000000..13c9abbb1da7cf2bce5db618c999f9c46d543777 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.components.ts @@ -0,0 +1,101 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core"; +import { SapiAtlasModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type"; +import { FilterGroupedParcellationPipe, GroupedParcellation } from "src/atlasComponents/sapiViews/core/parcellation"; + +export const darkThemePalette = [ + "#141414", + "#242424", + "#333333", +] + +export const lightThemePalette = [ + "#ffffff", + "#fafafa", + "#f5f5f5", +] + +export type ATP = { + atlas: SapiAtlasModel + template: SapiSpaceModel + parcellation: SapiParcellationModel +} + +function isATPGuard(atp: Record<string, unknown>): atp is Partial<ATP> { + const { atlas, template, parcellation } = atp + if (atlas && atlas["@type"] === "juelich/iav/atlas/v1.0.0") return true + if (template && template["@type"] === "https://openminds.ebrains.eu/sands/CoordinateSpace") return true + if (parcellation && parcellation["@type"] === "minds/core/parcellationatlas/v1.0.0") return true + return false +} + +const pipe = new FilterGroupedParcellationPipe() + + +@Component({ + selector: 'sxplr-pure-atp-selector', + templateUrl: `./pureATPSelector.template.html`, + styleUrls: [ + `./pureATPSelector.style.scss` + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) + +export class PureATPSelector implements OnChanges{ + + @Input('sxplr-pure-atp-selector-color-palette') + colorPalette: string[] = darkThemePalette + + @Input(`sxplr-pure-atp-selector-selected-atp`) + public selectedATP: ATP + + public selectedIds: string[] = [] + + @Input(`sxplr-pure-atp-selector-atlases`) + public allAtlases: SapiAtlasModel[] = [] + + @Input(`sxplr-pure-atp-selector-templates`) + public availableTemplates: SapiSpaceModel[] = [] + + @Input(`sxplr-pure-atp-selector-parcellations`) + public parcellations: SapiParcellationModel[] = [] + + public parcAndGroup: (GroupedParcellation|SapiParcellationModel)[] = [] + + @Input('sxplr-pure-atp-selector-is-busy') + public isBusy: boolean = false + + @Output('sxplr-pure-atp-selector-on-select') + selectLeafEmitter = new EventEmitter<Partial<ATP>>() + + getChildren(parc: GroupedParcellation|SapiParcellationModel){ + return (parc as GroupedParcellation).parcellations || [] + } + + selectLeaf(atp: Record<string, unknown>) { + if (this.isBusy) return + if (!isATPGuard(atp)) return + this.selectLeafEmitter.emit(atp) + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.selectedATP) { + if (!changes.selectedATP.currentValue) { + this.selectedIds = [] + } else { + const { atlas, parcellation, template } = changes.selectedATP.currentValue as ATP + this.selectedIds = [atlas?.["@id"], parcellation?.["@id"], template?.["@id"]].filter(v => !!v) + } + } + + if (changes.parcellations) { + if (!changes.parcellations.currentValue) { + this.parcAndGroup = [] + } else { + this.parcAndGroup = [ + ...pipe.transform(changes.parcellations.currentValue, true), + ...pipe.transform(changes.parcellations.currentValue, false), + ] + } + } + } +} diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.stories.ts b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..421b47409aee73dbd34d66f492811cd00baa3b1d --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.stories.ts @@ -0,0 +1,141 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { MatButtonModule } from "@angular/material/button"; +import { action } from "@storybook/addon-actions"; +import { componentWrapperDecorator, Meta, moduleMetadata, Story } from "@storybook/angular"; +import { atlasId, provideDarkTheme } from "src/atlasComponents/sapi/stories.base"; +import { SapiAtlasModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type"; +import { UtilModule } from "src/util"; +import { ATPSelectorModule } from "../module"; +import { darkThemePalette } from "./pureATPSelector.components" +import { loadAtlasEtcData, wrapperDecoratorFn } from "../story.base" +import { provideMockStore } from "@ngrx/store/testing"; + +@Component({ + selector: 'atlas-selector-wrapper-story', + template: + `<sxplr-pure-atp-selector + [sxplr-pure-atp-selector-color-palette]="[atlasColor, spaceColor, parcellationColor]" + [sxplr-pure-atp-selector-selected-atp]="selectedATP" + [sxplr-pure-atp-selector-atlases]="allAtlases" + [sxplr-pure-atp-selector-templates]="availableTemplates" + [sxplr-pure-atp-selector-parcellations]="parcs" + [sxplr-pure-atp-selector-is-busy]="isBusy" + (sxplr-pure-atp-selector-on-select)="selectLeaf($event)" + > + <button mat-icon-button + (click)="toggleParcellationVisibility()" + parcellation-chip-suffix + iav-stop="mousedown click"> + <i class="fas fa-eye"></i> + </button> + </sxplr-pure-atp-selector>`, + styles: [ + `[parcellation-chip-suffix] + { + margin-right: -1rem; + margin-left: 0.2rem; + }` + ] +}) +class AtlasLayerSelectorWrapper { + atlasColor: string = darkThemePalette[0] + spaceColor: string = darkThemePalette[1] + parcellationColor: string = darkThemePalette[2] + allAtlases: SapiAtlasModel[] + availableTemplates: SapiSpaceModel[] + selectedATP: { + atlas: SapiAtlasModel + parcellation: SapiParcellationModel + template: SapiSpaceModel + } + parcs: SapiParcellationModel[] + isBusy: boolean = false + + selectLeaf(arg: { + atlas: SapiAtlasModel + parcellation: SapiParcellationModel + template: SapiSpaceModel + }) {} + + toggleParcellationVisibility() { + + } +} + +export default { + component: AtlasLayerSelectorWrapper, + decorators: [ + moduleMetadata({ + imports: [ + CommonModule, + ATPSelectorModule, + MatButtonModule, + UtilModule, + ], + declarations: [ + AtlasLayerSelectorWrapper + ], + providers: [ + ...provideDarkTheme, + provideMockStore(), + ] + }), + componentWrapperDecorator(wrapperDecoratorFn) + ] +} as Meta + +const Template: Story<AtlasLayerSelectorWrapper> = (args: AtlasLayerSelectorWrapper, { loaded }) => { + + const { + combinedAtlas, + atlases + } = loaded + const { atlas, parcs, spaces } = combinedAtlas + const selectedATP = { + atlas, parcellation: parcs[0], template: spaces[0] + } + const { + atlasColor, + spaceColor, + parcellationColor, + } = args + return ({ + props: { + ...args, + allAtlases: atlases, + parcs, + availableTemplates: spaces, + selectedATP: { + atlas, + parcellation: parcs[0], + template: spaces[0] + }, + selectLeaf: action(`selectLeaf`), + toggleParcellationVisibility: action('toggleParcellationVisibility'), + atlasColor, + spaceColor, + parcellationColor, + } + }) +} + +export const Default = Template.bind({}) +Default.args = { + atlasColor: darkThemePalette[0], + spaceColor: darkThemePalette[1], + parcellationColor: darkThemePalette[2], +} +Default.loaders = [ + async () => { + const { + atlases, + combinedAtlases + } = await loadAtlasEtcData() + + return { + atlases, + combinedAtlas: combinedAtlases.filter(v => v.atlas["@id"] === atlasId.human)[0] + } + } +] diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.style.scss b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.style.scss new file mode 100644 index 0000000000000000000000000000000000000000..f5c72a7f8bcd6303da0c2c95f4e1cf6d5ec72e56 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.style.scss @@ -0,0 +1,30 @@ +:host +{ + display: inline-flex; + flex-direction: row-reverse; +} + +.full-sized-button +{ + width: 100%; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.icons +{ + margin-right: -1rem; + margin-left: 0.2rem; +} + +sxplr-smart-chip:not(:last-child) +{ + margin-left: -2.5rem; + + .chip-text + { + padding-left: 1rem; + } +} diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html new file mode 100644 index 0000000000000000000000000000000000000000..295862fa4208bf50833a3a170a2458e6ba0412fb --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html @@ -0,0 +1,122 @@ +<ng-template [ngIf]="selectedATP" let-ATP> + + <!-- parcellation smart chip --> + <sxplr-smart-chip *ngIf="ATP.parcellation && parcAndGroup.length > 1" + [items]="parcAndGroup" + [color]="colorPalette[2]" + [getChildren]="getChildren" + (itemClicked)="selectLeaf({ parcellation: $event })" + [elevation]="2" + [disabled]="isBusy"> + <ng-template sxplrSmartChipContent> + <span class="chip-text"> + <span> + </span> + {{ ATP.parcellation.name }} + </span> + + <ng-content select="[parcellation-chip-suffix]"> + </ng-content> + + <button iav-stop="mousedown click" + class="icons" + mat-icon-button + sxplr-dialog + [sxplr-dialog-size]="null" + [sxplr-dialog-data]="{ + title: ATP.parcellation.name || ATP.parcellation.fullName, + descMd: (ATP.parcellation.brainAtlasVersions || [])[0]?.versionInnovation, + actions: ATP.parcellation | parcellationDoiPipe + }"> + <i class="fas fa-info"></i> + </button> + </ng-template> + <ng-template sxplrSmartChipMenu let-parc> + + <ng-container *ngTemplateOutlet="optionTmpl; context: { + $implicit: parc, + overridePrefixIconTmpl: (parc | parcellationGroupSelected : ATP.parcellation) + ? halfSelectedTmpl + : null + }"> + </ng-container> + </ng-template> + </sxplr-smart-chip> + + <!-- space smart chip --> + <sxplr-smart-chip *ngIf="ATP.template && availableTemplates.length > 1" + [items]="availableTemplates" + [color]="colorPalette[1]" + (itemClicked)="selectLeaf({ template: $event })" + [elevation]="4" + [disabled]="isBusy"> + <ng-template sxplrSmartChipContent> + <span class="chip-text"> + {{ ATP.template.fullName }} + </span> + </ng-template> + <ng-template sxplrSmartChipMenu let-space> + <ng-container *ngTemplateOutlet="optionTmpl; context: { $implicit: space }"></ng-container> + </ng-template> + </sxplr-smart-chip> + + <!-- atlas smart chip --> + <sxplr-smart-chip *ngIf="ATP.atlas" + [items]="allAtlases" + [color]="colorPalette[0]" + (itemClicked)="selectLeaf({ atlas: $event})" + [elevation]="6" + [disabled]="isBusy"> + <ng-template sxplrSmartChipContent> + <span class="chip-text"> + {{ ATP.atlas.name }} + </span> + </ng-template> + <ng-template sxplrSmartChipMenu let-atlas> + <ng-container *ngTemplateOutlet="optionTmpl; context: { $implicit: atlas }"></ng-container> + </ng-template> + </sxplr-smart-chip> +</ng-template> + +<!-- half selected --> +<!-- only active in nested menus (e.g. parcellation groups) --> +<ng-template #halfSelectedTmpl> + <mat-icon fontSet="far" fontIcon="fa-circle"></mat-icon> +</ng-template> + +<!-- option template --> +<ng-template + #optionTmpl + let-item + let-overridePrefixIconTmpl="overridePrefixIconTmpl" + let-overrideSuffixIcon="overrideSuffixIcon"> + + <!-- prefix --> + <ng-template [ngIf]="overridePrefixIconTmpl" [ngIfElse]="defaultPrefix"> + <ng-template [ngTemplateOutlet]="overridePrefixIconTmpl"></ng-template> + </ng-template> + <ng-template #defaultPrefix> + <ng-template [ngIf]="selectedIds" let-selectedIds> + <mat-icon + fontSet="fas" + [fontIcon]="selectedIds.includes(item['@id']) ? 'fa-circle' : 'fa-none'" + > + </mat-icon> + </ng-template> + </ng-template> + + <!-- button body --> + <span *ngIf="item" class="full-sized-button"> + {{ item.version?.name || item.name || item.fullName }} + </span> + + <!-- suffix --> + <ng-template [ngIf]="overrideSuffixIcon"> + <i [class]="overrideSuffixIcon"></i> + </ng-template> +</ng-template> + + +<ng-template #isBusyTmpl> + +</ng-template> \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/story.base.ts b/src/atlasComponents/sapiViews/core/rich/ATPSelector/story.base.ts new file mode 100644 index 0000000000000000000000000000000000000000..86b1eae6592a5fb89fdea156bafa6b6d696b0efe --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/story.base.ts @@ -0,0 +1,42 @@ +import { getAtlases, getParcellations, getSpaces } from "src/atlasComponents/sapi/stories.base" +import { SapiAtlasModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type" + +export const wrapperDecoratorFn = (story: string) => ` + <style> + .wrapper { + width: 100%; + height: 100%; + background-color: white; + + display: flex; + flex-direction: column-reverse; + border: 1px solid rgba(128, 128, 128, 0.5); + } + :host-context([darktheme="true"]) .wrapper + { + background-color: #2f2f2f; + } + </style> + <div class="wrapper">${story}</div> +` + +export type ReturnAtlas = { + atlas: SapiAtlasModel + spaces: SapiSpaceModel[] + parcs: SapiParcellationModel[] +} + +export const loadAtlasEtcData = async () => { + const combinedAtlases: ReturnAtlas[] = [] + const atlases = await getAtlases() + for (const atlas of atlases) { + const parcs = await getParcellations(atlas["@id"]) + const spaces = await getSpaces(atlas["@id"]) + combinedAtlases.push({ + atlas, + parcs, + spaces + }) + } + return { combinedAtlases, atlases } +} diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5a3ef9eeebef4e6a2cdaba8d5a50aa000cdb06a --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts @@ -0,0 +1,140 @@ +import { Component, Inject, OnDestroy } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { Store } from "@ngrx/store"; +import { Observable, of, Subject, Subscription } from "rxjs"; +import { filter, map, switchMap, tap, withLatestFrom } from "rxjs/operators"; +import { SAPI } from "src/atlasComponents/sapi/sapi.service"; +import { ParcellationSupportedInSpacePipe } from "src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe"; +import { atlasSelection } from "src/state"; +import { fromRootStore } from "src/state/atlasSelection"; +import { DialogFallbackCmp } from "src/ui/dialogInfo"; +import { DARKTHEME } from "src/util/injectionTokens"; +import { ParcellationVisibilityService } from "../../../parcellation/parcellationVis.service"; +import { darkThemePalette, lightThemePalette, ATP } from "../pureDumb/pureATPSelector.components" + +function isATPGuard(obj: any): obj is ATP { + if (!obj) return false + return obj.atlas || obj.template || obj.parcellation +} + +@Component({ + selector: 'sxplr-wrapper-atp-selector', + templateUrl: './wrapper.template.html', + styleUrls: [ + `./wrapper.style.css` + ] +}) + +export class WrapperATPSelector implements OnDestroy{ + darkThemePalette = darkThemePalette + lightThemePalette = lightThemePalette + + #subscription: Subscription[] = [] + #parcSupportedInSpacePipe = new ParcellationSupportedInSpacePipe(this.sapi) + + #askUser(title: string, descMd: string): Observable<boolean> { + const agree = "OK" + return this.dialog.open(DialogFallbackCmp, { + data: { + title, + descMd, + actions: [agree] + } + }).afterClosed().pipe( + map(val => val === agree) + ) + } + + selectedATP$ = this.store$.pipe( + fromRootStore.distinctATP(), + ) + + allAtlases$ = this.sapi.atlases$ + availableTemplates$ = this.store$.pipe( + fromRootStore.allAvailSpaces(this.sapi), + ) + parcs$ = this.store$.pipe( + fromRootStore.allAvailParcs(this.sapi), + ) + isBusy$ = new Subject<boolean>() + + parcellationVisibility$ = this.svc.visibility$ + + constructor( + private dialog: MatDialog, + private store$: Store, + private sapi: SAPI, + private svc: ParcellationVisibilityService, + @Inject(DARKTHEME) public darktheme$: Observable<boolean> + ){ + this.#subscription.push( + this.selectLeaf$.pipe( + tap(() => this.isBusy$.next(true)), + withLatestFrom(this.selectedATP$), + switchMap(([{ atlas, template, parcellation }, selectedATP]) => { + if (atlas) { + /** + * do not need to ask permission to switch atlas + */ + return of({ atlas }) + } + if (template) { + return this.#parcSupportedInSpacePipe.transform(selectedATP.parcellation, template).pipe( + switchMap(supported => supported + ? of({ template }) + : this.#askUser(`Incompatible parcellation`, `Attempting to load template **${template.fullName}**, which does not support parcellation **${selectedATP.parcellation.name}**. Proceed anyway and load the default parcellation?`).pipe( + switchMap(flag => of(flag ? { template } : null)) + )) + ) + } + if (parcellation) { + return this.#parcSupportedInSpacePipe.transform(parcellation, selectedATP.template).pipe( + switchMap(supported=> supported + ? of({ parcellation }) + : this.#askUser(`Incompatible template`, `Attempting to load parcellation **${parcellation.name}**, which is not supported in template **${selectedATP.template.fullName}**. Proceed anyway and load the default template?`).pipe( + switchMap(flag => of(flag ? { parcellation } : null)) + )) + ) + } + return of(null) + }), + filter(val => { + this.isBusy$.next(false) + return !!val + }) + ).subscribe((obj) => { + + if (!isATPGuard(obj)) return + const { atlas, parcellation, template } = obj + if (atlas) { + this.store$.dispatch( + atlasSelection.actions.selectAtlas({ atlas }) + ) + } + if (parcellation) { + this.store$.dispatch( + atlasSelection.actions.selectParcellation({ parcellation }) + ) + } + if (template) { + this.store$.dispatch( + atlasSelection.actions.selectTemplate({ template }) + ) + } + }) + ) + } + + private selectLeaf$ = new Subject<ATP>() + selectLeaf(atp: ATP) { + this.selectLeaf$.next(atp) + } + + toggleParcellationVisibility() { + this.svc.toggleVisibility() + } + + ngOnDestroy(): void { + while (this.#subscription.length > 0) this.#subscription.pop().unsubscribe() + } +} diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.stories.ts b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..8dbf609657ac3efc72d6a2cc24c9b35523528d55 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.stories.ts @@ -0,0 +1,81 @@ +import { Component } from "@angular/core"; +import { MatDialogModule } from "@angular/material/dialog"; +import { Store } from "@ngrx/store"; +import { action } from "@storybook/addon-actions"; +import { componentWrapperDecorator, Meta, moduleMetadata, Story } from "@storybook/angular"; +import { atlasId, provideDarkTheme } from "src/atlasComponents/sapi/stories.base"; +import { atlasSelection, RootStoreModule } from "src/state"; +import { ParcellationVisibilityService } from "../../../parcellation/parcellationVis.service"; +import { ATPSelectorModule } from "../module"; +import { ATP } from "../pureDumb/pureATPSelector.components"; +import { loadAtlasEtcData, wrapperDecoratorFn } from "../story.base"; + +@Component({ + selector: 'wrapper-wrapper', + template: '<sxplr-wrapper-atp-selector></sxplr-wrapper-atp-selector>' +}) +class Wrapper{ + set ATP(atp: ATP) { + const { atlas, parcellation, template } = atp + + this.store$.dispatch( + atlasSelection.actions.setAtlasSelectionState({ + selectedAtlas: atlas, + selectedTemplate: template, + selectedParcellation: parcellation + }) + ) + + this.store$.dispatch = action('dispatch') + } + + constructor(private store$: Store) {} +} + +export default { + component: Wrapper, + decorators: [ + moduleMetadata({ + imports: [ + ATPSelectorModule, + MatDialogModule, + RootStoreModule, + ], + providers: [ + ...provideDarkTheme, + ParcellationVisibilityService, + ] + }), + componentWrapperDecorator(wrapperDecoratorFn) + ] +} as Meta + +const Template: Story<Wrapper> = (args: Wrapper, { loaded }) => { + const { combinedAtlas } = loaded + return { + props: { + ...args, + ATP: { + atlas: combinedAtlas.atlas, + parcellation: combinedAtlas.parcs[0], + template: combinedAtlas.spaces[0], + } + } + } +} + +export const Default = Template.bind({}) + +Default.loaders = [ + async () => { + const { + atlases, + combinedAtlases + } = await loadAtlasEtcData() + + return { + atlases, + combinedAtlas: combinedAtlases.filter(v => v.atlas["@id"] === atlasId.human)[0] + } + } +] diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.style.css b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.style.css new file mode 100644 index 0000000000000000000000000000000000000000..a738cc567de677401989a85134815ceb729bd540 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.style.css @@ -0,0 +1,5 @@ +[parcellation-chip-suffix] +{ + margin-right: -1rem; + margin-left: 0.2rem; +} diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.template.html b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.template.html new file mode 100644 index 0000000000000000000000000000000000000000..bf9b3f0e786ce5fa70818458749e48dc48c85c48 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.template.html @@ -0,0 +1,18 @@ +<sxplr-pure-atp-selector + [sxplr-pure-atp-selector-color-palette]="(darktheme$ | async) ? darkThemePalette : lightThemePalette" + [sxplr-pure-atp-selector-selected-atp]="selectedATP$ | async" + [sxplr-pure-atp-selector-atlases]="allAtlases$ | async" + [sxplr-pure-atp-selector-templates]="availableTemplates$ | async" + [sxplr-pure-atp-selector-parcellations]="parcs$ | async" + [sxplr-pure-atp-selector-is-busy]="isBusy$ | async" + (sxplr-pure-atp-selector-on-select)="selectLeaf($event)" + > + <button mat-icon-button + (click)="toggleParcellationVisibility()" + parcellation-chip-suffix + iav-stop="mousedown click" + [iav-key-listener]="[{'type': 'keydown', 'key': 'q', 'capture': true, 'target': 'document' }]" + (iav-key-event)="toggleParcellationVisibility()" > + <i class="fas" [ngClass]="(parcellationVisibility$ | async) ? 'fa-eye' : 'fa-eye-slash'"></i> + </button> +</sxplr-pure-atp-selector> diff --git a/src/atlasComponents/sapiViews/core/rich/module.ts b/src/atlasComponents/sapiViews/core/rich/module.ts index 8f946335ee020a073f3ff6f74124360513db6123..f8fcc8a936495f8e769e33f0756882dde15f9532 100644 --- a/src/atlasComponents/sapiViews/core/rich/module.ts +++ b/src/atlasComponents/sapiViews/core/rich/module.ts @@ -9,6 +9,7 @@ import { SapiViewsCoreRegionModule } from "../region"; import { HighlightPipe } from "./regionsHierarchy/highlight.pipe"; import { SapiViewsCoreRichRegionsHierarchy } from "./regionsHierarchy/regionsHierarchy.component"; import { SapiViewsCoreRichRegionListSearch } from "./regionsListSearch/regionListSearch.component"; +import { SapiViewsCoreRichRegionListTemplateDirective } from "./regionsListSearch/regionListSearchTmpl.directive"; @NgModule({ imports: [ @@ -24,9 +25,11 @@ import { SapiViewsCoreRichRegionListSearch } from "./regionsListSearch/regionLis SapiViewsCoreRichRegionListSearch, SapiViewsCoreRichRegionsHierarchy, HighlightPipe, + SapiViewsCoreRichRegionListTemplateDirective, ], exports: [ SapiViewsCoreRichRegionListSearch, + SapiViewsCoreRichRegionListTemplateDirective, SapiViewsCoreRichRegionsHierarchy, ] }) diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html index 9c085e07cef508f4de0506fb740705c341616790..f3a4e82d473fcb11a80abf856c560f79e722ac89 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html +++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html @@ -1,20 +1,18 @@ -<form class="sxplr-custom-cmp text sxplr-w-100"> - <mat-form-field class="sxplr-w-100"> - <input - [placeholder]="placeholderText" - type="text" - matInput - name="searchTerm" - [formControl]="searchFormControl" - autocomplete="off"> +<mat-form-field class="sxplr-custom-cmp text sxplr-w-100"> + <input + [placeholder]="placeholderText" + type="text" + matInput + name="searchTerm" + [formControl]="searchFormControl" + autocomplete="off"> - <!-- search input suffix --> - <div matSuffix> - <ng-content select="[search-input-suffix]"></ng-content> - </div> + <!-- search input suffix --> + <div matSuffix> + <ng-content select="[search-input-suffix]"></ng-content> + </div> - </mat-form-field> -</form> +</mat-form-field> <ng-template #tmplRef let-region> <div class="mat-body sxplr-d-flex sxplr-align-items-center sxplr-h-100 region-tmpl" diff --git a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts index 86970540a7e41edcca06a35c2b4cb366ae542077..d169eb904f4ce01fe5d9753a95acce50180d326b 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts +++ b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts @@ -1,9 +1,10 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, TemplateRef } from "@angular/core"; +import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, Input, Output, TemplateRef } from "@angular/core"; import { SapiRegionModel } from "src/atlasComponents/sapi/type"; import { ARIA_LABELS } from "common/constants" import { UntypedFormControl } from "@angular/forms"; import { debounceTime, distinctUntilChanged, map, startWith } from "rxjs/operators"; import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; +import { SapiViewsCoreRichRegionListTemplateDirective } from "./regionListSearchTmpl.directive"; /** * Filter function, which determines whether the region will be included in the list of autocompleted search. @@ -34,6 +35,8 @@ export class SapiViewsCoreRichRegionListSearch { ARIA_LABELS = ARIA_LABELS + showNOptions = 4 + private _regions: SapiRegionModel[] = [] get regions(){ return this._regions @@ -43,6 +46,9 @@ export class SapiViewsCoreRichRegionListSearch { this._regions = val.filter(filterRegionForListSearch) } + @ContentChild(SapiViewsCoreRichRegionListTemplateDirective) + regionTmplDirective: SapiViewsCoreRichRegionListTemplateDirective + @Input('sxplr-sapiviews-core-rich-regionlistsearch-region-template-ref') regionTemplateRef: TemplateRef<any> @@ -54,18 +60,22 @@ export class SapiViewsCoreRichRegionListSearch { public searchFormControl = new UntypedFormControl() - public autocompleteList$ = this.searchFormControl.valueChanges.pipe( + public searchedList$ = this.searchFormControl.valueChanges.pipe( startWith(''), distinctUntilChanged(), debounceTime(160), map((searchTerm: string | SapiRegionModel) => { if (typeof searchTerm === "string") { - return this.regions.filter(filterRegionViaSearch(searchTerm)).slice(0,5) + return this.regions.filter(filterRegionViaSearch(searchTerm)) } return [] }) ) + public autocompleteList$ = this.searchedList$.pipe( + map(list => list.slice(0, this.showNOptions)) + ) + displayFn(region: SapiRegionModel){ return region?.name || '' } diff --git a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.stories.ts b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.stories.ts index 2c63c54de2b32ef0bae180e56ab59639bafa0fa6..872925713ec79672db9a07cff96f03839c24a28e 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.stories.ts +++ b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.stories.ts @@ -5,9 +5,10 @@ import { SAPI } from "src/atlasComponents/sapi" import { atlasId, provideDarkTheme, getParcRegions, parcId, spaceId } from "src/atlasComponents/sapi/stories.base" import { SapiViewsCoreRichModule } from "../module" import { SapiViewsCoreRichRegionListSearch } from "./regionListSearch.component" -import { Pipe, PipeTransform, SecurityContext } from "@angular/core" +import { Pipe, PipeTransform } from "@angular/core" import { SapiRegionModel } from "src/atlasComponents/sapi/type" -import { DomSanitizer, SafeHtml } from "@angular/platform-browser" +import { DomSanitizer } from "@angular/platform-browser" +import { SapiViewsCoreRegionModule } from "../../region" @Pipe({ name: 'regionTmplPipe', @@ -38,6 +39,7 @@ export default { imports: [ HttpClientModule, SapiViewsCoreRichModule, + SapiViewsCoreRegionModule, ], providers: [ SAPI, @@ -63,10 +65,8 @@ const Template: Story<SapiViewsCoreRichRegionListSearch> = (args: SapiViewsCoreR tmplRef } = parameters const template = ` - ${tmplRef ? ('<ng-template #tmplref let-region>' + tmplRef + '</ng-template>') : ''} - <sxplr-sapiviews-core-rich-regionlistsearch - ${tmplRef ? '[sxplr-sapiviews-core-rich-regionlistsearch-region-template-ref]="tmplref"' : ''} - > + <sxplr-sapiviews-core-rich-regionlistsearch> + ${tmplRef ? ('<ng-template regionTemplate let-region>' + tmplRef + '</ng-template>') : ''} ${contentProjection || ''} </sxplr-sapiviews-core-rich-regionlistsearch> ` @@ -80,6 +80,9 @@ const Template: Story<SapiViewsCoreRichRegionListSearch> = (args: SapiViewsCoreR }) } Template.loaders = [] +Template.parameters = { + tmplRef: `<span>{{ region.name }}</span>` +} const asyncLoader = async (atlasId: string, parcId: string, spaceId: string) => { const regions = await getParcRegions(atlasId, parcId, spaceId) @@ -97,6 +100,14 @@ const getContentProjection = ({ searchInputSuffix }) => { } export const Default = Template.bind({}) +Default.parameters = { + ...Template.parameters, + tmplRef: ` +<sxplr-sapiviews-core-region-region-list-item + [sxplr-sapiviews-core-region-region]="region"> +</sxplr-sapiviews-core-region-region-list-item> +` +} Default.loaders = [ async () => { const { regions } = await asyncLoader(atlasId.human, parcId.human.jba29, spaceId.human.mni152) @@ -112,6 +123,7 @@ InputSuffix.loaders = [ } ] InputSuffix.parameters = { + ...Template.parameters, contentProjection: getContentProjection({ searchInputSuffix: `SUFFIX` }) @@ -125,7 +137,8 @@ TemplatedRegionSuffix.loaders = [ } ] TemplatedRegionSuffix.parameters = { - tmplRef: `<span [innerHTML]="region | regionTmplPipe"></span>` + ...Template.parameters, + tmplRef: `<span [innerHTML]="region | regionTmplPipe"></span><span>{{ region.name }}</span>` } diff --git a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html index 379454ff524357eb9a5717253349a17dac8df6ff..7f381436634978627b83709d6cf3ad40e0a8a6bf 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html +++ b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html @@ -32,21 +32,33 @@ *ngFor="let region of autocompleteList$ | async" [value]="region"> - <div class="sxplr-d-flex sxplr-justify-content-space-between"> + <ng-template [ngIf]="regionTmplDirective"> - <sxplr-sapiviews-core-region-region-list-item - [sxplr-sapiviews-core-region-region]="region"> - </sxplr-sapiviews-core-region-region-list-item> - <ng-template - [ngIf]="regionTemplateRef" - [ngTemplateOutlet]="regionTemplateRef" + [ngTemplateOutlet]="regionTmplDirective.tmplRef" [ngTemplateOutletContext]="{ $implicit: region }"> - </ng-template> - </div> + </ng-template> </mat-option> + <ng-template [ngIf]="searchedList$ | async" let-searchedList> + + <mat-option *ngIf="searchedList.length > showNOptions" + [disabled]="true"> + + <ng-template [ngIf]="regionTmplDirective"> + + <ng-template + [ngTemplateOutlet]="regionTmplDirective.tmplRef" + [ngTemplateOutletContext]="{ + $implicit: { + name: '... and ' + (searchedList.length - showNOptions) + ' more' + } + }"> + </ng-template> + </ng-template> + </mat-option> + </ng-template> </mat-autocomplete> \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearchTmpl.directive.ts b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearchTmpl.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..ecc282ee47ba3a328c13444d7890afcd8c62047b --- /dev/null +++ b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearchTmpl.directive.ts @@ -0,0 +1,10 @@ +import { Directive, TemplateRef } from "@angular/core"; +import { SapiRegionModel } from "src/atlasComponents/sapi/type"; + +@Directive({ + selector: 'ng-template[region-template],ng-template[regionTemplate]' +}) + +export class SapiViewsCoreRichRegionListTemplateDirective{ + constructor(public tmplRef: TemplateRef<SapiRegionModel>){} +} diff --git a/src/atlasComponents/sapiViews/core/space/module.ts b/src/atlasComponents/sapiViews/core/space/module.ts index 35b96ce90dd005580407c05411d923986e2ad9d4..57dba91de5499f6e9f3c82e57f7f14f692512411 100644 --- a/src/atlasComponents/sapiViews/core/space/module.ts +++ b/src/atlasComponents/sapiViews/core/space/module.ts @@ -2,23 +2,26 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { ComponentsModule } from "src/components"; import { SapiViewsCoreSpaceBoundingBox } from "./boundingBox.directive"; -import { PreviewSpaceUrlPipe } from "./previewSpaceUrl.pipe"; -import { SapiViewsCoreSpaceSpaceTile } from "./tile/space.tile.component"; +import { AngularMaterialModule } from "src/sharedModules"; +import { DialogModule } from "src/ui/dialogInfo/module"; +import { SapiViewsUtilModule } from "../../util"; +import {UtilModule} from "src/util"; @NgModule({ imports: [ CommonModule, ComponentsModule, + AngularMaterialModule, + DialogModule, + SapiViewsUtilModule, + UtilModule ], declarations: [ - SapiViewsCoreSpaceSpaceTile, - PreviewSpaceUrlPipe, SapiViewsCoreSpaceBoundingBox, ], exports: [ - SapiViewsCoreSpaceSpaceTile, SapiViewsCoreSpaceBoundingBox, ] }) -export class SapiViewsCoreSpaceModule{} \ No newline at end of file +export class SapiViewsCoreSpaceModule{} diff --git a/src/atlasComponents/sapiViews/core/space/previewSpaceUrl.pipe.ts b/src/atlasComponents/sapiViews/core/space/previewSpaceUrl.pipe.ts deleted file mode 100644 index 8bbecdf23e0cf041912d5293c3be12eef5d5f7b6..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/space/previewSpaceUrl.pipe.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core" -import { SapiSpaceModel } from "src/atlasComponents/sapi" - -const previewImgMap = new Map([ - ['minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588', 'bigbrain.png'], - ['minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2', 'icbm2009c.png'], - ['minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992', 'colin27.png'], - - ['minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9', 'allen-mouse.png'], - - ['minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8', 'waxholm.png'], - - ['minds/core/referencespace/v1.0.0/tmp-fsaverage', 'freesurfer.png'], - ['minds/core/referencespace/v1.0.0/tmp-fsaverage6', 'freesurfer.png'], - - ['minds/core/referencespace/v1.0.0/tmp-hcp32k', 'freesurfer.png'], - ['minds/core/referencespace/v1.0.0/MEBRAINS_T1.masked', 'primate.png'], - -]) - -@Pipe({ - name: 'previewSpaceUrl', - pure: true -}) - -export class PreviewSpaceUrlPipe implements PipeTransform{ - public transform(tile: SapiSpaceModel){ - const filename = previewImgMap.get(tile['@id']) - return filename && `assets/images/atlas-selection/${filename}` - } -} diff --git a/src/atlasComponents/sapiViews/core/space/tile/space.tile.component.ts b/src/atlasComponents/sapiViews/core/space/tile/space.tile.component.ts deleted file mode 100644 index e9aaab3bebbdce796155a754857d9b0ff67ce090..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/space/tile/space.tile.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Component, Input } from "@angular/core"; -import { SapiSpaceModel } from "src/atlasComponents/sapi"; - -@Component({ - selector: `sxplr-sapiviews-core-space-tile`, - templateUrl: `./space.tile.template.html`, - styleUrls: [ - `./space.tile.style.css` - ] -}) - -export class SapiViewsCoreSpaceSpaceTile { - @Input('sxplr-sapiviews-core-space-tile-space') - space: SapiSpaceModel - - @Input('sxplr-sapiviews-core-space-tile-selected') - selected: boolean = false - -} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/space/tile/space.tile.stories.ts b/src/atlasComponents/sapiViews/core/space/tile/space.tile.stories.ts deleted file mode 100644 index fd385d0f599b21101fdc6021714ad78e921d5178..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/space/tile/space.tile.stories.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { CommonModule } from "@angular/common" -import { HttpClientModule } from "@angular/common/http" -import { Component, Input } from "@angular/core" -import { Meta, moduleMetadata, Story } from "@storybook/angular" -import { SAPI, SapiSpaceModel } from "src/atlasComponents/sapi" -import { atlasId, spaceId, getAtlas, getSpace, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" -import { AngularMaterialModule } from "src/sharedModules" -import { SapiViewsCoreSpaceModule } from "../module" -import { SapiViewsCoreSpaceSpaceTile } from "./space.tile.component" - -@Component({ - selector: `space-tile-wrapper`, - template: ` - <mat-accordion> - <mat-expansion-panel *ngFor="let item of spaces | keyvalue"> - - <mat-expansion-panel-header> - {{ item.key }} - </mat-expansion-panel-header> - - <ng-template matExpansionPanelContent> - <div class="sxplr-d-inline-flex align-items-start"> - <sxplr-sapiviews-core-space-tile - *ngFor="let spc of item.value" - [sxplr-sapiviews-core-space-tile-space]="spc" - [sxplr-sapiviews-core-space-tile-selected]="spc['@id'] === selected" - class="sxplr-m-2"> - </sxplr-sapiviews-core-space-tile> - </div> - </ng-template> - - </mat-expansion-panel> - </mat-accordion> - `, - styles: [ - `sxplr-sapiviews-core-space-tile { display: inline-block; max-width: 8rem; }` - ] -}) - -class SpaceTileWrapper{ - @Input() - spaces: Record<string, SapiSpaceModel[]> = {} - - @Input() - selected: string = spaceId.human.mni152 -} - -export default { - component: SpaceTileWrapper, - decorators: [ - moduleMetadata({ - imports: [ - CommonModule, - HttpClientModule, - SapiViewsCoreSpaceModule, - AngularMaterialModule, - ], - providers: [ - SAPI, - ...provideDarkTheme, - ], - declarations: [ - SpaceTileWrapper - ] - }) - ], -} as Meta - -const Template: Story<SapiViewsCoreSpaceSpaceTile> = (args: SapiViewsCoreSpaceSpaceTile, { loaded }) => { - const { - spaces - } = loaded - - return ({ - props: { - ...args, - spaces - } - }) -} -Template.loaders = [ - -] - -const asyncLoader = async () => { - const spaces: Record<string, SapiSpaceModel[]> = {} - for (const species in atlasId) { - const atlasDetail = await getAtlas(atlasId[species]) - spaces[species] = [] - - for (const spc of atlasDetail.spaces) { - const spcDetail = await getSpace(atlasDetail['@id'], spc['@id']) - spaces[species].push(spcDetail) - } - } - - return { - spaces - } -} - -export const Default = Template.bind({}) -Default.args = { - selected: spaceId.human.mni152 -} -Default.loaders = [ - - async () => { - const { - spaces - } = await asyncLoader() - return { - spaces - } - } -] diff --git a/src/atlasComponents/sapiViews/core/space/tile/space.tile.style.css b/src/atlasComponents/sapiViews/core/space/tile/space.tile.style.css deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/atlasComponents/sapiViews/core/space/tile/space.tile.template.html b/src/atlasComponents/sapiViews/core/space/tile/space.tile.template.html deleted file mode 100644 index 74ba27c9a7384a0f470b083f4aa5a1df4489401a..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/space/tile/space.tile.template.html +++ /dev/null @@ -1,7 +0,0 @@ -<tile-cmp - *ngIf="space" - [tile-text]="space.fullName" - [tile-image-src]="space | previewSpaceUrl" - [tile-selected]="selected"> - -</tile-cmp> \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/util/module.ts b/src/atlasComponents/sapiViews/util/module.ts index 53a3a88c549821dfe9a7ee654c93dac7747cd368..2ab8ff874d96216b9ad2528ac1ed83a3e95c204d 100644 --- a/src/atlasComponents/sapiViews/util/module.ts +++ b/src/atlasComponents/sapiViews/util/module.ts @@ -3,10 +3,8 @@ import { AddUnitAndJoin } from "./addUnitAndJoin.pipe"; import { EqualityPipe } from "./equality.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: [ @@ -16,8 +14,6 @@ import { SpaceSupportedInCurrentParcellationPipe } from "./spaceSupportedInCurre AddUnitAndJoin, IncludesPipe, ParcellationSupportedInSpacePipe, - ParcellationSupportedInCurrentSpace, - SpaceSupportedInCurrentParcellationPipe, ], exports: [ EqualityPipe, @@ -26,8 +22,6 @@ import { SpaceSupportedInCurrentParcellationPipe } from "./spaceSupportedInCurre AddUnitAndJoin, IncludesPipe, ParcellationSupportedInSpacePipe, - ParcellationSupportedInCurrentSpace, - SpaceSupportedInCurrentParcellationPipe, ] }) diff --git a/src/atlasComponents/sapiViews/util/parcellationSupportedInCurrentSpace.pipe.ts b/src/atlasComponents/sapiViews/util/parcellationSupportedInCurrentSpace.pipe.ts deleted file mode 100644 index 3fd1069f0b13c69822d115aba804ff93e81c6c27..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/util/parcellationSupportedInCurrentSpace.pipe.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 index 42eec193e93fcd29dcf576c45135bf0b0f892eef..8d69ecf0ce16d1df1227b985471003a8280110d5 100644 --- a/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts +++ b/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts @@ -45,4 +45,4 @@ export class ParcellationSupportedInSpacePipe implements PipeTransform{ map(volumes => volumes.some(v => v.data.space["@id"] === tmplId)) ) } -} +} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/util/spaceSupportedInCurrentParcellation.pipe.ts b/src/atlasComponents/sapiViews/util/spaceSupportedInCurrentParcellation.pipe.ts deleted file mode 100644 index e462fcc6bf41fc412852a9ef8d3404023eff96d6..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/util/spaceSupportedInCurrentParcellation.pipe.ts +++ /dev/null @@ -1,38 +0,0 @@ -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/smartChip/component/smartChip.component.ts b/src/components/smartChip/component/smartChip.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d933960efe0b817d2cb3c29b23b306c62706df04 --- /dev/null +++ b/src/components/smartChip/component/smartChip.component.ts @@ -0,0 +1,89 @@ +import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, HostBinding, Input, OnChanges, Output, SimpleChanges } from "@angular/core"; +import { SmartChipContent } from "../smartChip.content.directive" +import { SmartChipMenu } from "../smartChip.menu.directive"; +import { rgbToHsl, hexToRgb } from 'common/util' + +const cssColorToHsl = (input: string) => { + if (/rgb/i.test(input)) { + const match = /\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(input) + if (!match) throw new Error(`rgb cannot be extracted ${input}`) + const rgb = [ + parseInt(match[1]), + parseInt(match[2]), + parseInt(match[3]), + ] + const [_h, _s, l] = rgbToHsl(...rgb) + return l + } + if (/hsl/i.test(input)) { + const match = /\((.*)\)/.exec(input) + const [h, s, l] = match[1].split(",") + const trimmedL = l.trim() + if (/%$/.test(trimmedL)) { + const match = /^([0-9]+)%/.exec(trimmedL) + return parseInt(match[1]) / 100 + } + } + if (/^#/i.test(input) && input.length === 7) { + const [r, g, b] = hexToRgb(input) + const [_h, _s, l] = rgbToHsl(r, g, b) + return l + } + throw new Error(`Cannot parse css color: ${input}`) +} + +@Component({ + selector: `sxplr-smart-chip`, + templateUrl: `./smartChip.template.html`, + styleUrls: [ + `/smartChip.style.css` + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class SmartChip<T extends object> implements OnChanges{ + @Input('color') + color = `rgba(200, 200, 200, 1)` + + @Input('disabled') + disabled: boolean = false + + @Input('elevation') + elevation: number = 4 // translates to mat-elevation-z{elevation} + + smartChipClass = 'mat-elevation-z4' + + @Input('items') + items: T[] = [] + + @Input('getChildren') + getChildren: (item: T) => T[] = item => item['children'] || [] + + @Output('itemClicked') + itemClicked = new EventEmitter<T>() + + @ContentChild(SmartChipContent) + contentTmpl: SmartChipContent + + @ContentChild(SmartChipMenu) + menuTmpl: SmartChipMenu + + @HostBinding('class') + darkTheme: string = 'lighttheme' + + ngOnChanges(simpleChanges: SimpleChanges) { + if (simpleChanges.color) { + const brightness = cssColorToHsl(this.color) + this.darkTheme = brightness < 0.4 + ? 'darktheme' + : 'lighttheme' + } + + if (simpleChanges.disabled || simpleChanges.elevation) { + this.smartChipClass = [ + this.disabled ? 'disabled' : null, + `mat-elevation-z${this.elevation}` + ].filter(v => !!v).join(' ') + } + } +} diff --git a/src/components/smartChip/component/smartChip.style.css b/src/components/smartChip/component/smartChip.style.css new file mode 100644 index 0000000000000000000000000000000000000000..3b0e266234f8891616d14e3aa9db2aa30f701159 --- /dev/null +++ b/src/components/smartChip/component/smartChip.style.css @@ -0,0 +1,41 @@ +:host +{ + margin: 0.5rem 1rem; + min-height: 32px; + height: 1px; +} + +.smart-chip +{ + box-sizing: border-box; + height: 100%; + + padding: 0.5rem 1rem; + border-radius: 1rem; + + display: inline-flex; + flex-direction: row; + align-items: center; +} + +.smart-chip +{ + opacity: 1.0; + transition: opacity 160ms ease-in-out; +} + +.smart-chip:not(.disabled):hover +{ + cursor: default; +} + +.smart-chip.disabled +{ + opacity: 0.5; + pointer-events: none; +} + +.smart-chip.disabled:hover +{ + cursor: not-allowed; +} diff --git a/src/components/smartChip/component/smartChip.template.html b/src/components/smartChip/component/smartChip.template.html new file mode 100644 index 0000000000000000000000000000000000000000..c82394e1d9f2157ffb874af1d41789fb7e52c6aa --- /dev/null +++ b/src/components/smartChip/component/smartChip.template.html @@ -0,0 +1,61 @@ +<div [style.background-color]="color" + [matMenuTriggerFor]="mainMenu" + matRipple + [ngClass]="smartChipClass" + class="mat-body smart-chip sxplr-custom-cmp text"> + <ng-template [ngTemplateOutlet]="contentTmpl?.templateRef || fallbackContentTmpl"> + </ng-template> +</div> + +<!-- main menu is fired from chip --> +<mat-menu #mainMenu="matMenu"> + <ng-template ngFor [ngForOf]="items" let-item> + + <!-- if item is has submenu --> + <ng-template [ngIf]="item | hasSubMenu : getChildren" [ngIfElse]="noSubMenuTmpl"> + <button + mat-menu-item + [matMenuTriggerFor]="subMenu" + [matMenuTriggerData]="{ $implicit: getChildren(item) }"> + <ng-container *ngTemplateOutlet="menuTmpl?.templateRef || fallbackMenu; context: { + $implicit: item + }"> + </ng-container> + </button> + + </ng-template> + + <!-- if item has no submenu --> + <ng-template #noSubMenuTmpl> + <ng-container *ngTemplateOutlet="leafTmpl; context: { $implicit: item }"></ng-container> + </ng-template> + + </ng-template> +</mat-menu> + +<!-- submenu (fired from menu item) --> +<mat-menu #subMenu="matMenu"> + <ng-template matMenuContent let-items> + <ng-template ngFor [ngForOf]="items" let-item> + <ng-container *ngTemplateOutlet="leafTmpl; context: { $implicit: item }"></ng-container> + </ng-template> + </ng-template> +</mat-menu> + +<!-- template to render the leaf nodes --> +<ng-template #leafTmpl let-item> + <button mat-menu-item (click)="itemClicked.emit(item)"> + <ng-container *ngTemplateOutlet="menuTmpl?.templateRef || fallbackMenu; context: { + $implicit: item + }"> + </ng-container> + </button> +</ng-template> + +<ng-template #fallbackContentTmpl> + Fallback Content +</ng-template> + +<ng-template #fallbackMenu let-item> + {{ item.name || 'Item Name' }} +</ng-template> diff --git a/src/components/smartChip/hasSubmenu.pipe.ts b/src/components/smartChip/hasSubmenu.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..32553eb6302e00ee6ae662b20b1950d6128dc76a --- /dev/null +++ b/src/components/smartChip/hasSubmenu.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'hasSubMenu', + pure: true +}) + +export class HasSubMenuPipe<T extends object> implements PipeTransform{ + public transform(item: T, getChildren: (obj: T) => T[]): boolean { + return (getChildren(item) || []).length > 0 + } +} diff --git a/src/components/smartChip/index.ts b/src/components/smartChip/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..fd61c840454ef0455d4a6253fa4b90133d4ddf51 --- /dev/null +++ b/src/components/smartChip/index.ts @@ -0,0 +1,3 @@ +export { SmartChipModule } from "./module" +export { SmartChip } from "./component/smartChip.component" +export { SmartChipContent } from "./smartChip.content.directive" diff --git a/src/components/smartChip/module.ts b/src/components/smartChip/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..00fe423cc79816bf0b9cb871e3c8392aacc09520 --- /dev/null +++ b/src/components/smartChip/module.ts @@ -0,0 +1,31 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { MatRippleModule } from "@angular/material/core"; +import { MatMenuModule } from "@angular/material/menu"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { SmartChip } from "./component/smartChip.component"; +import { HasSubMenuPipe } from "./hasSubmenu.pipe"; +import { SmartChipContent } from "./smartChip.content.directive"; +import { SmartChipMenu } from "./smartChip.menu.directive"; + +@NgModule({ + imports: [ + CommonModule, + MatMenuModule, + BrowserAnimationsModule, + MatRippleModule, + ], + declarations: [ + SmartChipMenu, + SmartChipContent, + SmartChip, + HasSubMenuPipe, + ], + exports: [ + SmartChipMenu, + SmartChipContent, + SmartChip, + ] +}) + +export class SmartChipModule{} diff --git a/src/components/smartChip/smartChip.content.directive.ts b/src/components/smartChip/smartChip.content.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..4026fdf61f22814fe2f7cd05b42662089b82018b --- /dev/null +++ b/src/components/smartChip/smartChip.content.directive.ts @@ -0,0 +1,9 @@ +import { Directive, Inject, TemplateRef } from "@angular/core"; + +@Directive({ + selector: `ng-template[sxplrSmartChipContent]` +}) + +export class SmartChipContent { + constructor(@Inject(TemplateRef) public templateRef: TemplateRef<unknown>){} +} diff --git a/src/components/smartChip/smartChip.menu.directive.ts b/src/components/smartChip/smartChip.menu.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..138e5fdc04012d2b9e9ced5492f997c98228533a --- /dev/null +++ b/src/components/smartChip/smartChip.menu.directive.ts @@ -0,0 +1,9 @@ +import { Directive, Inject, TemplateRef } from "@angular/core"; + +@Directive({ + selector: `ng-template[sxplrSmartChipMenu]` +}) + +export class SmartChipMenu { + constructor(@Inject(TemplateRef) public templateRef: TemplateRef<unknown>){} +} diff --git a/src/components/smartChip/smartChip.stories.ts b/src/components/smartChip/smartChip.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..791c9372e8a71318b0633d00e75ce7de003d7a50 --- /dev/null +++ b/src/components/smartChip/smartChip.stories.ts @@ -0,0 +1,160 @@ +import { CommonModule } from "@angular/common"; +import { Component, Pipe, PipeTransform } from "@angular/core"; +import { MatDividerModule } from "@angular/material/divider"; +import { MatExpansionModule } from "@angular/material/expansion"; +import { Meta, moduleMetadata, Story } from "@storybook/angular"; +import { provideDarkTheme } from "src/atlasComponents/sapi/stories.base"; +import { SmartChipModule } from "./module"; + + +const complex1 = ` +<sxplr-smart-chip [color]="color" [items]="inventory"> + <span *sxplrSmartChipContent>menu example</span> + <ng-template sxplrSmartChipMenu let-inv> + {{ inv.name }} + </ng-template> +</sxplr-smart-chip> +` + +const complex2 = ` + +<sxplr-smart-chip [color]="color" [items]="categories" [getChildren]="getChildren"> + <span *sxplrSmartChipContent>submenu example</span> + <ng-template sxplrSmartChipMenu let-item> + {{ item.categoryName || item.name }} + </ng-template> +</sxplr-smart-chip> +` + +const input = { + complex1, + complex2, +} + +const getHtmlSnippet = (key: string) => ` +<mat-expansion-panel> +<mat-expansion-panel-header> + <mat-panel-title> + Code + </mat-panel-title> +</mat-expansion-panel-header> +<pre> + <code [innerHTML]="input.${key} | showHtmlCode"> + </code> +</pre> +</mat-expansion-panel> +${input[key]} +` + +@Pipe({ + name: 'showHtmlCode', + pure: true +}) +class ShowHtmlCode implements PipeTransform { + public transform(input: string) { + return input + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + } +} + +type Inventory = { + name: string + quantity: number +} + +type Category = { + categoryName: string + inventories: Inventory[] +} + +@Component({ + selector: 'smart-chip-container', + template: ` + <div class="container"> + ${Object.keys(input).map(key => getHtmlSnippet(key)).join('\n<mat-divider></mat-divider>\n')} + </div> + `, + styles: [ + `.mat-divider{ margin: 1rem; } .container{ display: flex; flex-direction: column; }` + ] +}) +class SmartChipContainerCmp{ + color:string + + inventory: Inventory[] = [{ + name: 'banana', + quantity: 10, + }, { + name: 'apple', + quantity: 5, + }] + + categories: Category[] = [{ + categoryName: 'fruits', + inventories: this.inventory + }, { + categoryName: 'processed food', + inventories: [{ + name: 'pizza', + quantity: 1000, + }, { + name: 'kebab', + quantity: 12 + }] + }, { + categoryName: 'xmas food', + inventories: [] + }] + + getChildren(category: Category) { + return category.inventories + } + input = input +} + +export default { + component: SmartChipContainerCmp, + decorators: [ + moduleMetadata({ + imports: [ + CommonModule, + SmartChipModule, + MatExpansionModule, + MatDividerModule, + ], + declarations: [ + ShowHtmlCode, + ], + providers: [ + ...provideDarkTheme, + ] + }) + ] +} as Meta + +const Template: Story<SmartChipContainerCmp> = (args: SmartChipContainerCmp) => { + const { color } = args + return ({ + props: { + color + } + }) +} + +export const Default = Template.bind({}) +Default.args = { + color: 'rgba(206, 221, 247, 1)', +} + + +// export const AtlasEtc = Template.bind({}) +// AtlasEtc.args = { +// color: 'rgba(206, 221, 247, 1)' +// } +// AtlasEtc.loaders = [ +// loadAtlasEtcData +// ] diff --git a/src/extra_styles.css b/src/extra_styles.css index 913f83b9dac50b67b5a00387c726933aeb1ed583..be7921bd06d387cb19123c41f97c0b7834bfdadb 100644 --- a/src/extra_styles.css +++ b/src/extra_styles.css @@ -877,11 +877,6 @@ how-to-cite img width: 100%; } -.mat-menu-panel.parc-smart-chip-menu-panel -{ - max-width: 100vw; -} - /* this is required to set snackbar to be none-interactive */ /* ignore css lint error, they don't know what they are talking about */ .cdk-overlay-pane:has(> .sxplr-pe-none) diff --git a/src/state/atlasSelection/actions.ts b/src/state/atlasSelection/actions.ts index 8787234b909287c331473dfe6d0cceb578375111..7cf6d778a8549575c94b8244266ad5cc213cb0a8 100644 --- a/src/state/atlasSelection/actions.ts +++ b/src/state/atlasSelection/actions.ts @@ -116,10 +116,6 @@ export const selectATPById = createAction( }>() ) -export const clearNonBaseParcLayer = createAction( - `${nameSpace} clearNonBaseParcLayer` -) - export const clearStandAloneVolumes = createAction( `${nameSpace} clearStandAloneVolumes` ) diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts index 2c95b75602df2ec483983d910f61ceccdcc7525d..a84da1c3dfaa44031c79b46a04dfe5334522e434 100644 --- a/src/state/atlasSelection/effects.ts +++ b/src/state/atlasSelection/effects.ts @@ -6,10 +6,7 @@ import { SAPI, SapiAtlasModel, SapiParcellationModel, SAPIRegion, SapiRegionMode import * as mainActions from "../actions" import { select, Store } from "@ngrx/store"; import { selectors, actions } from '.' -import { fromRootStore } from "./util"; import { AtlasSelectionState } from "./const" -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"; import { InterSpaceCoordXformSvc } from "src/atlasComponents/sapi/core/space/interSpaceCoordXform.service"; @@ -295,25 +292,6 @@ export class Effect { })) )) - onNonBaseLayerRemoval = createEffect(() => this.action.pipe( - ofType(actions.clearNonBaseParcLayer), - switchMapTo( - this.store.pipe( - fromRootStore.allAvailParcs(this.sapiSvc), - map(parcs => { - const baseLayers = parcs.filter(this.parcellationIsBaseLayerPipe.transform) - const newestLayer = this.orderParcellationByVersionPipe.transform(baseLayers) - return actions.selectParcellation({ - parcellation: newestLayer - }) - }) - ) - ) - )) - - private parcellationIsBaseLayerPipe = new ParcellationIsBaseLayer() - private orderParcellationByVersionPipe = new OrderParcellationByVersionPipe() - onClearStandAloneVolumes = createEffect(() => this.action.pipe( ofType(actions.clearStandAloneVolumes), mapTo(actions.setStandAloneVolumes({ diff --git a/src/state/atlasSelection/util.ts b/src/state/atlasSelection/util.ts index cf64466bb4375a6ffa06bb50ec99fea60ee5b8fd..2ee06893cafa819dc5a13784f74a9425f6645804 100644 --- a/src/state/atlasSelection/util.ts +++ b/src/state/atlasSelection/util.ts @@ -1,32 +1,42 @@ import { createSelector, select } from "@ngrx/store"; -import { forkJoin, pipe } from "rxjs"; +import { forkJoin, of, pipe } from "rxjs"; import { distinctUntilChanged, map, switchMap } from "rxjs/operators"; import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi"; import { jsonEqual } from "src/util/json"; 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 allAvailSpaces = (sapi: SAPI) => { + return pipe( + select(selectors.selectedAtlas), + switchMap(atlas => atlas + ? forkJoin( + atlas.spaces.map(spcWId => sapi.getSpaceDetail(atlas["@id"], spcWId["@id"])) + ) + : of([]) + ) ) -) +} const allAvailParcs = (sapi: SAPI) => pipe( select(selectors.selectedAtlas), - switchMap(atlas => - forkJoin( + switchMap(atlas => atlas + ? forkJoin( atlas.parcellations.map(parcWId => sapi.getParcDetail(atlas["@id"], parcWId["@id"])) ) + : of([]) ) ) const allAvailSpacesParcs = (sapi: SAPI) => pipe( select(selectors.selectedAtlas), - switchMap(atlas => - forkJoin({ + switchMap(atlas => atlas + ? forkJoin({ spaces: atlas.spaces.map(spcWId => sapi.getSpaceDetail(atlas["@id"], spcWId["@id"])), parcellation: atlas.parcellations.map(parcWId => sapi.getParcDetail(atlas["@id"], parcWId["@id"])), }) + : of({ + spaces: [], + parcellation: [] + }) ) ) diff --git a/src/ui/dialogInfo/dialog.directive.ts b/src/ui/dialogInfo/dialog.directive.ts index 63d886a6ef5fa172ea1a8cf17abb5587005a5751..eccb689f25f5e35c65375e3d5ebe32124f16c2b9 100644 --- a/src/ui/dialogInfo/dialog.directive.ts +++ b/src/ui/dialogInfo/dialog.directive.ts @@ -1,6 +1,6 @@ import { Directive, HostListener, Input, TemplateRef } from "@angular/core"; import { MatDialog, MatDialogConfig } from "@angular/material/dialog"; -import { MatSnackBar } from "@angular/material/snack-bar"; +import { DialogFallbackCmp } from "./tmpl/tmpl.component" type DialogSize = 's' | 'm' | 'l' | 'xl' @@ -39,20 +39,13 @@ export class DialogDirective{ @Input('sxplr-dialog-data') data: unknown - constructor( - private matDialog: MatDialog, - private snackbar: MatSnackBar, - ){ - } + constructor(private matDialog: MatDialog){} @HostListener('click') onClick(){ - if (!this.templateRef) { - return this.snackbar.open(`Cannot show dialog. sxplr-dialog template not provided`) - } - this.matDialog.open(this.templateRef, { + this.matDialog.open(this.templateRef || DialogFallbackCmp, { data: this.data, ...(sizeDict[this.size] || {}) }) } -} \ No newline at end of file +} diff --git a/src/ui/dialogInfo/index.ts b/src/ui/dialogInfo/index.ts index 354dde0cbdafd34674a303cfa52d0dd45aa3487f..8eae65c028d1384eb09ae8044ff9d6254eb43d95 100644 --- a/src/ui/dialogInfo/index.ts +++ b/src/ui/dialogInfo/index.ts @@ -1 +1,3 @@ -export { DialogDirective } from "./dialog.directive" \ No newline at end of file +export { DialogDirective } from "./dialog.directive" +export { DialogModule } from "./module" +export { DialogFallbackCmp } from "./tmpl/tmpl.component" diff --git a/src/ui/dialogInfo/module.ts b/src/ui/dialogInfo/module.ts index de09c12a621f36ca70b46354e7eda5a040802b70..82702efff50d103ec252164912342230bf830884 100644 --- a/src/ui/dialogInfo/module.ts +++ b/src/ui/dialogInfo/module.ts @@ -1,15 +1,23 @@ +import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; +import { MatButtonModule } from "@angular/material/button"; import { MatDialogModule } from "@angular/material/dialog"; -import { MatSnackBarModule } from "@angular/material/snack-bar"; +import { MarkdownModule } from "src/components/markdown"; +import { StrictLocalModule } from "src/strictLocal"; import { DialogDirective } from "./dialog.directive" +import { DialogFallbackCmp } from "./tmpl/tmpl.component"; @NgModule({ imports: [ - MatSnackBarModule, + CommonModule, MatDialogModule, + MatButtonModule, + MarkdownModule, + StrictLocalModule, ], declarations: [ DialogDirective, + DialogFallbackCmp, ], exports: [ DialogDirective, diff --git a/src/ui/dialogInfo/tmpl/tmpl.component.ts b/src/ui/dialogInfo/tmpl/tmpl.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5e2f505d75ace5f171b1f58094bbcd61f58bc49 --- /dev/null +++ b/src/ui/dialogInfo/tmpl/tmpl.component.ts @@ -0,0 +1,25 @@ +import { Component, Inject } from "@angular/core"; +import { MAT_DIALOG_DATA } from "@angular/material/dialog"; + +export type FallBackData = { + title: string + actions?: string[] + desc?: string + descMd?: string +} + +@Component({ + selector: 'sxplr-dialog-fallback-tmpl', + templateUrl: './tmpl.template.html', + styleUrls: [ + './tmpl.style.css' + ], +}) + +export class DialogFallbackCmp { + constructor( + @Inject(MAT_DIALOG_DATA) public data: FallBackData + ){ + + } +} diff --git a/src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.style.css b/src/ui/dialogInfo/tmpl/tmpl.stories.ts similarity index 100% rename from src/atlasComponents/sapiViews/core/atlas/dropdownAtlasSelector/dropdownAtlasSelector.style.css rename to src/ui/dialogInfo/tmpl/tmpl.stories.ts diff --git a/src/ui/dialogInfo/tmpl/tmpl.style.css b/src/ui/dialogInfo/tmpl/tmpl.style.css new file mode 100644 index 0000000000000000000000000000000000000000..63e67f07fea53d98e2523f373fa3e82bb312f001 --- /dev/null +++ b/src/ui/dialogInfo/tmpl/tmpl.style.css @@ -0,0 +1,4 @@ +mat-dialog-actions a i +{ + margin-right: 0.5rem; +} diff --git a/src/ui/dialogInfo/tmpl/tmpl.template.html b/src/ui/dialogInfo/tmpl/tmpl.template.html new file mode 100644 index 0000000000000000000000000000000000000000..31807ec667f1fc53fb4d35bed82e9ed36c77f355 --- /dev/null +++ b/src/ui/dialogInfo/tmpl/tmpl.template.html @@ -0,0 +1,40 @@ +<h1 *ngIf="data.title" mat-dialog-title> + {{ data.title }} +</h1> + +<div *ngIf="data.descMd || data.desc" mat-dialog-content class="mat-body"> + <ng-template [ngIf]="data.desc"> + {{ data.desc }} + </ng-template> + + <markdown-dom *ngIf="data.descMd" [markdown]="data.descMd"> + </markdown-dom> +</div> + + +<mat-dialog-actions align="start"> + + <ng-template ngFor [ngForOf]="data.actions || []" let-action> + <a *ngIf="action.startsWith('http'); else defaultActionTmpl" + [href]="action" + sxplr-hide-when-local + target="_blank" + mat-raised-button + color="primary"> + <i class="fas fa-external-link-alt"></i> + <span>Detail</span> + </a> + + <ng-template #defaultActionTmpl> + <button mat-raised-button + color="primary" + [mat-dialog-close]="action"> + {{ action }} + </button> + </ng-template> + </ng-template> + + <button mat-dialog-close mat-button> + Close + </button> +</mat-dialog-actions> diff --git a/src/ui/help/helpOnePager/helpOnePager.component.ts b/src/ui/help/helpOnePager/helpOnePager.component.ts index fb4d54137af64cc245c56e30beeaea1564417311..99bae8ca42978040304f88bde06000d0993ef6e4 100644 --- a/src/ui/help/helpOnePager/helpOnePager.component.ts +++ b/src/ui/help/helpOnePager/helpOnePager.component.ts @@ -17,13 +17,11 @@ const { default: QUICK_STARTER } = require('!!raw-loader!common/helpOnePager.md' export class HelpOnePager{ public ARIA_LABELS = ARIA_LABELS public QUICK_STARTER_MD = QUICK_STARTER - public extQuickStarter: string + public extQuickStarter: string = `quickstart` public userDoc: string constructor( private dialog: MatDialog, - ){ - this.extQuickStarter = `quickstart.html` - } + ){} howToCite(){ this.dialog.open(HowToCite) diff --git a/src/util/recursivePartial.ts b/src/util/recursivePartial.ts new file mode 100644 index 0000000000000000000000000000000000000000..6056208de8872d95a3e7fb8e143116bcaee463d4 --- /dev/null +++ b/src/util/recursivePartial.ts @@ -0,0 +1,3 @@ +export type RecursivePartial<T> = Partial<{ + [K in keyof T]: RecursivePartial<T[K]> +}> diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index e7107258f1c109ddb6f49266dc710c326ef4cbf9..597e574ea77d5f1d1a350e9ae236663d09e25f2a 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -29,6 +29,7 @@ import { ShareModule } from "src/share"; import { LeapModule } from "./leap/module"; import { environment } from "src/environments/environment" +import { ATPSelectorModule } from "src/atlasComponents/sapiViews/core/rich/ATPSelector"; @NgModule({ imports: [ @@ -50,6 +51,7 @@ import { environment } from "src/environments/environment" DialogModule, MouseoverModule, ShareModule, + ATPSelectorModule, ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : []) ], declarations: [ diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index f0b32efea3c8233a648e0277bfd38ddeaebe2074..cef42ef50ec244b4f503d545935bc9548d167d4d 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -8,7 +8,7 @@ import { IQuickTourData } from "src/ui/quickTour"; import { EnumViewerEvt, TContextArg, TSupportedViewers, TViewerEvent } from "../viewer.interface"; import { ContextMenuService, TContextMenuReg } from "src/contextMenuModule"; import { DialogService } from "src/services/dialogService.service"; -import { SAPI, SapiRegionModel } from "src/atlasComponents/sapi"; +import {SAPI, SapiAtlasModel, SapiRegionModel} from "src/atlasComponents/sapi"; import { atlasSelection, userInteraction, } from "src/state"; import { SapiSpatialFeatureModel, SapiFeatureModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type"; import { getUuid } from "src/util/fn"; @@ -94,6 +94,8 @@ export class ViewerCmp implements OnDestroy { shareReplay(1) ) + public fetchedAtlases$: Observable<SapiAtlasModel[]> = this.sapi.atlases$ + public selectedAtlas$ = this.selectedATP.pipe( map(({ atlas }) => atlas) ) @@ -104,10 +106,6 @@ export class ViewerCmp implements OnDestroy { map(({ parcellation }) => parcellation) ) - public allAvailableParcellations$ = this.store$.pipe( - atlasSelection.fromRootStore.allAvailParcs(this.sapi) - ) - public selectedRegions$ = this.store$.pipe( select(atlasSelection.selectors.selectedRegions), ) @@ -400,18 +398,6 @@ export class ViewerCmp implements OnDestroy { ) } - onDismissNonbaseLayer(): void{ - this.store$.dispatch( - atlasSelection.actions.clearNonBaseParcLayer() - ) - } - onSelectParcellation(parcellation: SapiParcellationModel): void{ - this.store$.dispatch( - atlasSelection.actions.selectParcellation({ - parcellation - }) - ) - } navigateTo(position: number[]): void { this.store$.dispatch( atlasSelection.actions.navigateTo({ diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css index e5a409a28036bfe88dab0ce9457272ed7647cf60..ce7549bda645ca175e0c23c25f204291e3847db9 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.style.css +++ b/src/viewerModule/viewerCmp/viewerCmp.style.css @@ -126,7 +126,7 @@ mat-list[dense].contextual-block .region-chip-suffix { - transform: scale(0.7); + transform: scale(0.9); margin-right: -0.25rem; } diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index b0e06fdaab9dcdca386cf69563c8334f2313336e..6774d54ae5b2d38cf04d0eecf13412f0ca98092e 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -376,13 +376,6 @@ [viewerLoaded]="viewerLoaded"> </top-menu-cmp> - <sxplr-sapiviews-core-atlas-dropdown-selector - class="v-align-top sxplr-pt-2 pe-all mt-2 sxplr-custom-cmp bg card m-2 mat-elevation-z2 d-inline-block" - quick-tour - [quick-tour-description]="quickTourAtlasSelector.description" - [quick-tour-order]="quickTourAtlasSelector.order"> - </sxplr-sapiviews-core-atlas-dropdown-selector> - </ng-template> @@ -403,32 +396,17 @@ <!-- bottom left --> <ng-template #bottomLeftTmpl let-showFullSideNav="showFullSideNav"> - <!-- atlas selector --> - <sxplr-sapiviews-core-atlas-tmplparcselector - *ngIf="viewerLoaded && !(isStandaloneVolumes$ | async)" - [iav-key-listener]="[{'type': 'keydown', 'key': 'Escape'}]" - (iav-key-event)="tmplParcSelector.closeSelector()" - (iav-outsideClick)="tmplParcSelector.closeSelector()" - #tmplParcSelector="sxplrSapiViewsCoreAtlasTmplparcselector"> - </sxplr-sapiviews-core-atlas-tmplparcselector> - <!-- scroll container --> <div class="sxplr-d-inline-flex sxplr-flex-wrap-nowrap sxplr-mxw-80vw sxplr-pe-all sxplr-of-x-auto - sxplr-of-y-hidden"> - - <!-- selected parcellation chip --> - <sxplr-sapiviews-core-parcellation-smartchip - class="sxplr-z-2 sxplr-mr-1" - [sxplr-sapiviews-core-parcellation-smartchip-parcellation]="parcellationSelected$ | async" - [sxplr-sapiviews-core-parcellation-smartchip-all-parcellations]="allAvailableParcellations$ | async" - (sxplr-sapiviews-core-parcellation-smartchip-dismiss-nonbase-layer)="onDismissNonbaseLayer()" - (sxplr-sapiviews-core-parcellation-smartchip-select-parcellation)="onSelectParcellation($event)" - > - </sxplr-sapiviews-core-parcellation-smartchip> + sxplr-of-y-hidden + sxplr-align-items-center"> + + <sxplr-wrapper-atp-selector class="sxplr-z-2"> + </sxplr-wrapper-atp-selector> <!-- selected region chip --> <sxplr-sapiviews-core-region-region-chip @@ -444,9 +422,7 @@ </div> <div suffix class="region-chip-suffix"> - <button mat-mini-fab - color="primary" - iav-stop="mousedown click" + <button mat-icon-button iav-stop="mousedown click" (click)="clearRoi()"> <i class="fas fa-times"></i> </button> @@ -515,28 +491,23 @@ <ng-template #autocompleteTmpl let-showTour="showTour"> <div class="sxplr-custom-cmp bg card ml-2 mr-2 mat-elevation-z8 pe-all auto-complete-container"> - <ng-template #selectedRegionCheckTmpl let-region> - <ng-template #fallbackTmpl> - <button mat-icon-button - class="sxplr-mt-a sxplr-mb-a"> - <i class="far fa-square"></i> - </button> - </ng-template> - - <button *ngIf="selectedRegions$ | async | includes : region; else fallbackTmpl" - mat-icon-button - color="primary" - class="sxplr-mt-a sxplr-mb-a"> - <i class="far fa-check-square"></i> - </button> - </ng-template> - <sxplr-sapiviews-core-rich-regionlistsearch [sxplr-sapiviews-core-rich-regionlistsearch-regions]="allAvailableRegions$ | async" [sxplr-sapiviews-core-rich-regionlistsearch-current-search]="selectedRegions$ | async | getProperty : 0 | getProperty : 'name'" - [sxplr-sapiviews-core-rich-regionlistsearch-region-template-ref]="selectedRegionCheckTmpl" (sxplr-sapiviews-core-rich-regionlistsearch-region-select)="selectRoi($event)"> - + <ng-template regionTemplate let-region> + <div class="sxplr-d-flex"> + <button + mat-icon-button + class="sxplr-mt-a sxplr-mb-a"> + <i [ngClass]="(selectedRegions$ | async | includes : region) ? 'fa-circle' : 'fa-none'" class="fas"></i> + </button> + + <sxplr-sapiviews-core-region-region-list-item + [sxplr-sapiviews-core-region-region]="region"> + </sxplr-sapiviews-core-region-region-list-item> + </div> + </ng-template> <button mat-icon-button search-input-suffix *ngIf="selectedRegions$ | async | getProperty : 'length'"