diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts index d8b1d2306ae27b9215fd7ca9ffa0f43c9208e0a3..585077f4b074579a9d96acf4ed725b4aa025f8a9 100644 --- a/src/atlasComponents/sapi/core/sapiRegion.ts +++ b/src/atlasComponents/sapi/core/sapiRegion.ts @@ -4,7 +4,7 @@ import { strToRgb, hexToRgb } from 'common/util' export class SAPIRegion{ - static GetDisplayColor(region: SapiRegionModel){ + static GetDisplayColor(region: SapiRegionModel): [number, number, number]{ if (!region) { throw new Error(`region must be provided!`) } diff --git a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html index 10a48ac5e779d57c46fba2d37a84b64ba6a67571..324120aa0f23b0d2f7504da98e2668c776c4fed0 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html +++ b/src/atlasComponents/sapiViews/core/parcellation/tile/parcellation.tile.template.html @@ -11,7 +11,7 @@ *ngFor="let parc of subParcellations"> <tile-cmp *ngIf="parc" - class="iv-custom-comp text" + class="sxplr-custom-cmp text" [tile-text]="parc.name" [tile-image-src]="parc | previewParcellationUrl" [tile-selected]="selected" diff --git a/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.template.html b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.template.html index 03db372cf5c31f39529a87d07bf1efa8c4bdb9c6..9b8262ee0441e16ce49b34d4a6d43ea9b60833b8 100644 --- a/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.template.html +++ b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.template.html @@ -23,7 +23,7 @@ }"> <mat-chip (click)="onClick($event)" - class="iv-custom-comp text" + class="sxplr-custom-cmp text" [style.backgroundColor]="regionRgbString" > <ng-template [ngTemplateOutlet]="prefixTmpl"></ng-template> diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html index 23d94a6cf393179dd4c553d54d23dc90a4facf24..65d3667d2c67e7a5c2d3379fbc775a0bb038bd1d 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html @@ -23,7 +23,7 @@ <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template> - <mat-card-title class="iv-custom-comp text"> + <mat-card-title class="sxplr-custom-cmp text"> {{ region.name }} </mat-card-title> diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts index b0daa753edfc2c14148e87249d01842be4e6d2d4..fa7fc0cd69178b883ed732dee3240b7e82738f85 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts +++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts @@ -36,6 +36,9 @@ export class SapiViewsCoreRichRegionsHierarchy { ) } + @Input('sxplr-sapiviews-core-rich-regionshierarchy-accent-regions') + accentedRegions: SapiRegionModel[] = [] + @Input('sxplr-sapiviews-core-rich-regionshierarchy-placeholder') placeholderText: string = 'Search all regions' diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html index 226c78741fcbe23ed773c903d88f0ed425816759..9c085e07cef508f4de0506fb740705c341616790 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html +++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html @@ -1,4 +1,4 @@ -<form class="iv-custom-comp text sxplr-w-100"> +<form class="sxplr-custom-cmp text sxplr-w-100"> <mat-form-field class="sxplr-w-100"> <input [placeholder]="placeholderText" @@ -18,6 +18,9 @@ <ng-template #tmplRef let-region> <div class="mat-body sxplr-d-flex sxplr-align-items-center sxplr-h-100 region-tmpl" + [ngClass]="{ + 'sxplr-custom-cmp accent': accentedRegions | includes : region + }" [innerHTML]="region.name | hightlightPipe : searchTerm"> </div> </ng-template> @@ -27,5 +30,6 @@ [sxplr-flat-hierarchy-is-parent]="isParent" [sxplr-flat-hierarchy-render-node-tmpl]="tmplRef" [sxplr-flat-hierarchy-tree-view-expand-on-init]="true" + sxplr-flat-hierarchy-tree-view-lineheight="24" (sxplr-flat-hierarchy-tree-view-node-clicked)="nodeClicked.emit($event)"> </sxplr-flat-hierarchy-tree-view> \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts index f2bb0d8b800b8177483f99eb291083a294e8d2de..58be89382e45d671cca1997a32e214c1bf7bb21c 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts +++ b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts @@ -70,10 +70,6 @@ export class SapiViewsCoreRichRegionListSearch { return region?.name || '' } - onInputFocus(){ - this.regions[0].name.includes('left') - } - optionSelected(opt: MatAutocompleteSelectedEvent) { const selectedRegion = opt.option.value as SapiRegionModel this.onOptionSelected.emit(selectedRegion) diff --git a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html index 31aa64f230ce4bf3dbbb00201472a86006b6426e..9cdee5d47c0e7a92a5b3165054b2ad1cc4184678 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html +++ b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html @@ -1,4 +1,4 @@ -<form class="iv-custom-comp text sxplr-w-100"> +<form class="sxplr-custom-cmp text sxplr-w-100"> <mat-form-field class="sxplr-w-100" floatLabel="never"> @@ -7,7 +7,6 @@ [value]="currentSearch" #trigger="matAutocompleteTrigger" type="text" - (focus)="onInputFocus()" matInput name="searchTerm" [attr.aria-label]="ARIA_LABELS.TEXT_INPUT_SEARCH_REGION" diff --git a/src/atlasComponents/userAnnotations/tools/line/line.template.html b/src/atlasComponents/userAnnotations/tools/line/line.template.html index e0b3b06a78c4ebb58e26e4d9254f6c59e74a2ca3..3f72bd1d7a590f34b3f20636a44a7ff46f9ce2e2 100644 --- a/src/atlasComponents/userAnnotations/tools/line/line.template.html +++ b/src/atlasComponents/userAnnotations/tools/line/line.template.html @@ -36,10 +36,10 @@ </div> <mat-menu #exportMenu="matMenu" xPosition="before"> - <div class="iv-custom-comp card text" + <div class="sxplr-custom-cmp card text" iav-stop="click"> - <div class="iv-custom-comp text"> + <div class="sxplr-custom-cmp text"> <textarea-copy-export [textarea-copy-export-label]="useFormat" [textarea-copy-export-text]="updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat" diff --git a/src/atlasComponents/userAnnotations/tools/point/point.template.html b/src/atlasComponents/userAnnotations/tools/point/point.template.html index b1b5ab285e11bc2926f3a00d4b8b0b39b4ab0579..805ab08767a59c9f202ae053b814150678bef881 100644 --- a/src/atlasComponents/userAnnotations/tools/point/point.template.html +++ b/src/atlasComponents/userAnnotations/tools/point/point.template.html @@ -35,10 +35,10 @@ </div> <mat-menu #exportMenu="matMenu" xPosition="before"> - <div class="iv-custom-comp card text" + <div class="sxplr-custom-cmp card text" iav-stop="click"> - <div class="iv-custom-comp text"> + <div class="sxplr-custom-cmp text"> <textarea-copy-export [textarea-copy-export-label]="useFormat" diff --git a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html index ea8665437974b0c1d29b31de294b72ed426f7095..6827915b80ecfb04504184c6519b9670cfa62cf0 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html @@ -36,10 +36,10 @@ </div> <mat-menu #exportMenu="matMenu" xPosition="before"> - <div class="iv-custom-comp card text" + <div class="sxplr-custom-cmp card text" iav-stop="click"> - <div class="iv-custom-comp text"> + <div class="sxplr-custom-cmp text"> <textarea-copy-export [textarea-copy-export-label]="useFormat" [textarea-copy-export-text]="updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat" diff --git a/src/components/flatHierarchy/treeView/treeView.component.ts b/src/components/flatHierarchy/treeView/treeView.component.ts index 18520093110b0f00809fc7e8ddd0c9282a8f9a5d..cab03376d07031cf42a1f2bc137edf8ef5c9a912 100644 --- a/src/components/flatHierarchy/treeView/treeView.component.ts +++ b/src/components/flatHierarchy/treeView/treeView.component.ts @@ -15,7 +15,7 @@ import { TreeNode } from "../const" export class SxplrFlatHierarchyTreeView<T extends object> implements OnChanges{ @HostBinding('class') - class = 'iv-custom-comp' + class = 'sxplr-custom-cmp' @Input('sxplr-flat-hierarchy-nodes') nodes: T[] = [] diff --git a/src/components/tile/tile.template.html b/src/components/tile/tile.template.html index d56ad788ba2b1bc0043f19df11f44e51c111f23d..1187193efd379373fabdb461f9f4883251fb5e41 100644 --- a/src/components/tile/tile.template.html +++ b/src/components/tile/tile.template.html @@ -9,7 +9,7 @@ backgroundColor: darktheme ? 'white' : 'black', color: darktheme ? 'black': 'white' }" - class="mat-elevation-z2 iv-custom-comp info-button"> + class="mat-elevation-z2 sxplr-custom-cmp info-button"> <small> <i class="fas fa-info"></i> </small> @@ -23,7 +23,7 @@ 'darktheme': darktheme, 'lighttheme': !darktheme }"> - <i class="fas fa-folder folder-container fa-2x iv-custom-comp text"></i> + <i class="fas fa-folder folder-container fa-2x sxplr-custom-cmp text"></i> </div> <img [src]="tileImgSrc" diff --git a/src/components/vButton/vButton.component.ts b/src/components/vButton/vButton.component.ts index f2720905b3ff3bb9ff75a5b01d9add65643b552e..3dbd98aa23dc24da26dfc564e52859cdd99ea5b7 100644 --- a/src/components/vButton/vButton.component.ts +++ b/src/components/vButton/vButton.component.ts @@ -19,7 +19,7 @@ export class IAVVerticalButton{ private color$ = new Subject<TIVBColor>() public class$ = this.color$.pipe( startWith('default'), - map(colorCls => `d-flex flex-column align-items-center iv-custom-comp ${colorCls} h-100`) + map(colorCls => `d-flex flex-column align-items-center sxplr-custom-cmp ${colorCls} h-100`) ) @Input() diff --git a/src/overwrite.scss b/src/overwrite.scss index 82efbd6c0103fcdb55177e984cc353f3b8b2a4fe..2a2e2468ea041d89ff6f1c2ef81d8de29c55adb6 100644 --- a/src/overwrite.scss +++ b/src/overwrite.scss @@ -244,3 +244,16 @@ $flex-wrap-vars: nowrap, wrap, wrap-reverse; flex-wrap: $flex-wrap-var; } } +$flex-directions: row,column; +@each $flex-direction in $flex-directions { + .#{$nsp}-flex-#{$flex-direction} { + flex-direction: $flex-direction; + } +} +.#{$nsp}-flex-var { + flex: 1 1 0px; +} + +.#{$nsp}-flex-static { + flex: 0 0 auto; +} \ No newline at end of file diff --git a/src/screenshot/screenshotCmp/screenshot.template.html b/src/screenshot/screenshotCmp/screenshot.template.html index ad7143b67c6c49b75f8341450b70633e997149eb..ee570082170d036ee4429f425ce4f0d112255d79 100644 --- a/src/screenshot/screenshotCmp/screenshot.template.html +++ b/src/screenshot/screenshotCmp/screenshot.template.html @@ -2,7 +2,7 @@ <ng-template #placeholderTmpl> <div class="d-flex align-items-center justify-content-center w-100 h-100 cover"> - <span class="iv-custom-comp text"> + <span class="sxplr-custom-cmp text"> <h2 class="mat-h2 text-center"> <span> Drag a box to take a screenshot or diff --git a/src/sharedModules/angularMaterial.module.ts b/src/sharedModules/angularMaterial.module.ts index b2d27d67a09a1ff277802b4318acec9e54281784..3c0a913f69beda1a06f24535f4326272e48eb519 100644 --- a/src/sharedModules/angularMaterial.module.ts +++ b/src/sharedModules/angularMaterial.module.ts @@ -97,7 +97,6 @@ const defaultDialogOption: MatDialogConfig = new MatDialogConfig() provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { ...defaultDialogOption, - panelClass: 'iav-dialog-class', }, }], }) diff --git a/src/state/atlasAppearance/const.ts b/src/state/atlasAppearance/const.ts index 17ebe814184485cbdc24dd05553f68c8397c1cf1..c6c24f31b6ffc7e8bebf9647f9b0b4f538c0f710 100644 --- a/src/state/atlasAppearance/const.ts +++ b/src/state/atlasAppearance/const.ts @@ -1,4 +1,4 @@ -import { SAPIRegion } from "src/atlasComponents/sapi/core" +import { SapiRegionModel } from "src/atlasComponents/sapi" export const nameSpace = `[state.atlasAppearance]` type CustomLayerBase = { @@ -7,7 +7,20 @@ type CustomLayerBase = { export type ColorMapCustomLayer = { clType: 'customlayer/colormap' | 'baselayer/colormap' - colormap: WeakMap<SAPIRegion, number[]> + colormap: WeakMap<SapiRegionModel, number[]> +} & CustomLayerBase + +export type ThreeSurferCustomLayer = { + clType: 'baselayer/threesurfer' + source: string + laterality: 'left' | 'right' + name: string +} & CustomLayerBase + +export type ThreeSurferCustomLabelLayer = { + clType: 'baselayer/threesurfer-label' + source: string + laterality: 'left' | 'right' } & CustomLayerBase export type NgLayerCustomLayer = { @@ -36,4 +49,4 @@ export type NgLayerCustomLayer = { * - clType facilitates viewer on how to interprete the custom layer * - id allows custom layer to be removed, if necessary */ -export type CustomLayer = ColorMapCustomLayer | NgLayerCustomLayer +export type CustomLayer = ColorMapCustomLayer | NgLayerCustomLayer | ThreeSurferCustomLayer | ThreeSurferCustomLabelLayer diff --git a/src/state/atlasSelection/actions.ts b/src/state/atlasSelection/actions.ts index 14504de34a7a5afbf92da526fb356c79d706a779..8e8ecf2e80f0d6724a4a13a3fe2c2ff9045cbcc0 100644 --- a/src/state/atlasSelection/actions.ts +++ b/src/state/atlasSelection/actions.ts @@ -30,8 +30,15 @@ export const setSelectedParcellationAllRegions = createAction( }>() ) -export const selectRegions = createAction( - `${nameSpace} selectRegions`, +export const selectRegion = createAction( + `${nameSpace} selectRegion`, + props<{ + region: SapiRegionModel + }>() +) + +export const setSelectedRegions = createAction( + `${nameSpace} setSelectedRegions`, props<{ regions: SapiRegionModel[] }>() diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts index a4b4800eb8561038cba0ddfdcb61e7a5115e0fc4..7620569b11e05d28463d7ee80f4bac482492cdb5 100644 --- a/src/state/atlasSelection/effects.ts +++ b/src/state/atlasSelection/effects.ts @@ -1,14 +1,15 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; -import { forkJoin, from, merge, of } from "rxjs"; +import { forkJoin, merge, of } from "rxjs"; import { filter, map, mapTo, switchMap, switchMapTo, withLatestFrom } from "rxjs/operators"; -import { SAPI } from "src/atlasComponents/sapi"; +import { SAPI, SAPIRegion, SapiRegionModel } from "src/atlasComponents/sapi"; import * as mainActions from "../actions" import { select, Store } from "@ngrx/store"; import { selectors, actions } from '.' import { fromRootStore } from "./util"; import { ParcellationIsBaseLayer } from "src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe"; import { OrderParcellationByVersionPipe } from "src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe"; +import { atlasAppearance } from ".."; @Injectable() export class Effect { @@ -62,6 +63,38 @@ export class Effect { ) )) + onATPSelectionClearBaseLayerColorMap = createEffect(() => this.store.pipe( + select(selectors.selectedParcAllRegions), + withLatestFrom( + this.store.pipe( + select(atlasAppearance.selectors.customLayers), + map(layers => layers.filter(l => l.clType === "baselayer/colormap")) + ) + ), + switchMap(([regions, layers]) => { + const map = new WeakMap<SapiRegionModel, number[]>() + for (const region of regions) { + map.set(region, SAPIRegion.GetDisplayColor(region)) + } + const actions = [ + ...layers.map(({ id }) => + atlasAppearance.actions.removeCustomLayer({ + id + }) + ), + atlasAppearance.actions.addCustomLayer({ + customLayer: { + clType: "baselayer/colormap", + id: 'base-colormap-id', + colormap: map + } + }) + ] + return of(...actions) + }) + )) + + onAtlasSelClearStandAloneVolumes = createEffect(() => this.action.pipe( ofType(actions.selectAtlas), mapTo(actions.setStandAloneVolumes({ @@ -71,7 +104,7 @@ export class Effect { onClearRegion = createEffect(() => this.action.pipe( ofType(actions.clearSelectedRegions), - mapTo(actions.selectRegions({ + mapTo(actions.setSelectedRegions({ regions: [] })) )) @@ -146,7 +179,7 @@ export class Effect { ).pipe( switchMapTo( of( - actions.selectRegions({ + actions.setSelectedRegions({ regions: [] }), actions.setSelectedParcellationAllRegions({ @@ -166,7 +199,7 @@ export class Effect { map(([ { region }, regions ]) => { const selectedRegionsIndicies = regions.map(r => r["@id"]) const roiIndex = selectedRegionsIndicies.indexOf(region["@id"]) - return actions.selectRegions({ + return actions.setSelectedRegions({ regions: roiIndex >= 0 ? [...regions.slice(0, roiIndex), ...regions.slice(roiIndex + 1)] : [...regions, region] diff --git a/src/state/atlasSelection/store.ts b/src/state/atlasSelection/store.ts index 7332711b69be6094ca9708e0945f958627921b6f..d051a5475cbd806ae2a7a8535ce7e0878926f33f 100644 --- a/src/state/atlasSelection/store.ts +++ b/src/state/atlasSelection/store.ts @@ -79,7 +79,31 @@ const reducer = createReducer( } ), on( - actions.selectRegions, + actions.selectRegion, + (state, { region }) => { + /** + * if roi does not have visualizedIn defined + * or internal identifier + * + * ignore + */ + if ( + !region.hasAnnotation?.visualizedIn + && region.hasAnnotation?.internalIdentifier === 'unknown' + ) { + return { ...state } + } + const selected = state.selectedRegions.includes(region) + return { + ...state, + selectedRegions: selected + ? [ ] + : [ region ] + } + } + ), + on( + actions.setSelectedRegions, (state, { regions }) => { return { ...state, diff --git a/src/theme.scss b/src/theme.scss index c7998b3816da0494de2e33fad025ad564e9b7cf9..6f0dcf85a110b9d810a7fd9f681f0c791f059783 100644 --- a/src/theme.scss +++ b/src/theme.scss @@ -13,8 +13,8 @@ $accent: map-get($color-config, accent); $warn: map-get($color-config, warn); - [iv-custom-comp], - .iv-custom-comp + [sxplr-custom-cmp], + .sxplr-custom-cmp { color: mat.get-color-from-palette($foreground, text); diff --git a/src/ui/config/configCmp/config.template.html b/src/ui/config/configCmp/config.template.html index 1972951cd1030eda31471e9b19aab33d95f0ff2a..f00d4f15fdb9883202a489521931b8d87bfd9457 100644 --- a/src/ui/config/configCmp/config.template.html +++ b/src/ui/config/configCmp/config.template.html @@ -59,7 +59,7 @@ <!-- viewer preference --> <mat-tab *ngIf="experimentalFlag" label="Viewer Preference"> - <div class="iv-custom-comp text sxplr-m-2"> + <div class="sxplr-custom-cmp text sxplr-m-2"> <div class="mat-h2"> Rearrange Viewports </div> @@ -133,14 +133,14 @@ </div> </current-layout> - <div class="iv-custom-comp text text-muted font-italic"> + <div class="sxplr-custom-cmp text text-muted font-italic"> Plane designation refers to default orientation (without oblique rotation). </div> </div> <!-- scroll window --> - <div class="sxplr-m-2 iv-custom-comp text"> + <div class="sxplr-m-2 sxplr-custom-cmp text"> <div class="mat-h2"> Select a viewports configuration </div> diff --git a/src/util/array.ts b/src/util/array.ts index b8484db0a12933020fba0fbfb3519f26e0896dc7..2c9cfeea640639773c0249db3f64406a6a67dac4 100644 --- a/src/util/array.ts +++ b/src/util/array.ts @@ -1,11 +1,13 @@ const defaultCmFn = <T>(o: T, n: T) => o === n -export function arrayEqual<T>(array1: T[], array2: T[], compareFn: (o1: T, o2: T) => boolean = defaultCmFn, order = false) { - if (order) { - for (const idx in array1) { - if (!compareFn(array1[idx], array2[idx])) return false +export function arrayEqual<T>(compareFn: (o1: T, o2: T) => boolean = defaultCmFn, order = false) { + return function(array1: T[], array2: T[]){ + if (order) { + for (const idx in array1) { + if (!compareFn(array1[idx], array2[idx])) return false + } + return true } - return true + return !!array1.every(it1 => array2.find(it2 => compareFn(it1, it2))) + && !!array2.every(it2 => array1.find(it1 => compareFn(it1, it2))) } - return !!array1.every(it1 => array2.find(it2 => compareFn(it1, it2))) - && !!array2.every(it2 => array1.find(it1 => compareFn(it1, it2))) } diff --git a/src/viewerModule/nehuba/config.service/util.ts b/src/viewerModule/nehuba/config.service/util.ts index a82cce3aa19f61b13e3ae2a4a8402c2ccab6cd7a..2e6533f6b3d2c9e881817ee3a3b032450b41453c 100644 --- a/src/viewerModule/nehuba/config.service/util.ts +++ b/src/viewerModule/nehuba/config.service/util.ts @@ -167,6 +167,7 @@ const BACKCOMAP_KEY_DICT = { export function getTmplNgLayer(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, spaceVolumes: SapiVolumeModel[]): Record<string, NgLayerSpec>{ const ngId = `_${MultiDimMap.GetKey(atlas["@id"], tmpl["@id"], "tmplImage")}` const tmplImage = spaceVolumes.find(v => "neuroglancer/precomputed" in v.data.detail) + if (!tmplImage) return {} return { [ngId]: { source: `precomputed://${tmplImage.data.url.replace(/^precomputed:\/\//, '')}`, @@ -238,24 +239,26 @@ export const getNgLayersFromVolumesATP = (volumes: CongregatedVolume, ATP: { atl } export const fromRootStore = { - getNgLayers: (store: Store, sapiSvc: SAPI) => pipe( - select(atlasSelection.selectors.selectedATP), - switchMap(ATP => - forkJoin({ - tmplVolumes: store.pipe( - nehubaStoreFromRootStore.getTmplVolumes(sapiSvc), - ), - tmplAuxMeshVolumes: store.pipe( - nehubaStoreFromRootStore.getAuxMeshVolumes(sapiSvc), - ), - parcVolumes: store.pipe( - nehubaStoreFromRootStore.getParcVolumes(sapiSvc), + getNgLayers: (store: Store, sapiSvc: SAPI) => { + return pipe( + select(atlasSelection.selectors.selectedATP), + switchMap(ATP => + forkJoin({ + tmplVolumes: store.pipe( + nehubaStoreFromRootStore.getTmplVolumes(sapiSvc), + ), + tmplAuxMeshVolumes: store.pipe( + nehubaStoreFromRootStore.getAuxMeshVolumes(sapiSvc), + ), + parcVolumes: store.pipe( + nehubaStoreFromRootStore.getParcVolumes(sapiSvc), + ) + }).pipe( + map(volumes => getNgLayersFromVolumesATP(volumes, ATP)) ) - }).pipe( - map(volumes => getNgLayersFromVolumesATP(volumes, ATP)) ) ) - ) + } } export function getRegionLabelIndex(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, parc: SapiParcellationModel, region: SapiRegionModel) { diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index d223772c926d95266ad5f985c8fd0ed6b07f891e..dfcb5ab6eb576f41d99931208ad9a5cabd7a82dd 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -356,7 +356,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni select(atlasAppearance.selectors.customLayers), debounceTime(16), map(cl => cl.filter(l => l.clType === "baselayer/nglayer") as NgLayerCustomLayer[]), - distinctUntilChanged((o, n) => arrayEqual(o, n, (oi, ni) => oi.id === ni.id)), + distinctUntilChanged(arrayEqual((oi, ni) => oi.id === ni.id)), filter(layers => layers.length > 0), map(ngBaseLayers => { return { @@ -633,17 +633,17 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni return newMainMap }, applyLayersColourMap: (map) => { - if (!this.layerCtrlService) { - throw new Error(`layerCtrlService not injected. Cannot call getLayersSegmentColourMap`) - } - const obj: IColorMap = {} - for (const [ key, value ] of map.entries()) { - const cmap = obj[key] = {} - for (const [ labelIdx, rgb ] of value.entries()) { - cmap[Number(labelIdx)] = rgb - } - } - this.layerCtrlService.overwriteColorMap$.next(obj) + // if (!this.layerCtrlService) { + // throw new Error(`layerCtrlService not injected. Cannot call getLayersSegmentColourMap`) + // } + // const obj: IColorMap = {} + // for (const [ key, value ] of map.entries()) { + // const cmap = obj[key] = {} + // for (const [ labelIdx, rgb ] of value.entries()) { + // cmap[Number(labelIdx)] = rgb + // } + // } + // this.layerCtrlService.overwriteColorMap$.next(obj) }, /** * TODO go via layerCtrl.service @@ -716,8 +716,8 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni const trueOnhoverSegments = this.onhoverSegments && this.onhoverSegments.filter(v => typeof v === 'object') if (!trueOnhoverSegments || (trueOnhoverSegments.length === 0)) return true this.store$.dispatch( - atlasSelection.actions.selectRegions({ - regions: trueOnhoverSegments.slice(0, 1) + atlasSelection.actions.selectRegion({ + region: trueOnhoverSegments[0] }) ) return true diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html index d2ec2c4e46889fe840f41e3764a976a48a132a04..575f8d188b05db891b340e321e83ef89341c93cf 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html @@ -219,7 +219,7 @@ </button> <div *ngIf="data.moreInfoFlag" - class="iv-custom-comp darker-bg overflow-hidden grid-wide-3"> + class="sxplr-custom-cmp darker-bg overflow-hidden grid-wide-3"> <ng-layer-tune advanced-control="true" [ngLayerName]="data.layerName" diff --git a/src/viewerModule/nehuba/statusCard/statusCard.template.html b/src/viewerModule/nehuba/statusCard/statusCard.template.html index 8f0c517ee63e00f7509403509cfa187489e82cbe..026a49446b2d44ba2d24addbeb80419c4fd34e2a 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.template.html +++ b/src/viewerModule/nehuba/statusCard/statusCard.template.html @@ -106,7 +106,7 @@ <!-- minimised status bar --> <ng-template #showMin> - <div class="iv-custom-comp text of-visible text-nowrap d-inline-flex align-items-center m-1 mt-3" + <div class="sxplr-custom-cmp text of-visible text-nowrap d-inline-flex align-items-center m-1 mt-3" iav-media-query #media="iavMediaQuery"> diff --git a/src/viewerModule/nehuba/viewerCtrl/change-perspective-orientation/changePerspectiveOrientation.component.html b/src/viewerModule/nehuba/viewerCtrl/change-perspective-orientation/changePerspectiveOrientation.component.html index c194ea9659faa473dbf30de56ace6b5e6e83901d..4aa94fa5d1bce357ba7caba1c6a1b3f901cdc30c 100644 --- a/src/viewerModule/nehuba/viewerCtrl/change-perspective-orientation/changePerspectiveOrientation.component.html +++ b/src/viewerModule/nehuba/viewerCtrl/change-perspective-orientation/changePerspectiveOrientation.component.html @@ -4,7 +4,7 @@ <mat-menu #perspectiveOrientationMenu="matMenu"> - <div class="d-flex align-items-center iv-custom-comp text"> + <div class="d-flex align-items-center sxplr-custom-cmp text"> <button mat-button color="basic" class="flex-grow-1 text-left font-weight-normal" (click)="set3DViewPoint('coronal', 'first')"> Coronal view @@ -15,7 +15,7 @@ </button> </div> - <div class="d-flex align-items-center iv-custom-comp text"> <!--mat-menu-item--> + <div class="d-flex align-items-center sxplr-custom-cmp text"> <!--mat-menu-item--> <button mat-button color="basic" class="flex-grow-1 text-left font-weight-normal" (click)="set3DViewPoint('sagittal', 'first')"> Sagittal view @@ -26,7 +26,7 @@ </button> </div> - <div class="d-flex align-items-center iv-custom-comp text"> <!--mat-menu-item--> + <div class="d-flex align-items-center sxplr-custom-cmp text"> <!--mat-menu-item--> <button mat-button color="basic" class="flex-grow-1 text-left font-weight-normal" (click)="set3DViewPoint('axial', 'first')"> Axial view diff --git a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.template.html b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.template.html index f029408a905a925885518c0c834ec34ad77362c7..149666c1c40bdcab574133178809f45fb885897e 100644 --- a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.template.html +++ b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.template.html @@ -1,13 +1,13 @@ <mat-divider class="mt-2 mb-2"></mat-divider> -<h3 class="iv-custom-comp text mat-h3"> +<h3 class="sxplr-custom-cmp text mat-h3"> Perspective View </h3> <mat-slide-toggle [(ngModel)]="removeOctantFlag" [aria-label]="ARIA_LABELS.TOGGLE_FRONTAL_OCTANT" name="remove-frontal-octant"> - <span class="iv-custom-comp text"> + <span class="sxplr-custom-cmp text"> Remove frontal octant </span> </mat-slide-toggle> @@ -19,7 +19,7 @@ [formControlName]="auxMesh['@id']" class="d-block" [name]="'toggle-aux-mesh-' + auxMesh['@id']"> - <span class="iv-custom-comp text"> + <span class="sxplr-custom-cmp text"> {{ auxMesh.displayName || auxMesh.name }} </span> </mat-slide-toggle> diff --git a/src/viewerModule/threeSurfer/module.ts b/src/viewerModule/threeSurfer/module.ts index 21f11580befb07553f6f613b3b0f86fa2366e8d6..7a1f1c3e3c908f64451cebc7db7e513d7a1f9e40 100644 --- a/src/viewerModule/threeSurfer/module.ts +++ b/src/viewerModule/threeSurfer/module.ts @@ -1,11 +1,14 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule } from "@angular/forms"; +import { StoreModule } from "@ngrx/store"; import { ComponentsModule } from "src/components"; import { AngularMaterialModule } from "src/sharedModules"; import { UtilModule } from "src/util"; import { ThreeSurferGlueCmp } from "./threeSurferGlue/threeSurfer.component"; import { ThreeSurferViewerConfig } from "./tsViewerConfig/tsViewerConfig.component"; +import { nameSpace, reducer, ThreeSurferEffects } from "./store" +import { EffectsModule } from "@ngrx/effects"; @NgModule({ imports: [ @@ -14,6 +17,13 @@ import { ThreeSurferViewerConfig } from "./tsViewerConfig/tsViewerConfig.compone UtilModule, FormsModule, ComponentsModule, + StoreModule.forFeature( + nameSpace, + reducer + ), + EffectsModule.forFeature([ + ThreeSurferEffects, + ]) ], declarations: [ ThreeSurferGlueCmp, diff --git a/src/viewerModule/threeSurfer/store/actions.ts b/src/viewerModule/threeSurfer/store/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..613fcbbfc7b943dfc412717987314a2b34fb5fb6 --- /dev/null +++ b/src/viewerModule/threeSurfer/store/actions.ts @@ -0,0 +1,9 @@ +import { createAction, props } from "@ngrx/store"; +import { nameSpace } from "./const" + +export const selectVolumeById = createAction( + `${nameSpace} selectVolumeById`, + props<{ + id: string + }>() +) diff --git a/src/viewerModule/threeSurfer/store/const.ts b/src/viewerModule/threeSurfer/store/const.ts new file mode 100644 index 0000000000000000000000000000000000000000..5643484e1b0628d986eb70dec222903a1fbfe6cd --- /dev/null +++ b/src/viewerModule/threeSurfer/store/const.ts @@ -0,0 +1 @@ +export const nameSpace = `[threeSurfer]` \ No newline at end of file diff --git a/src/viewerModule/threeSurfer/store/effects.ts b/src/viewerModule/threeSurfer/store/effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..9357427c2054109e8978e1d99d79f18faf489120 --- /dev/null +++ b/src/viewerModule/threeSurfer/store/effects.ts @@ -0,0 +1,154 @@ +import { Injectable } from "@angular/core"; +import { createEffect } from "@ngrx/effects"; +import { select, Store } from "@ngrx/store"; +import { EMPTY, forkJoin, merge, Observable, of, pipe, throwError } from "rxjs"; +import { debounceTime, map, switchMap, withLatestFrom, filter, take, shareReplay, tap, distinctUntilChanged } from "rxjs/operators"; +import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi"; +import { atlasAppearance, atlasSelection } from "src/state"; +import { ThreeSurferCustomLabelLayer, ThreeSurferCustomLayer } from "src/state/atlasAppearance/const"; +import * as selectors from "./selectors" +import * as actions from "./actions" + +export const fromATP = { + getThreeSurfaces: (sapi: SAPI) => { + return pipe( + filter( + ({ atlas, template, parcellation }: {atlas: SapiAtlasModel, template: SapiSpaceModel, parcellation: SapiParcellationModel}) => !!atlas && !!template && !!parcellation + ), + switchMap(({ atlas, template, parcellation }: {atlas: SapiAtlasModel, template: SapiSpaceModel, parcellation: SapiParcellationModel}) => + forkJoin({ + surfaces: sapi.getSpace(atlas["@id"], template["@id"]) + .getVolumes() + .then(volumes => volumes.filter(vol => vol.data.type === "gii")), + labels: sapi.getParcellation(atlas["@id"], parcellation["@id"]) + .getVolumes() + .then(volumes => + volumes.filter(vol => + vol.data.type === "gii-label" && + vol.data.space["@id"] === template["@id"] + ) + ) + }) + ) + ) + } +} + +@Injectable() +export class ThreeSurferEffects { + + private onATP$ = this.store.pipe( + select(atlasSelection.selectors.selectedATP) + ) + + private selectedSurfaceId$ = this.store.pipe( + select(selectors.getSelectedVolumeId), + distinctUntilChanged() + ) + + private threeSurferBaseCustomLayers$: Observable<ThreeSurferCustomLayer[]> = this.store.pipe( + select(atlasAppearance.selectors.customLayers), + map( + cl => cl.filter(layer => layer.clType === "baselayer/threesurfer") as ThreeSurferCustomLayer[] + ) + ) + + onATPClearBaseLayers = createEffect(() => merge( + this.onATP$, + this.selectedSurfaceId$, + ).pipe( + withLatestFrom( + this.threeSurferBaseCustomLayers$ + ), + switchMap(([_, layers]) => + of( + ...layers.map(layer => + atlasAppearance.actions.removeCustomLayer({ + id: layer.id + }) + ) + ) + ) + )) + + public onATPDebounceThreeSurferLayers$ = this.onATP$.pipe( + debounceTime(16), + fromATP.getThreeSurfaces(this.sapi), + shareReplay(1), + ) + + onATPDebounceHasSurfaceVolumes = createEffect(() => this.onATPDebounceThreeSurferLayers$.pipe( + switchMap(({ surfaces }) => { + const defaultSurface = surfaces.find(s => s.metadata.shortName === "pial") || surfaces[0] + if (!defaultSurface) return EMPTY + return of( + actions.selectVolumeById({ + id: defaultSurface["@id"] + }) + ) + }) + )) + + onSurfaceSelected = createEffect(() => this.selectedSurfaceId$.pipe( + switchMap(id => this.onATPDebounceThreeSurferLayers$.pipe( + switchMap(({ surfaces }) => { + if (surfaces.length === 0) return EMPTY + + const layers: ThreeSurferCustomLayer[] = [] + /** + * select the pial or first one by default + */ + const selectedSrc = surfaces.find(s => s["@id"] === id) + + if (!(selectedSrc.data?.url_map)) { + return throwError(`Expecting surfaces[0].data.url_map to be defined, but is not.`) + } + + for (const key in selectedSrc.data.url_map) { + layers.push({ + clType: 'baselayer/threesurfer', + id: `${selectedSrc["@id"]}-${key}`, + name: `${selectedSrc["@id"]}-${key}`, + laterality: key as 'left' | 'right', + source: selectedSrc.data.url_map[key] + }) + } + return of(...[ + ...layers.map(customLayer => + atlasAppearance.actions.addCustomLayer({ + customLayer + }) + ) + ]) + }) + )) + )) + + onATPDebounceAddBaseLayers$ = createEffect(() => this.onATPDebounceThreeSurferLayers$.pipe( + switchMap(({ labels }) => { + const labelMaps: ThreeSurferCustomLabelLayer[] = [] + for (const label of labels) { + labelMaps.push({ + clType: 'baselayer/threesurfer-label', + id: `${label["@id"]}-${label.metadata.shortName}`, + laterality: label.metadata.shortName as 'left' | 'right', + source: label.data.url + }) + } + return of( + ...labelMaps.map(customLayer => + atlasAppearance.actions.addCustomLayer({ + customLayer + }) + ) + ) + }) + )) + + constructor( + private store: Store, + private sapi: SAPI, + ){ + + } +} \ No newline at end of file diff --git a/src/viewerModule/threeSurfer/store/index.ts b/src/viewerModule/threeSurfer/store/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e84f687f54017d80038471ae103fa2f4131e3626 --- /dev/null +++ b/src/viewerModule/threeSurfer/store/index.ts @@ -0,0 +1,5 @@ +export { reducer, Store, defaultStore } from "./store" +export * as actions from "./actions" +export * as selectors from "./selectors" +export { nameSpace } from "./const" +export { ThreeSurferEffects } from "./effects" \ No newline at end of file diff --git a/src/viewerModule/threeSurfer/store/selectors.ts b/src/viewerModule/threeSurfer/store/selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..82c32604a57a26efac7881f9345fdb6c38cf9c2c --- /dev/null +++ b/src/viewerModule/threeSurfer/store/selectors.ts @@ -0,0 +1,10 @@ +import { createSelector } from "@ngrx/store" +import { nameSpace } from "./const" +import { Store } from "./store" + +const selectStore = state => state[nameSpace] as Store + +export const getSelectedVolumeId = createSelector( + selectStore, + ({ selectedVolumeId }) => selectedVolumeId +) diff --git a/src/viewerModule/threeSurfer/store/store.ts b/src/viewerModule/threeSurfer/store/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..b830d4278e88bfea95ca9a5f0697fb68f7db2171 --- /dev/null +++ b/src/viewerModule/threeSurfer/store/store.ts @@ -0,0 +1,24 @@ +import { createReducer, on } from "@ngrx/store"; +import * as actions from "./actions" + +export type Store = { + selectedVolumeId: string +} + + +export const defaultStore: Store = { + selectedVolumeId: null +} + +export const reducer = createReducer( + defaultStore, + on( + actions.selectVolumeById, + (state, { id }) => { + return { + ...state, + selectedVolumeId: id + } + } + ) +) diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index 0a62b9a08053b43328afd1300ae638d2c3c90e17..d433d11c79969e6f63bf3df71a23226f56749ff1 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -1,20 +1,22 @@ -import { Component, Input, Output, EventEmitter, ElementRef, OnChanges, OnDestroy, AfterViewInit, Inject, Optional } from "@angular/core"; +import { Component, Output, EventEmitter, ElementRef, OnChanges, OnDestroy, AfterViewInit, Inject, Optional, ChangeDetectionStrategy } from "@angular/core"; import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.interface"; -import { TThreeSurferConfig, TThreeSurferMode } from "../types"; -import { parseContext } from "../util"; -import { retry, flattenRegions } from 'common/util' -import { Observable, Subject } from "rxjs"; -import { debounceTime, filter, switchMap } from "rxjs/operators"; +import { combineLatest, Observable, Subject } from "rxjs"; +import { debounceTime, distinctUntilChanged, filter, map, shareReplay, tap } from "rxjs/operators"; import { ComponentStore } from "src/viewerModule/componentStore"; import { select, Store } from "@ngrx/store"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { MatSnackBar } from "@angular/material/snack-bar"; import { CONST } from 'common/constants' import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service"; -import { getUuid, switchMapWaitFor } from "src/util/fn"; +import { getUuid } from "src/util/fn"; import { AUTO_ROTATE, TInteralStatePayload, ViewerInternalStateSvc } from "src/viewerModule/viewerInternalState.service"; -import { actions } from "src/state/atlasSelection"; -import { atlasSelection } from "src/state"; +import { atlasAppearance, atlasSelection } from "src/state"; +import { ThreeSurferCustomLabelLayer, ThreeSurferCustomLayer, ColorMapCustomLayer } from "src/state/atlasAppearance/const"; +import { SapiRegionModel, SapiVolumeModel } from "src/atlasComponents/sapi"; +import { getRegionLabelIndex } from "src/viewerModule/nehuba/config.service"; +import { arrayEqual } from "src/util/array"; +import { ThreeSurferEffects } from "../store/effects"; +import { selectors, actions } from "../store" const viewerType = 'ThreeSurfer' type TInternalState = { @@ -27,10 +29,10 @@ type TInternalState = { hemisphere: 'left' | 'right' | 'both' } const pZoomFactor = 5e3 -const preferredFsMode = 'pial' type THandlingCustomEv = { - regions: ({ name?: string, error?: string })[] + regions: SapiRegionModel[] + error?: string evMesh?: { faceIndex: number verticesIndicies: number[] @@ -42,16 +44,27 @@ type TCameraOrientation = { perspectiveZoom: number } -const threshold = 1e-3 - -function getHemisphereKey(region: { name: string }){ - return /left/.test(region.name) - ? 'left' - : /right/.test(region.name) - ? 'right' - : null +type TThreeGeometry = { + visible: boolean +} +type GiiInstance = {} +type TThreeSurfer = { + loadMesh: (url: string) => Promise<TThreeGeometry> + unloadMesh: (geom: TThreeGeometry) => void + redraw: (geom: TThreeGeometry) => void + applyColorMap: (geom: TThreeGeometry, idxMap?: number[], custom?: { usePreset?: any, custom?: Map<number, number[]> }) => void + loadColormap: (url: string) => Promise<GiiInstance> + setupAnimation: () => void + dispose: () => void + control: any + camera: any + customColormap: WeakMap<TThreeGeometry, any> } +type LateralityRecord<T> = Record<string, T> + +const threshold = 1e-3 + function cameraNavsAreSimilar(c1: TCameraOrientation, c2: TCameraOrientation){ if (c1 === c2) return true if (!!c1 && !!c2) return true @@ -74,40 +87,116 @@ function cameraNavsAreSimilar(c1: TCameraOrientation, c2: TCameraOrientation){ styleUrls: [ './threeSurfer.style.css' ], - providers: [ ComponentStore ] + providers: [ ComponentStore ], + changeDetection: ChangeDetectionStrategy.OnPush }) -export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, AfterViewInit, OnDestroy { - - private loanedColorMap = new WeakSet() - - @Input() - selectedTemplate: any +export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit, OnDestroy { - @Input() - selectedParcellation: any @Output() viewerEvent = new EventEmitter<TViewerEvent<'threeSurfer'>>() private domEl: HTMLElement - private config: TThreeSurferConfig - public modes: TThreeSurferMode[] = [] - public selectedMode: string - private mainStoreCameraNav: TCameraOrientation = null private localCameraNav: TCameraOrientation = null - public allKeys: {name: string, checked: boolean}[] = [] + public lateralityMeshRecord: LateralityRecord<{ + visible: boolean + meshLayer: ThreeSurferCustomLayer + mesh: TThreeGeometry + }> = {} + + public latLblIdxRecord: LateralityRecord<{ + indexLayer: ThreeSurferCustomLabelLayer + labelIndices: number[] + }> = {} private internalStateNext: (arg: TInteralStatePayload<TInternalState>) => void - private regionMap: Map<string, Map<number, any>> = new Map() - private mouseoverRegions = [] + private mouseoverRegions: SapiRegionModel[] = [] - private raf: number + private selectedRegions$ = this.store$.pipe( + select(atlasSelection.selectors.selectedRegions) + ) + + private customLayers$ = this.store$.pipe( + select(atlasAppearance.selectors.customLayers), + distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)), + shareReplay(1) + ) + public meshLayers$: Observable<ThreeSurferCustomLayer[]> = this.customLayers$.pipe( + map(layers => layers.filter(l => l.clType === "baselayer/threesurfer") as ThreeSurferCustomLayer[]), + distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)), + ) + + private vertexIndexLayers$: Observable<ThreeSurferCustomLabelLayer[]> = this.customLayers$.pipe( + map(layers => layers.filter(l => l.clType === "baselayer/threesurfer-label") as ThreeSurferCustomLabelLayer[]), + distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)), + ) + + /** + * maps laterality to label index to sapi region + */ + private latLblIdxToRegionRecord: LateralityRecord<Record<number, SapiRegionModel>> = {} + private latLblIdxToRegionRecord$: Observable<LateralityRecord<Record<number, SapiRegionModel>>> = combineLatest([ + this.store$.pipe( + select(atlasSelection.selectors.selectedATP) + ), + this.store$.pipe( + select(atlasSelection.selectors.selectedParcAllRegions), + ) + ]).pipe( + map(([ { atlas, parcellation, template }, regions]) => { + const returnObj = { + 'left': {} as Record<number, SapiRegionModel>, + 'right': {} as Record<number, SapiRegionModel> + } + + for (const region of regions) { + const idx = getRegionLabelIndex(atlas, template, parcellation, region) + if (idx) { + let key : 'left' | 'right' + if ( /left/i.test(region.name) ) key = 'left' + if ( /right/i.test(region.name) ) key = 'right' + if (!key) { + /** + * TODO + * there are ... more regions than expected, which has label index without laterality + */ + continue + } + returnObj[key][idx] = region + } + } + return returnObj + }) + ) + + /** + * colormap in use (both base & custom) + */ + + private colormapInUse: ColorMapCustomLayer + private colormaps$: Observable<ColorMapCustomLayer[]> = this.customLayers$.pipe( + map(layers => layers.filter(l => l.clType === "baselayer/colormap" || l.clType === "customlayer/colormap") as ColorMapCustomLayer[]), + ) + + /** + * show delination map + */ + private showDelineation: boolean = true + + public threeSurferSurfaceLayers$ = this.effect.onATPDebounceThreeSurferLayers$.pipe( + map(({ surfaces }) => surfaces) + ) + public selectedSurfaceLayerId$ = this.store$.pipe( + select(selectors.getSelectedVolumeId) + ) + constructor( + private effect: ThreeSurferEffects, private el: ElementRef, - private store$: Store<any>, + private store$: Store, private navStateStoreRelay: ComponentStore<{ perspectiveOrientation: [number, number, number, number], perspectiveZoom: number }>, private snackbar: MatSnackBar, @Optional() intViewerStateSvc: ViewerInternalStateSvc, @@ -151,40 +240,42 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af setViewerHandle({ add3DLandmarks: nyi, loadLayer: nyi, - applyLayersColourMap: (map: Map<string, Map<number, { red: number, green: number, blue: number }>>) => { - if (this.loanedColorMap.has(map)) { - this.externalHemisphLblColorMap = null - } else { - - const applyCm = new Map() - for (const [hem, m] of map.entries()) { - const nMap = new Map() - applyCm.set(hem, nMap) - for (const [lbl, vals] of m.entries()) { - const { red, green, blue } = vals - nMap.set(lbl, [red/255, green/255, blue/255]) - } - } - this.externalHemisphLblColorMap = applyCm - } - this.applyColorMap() + applyLayersColourMap: function(map: Map<string, Map<number, { red: number, green: number, blue: number }>>){ + throw new Error(`NYI`) + // if (this.loanedColorMap.has(map)) { + // this.externalHemisphLblColorMap = null + // } else { + + // const applyCm = new Map() + // for (const [hem, m] of map.entries()) { + // const nMap = new Map() + // applyCm.set(hem, nMap) + // for (const [lbl, vals] of m.entries()) { + // const { red, green, blue } = vals + // nMap.set(lbl, [red/255, green/255, blue/255]) + // } + // } + // this.externalHemisphLblColorMap = applyCm + // } + // this.applyColorMap() }, getLayersSegmentColourMap: () => { - const map = this.getColormapCopy() - const outmap = new Map<string, Map<number, { red: number, green: number, blue: number }>>() - for (const [ hem, m ] of map.entries()) { - const nMap = new Map<number, {red: number, green: number, blue: number}>() - outmap.set(hem, nMap) - for (const [ lbl, vals ] of m.entries()) { - nMap.set(lbl, { - red: vals[0] * 255, - green: vals[1] * 255, - blue: vals[2] * 255, - }) - } - } - this.loanedColorMap.add(outmap) - return outmap + throw new Error(`NYI`) + // const map = this.getColormapCopy() + // const outmap = new Map<string, Map<number, { red: number, green: number, blue: number }>>() + // for (const [ hem, m ] of map.entries()) { + // const nMap = new Map<number, {red: number, green: number, blue: number}>() + // outmap.set(hem, nMap) + // for (const [ lbl, vals ] of m.entries()) { + // nMap.set(lbl, { + // red: vals[0] * 255, + // green: vals[1] * 255, + // blue: vals[2] * 255, + // }) + // } + // } + // this.loanedColorMap.add(outmap) + // return outmap }, getNgHash: nyi, hideAllSegments: nyi, @@ -208,53 +299,6 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af () => setViewerHandle(null) ) - const sub = this.store$.pipe( - select(atlasSelection.selectors.selectedRegions) - ).subscribe(() => { - - /** - * TODO - * fix ... this? - */ - - // if (this.roi$) { - // const sub = this.roi$.pipe( - // switchMap(switchMapWaitFor({ - // condition: () => this.colormapLoaded - // })) - // ).subscribe(r => { - // try { - // if (!r) throw new Error(`No region selected.`) - // const cmap = this.getColormapCopy() - // const hemisphere = getHemisphereKey(r) - // if (!hemisphere) { - // this.snackbar.open(CONST.CANNOT_DECIPHER_HEMISPHERE, 'Dismiss', { - // duration: 3000 - // }) - // throw new Error(CONST.CANNOT_DECIPHER_HEMISPHERE) - // } - // for (const [ hem, m ] of cmap.entries()) { - // for (const lbl of m.keys()) { - // if (hem !== hemisphere || lbl !== r.labelIndex) { - // m.set(lbl, [1, 1, 1]) - // } - // } - // } - // this.internalHemisphLblColorMap = cmap - // } catch (e) { - // this.internalHemisphLblColorMap = null - // } - - // this.applyColorMap() - // }) - // this.onDestroyCb.push( - // () => sub.unsubscribe() - // ) - // } - - }) - this.onDestroyCb.push(() => sub.unsubscribe()) - /** * intercept click and act */ @@ -274,10 +318,9 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af return true } - // TODO check why typing is all messed up here const regions = this.mouseoverRegions.slice(0, 1) as any[] this.store$.dispatch( - actions.selectRegions({ regions }) + atlasSelection.actions.setSelectedRegions({ regions }) ) return true } @@ -325,7 +368,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af */ const navStateSub = this.navStateStoreRelay.select(s => s).subscribe(v => { this.store$.dispatch( - actions.navigateTo({ + atlasSelection.actions.navigateTo({ navigation: { position: [0, 0, 0], orientation: [0, 0, 0, 1], @@ -372,23 +415,9 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af ) } - tsRef: any - loadedMeshes: { - threeSurfer: any - mesh: string - colormap: string - hemisphere: string - vIdxArr: number[] - }[] = [] - private hemisphLblColorMap: Map<string, Map<number, [number, number, number]>> = new Map() - private internalHemisphLblColorMap: Map<string, Map<number, [number, number, number]>> - private externalHemisphLblColorMap: Map<string, Map<number, [number, number, number]>> - - get activeColorMap() { - if (this.externalHemisphLblColorMap) return this.externalHemisphLblColorMap - if (this.internalHemisphLblColorMap) return this.internalHemisphLblColorMap - return this.hemisphLblColorMap - } + private tsRef: TThreeSurfer + private selectedRegions: SapiRegionModel[] = [] + private relayStoreLock: () => void = null private tsRefInitCb: ((tsRef: any) => void)[] = [] private toTsRef(callback: (tsRef: any) => void) { @@ -399,84 +428,47 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af this.tsRefInitCb.push(callback) } - private unloadAllMeshes() { - this.allKeys = [] - while(this.loadedMeshes.length > 0) { - const m = this.loadedMeshes.pop() - this.tsRef.unloadMesh(m.threeSurfer) - } - this.hemisphLblColorMap.clear() - this.colormapLoaded = false - } + private async loadMeshes(layers: ThreeSurferCustomLayer[]) { + if (!this.tsRef) throw new Error(`loadMeshes error: this.tsRef is not defined!!`) - public async loadMode(mode: TThreeSurferMode) { - - this.unloadAllMeshes() - - this.selectedMode = mode.name - const { meshes } = mode - await retry(async () => { - for (const singleMesh of meshes) { - const { hemisphere } = singleMesh - if (!this.regionMap.has(hemisphere)) throw new Error(`regionmap does not have hemisphere defined!`) - } - }, { - timeout: 32, - retries: 10 - }) - for (const singleMesh of meshes) { - const { mesh, colormap, hemisphere } = singleMesh - this.allKeys.push({name: hemisphere, checked: true}) - - const tsM = await this.tsRef.loadMesh( - parseContext(mesh, [this.config['@context']]) - ) - - if (!this.regionMap.has(hemisphere)) continue - const rMap = this.regionMap.get(hemisphere) - const applyCM = new Map() - for (const [ lblIdx, region ] of rMap.entries()) { - applyCM.set(lblIdx, (region.rgb || [200, 200, 200]).map(v => v/255)) + /** + * remove the layers... + */ + for (const layer of layers) { + if (!!this.lateralityMeshRecord[layer.laterality]) { + this.tsRef.unloadMesh(this.lateralityMeshRecord[layer.laterality].mesh) } + } - const tsC = await this.tsRef.loadColormap( - parseContext(colormap, [this.config['@context']]) - ) - - let colorIdx = tsC[0].getData() - if (tsC[0].attributes.DataType === 'NIFTI_TYPE_INT16') { - colorIdx = (window as any).ThreeSurfer.GiftiBase.castF32UInt16(colorIdx) + for (const layer of layers) { + const threeMesh = await this.tsRef.loadMesh(layer.source) + this.lateralityMeshRecord[layer.laterality] = { + visible: true, + meshLayer: layer, + mesh: threeMesh } - - this.loadedMeshes.push({ - threeSurfer: tsM, - colormap, - mesh, - hemisphere, - vIdxArr: colorIdx - }) - - this.hemisphLblColorMap.set(hemisphere, applyCM) } - this.colormapLoaded = true - this.applyColorMap() + this.applyColor() } - private colormapLoaded = false + private async loadVertexIndexMap(layers: ThreeSurferCustomLabelLayer[]) { + if (!this.tsRef) throw new Error(`loadVertexIndexMap error: this.tsRef is not defined!!`) + for (const layer of layers) { + const giiInstance = await this.tsRef.loadColormap(layer.source) - private getColormapCopy(): Map<string, Map<number, [number, number, number]>> { - const outmap = new Map() - for (const [key, value] of this.hemisphLblColorMap.entries()) { - outmap.set(key, new Map(value)) + let labelIndices: number[] = giiInstance[0].getData() + if (giiInstance[0].attributes.DataType === 'NIFTI_TYPE_INT16') { + labelIndices = (window as any).ThreeSurfer.GiftiBase.castF32UInt16(labelIndices) + } + this.latLblIdxRecord[layer.laterality] = { + indexLayer: layer, + labelIndices + } } - return outmap + this.applyColor() } - /** - * TODO perhaps debounce calls to applycolormap - * so that the colormap doesn't "flick" - */ - private applyColorMap(){ + private applyColor() { /** * on apply color map, reset mesh visibility * this issue is more difficult to solve than first anticiplated. @@ -486,72 +478,34 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af * 2/ hide hemisphere, select region, unhide hemisphere * 3/ select region, hide hemisphere, deselect region */ - for (const key of this.allKeys) { - key.checked = true - } - for (const mesh of this.loadedMeshes) { - const { hemisphere, threeSurfer, vIdxArr } = mesh - const applyCM = this.activeColorMap.get(hemisphere) - this.tsRef.applyColorMap(threeSurfer, vIdxArr, - { - custom: applyCM - } - ) - } - } - - async ngOnChanges(){ - if (this.tsRef) { - this.ngOnDestroy() - this.ngAfterViewInit() - } - if (this.selectedTemplate) { - - /** - * wait until threesurfer is defined in window - */ - await retry(async () => { - if (typeof (window as any).ThreeSurfer === 'undefined') throw new Error('ThreeSurfer not yet defined') - }, { - timeout: 160, - retries: 10, - }) - - this.config = this.selectedTemplate['three-surfer'] - // somehow curv ... cannot be parsed properly by gifti parser... something about points missing - this.modes = this.config.modes.filter(m => !/curv/.test(m.name)) - if (!this.tsRef) { - this.tsRef = new (window as any).ThreeSurfer(this.domEl, {highlightHovered: true}) - this.onDestroyCb.push( - () => { - this.tsRef.dispose() - this.tsRef = null - } - ) - this.tsRef.control.enablePan = false - while (this.tsRefInitCb.length > 0) this.tsRefInitCb.pop()(this.tsRef) + if (!this.colormapInUse) return + const isBaseCM = this.colormapInUse?.clType === "baselayer/colormap" + + for (const laterality in this.lateralityMeshRecord) { + const { mesh } = this.lateralityMeshRecord[laterality] + if (!this.latLblIdxRecord[laterality]) continue + const { labelIndices } = this.latLblIdxRecord[laterality] + + const lblIdxToRegionRecord = this.latLblIdxToRegionRecord[laterality] + if (!lblIdxToRegionRecord) { + this.tsRef.applyColorMap(mesh, labelIndices) + continue } - - const flattenedRegions = flattenRegions(this.selectedParcellation.regions) - for (const region of flattenedRegions) { - if (region.labelIndex) { - const hemisphere = getHemisphereKey(region) - if (!hemisphere) throw new Error(`region ${region.name} does not have hemisphere defined`) - if (!this.regionMap.has(hemisphere)) { - this.regionMap.set(hemisphere, new Map()) - } - const rMap = this.regionMap.get(hemisphere) - rMap.set(region.labelIndex, region) + const map = new Map<number, number[]>() + for (const lblIdx in lblIdxToRegionRecord) { + const region = lblIdxToRegionRecord[lblIdx] + let color: number[] + if (!this.showDelineation) { + color = [1,1,1] + } else if (isBaseCM && this.selectedRegions.length > 0 && !this.selectedRegions.includes(region)) { + color = [1,1,1] + } else { + color = (this.colormapInUse.colormap.get(region) || [255, 255, 255]).map(v => v/255) } + map.set(Number(lblIdx), color) } - - // load preferredFsMode or mode0 by default - const loadMode = this.config.modes.find(m => m.name === preferredFsMode) || this.config.modes[0] - this.loadMode(loadMode) - - this.viewerEvent.emit({ - type: EnumViewerEvt.VIEWERLOADED, - data: true + this.tsRef.applyColorMap(mesh, labelIndices, { + custom: map }) } } @@ -571,59 +525,66 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af return this.handleMouseoverEvent(custEv) } - const evGeom = detail.mesh.geometry - const evVertIdx = detail.mesh.verticesIdicies - const found = this.loadedMeshes.find(({ threeSurfer }) => threeSurfer === evGeom) - if (!found) return this.handleMouseoverEvent(custEv) - - /** - * check if the mesh is toggled off - * if so, do not proceed - */ - const checkKey = this.allKeys.find(key => key.name === found.hemisphere) - if (checkKey && !checkKey.checked) return - - const { hemisphere: key, vIdxArr } = found - - if (!key || !evVertIdx) { - return this.handleMouseoverEvent(custEv) - } + const { + geometry: evGeometry, + // typo in three-surfer + verticesIdicies: evVerticesIndicies, + } = detail.mesh as { geometry: TThreeGeometry, verticesIdicies: number[] } - const labelIdxSet = new Set<number>() - - for (const vIdx of evVertIdx) { - labelIdxSet.add( - vIdxArr[vIdx] - ) - } - if (labelIdxSet.size === 0) { - return this.handleMouseoverEvent(custEv) - } + for (const laterality in this.lateralityMeshRecord) { + const meshRecord = this.lateralityMeshRecord[laterality] + if (meshRecord.mesh !== evGeometry) { + continue + } + /** + * if either labelindex record or colormap record is undefined for this laterality, emit empty event + */ + if (!this.latLblIdxRecord[laterality] || !this.latLblIdxToRegionRecord[laterality]) { + return this.handleMouseoverEvent(custEv) + } + const labelIndexRecord = this.latLblIdxRecord[laterality] + const regionRecord = this.latLblIdxToRegionRecord[laterality] - const hemisphereMap = this.regionMap.get(key) + /** + * check if the mesh is toggled off + * if so, do not proceed + */ + if (!meshRecord.visible) { + return + } - if (!hemisphereMap) { - custEv.regions = Array.from(labelIdxSet).map(v => { - return { - error: `unknown#${v}` + /** + * translate vertex indices to label indicies via set, to remove duplicates + */ + const labelIndexSet = new Set<number>() + for (const idx of evVerticesIndicies){ + const labelOfInterest = labelIndexRecord.labelIndices[idx] + if (!labelOfInterest) { + continue } - }) - return this.handleMouseoverEvent(custEv) - } + labelIndexSet.add(labelOfInterest) + } - custEv.regions = Array.from(labelIdxSet) - .map(lblIdx => { - const ontoR = hemisphereMap.get(lblIdx) - if (ontoR) { - return ontoR - } else { - return { - error: `unkonwn#${lblIdx}` - } + /** + * decode label index to region + */ + if (labelIndexSet.size === 0) { + return this.handleMouseoverEvent(custEv) + } + for (const labelIndex of Array.from(labelIndexSet)) { + if (!regionRecord[labelIndex]) { + custEv.error = `${custEv.error || ''} Cannot decode label index ${labelIndex}` + continue } - }) - return this.handleMouseoverEvent(custEv) + const region = regionRecord[labelIndex] + custEv.regions.push(region) + } + /** + * return handle event + */ + return this.handleMouseoverEvent(custEv) + } } private cameraEv$ = new Subject<{ position: { x: number, y: number, z: number }, zoom: number }>() @@ -634,7 +595,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af "@type": 'TViewerInternalStateEmitterEvent', viewerType, payload: { - mode: this.selectedMode, + mode: '', camera: detail.position, hemisphere: 'both' } @@ -657,42 +618,109 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af this.onDestroyCb.push( () => this.domEl.removeEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME_UPDATED, customEvHandler) ) + this.tsRef = new (window as any).ThreeSurfer(this.domEl, {highlightHovered: true}) + + this.onDestroyCb.push( + () => { + this.tsRef.dispose() + this.tsRef = null + } + ) + this.tsRef.control.enablePan = false + while (this.tsRefInitCb.length > 0) this.tsRefInitCb.pop()(this.tsRef) + + const meshSub = this.meshLayers$.pipe( + distinctUntilChanged(), + debounceTime(16), + ).subscribe(layers => { + this.loadMeshes(layers) + }) + const vertexIdxSub = this.vertexIndexLayers$.subscribe(layers => this.loadVertexIndexMap(layers)) + const roiSelectedSub = this.selectedRegions$.subscribe(regions => { + this.selectedRegions = regions + this.applyColor() + }) + const colormapSub = this.colormaps$.subscribe(cm => { + this.colormapInUse = cm[0] || null + this.applyColor() + }) + const recordToRegionSub = this.latLblIdxToRegionRecord$.subscribe(val => this.latLblIdxToRegionRecord = val) + const hideDelineationSub = this.store$.pipe( + select(atlasAppearance.selectors.showDelineation) + ).subscribe(flag => { + this.showDelineation = flag + this.applyColor() + /** + * apply color resets mesh visibility + */ + this.updateMeshVisibility() + }) + + this.onDestroyCb.push(() => { + meshSub.unsubscribe() + vertexIdxSub.unsubscribe() + roiSelectedSub.unsubscribe() + colormapSub.unsubscribe() + recordToRegionSub.unsubscribe() + hideDelineationSub.unsubscribe() + }) + + this.viewerEvent.emit({ + type: EnumViewerEvt.VIEWERLOADED, + data: true + }) } public mouseoverText: string private handleMouseoverEvent(ev: THandlingCustomEv){ - const { regions: mouseover, evMesh } = ev + const { regions: mouseover, evMesh, error } = ev this.mouseoverRegions = mouseover this.viewerEvent.emit({ type: EnumViewerEvt.VIEWER_CTX, data: { viewerType: 'threeSurfer', payload: { - fsversion: this.selectedMode, + fsversion: '', faceIndex: evMesh?.faceIndex, vertexIndices: evMesh?.verticesIndicies, position: [], - _mouseoverRegion: mouseover.filter(el => !el.error) + regions: mouseover, + error } } }) - this.mouseoverText = mouseover.length === 0 ? - null : - mouseover.map( - el => el.name || el.error - ).join(' / ') + this.mouseoverText = '' + if (mouseover.length > 0) { + this.mouseoverText += mouseover.map(el => el.name).join(' / ') + } + if (error) { + this.mouseoverText += `::error: ${error}` + } + if (this.mouseoverText === '') this.mouseoverText = null } - public handleCheckBox(key: { name: string, checked: boolean }, flag: boolean){ - const foundMesh = this.loadedMeshes.find(m => m.hemisphere === key.name) - if (!foundMesh) { - throw new Error(`Cannot find mesh with name: ${key.name}`) - } - const meshObj = this.tsRef.customColormap.get(foundMesh.threeSurfer) - if (!meshObj) { - throw new Error(`mesh obj not found!`) + public updateMeshVisibility(){ + + for (const key in this.lateralityMeshRecord) { + + const latMeshRecord = this.lateralityMeshRecord[key] + if (!latMeshRecord) { + return + } + const meshObj = this.tsRef.customColormap.get(latMeshRecord.mesh) + if (!meshObj) { + throw new Error(`mesh obj not found!`) + } + meshObj.mesh.visible = latMeshRecord.visible } - meshObj.mesh.visible = flag + } + + switchSurfaceLayer(layer: SapiVolumeModel){ + this.store$.dispatch( + actions.selectVolumeById({ + id: layer["@id"] + }) + ) } private onDestroyCb: (() => void) [] = [] @@ -700,10 +728,4 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af ngOnDestroy() { while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()() } - - toggleMode(){ - const currIdx = this.modes.findIndex(m => m.name === this.selectedMode) - const newIdx = (currIdx + 1) % this.modes.length - this.loadMode(this.modes[newIdx]) - } } diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html index cd04b28dc5a327f5461719c200ce01ab3361ef51..2a9703618ce87100f39bbd6470f88d4cd5bd5a57 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html @@ -1,5 +1,5 @@ <span *ngIf="mouseoverText" - class="mouseover iv-custom-comp text"> + class="mouseover sxplr-custom-cmp text"> {{ mouseoverText }} </span> @@ -7,8 +7,6 @@ <!-- selector & configurator --> <button mat-icon-button - [iav-key-listener]="[{ type: 'keydown', key: 'q', target: 'document' }]" - (iav-key-event)="toggleMode()" color="primary" class="pe-all" [matMenuTriggerFor]="fsModeSelMenu"> @@ -20,30 +18,27 @@ <!-- selector/configurator menu --> <mat-menu #fsModeSelMenu="matMenu"> - <div class="iv-custom-comp text sxplr-pl-2 m-2"> - <mat-checkbox *ngFor="let key of allKeys" + <div class="sxplr-custom-cmp text sxplr-pl-2 m-2"> + <mat-checkbox *ngFor="let item of lateralityMeshRecord | keyvalue" class="d-block" iav-stop="click" - (ngModelChange)="handleCheckBox(key, $event)" - [(ngModel)]="key.checked"> - {{ key.name }} + (change)="updateMeshVisibility()" + [(ngModel)]="item.value.visible"> + {{ item.key }} </mat-checkbox> </div> <mat-divider></mat-divider> - <button *ngFor="let mode of modes" + + <button *ngFor="let surfaceLayer of threeSurferSurfaceLayers$ | async" mat-menu-item - (click)="loadMode(mode)" + (click)="switchSurfaceLayer(surfaceLayer)" color="primary"> <mat-icon fontSet="fas" - [fontIcon]="mode.name === selectedMode ? 'fa-circle' : 'fa-none'"> + [fontIcon]="surfaceLayer['@id'] === (selectedSurfaceLayerId$ | async) ? 'fa-circle' : 'fa-none'"> </mat-icon> <span> - {{ mode.name }} + {{ surfaceLayer.metadata.shortName }} </span> - <markdown-dom *ngIf="mode.name === selectedMode" - class="d-inline-block" - markdown="`[q]`"> - </markdown-dom> </button> </mat-menu> diff --git a/src/viewerModule/threeSurfer/types.ts b/src/viewerModule/threeSurfer/types.ts index 8ec78e98787d2e05faeb976e272fdc2981809fcd..9a15c5642d7c04e5c5057a412532eaabe51be8fe 100644 --- a/src/viewerModule/threeSurfer/types.ts +++ b/src/viewerModule/threeSurfer/types.ts @@ -1,4 +1,4 @@ -import { IContext } from './util' +import { SapiRegionModel } from 'src/atlasComponents/sapi' export type TThreeSurferMesh = { colormap: string @@ -6,20 +6,11 @@ export type TThreeSurferMesh = { hemisphere: 'left' | 'right' } -export type TThreeSurferMode = { - name: string - meshes: TThreeSurferMesh[] -} - -export type TThreeSurferConfig = { - ['@context']: IContext - modes: TThreeSurferMode[] -} - export type TThreeSurferContextInfo = { position: number[] faceIndex: number vertexIndices: number[] fsversion: string - _mouseoverRegion: { name: string, error?: string }[] + regions: SapiRegionModel[] + error?: string } diff --git a/src/viewerModule/threeSurfer/util.ts b/src/viewerModule/threeSurfer/util.ts index 34a0d85065cf7104d57faf08790dd943804f7b26..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/src/viewerModule/threeSurfer/util.ts +++ b/src/viewerModule/threeSurfer/util.ts @@ -1,14 +0,0 @@ -export interface IContext { - [key: string]: string -} - -export function parseContext(input: string, contexts: IContext[]){ - let output = input - for (const context of contexts) { - for (const key in context) { - const re = new RegExp(`${key}:`, 'g') - output = output.replace(re, context[key]) - } - } - return output -} diff --git a/src/viewerModule/viewer.interface.ts b/src/viewerModule/viewer.interface.ts index 2f86f5eaa7229713bc6d16e7c8dad6111bfeecb0..cbc85aca0e3e2620a0903b4e332dcb92386e8db0 100644 --- a/src/viewerModule/viewer.interface.ts +++ b/src/viewerModule/viewer.interface.ts @@ -59,9 +59,6 @@ export type TViewerEvent<T extends keyof IViewerCtx> = TViewerEventViewerLoaded export type TSupportedViewers = keyof IViewerCtx export interface IViewer<K extends keyof IViewerCtx> { - - selectedTemplate: any - selectedParcellation: any viewerCtrlHandler?: IViewerCtrl viewerEvent: EventEmitter<TViewerEvent<K>> } diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index d0ef3818805ae74c1087478b06f04412889615ca..02e48a907ca71600df58d375310b8bad80607bc2 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -289,7 +289,7 @@ export class ViewerCmp implements OnDestroy { } if (context.viewerType === 'threeSurfer') { - hoveredRegions = (context as TContextArg<'threeSurfer'>).payload._mouseoverRegion + hoveredRegions = (context as TContextArg<'threeSurfer'>).payload.regions } if (hoveredRegions.length > 0) { @@ -324,8 +324,8 @@ export class ViewerCmp implements OnDestroy { public selectRoi(roi: SapiRegionModel) { this.store$.dispatch( - actions.selectRegions({ - regions: [ roi ] + actions.selectRegion({ + region: roi }) ) } diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css index bc4d6b48d991b1d7f74fd7e2d2d1545ee164537a..4d6407b910bb98bb8791bdebfcb2321bac7c835b 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.style.css +++ b/src/viewerModule/viewerCmp/viewerCmp.style.css @@ -80,4 +80,12 @@ sxplr-sapiviews-core-region-region-chip [prefix] .auto-complete-container > button { flex: 0 0 auto; -} \ No newline at end of file +} + +.min-tray-explr-btn +{ + width: 100%; + padding-left: 0.5rem; + padding-right: 0.5rem; + margin-top: -1rem; +} diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 4ac57dfe0614bb8e8c030c912a2d5d189b2a5006..4a0264bb7275d928ea6f1583ad029f207c29e049 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -23,7 +23,7 @@ (@openClose.done)="$event.toState === 'closed' && drawer.close()" [autoFocus]="false" [disableClose]="true" - class="iv-custom-comp darker-bg sxplr-p-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2 z-index-10"> + class="sxplr-custom-cmp darker-bg sxplr-p-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2 z-index-10"> <!-- entry template --> <ng-template [ngIf]="viewerMode$ | async" let-mode [ngIfElse]="regularTmpl"> @@ -204,7 +204,7 @@ <!-- such a gross implementation --> <!-- TODO fix this --> - <div class="sxplr-mt-1-n w-100 sxplr-pl-1 sxplr-pr-1" + <div class="min-tray-explr-btn" sxplr-sapiviews-core-region [sxplr-sapiviews-core-region-atlas]="selectedAtlas$ | async" [sxplr-sapiviews-core-region-template]="templateSelected$ | async" @@ -224,7 +224,7 @@ 'lighttheme': !sapiRegion.regionDarkmode }" [style.backgroundColor]="sapiRegion.regionRgbString"> - <span class="text iv-custom-comp"> + <span class="text sxplr-custom-cmp"> Explore </span> </button> @@ -335,7 +335,7 @@ </top-menu-cmp> <sxplr-sapiviews-core-atlas-dropdown-selector - class="v-align-top sxplr-pt-2 pe-all mt-2 iv-custom-comp bg card m-2 mat-elevation-z2 d-inline-block" + 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"> @@ -434,9 +434,7 @@ <!-- three surfer (free surfer viewer) --> <three-surfer-glue-cmp class="d-block w-100 h-100 position-absolute left-0 tosxplr-p-0" *ngSwitchCase="'threeSurfer'" - (viewerEvent)="handleViewerEvent($event)" - [selectedTemplate]="templateSelected$ | async" - [selectedParcellation]="parcellationSelected$ | async"> + (viewerEvent)="handleViewerEvent($event)"> </three-surfer-glue-cmp> <!-- if not supported, show not supported message --> @@ -459,19 +457,24 @@ <!-- region-hierarchy-tmpl --> <ng-template #regionHierarchyTmpl> - <div class="sxplr-w-100 sxplr-h-100"> + <div class="sxplr-d-flex sxplr-flex-column sxplr-h-100"> <sxplr-sapiviews-core-rich-regionshierarchy - class="sxplr-w-100 sxplr-h-100" + class="sxplr-w-100 sxplr-flex-var" [sxplr-sapiviews-core-rich-regionshierarchy-regions]="allAvailableRegions$ | async" - (sxplr-sapiviews-core-rich-regionshierarchy-region-select)="navigateTo($event)" + [sxplr-sapiviews-core-rich-regionshierarchy-accent-regions]="selectedRegions$ | async" + (sxplr-sapiviews-core-rich-regionshierarchy-region-select)="selectRoi($event)" > </sxplr-sapiviews-core-rich-regionshierarchy> + + <mat-dialog-actions align="center" class="sxplr-flex-static"> + <button mat-button mat-dialog-close>Close</button> + </mat-dialog-actions> </div> </ng-template> <!-- auto complete search box --> <ng-template #autocompleteTmpl let-showTour="showTour"> - <div class="iv-custom-comp bg card ml-2 mr-2 mat-elevation-z8 pe-all auto-complete-container"> + <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> @@ -620,7 +623,7 @@ [matBadge]="badge" [matBadgeColor]="badgeColor || 'warn'"> - <span [ngClass]="{'iv-custom-comp text': !!customColor}"> + <span [ngClass]="{'sxplr-custom-cmp text': !!customColor}"> <i class="fas" [ngClass]="fontIcon || 'fa-question'"></i> </span> </button> @@ -969,7 +972,7 @@ <div *ngFor="let feature of spatialFeatureBbox.features$ | async" mat-ripple (click)="showDataset(feature)" - class="iv-custom-comp hoverable w-100 overflow-hidden text-overflow-ellipses"> + class="sxplr-custom-cmp hoverable w-100 overflow-hidden text-overflow-ellipses"> {{ feature.name }} </div> </mat-card>