diff --git a/deploy/package-lock.json b/deploy/package-lock.json index 01318d6f2c5675ccc18f035b5cb0351b4825541f..b297a940a528a77b31bcfd3b70fe90ead1d08b91 100644 --- a/deploy/package-lock.json +++ b/deploy/package-lock.json @@ -5428,4 +5428,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/docs/releases/v2.8.1.md b/docs/releases/v2.9.0.md similarity index 50% rename from docs/releases/v2.8.1.md rename to docs/releases/v2.9.0.md index ef863e589530938ead7955d11b74eb65eaccdc42..3f29c6942d4a1844951eff0c209e36072c168021 100644 --- a/docs/releases/v2.8.1.md +++ b/docs/releases/v2.9.0.md @@ -1,4 +1,8 @@ -# v2.8.1 +# v2.9.0 + +## Feature + +- Added minimap picture-in-picture in single panel mode ## Behind the scenes @@ -7,4 +11,6 @@ ## Bugfix +- Select default connectivity profile +- Show connectivity dataset info - Mini region search on Enter no longer resets the page diff --git a/mkdocs.yml b/mkdocs.yml index 1961c4105309dcac8b1afcd4a1d87fdef9c05f21..d6c7b67e173b7577f1e79a7a4fda0d59d92bcbfd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,7 +33,7 @@ nav: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: - - v2.8.1: 'releases/v2.8.1.md' + - v2.9.0: 'releases/v2.9.0.md' - v2.8.0: 'releases/v2.8.0.md' - v2.7.7: 'releases/v2.7.7.md' - v2.7.6: 'releases/v2.7.6.md' diff --git a/package.json b/package.json index 5845f4d4767e0c564a10fd5ee5ee37009e7e586e..a38aed3a21dee98a92205763be72d4d0b0202e92 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interactive-viewer", - "version": "2.8.1", + "version": "2.9.0", "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", "scripts": { "lint": "eslint src --ext .ts", diff --git a/src/assets/images/persp-view/bigbrain-axial.png b/src/assets/images/persp-view/bigbrain-axial.png new file mode 100644 index 0000000000000000000000000000000000000000..6919293e6dfaa7c9c07e0a63429dcac06426f22b Binary files /dev/null and b/src/assets/images/persp-view/bigbrain-axial.png differ diff --git a/src/assets/images/persp-view/bigbrain-coronal.png b/src/assets/images/persp-view/bigbrain-coronal.png new file mode 100644 index 0000000000000000000000000000000000000000..fc1a13cd7002be2c84d5b49dd6ed93c429acec6f Binary files /dev/null and b/src/assets/images/persp-view/bigbrain-coronal.png differ diff --git a/src/assets/images/persp-view/bigbrain-sag.png b/src/assets/images/persp-view/bigbrain-sag.png new file mode 100644 index 0000000000000000000000000000000000000000..5ccf92c68d491a5af4a9b932e1978dcb9888f0cd Binary files /dev/null and b/src/assets/images/persp-view/bigbrain-sag.png differ diff --git a/src/assets/images/persp-view/colin-axial.png b/src/assets/images/persp-view/colin-axial.png new file mode 100644 index 0000000000000000000000000000000000000000..6ac1d819230a0044d59f66501d5a622a44eea319 Binary files /dev/null and b/src/assets/images/persp-view/colin-axial.png differ diff --git a/src/assets/images/persp-view/colin-coronal.png b/src/assets/images/persp-view/colin-coronal.png new file mode 100644 index 0000000000000000000000000000000000000000..92162fdb35e24521572c48a26363ea67eea16aa1 Binary files /dev/null and b/src/assets/images/persp-view/colin-coronal.png differ diff --git a/src/assets/images/persp-view/colin-sag.png b/src/assets/images/persp-view/colin-sag.png new file mode 100644 index 0000000000000000000000000000000000000000..db5c471e8420b5c1df66418c3ec3fdb62dac2115 Binary files /dev/null and b/src/assets/images/persp-view/colin-sag.png differ diff --git a/src/assets/images/persp-view/icbm152-axial.png b/src/assets/images/persp-view/icbm152-axial.png new file mode 100644 index 0000000000000000000000000000000000000000..c7d915087410753129526540cf552b1df085f9e4 Binary files /dev/null and b/src/assets/images/persp-view/icbm152-axial.png differ diff --git a/src/assets/images/persp-view/icbm152-coronal.png b/src/assets/images/persp-view/icbm152-coronal.png new file mode 100644 index 0000000000000000000000000000000000000000..627370ce455b8f22279f7cd7a3ade2ebfca92a26 Binary files /dev/null and b/src/assets/images/persp-view/icbm152-coronal.png differ diff --git a/src/assets/images/persp-view/icbm152-sag.png b/src/assets/images/persp-view/icbm152-sag.png new file mode 100644 index 0000000000000000000000000000000000000000..640419067e88b07c79154a9dea54ce30e143ccee Binary files /dev/null and b/src/assets/images/persp-view/icbm152-sag.png differ diff --git a/src/assets/images/persp-view/monkey-axial.png b/src/assets/images/persp-view/monkey-axial.png new file mode 100644 index 0000000000000000000000000000000000000000..ff99d0987bc4d44c0572944cfcae326a85a5bbc6 Binary files /dev/null and b/src/assets/images/persp-view/monkey-axial.png differ diff --git a/src/assets/images/persp-view/monkey-coronal.png b/src/assets/images/persp-view/monkey-coronal.png new file mode 100644 index 0000000000000000000000000000000000000000..7c4f1e9bca768968fdd9ea76bf5262b00d7f73a3 Binary files /dev/null and b/src/assets/images/persp-view/monkey-coronal.png differ diff --git a/src/assets/images/persp-view/monkey-sag.png b/src/assets/images/persp-view/monkey-sag.png new file mode 100644 index 0000000000000000000000000000000000000000..95333bc9fd2de694e5d99218d10b636160946c76 Binary files /dev/null and b/src/assets/images/persp-view/monkey-sag.png differ diff --git a/src/assets/images/persp-view/mouse-axial.png b/src/assets/images/persp-view/mouse-axial.png new file mode 100644 index 0000000000000000000000000000000000000000..86d952265b2edf0e355efb2bfd6eff382e971941 Binary files /dev/null and b/src/assets/images/persp-view/mouse-axial.png differ diff --git a/src/assets/images/persp-view/mouse-coronal.png b/src/assets/images/persp-view/mouse-coronal.png new file mode 100644 index 0000000000000000000000000000000000000000..4503c53e7b97766a3aa97b3e35837e193c7ad4a1 Binary files /dev/null and b/src/assets/images/persp-view/mouse-coronal.png differ diff --git a/src/assets/images/persp-view/mouse-sag.png b/src/assets/images/persp-view/mouse-sag.png new file mode 100644 index 0000000000000000000000000000000000000000..345fa1ba683ca417a01cce73c521344a5334f3e8 Binary files /dev/null and b/src/assets/images/persp-view/mouse-sag.png differ diff --git a/src/assets/images/persp-view/rat-axial.png b/src/assets/images/persp-view/rat-axial.png new file mode 100644 index 0000000000000000000000000000000000000000..cbfc7e62f1ee0742f51ed7444b66904249e9952f Binary files /dev/null and b/src/assets/images/persp-view/rat-axial.png differ diff --git a/src/assets/images/persp-view/rat-coronal.png b/src/assets/images/persp-view/rat-coronal.png new file mode 100644 index 0000000000000000000000000000000000000000..2b6128b0c4a0b3f2850fcf92b066b4b15f6f777c Binary files /dev/null and b/src/assets/images/persp-view/rat-coronal.png differ diff --git a/src/assets/images/persp-view/rat-sag.png b/src/assets/images/persp-view/rat-sag.png new file mode 100644 index 0000000000000000000000000000000000000000..3456b8f495cd764c8a36046774cd92ed84699c35 Binary files /dev/null and b/src/assets/images/persp-view/rat-sag.png differ diff --git a/src/atlasComponents/constants.ts b/src/atlasComponents/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a06a57f2deb55d0d5714e340425d776fae9ce35 --- /dev/null +++ b/src/atlasComponents/constants.ts @@ -0,0 +1,5 @@ +export enum EnumClassicalView { + CORONAL = "Coronal", + SAGITTAL = "Sagittal", + AXIAL = "Axial", +} diff --git a/src/atlasComponents/sapi/core/sapiSpace.ts b/src/atlasComponents/sapi/core/sapiSpace.ts index 5f61ae6f68240437d2a9510b4744d64d16e54977..af2171077788d7a8a879590e6c89305883f90e7c 100644 --- a/src/atlasComponents/sapi/core/sapiSpace.ts +++ b/src/atlasComponents/sapi/core/sapiSpace.ts @@ -1,4 +1,4 @@ -import { Observable } from "rxjs" +import { Observable, throwError } from "rxjs" import { SAPI } from '../sapi.service' import { camelToSnake } from 'common/util' import {SapiQueryPriorityArg, SapiSpaceModel, SapiSpatialFeatureModel, SapiVolumeModel} from "../type" @@ -84,4 +84,23 @@ export class SAPISpace{ )) ) } + + getTemplateSize() { + return this.getVolumes().pipe( + switchMap(volumes => { + const ngVolumes = volumes.filter(vol => vol["@type"] === "spy/volume/neuroglancer/precomputed") + if (ngVolumes.length === 0) return throwError(`template ${this.id} has no ng volume.`) + return this.sapi.httpGet<any>(`${ngVolumes[0].data.url}/info`).pipe( + map(infoJson => { + const { resolution, size } = infoJson.scales[0] + return { + voxel: size as [number, number, number], + real: [0, 1, 2].map(idx => resolution[idx] * size[idx]) as [number, number, number], + transform: ngVolumes[0].data?.detail?.['neuroglancer/precomputed']?.['transform'] as number[][] + } + }) + ) + }) + ) + } } 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 547567b429028c78987f822ea0f613b60b9446c5..18f77fc1b93f298b44f0a0abca283971145655fe 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 @@ -115,10 +115,10 @@ <sxplr-sapiviews-features-connectivity-browser class="pe-all flex-shrink-1" [region]="region" - [types]="hasConnectivityDirective.availableModalities" [sxplr-sapiviews-features-connectivity-browser-atlas]="atlas" [sxplr-sapiviews-features-connectivity-browser-parcellation]="parcellation" [accordionExpanded]="expandedPanel === CONST.CONNECTIVITY" + [types]="hasConnectivityDirective.availableModalities" > </sxplr-sapiviews-features-connectivity-browser> </ng-template> diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts index 9442d04c0d9112715b020c7d5d8da535c31275bd..084a7ed862896c530146930484e131b488b06470 100644 --- a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts +++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts @@ -108,7 +108,6 @@ describe('ConnectivityComponent', () => { beforeEach(async () => { fixture = TestBed.createComponent(ConnectivityBrowserComponent) component = fixture.componentInstance - component.types = types const atlas = 'atlases/juelich/iav/atlas/v1.0.0/1' const parcellation = 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290' @@ -116,8 +115,7 @@ describe('ConnectivityComponent', () => { component.atlas = { '@id': atlas } as SapiAtlasModel component.parcellation = { '@id': parcellation } as SapiParcellationModel - - component.selectType('StreamlineCounts') + component.types = types const url = `${endp}/atlases/${encodeURIComponent(atlas)}/parcellations/${encodeURIComponent(parcellation)}/features?type=${component.selectedTypeId}&size=${100}&page=${1}` diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts index 850d763b82a330e69d402ae769b4956c3c1ee57a..579e34570f4bb3d2abf69fe7af7d60ea9eefe433 100644 --- a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts +++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts @@ -18,6 +18,7 @@ import { HttpClient } from "@angular/common/http"; @Component({ selector: 'sxplr-sapiviews-features-connectivity-browser', templateUrl: './connectivityBrowser.template.html', + styleUrls: ['./connectivityBrowser.style.scss'] }) export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { @@ -57,8 +58,6 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { } - @Input() types: SapiModalityModel[] = [] - public selectedType: string public selectedTypeId: string public selectedCohort: string @@ -105,6 +104,16 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { public logDisabled: boolean = true public logChecked: boolean = true + private _types: SapiModalityModel[] = [] + @Input() + set types(val) { + this._types = val + if (val && val.length) this.selectType(val[0].name) + } + get types() { + return this._types + } + @ViewChild('connectivityComponent', {read: ElementRef}) public connectivityComponentElement: ElementRef<any> @ViewChild('fullConnectivityGrid') public fullConnectivityGridElement: ElementRef<any> @@ -214,6 +223,7 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { this.cohorts = [...new Set(this.fetchedItems.map(item => item.cohort))] this.fetching = false this.changeDetectionRef.detectChanges() + this.selectCohort(this.cohorts[0]) } }) } @@ -265,20 +275,23 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { }) : ds fetchConnectivity(datasetId=null) { - this.sapi.getParcellation(this.atlas["@id"], this.parcellation["@id"]).getFeatureInstance(datasetId || this.selectedDataset['@id']) - .pipe(catchError(() => { - this.fetching = false - return of(null) - })) - .subscribe(ds=> { - this.selectedDataset = this.fixDatasetFormat(ds) - this.setMatrixData(ds) - this.fetching = false - }) + const parcellation = this.sapi.getParcellation(this.atlas["@id"], this.parcellation["@id"]) + if (parcellation) { + parcellation.getFeatureInstance(datasetId || this.selectedDataset['@id']) + .pipe(catchError(() => { + this.fetching = false + return of(null) + })) + .subscribe(ds => { + this.selectedDataset = this.fixDatasetFormat(ds) + this.setMatrixData(ds) + this.fetching = false + }) + } } // ToDo need to be fixed on configuration side - fixHemisphereNaming(area) { + fixHemisphereNaming(area: string) { if (area.includes(' - left hemisphere')) { return area.replace('- left hemisphere', 'left') } else if (area.includes(' - right hemisphere')) { diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.style.scss b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.style.scss new file mode 100644 index 0000000000000000000000000000000000000000..577f01f651e1919092957a31dd898c213594b00a --- /dev/null +++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.style.scss @@ -0,0 +1,3 @@ +::ng-deep label { + margin-bottom: 0 !important; +} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html index b59216a944a65d0001a2d0160035bde3b78e0f02..df2a233d6349bfe8de3b4e06ebca492e2d4695fa 100644 --- a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html +++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html @@ -93,8 +93,16 @@ [disabled]="logDisabled || noConnectivityForRegion">Log 10</mat-checkbox> <button mat-button [matMenuTriggerFor]="exportMenu" [disabled]="!connectedAreas.value"> - <i class="fas fa-download mb-2 mr-2"></i> - <label>Export</label> + <i class="fas fa-download mr-2"></i> + <span>Export</span> + </button> + <button *ngIf="selectedDataset" iav-stop="mousedown click" class="icons" mat-icon-button sxplr-dialog [sxplr-dialog-size]="null" + [sxplr-dialog-data]="{ + title: selectedDataset?.name, + descMd: selectedDataset?.description + '' + selectedDataset?.authors.join(), + actions: selectedDataset | connectivityDoiPipe + }"> + <i class="fas fa-info"></i> </button> </div> diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityDoi.pipe.ts b/src/atlasComponents/sapiViews/features/connectivity/connectivityDoi.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee527989cefa890fb8af7f1cf2fd8bc52411912b --- /dev/null +++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityDoi.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from "@angular/core" +import { SapiParcellationFeatureModel } from "src/atlasComponents/sapi/type" + +@Pipe({ + name: 'connectivityDoiPipe', + pure: true + }) + + export class ConnectivityDoiPipe implements PipeTransform { + public transform(dataset: SapiParcellationFeatureModel): string[] { + const url = `https://search.kg.ebrains.eu/instances/${dataset['dataset_id']}` + return [url] + } + } + \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/connectivity/module.ts b/src/atlasComponents/sapiViews/features/connectivity/module.ts index 82134d44f1a4677d2c15ec0c9abcd225a33e73dc..d796c5f22ce7b132574eadbcda22f8a4f672d949 100644 --- a/src/atlasComponents/sapiViews/features/connectivity/module.ts +++ b/src/atlasComponents/sapiViews/features/connectivity/module.ts @@ -5,16 +5,20 @@ import {ConnectivityBrowserComponent} from "src/atlasComponents/sapiViews/featur import {HasConnectivity} from "src/atlasComponents/sapiViews/features/connectivity/hasConnectivity.directive"; import {AngularMaterialModule} from "src/sharedModules"; import {FormsModule} from "@angular/forms"; +import { DialogModule } from "src/ui/dialogInfo"; +import { ConnectivityDoiPipe } from "./connectivityDoi.pipe"; @NgModule({ imports: [ CommonModule, FormsModule, - AngularMaterialModule + AngularMaterialModule, + DialogModule ], declarations: [ ConnectivityBrowserComponent, - HasConnectivity + HasConnectivity, + ConnectivityDoiPipe ], exports: [ ConnectivityBrowserComponent, diff --git a/src/layouts/currentLayout/currentLayout.component.ts b/src/layouts/currentLayout/currentLayout.component.ts index 0e6a4b56db66d3afbd888e7bb66a9bace4a0a8a5..90d5d25521d004456e845d1f697719ee1c543e52 100644 --- a/src/layouts/currentLayout/currentLayout.component.ts +++ b/src/layouts/currentLayout/currentLayout.component.ts @@ -19,6 +19,7 @@ export class CurrentLayout { FOUR_PANEL: "FOUR_PANEL", H_ONE_THREE: "H_ONE_THREE", SINGLE_PANEL: "SINGLE_PANEL", + PIP_PANEL: "PIP_PANEL", V_ONE_THREE: "V_ONE_THREE" } diff --git a/src/layouts/currentLayout/currentLayout.template.html b/src/layouts/currentLayout/currentLayout.template.html index 1079302ee3c7ae96177427ef21ad262d96f1a2ad..8ec0c59768885aeea52925836d939c79cadac61e 100644 --- a/src/layouts/currentLayout/currentLayout.template.html +++ b/src/layouts/currentLayout/currentLayout.template.html @@ -63,6 +63,25 @@ <ng-content *ngTemplateOutlet="celliv"></ng-content> </ng-container> </layout-single-panel> + <picture-in-picture-panel + *ngSwitchCase="panelModes.PIP_PANEL" + class="d-block sxplr-w-100 sxplr-h-100"> + <ng-container cell-i> + <ng-content *ngTemplateOutlet="celli"></ng-content> + </ng-container> + <ng-container cell-ii> + <ng-content *ngTemplateOutlet="cellii"></ng-content> + </ng-container> + <ng-container cell-iii> + <ng-content *ngTemplateOutlet="celliii"></ng-content> + </ng-container> + <ng-container cell-iv> + <ng-content *ngTemplateOutlet="celliv"></ng-content> + </ng-container> + <div picture-in-picture> + <ng-content *ngTemplateOutlet="pictureInPictureTmp"></ng-content> + </div> + </picture-in-picture-panel> <div *ngSwitchDefault> A panel mode which I have never seen before ... {{ useLayout }} </div> @@ -79,4 +98,7 @@ </ng-template> <ng-template #celliv> <ng-content select="[cell-iv]"></ng-content> -</ng-template> \ No newline at end of file +</ng-template> +<ng-template #pictureInPictureTmp> + <ng-content select="[picture-in-picture]"></ng-content> +</ng-template> diff --git a/src/layouts/layout.module.ts b/src/layouts/layout.module.ts index 40cedc4f12736cadd766c749d55ab3e1139b4a43..773a1e5ab248c44e05479934243df5aefc1a233f 100644 --- a/src/layouts/layout.module.ts +++ b/src/layouts/layout.module.ts @@ -6,6 +6,7 @@ import { CurrentLayout } from "./currentLayout/currentLayout.component"; import { FourCornersCmp } from "./fourCorners/fourCorners.component"; import { FourPanelLayout } from "./layouts/fourPanel/fourPanel.component"; import { HorizontalOneThree } from "./layouts/h13/h13.component"; +import { PictureInPicturePanel } from "./layouts/pip/pip.component"; import { SinglePanel } from "./layouts/single/single.component"; import { VerticalOneThree } from "./layouts/v13/v13.component"; @@ -22,6 +23,7 @@ import { VerticalOneThree } from "./layouts/v13/v13.component"; FourPanelLayout, HorizontalOneThree, SinglePanel, + PictureInPicturePanel, VerticalOneThree, ], exports : [ @@ -31,6 +33,7 @@ import { VerticalOneThree } from "./layouts/v13/v13.component"; FourPanelLayout, HorizontalOneThree, SinglePanel, + PictureInPicturePanel, VerticalOneThree, ], }) diff --git a/src/layouts/layouts/pip/pip.component.ts b/src/layouts/layouts/pip/pip.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c6799dd183d377bd94c8359f64dc1822918821a --- /dev/null +++ b/src/layouts/layouts/pip/pip.component.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: 'picture-in-picture-panel', + templateUrl: './pip.template.html', + styleUrls: [ + './pip.style.css', + ], +}) + +export class PictureInPicturePanel { + +} diff --git a/src/layouts/layouts/pip/pip.style.css b/src/layouts/layouts/pip/pip.style.css new file mode 100644 index 0000000000000000000000000000000000000000..5bb8aef140d3082787c1dd336c3ab3d94eff99c5 --- /dev/null +++ b/src/layouts/layouts/pip/pip.style.css @@ -0,0 +1,12 @@ +.major-column +{ + flex: 0 0 100%; +} +.minor-column +{ + flex: 0 0 0%; +} +.picture-in-picture-margin { + bottom: 50px; + right: 50px; +} \ No newline at end of file diff --git a/src/layouts/layouts/pip/pip.template.html b/src/layouts/layouts/pip/pip.template.html new file mode 100644 index 0000000000000000000000000000000000000000..df1e76de3f5d77f0f1f52b064dbe302b72c02172 --- /dev/null +++ b/src/layouts/layouts/pip/pip.template.html @@ -0,0 +1,8 @@ +<div class="w-100 h-100"> + <ng-content select="[cell-i]"></ng-content> + + <div class="position-fixed picture-in-picture-margin"> + <ng-content select="[picture-in-picture]"></ng-content> + </div> + +</div> diff --git a/src/state/userInterface/const.ts b/src/state/userInterface/const.ts index 929102239aefb764442a329351c02542a293fe8b..404a37241e3b3d1eb4d9581a8839c7517679b1da 100644 --- a/src/state/userInterface/const.ts +++ b/src/state/userInterface/const.ts @@ -2,4 +2,5 @@ export const nameSpace = `[state.ui]` export type PanelMode = 'FOUR_PANEL' | 'V_ONE_THREE' | 'H_ONE_THREE' -| 'SINGLE_PANEL' \ No newline at end of file +| 'SINGLE_PANEL' +| 'PIP_PANEL' \ No newline at end of file diff --git a/src/state/userInterface/effects.ts b/src/state/userInterface/effects.ts index 6a213f60a78cb2d648badd77d56be1f4a1e1e65a..6b5d98bdd6ea423fd59f0450e1f577902b6e4c4c 100644 --- a/src/state/userInterface/effects.ts +++ b/src/state/userInterface/effects.ts @@ -72,7 +72,7 @@ export class Effects{ ), switchMap(([ { targetIndex }, panelMode ]) => { const newMode: userInterface.PanelMode = panelMode === "FOUR_PANEL" - ? "SINGLE_PANEL" + ? "PIP_PANEL" : "FOUR_PANEL" const newOrder = newMode === "FOUR_PANEL" ? "0123" @@ -98,7 +98,7 @@ export class Effects{ select(userInterface.selectors.panelOrder) ), ), - filter(([_, panelMode, _1]) => panelMode === "SINGLE_PANEL"), + filter(([_, panelMode, _1]) => ['SINGLE_PANEL', 'PIP_PANEL'].includes(panelMode)), map(([_, _1, panelOrder]) => userInterface.actions.setPanelOrder({ order: [ ...panelOrder.split('').slice(1), diff --git a/src/ui/config/configCmp/config.component.ts b/src/ui/config/configCmp/config.component.ts index 494403b61842914fa93cfd377d8aa2ffe907ca5f..3d1dfdce600504a6fb92b684560e61adb749d26e 100644 --- a/src/ui/config/configCmp/config.component.ts +++ b/src/ui/config/configCmp/config.component.ts @@ -34,6 +34,7 @@ export class ConfigComponent implements OnInit, OnDestroy { FOUR_PANEL: "FOUR_PANEL", H_ONE_THREE: "H_ONE_THREE", SINGLE_PANEL: "SINGLE_PANEL", + PIP_PANEL: "PIP_PANEL", V_ONE_THREE: "V_ONE_THREE", } diff --git a/src/ui/config/configCmp/config.template.html b/src/ui/config/configCmp/config.template.html index eab836a149b2f949cd154ea2e49fe572a90dca6b..85bbb90d53f68e253e52e99407245283417d4945 100644 --- a/src/ui/config/configCmp/config.template.html +++ b/src/ui/config/configCmp/config.template.html @@ -222,6 +222,23 @@ previewTmpl: singlePanelTmpl }"> </ng-template> + + <!-- picture-in-picture --> + <ng-template #pipPanelTmpl> + <picture-in-picture-panel class="d-block w-10em h-7em"> + <div class="border chunky" cell-i></div> + <div class="border chunky" cell-ii></div> + <div class="border chunky" cell-iii></div> + <div class="border chunky" cell-iv></div> + </picture-in-picture-panel> + </ng-template> + <ng-template [ngTemplateOutlet]="panelModeBtnTmpl" + [ngTemplateOutletContext]="{ + panelMode: panelModes.PIP_PANEL, + previewTmpl: pipPanelTmpl + }"> + </ng-template> + </div> </mat-tab> </mat-tab-group> diff --git a/src/viewerModule/nehuba/layoutOverlay/module.ts b/src/viewerModule/nehuba/layoutOverlay/module.ts index 4160b8d12d401b9acd2d7a0fc65f3af288d9261d..b19dbd7041aea8a179e4647155a8bcaaee6fa3b1 100644 --- a/src/viewerModule/nehuba/layoutOverlay/module.ts +++ b/src/viewerModule/nehuba/layoutOverlay/module.ts @@ -9,7 +9,6 @@ import { UtilModule } from "src/util"; import { WindowResizeModule } from "src/util/windowResize"; import { LayoutModule } from "src/layouts/layout.module"; import { MatButtonModule } from "@angular/material/button"; -import { MatTooltipModule } from "@angular/material/tooltip"; @NgModule({ imports: [ @@ -22,7 +21,7 @@ import { MatTooltipModule } from "@angular/material/tooltip"; UtilModule, WindowResizeModule, MatButtonModule, - MatTooltipModule, + MatMenuModule ], declarations: [ NehubaLayoutOverlay, diff --git a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts index 2fcf3b824328773660a44e725872f1aafd36dfd5..a6e58b8462cb398d87f2f6b26b61607110950f2d 100644 --- a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts +++ b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts @@ -3,10 +3,11 @@ import { select, Store } from "@ngrx/store"; import { combineLatest, fromEvent, interval, merge, Observable, of, Subject, Subscription } from "rxjs"; import { userInterface } from "src/state"; import { NehubaViewerUnit } from "../../nehubaViewer/nehubaViewer.component"; -import { NEHUBA_INSTANCE_INJTKN, takeOnePipe, getFourPanel, getHorizontalOneThree, getSinglePanel, getVerticalOneThree } from "../../util"; +import { NEHUBA_INSTANCE_INJTKN, takeOnePipe, getFourPanel, getHorizontalOneThree, getSinglePanel, getPipPanel, getVerticalOneThree } from "../../util"; import { QUICKTOUR_DESC, ARIA_LABELS, IDS } from 'common/constants' import { IQuickTourData } from "src/ui/quickTour/constrants"; import { debounce, debounceTime, distinctUntilChanged, filter, map, mapTo, switchMap, take } from "rxjs/operators"; +import {panelOrder} from "src/state/userInterface/selectors"; @Component({ selector: `nehuba-layout-overlay`, @@ -45,7 +46,7 @@ export class NehubaLayoutOverlay implements OnDestroy{ } handleCycleViewEvent(): void { - if (this.currentPanelMode !== "SINGLE_PANEL") return + if (!["SINGLE_PANEL", 'PIP_PANEL'].includes(this.currentPanelMode)) return this.store$.dispatch( userInterface.actions.cyclePanelMode() ) @@ -111,6 +112,12 @@ export class NehubaLayoutOverlay implements OnDestroy{ public volumeChunkLoading$: Subject<boolean> = new Subject() + public showPipPerspectiveView$ = this.store$.pipe( + select(panelOrder), + distinctUntilChanged(), + map(po => po[0] !== '3') + ) + constructor( private store$: Store, private cdr: ChangeDetectorRef, @@ -126,7 +133,7 @@ export class NehubaLayoutOverlay implements OnDestroy{ } private onNewNehubaUnit(nehubaUnit: NehubaViewerUnit){ - + while(this.nehubaUnitSubs.length) this.nehubaUnitSubs.pop().unsubscribe() this.nehubaViewPanels = [] this.nanometersToOffsetPixelsFn = [] @@ -223,7 +230,7 @@ export class NehubaLayoutOverlay implements OnDestroy{ this.panelMode$, this.panelOrder$, ]).pipe( - debounce(() => + debounce(() => nehubaUnit?.nehubaViewer?.ngviewer ? of(true) : interval(16).pipe( @@ -232,19 +239,19 @@ export class NehubaLayoutOverlay implements OnDestroy{ ) ) ).subscribe(([mode, panelOrder]) => { - + this.currentPanelMode = mode as userInterface.PanelMode this.currentOrder = panelOrder const viewPanels = panelOrder.split('').map(v => Number(v)).map(idx => this.nehubaViewPanels[idx]) as [HTMLElement, HTMLElement, HTMLElement, HTMLElement] - + /** * TODO smarter with event stream */ if (!viewPanels.every(v => !!v)) { return } - + switch (this.currentPanelMode) { case "H_ONE_THREE": { const element = removeExistingPanels() @@ -270,12 +277,18 @@ export class NehubaLayoutOverlay implements OnDestroy{ element.appendChild(newEl) break; } + case "PIP_PANEL": { + const element = removeExistingPanels() + const newEl = getPipPanel(viewPanels) + element.appendChild(newEl) + break; + } default: } for (const panel of viewPanels) { (panel as HTMLElement).classList.add('neuroglancer-panel') } - + this.detectChanges() nehubaUnit.redraw() }) @@ -287,7 +300,7 @@ export class NehubaLayoutOverlay implements OnDestroy{ public detectChanges(): void { this.cdr.detectChanges() } - + private nehubaUnit: NehubaViewerUnit private findPanelIndex = (panel: HTMLElement) => this.viewPanelWeakMap.get(panel) diff --git a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.style.css b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.style.css index beea6a73ad8a3e22daa03c55a5d6022f175edda4..6d42ed6d58d79788b5f4188be14cb21379bf4246 100644 --- a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.style.css +++ b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.style.css @@ -38,4 +38,4 @@ current-layout { opacity: 1.0 !important; pointer-events: all !important; -} +} \ No newline at end of file diff --git a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html index e43354b855f1e5ef945172019993701c1b6e8fa3..5ae334a2db080f80a983734c1004c3e3e46e231b 100644 --- a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html +++ b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html @@ -25,6 +25,9 @@ [quick-tour-order]="quickTour3dViewSlide.order"> <ng-content *ngTemplateOutlet="ngPanelOverlayTmpl; context: { panelIndex: currentOrder | getProperty : 3 | parseAsNumber }"></ng-content> </div> + <div *ngIf="showPipPerspectiveView$ | async" class="w-100 h-100 position-relative" style="z-index: 100" picture-in-picture > + <nehuba-perspective-view-slider class="pe-all"></nehuba-perspective-view-slider> + </div> </current-layout> <!-- slice view overlay tmpl --> @@ -113,7 +116,7 @@ color="primary" (click)="toggleMaximiseMinimise(panelIndex)"> <ng-template - [ngIf]="currentPanelMode === 'SINGLE_PANEL'" + [ngIf]="currentPanelMode === 'SINGLE_PANEL' || currentPanelMode === 'PIP_PANEL'" [ngIfElse]="expandTmpl"> <i class="fas fa-compress"></i> </ng-template> diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 4d38f0487b48b24a678e8a34b454d521d102e94e..465d3144979c3e6997e4dbefcb12f6009ab9ea21 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -64,8 +64,8 @@ export class NehubaViewerUnit implements OnDestroy { public ngIdSegmentsMap: Record<string, number[]> = {} public viewerPosInVoxel$ = new BehaviorSubject(null) - public viewerPosInReal$ = new BehaviorSubject(null) - public mousePosInVoxel$ = new BehaviorSubject(null) + public viewerPosInReal$ = new BehaviorSubject<[number, number, number]>(null) + public mousePosInVoxel$ = new BehaviorSubject<[number, number, number]>(null) public mousePosInReal$ = new BehaviorSubject(null) private exportNehuba: any @@ -76,7 +76,7 @@ export class NehubaViewerUnit implements OnDestroy { @Output() public nehubaReady: EventEmitter<null> = new EventEmitter() @Output() public layersChanged: EventEmitter<null> = new EventEmitter() private layersChangedHandler: any - @Output() public viewerPositionChange: EventEmitter<any> = new EventEmitter() + @Output() public viewerPositionChange: EventEmitter<{ orientation: number[], perspectiveOrientation: number[], perspectiveZoom: number, zoom: number, position: number[], positionReal?: boolean }> = new EventEmitter() @Output() public mouseoverSegmentEmitter: EventEmitter<{ segmentId: number | null @@ -814,7 +814,7 @@ export class NehubaViewerUnit implements OnDestroy { .filter(v => typeof v !== 'undefined' && v !== null) .subscribe(v => { this.navPosReal = Array.from(v) as [number, number, number] - this.viewerPosInReal$.next(Array.from(v)) + this.viewerPosInReal$.next(Array.from(v) as [number, number, number]) }) this._s5$ = this.nehubaViewer.navigationState.position.inVoxels .filter(v => typeof v !== 'undefined' && v !== null) @@ -832,7 +832,7 @@ export class NehubaViewerUnit implements OnDestroy { .filter(v => typeof v !== 'undefined' && v !== null) .subscribe(v => { this.mousePosVoxel = Array.from(v) as [number, number, number] - this.mousePosInVoxel$.next(Array.from(v)) + this.mousePosInVoxel$.next(Array.from(v) as [number, number, number] ) }) } diff --git a/src/viewerModule/nehuba/store/store.ts b/src/viewerModule/nehuba/store/store.ts index 6782fa77c723fe8457f268df1d74f63535556b58..0dd7043f3ac0da58ad43d7211fcce3983d6b85d2 100644 --- a/src/viewerModule/nehuba/store/store.ts +++ b/src/viewerModule/nehuba/store/store.ts @@ -7,11 +7,12 @@ import { INehubaFeature } from "./type"; * TODO port from global store to feature store */ -enum EnumPanelMode { +export enum EnumPanelMode { FOUR_PANEL = 'FOUR_PANEL', V_ONE_THREE = 'V_ONE_THREE', H_ONE_THREE = 'H_ONE_THREE', SINGLE_PANEL = 'SINGLE_PANEL', + PIP_PANEL = 'PIP_PANEL', } const defaultState: INehubaFeature = { diff --git a/src/viewerModule/nehuba/util.ts b/src/viewerModule/nehuba/util.ts index 71d7a985a09c1ecfa3dd79fdac4c75f3c380bef9..1073c1bde8e78db2c490b44ab6bc17c65b8aab92 100644 --- a/src/viewerModule/nehuba/util.ts +++ b/src/viewerModule/nehuba/util.ts @@ -60,6 +60,13 @@ mapModeIdxClass.set("SINGLE_PANEL", new Map([ [3, {}], ])) +mapModeIdxClass.set("PIP_PANEL", new Map([ + [0, { top, left, right, bottom }], + [1, {}], + [2, {}], + [3, {}], +])) + mapModeIdxClass.set("H_ONE_THREE", new Map([ [0, { top, left, bottom }], [1, { top, right }], @@ -144,16 +151,26 @@ export const getFourPanel = (panels: [HTMLElement, HTMLElement, HTMLElement, HTM export const getSinglePanel = (panels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement]): HTMLDivElement => { washPanels(panels) + return getFullViewPanel(panels) +} + +export const getPipPanel = (panels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement]): HTMLDivElement => { + washPanels(panels) + + return getFullViewPanel(panels) +} + +const getFullViewPanel = (panels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement]): HTMLDivElement => { + panels.forEach((panel, idx) => addTouchSideClasses(panel, idx, "SINGLE_PANEL")) const majorContainer = makeRow(panels[0]) - const minorContainer = makeRow(panels[1], panels[2], panels[3]) - majorContainer.style.flexBasis = '100%' - minorContainer.style.flexBasis = '0%' + const minorContainer = makeRow(panels[1], panels[2], panels[3]) + minorContainer.style.flexBasis = '0%' minorContainer.className = '' - minorContainer.style.height = '0px' + return makeRow(majorContainer, minorContainer) } diff --git a/src/viewerModule/nehuba/viewerCtrl/effects.ts b/src/viewerModule/nehuba/viewerCtrl/effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e62acc8e260133181cff0412135ccdf2a7846f6 --- /dev/null +++ b/src/viewerModule/nehuba/viewerCtrl/effects.ts @@ -0,0 +1,17 @@ +import { Injectable } from "@angular/core"; +import { createEffect } from "@ngrx/effects"; +import { Store } from "@ngrx/store"; +import { mapTo } from "rxjs/operators"; +import { atlasSelection, userInterface } from "src/state"; + +@Injectable() +export class ViewerCtrlEffects { + onTemplateChangeResetLayout$ = createEffect(() => this.store$.pipe( + atlasSelection.fromRootStore.distinctATP(), + mapTo(userInterface.actions.setPanelMode({ + panelMode: "FOUR_PANEL" + })) + )) + + constructor(private store$: Store){} +} diff --git a/src/viewerModule/nehuba/viewerCtrl/module.ts b/src/viewerModule/nehuba/viewerCtrl/module.ts index 4052de0ac81de5cc6dd9effc75163429de938b4b..082107630ef288fa064e13bb3ec779094ee2abe3 100644 --- a/src/viewerModule/nehuba/viewerCtrl/module.ts +++ b/src/viewerModule/nehuba/viewerCtrl/module.ts @@ -5,7 +5,11 @@ import { ComponentsModule } from "src/components"; import { AngularMaterialModule } from "src/sharedModules"; import { UtilModule } from "src/util"; import { ViewerCtrlCmp } from "./viewerCtrlCmp/viewerCtrlCmp.component"; +import { PerspectiveViewSlider } from "./perspectiveViewSlider/perspectiveViewSlider.component"; import { SnapPerspectiveOrientationCmp } from "src/viewerModule/nehuba/viewerCtrl/snapPerspectiveOrientation/snapPerspectiveOrientation.component"; +import { WindowResizeModule } from "src/util/windowResize"; +import { EffectsModule } from "@ngrx/effects"; +import { ViewerCtrlEffects } from "./effects" @NgModule({ imports: [ @@ -15,13 +19,19 @@ import { SnapPerspectiveOrientationCmp } from "src/viewerModule/nehuba/viewerCtr FormsModule, ReactiveFormsModule, ComponentsModule, + WindowResizeModule, + EffectsModule.forFeature([ + ViewerCtrlEffects + ]) ], declarations: [ ViewerCtrlCmp, - SnapPerspectiveOrientationCmp + PerspectiveViewSlider, + SnapPerspectiveOrientationCmp, ], exports: [ - ViewerCtrlCmp + ViewerCtrlCmp, + PerspectiveViewSlider ] }) diff --git a/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.component.ts b/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..56a3621904da2da3743b66e6b415cca5c2d70724 --- /dev/null +++ b/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.component.ts @@ -0,0 +1,403 @@ +import { Component, OnDestroy, Inject, ViewChild, ChangeDetectionStrategy } from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { select, Store } from "@ngrx/store"; +import { combineLatest, concat, NEVER, Observable, of, Subject, Subscription } from "rxjs"; +import { switchMap, distinctUntilChanged, map, debounceTime, shareReplay, take, withLatestFrom } from "rxjs/operators"; +import { SAPI, SapiSpaceModel } from "src/atlasComponents/sapi"; +import { fromRootStore } from "src/state/atlasSelection"; +import { selectedTemplate } from "src/state/atlasSelection/selectors"; +import { panelMode, panelOrder } from "src/state/userInterface/selectors"; +import { ResizeObserverDirective } from "src/util/windowResize"; +import { NehubaViewerUnit } from "../../nehubaViewer/nehubaViewer.component"; +import { EnumPanelMode } from "../../store/store"; +import { NEHUBA_INSTANCE_INJTKN } from "../../util"; +import { EnumClassicalView } from "src/atlasComponents/constants" +import { atlasSelection } from "src/state"; +import { floatEquality } from "common/util" + +const MAX_DIM = 200 + +type AnatomicalOrientation = 'ap' | 'si' | 'rl' // anterior-posterior, superior-inferior, right-left +type RangeOrientation = 'horizontal' | 'vertical' +const anatOriToIdx: Record<AnatomicalOrientation, number> = { + 'rl': 0, + 'ap': 1, + 'si': 2 +} + +function getDim(triplet: number[], view: EnumClassicalView) { + if (view === EnumClassicalView.AXIAL) { + return [triplet[0], triplet[1]] + } + if (view === EnumClassicalView.CORONAL) { + return [triplet[0], triplet[2]] + } + if (view === EnumClassicalView.SAGITTAL) { + return [triplet[1], triplet[2]] + } +} + +@Component({ + selector: 'nehuba-perspective-view-slider', + templateUrl: './perspectiveViewSlider.template.html', + styleUrls: ['./perspectiveViewSlider.style.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) + +export class PerspectiveViewSlider implements OnDestroy { + + @ViewChild(ResizeObserverDirective) + resizeDirective: ResizeObserverDirective + + public minimapControl = new FormControl() + public recalcViewportSize$ = new Subject() + + private selectedTemplate$ = this.store$.pipe( + select(selectedTemplate), + distinctUntilChanged((o, n) => o?.["@id"] === n?.["@id"]), + ) + private subscriptions: Subscription[] = [] + private maximisedPanelIndex$ = combineLatest([ + this.store$.pipe( + select(panelMode), + distinctUntilChanged(), + ), + this.store$.pipe( + select(panelOrder), + distinctUntilChanged(), + ), + ]).pipe( + map(([ mode, order ]) => { + if (!([EnumPanelMode.PIP_PANEL, EnumPanelMode.SINGLE_PANEL].includes(mode as EnumPanelMode))) { + return null + } + return Number(order[0]) + }) + ) + + private viewportSize$ = concat( + of(null), // emit on init + this.recalcViewportSize$, + ).pipe( + debounceTime(160), + map(() => { + const panel = document.getElementsByClassName('neuroglancer-panel') as HTMLCollectionOf<HTMLElement> + if (!(panel?.[0])) { + return null + } + return { + width: panel[0].offsetWidth, + height: panel[0].offsetHeight + } + }), + shareReplay(1), + ) + + private navPosition$: Observable<{real: [number, number, number], voxel: [number, number, number]}> = this.nehubaViewer$.pipe( + switchMap(viewer => { + if (!viewer) return of(null) + return combineLatest([ + viewer.viewerPosInReal$, + viewer.viewerPosInVoxel$, + ]).pipe( + map(([ real, voxel ]) => { + return { real, voxel } + }) + ) + }), + shareReplay(1) + ) + + private rangeControlSetting$ = this.maximisedPanelIndex$.pipe( + map(maximisedPanelIndex => { + let anatomicalOrientation: AnatomicalOrientation = null + let rangeOrientation: RangeOrientation = null + let minimapView: EnumClassicalView + let sliceView: EnumClassicalView + if (maximisedPanelIndex === 0) { + anatomicalOrientation = 'ap' + rangeOrientation = 'horizontal' + minimapView = EnumClassicalView.SAGITTAL + sliceView = EnumClassicalView.CORONAL + } + if (maximisedPanelIndex === 1) { + anatomicalOrientation = 'rl' + rangeOrientation = 'horizontal' + minimapView = EnumClassicalView.CORONAL + sliceView = EnumClassicalView.SAGITTAL + } + if (maximisedPanelIndex === 2) { + anatomicalOrientation = 'si' + rangeOrientation = 'vertical' + minimapView = EnumClassicalView.CORONAL + sliceView = EnumClassicalView.AXIAL + } + return { + anatomicalOrientation, + rangeOrientation, + minimapView, + sliceView + } + }) + ) + + public rangeControlIsVertical$ = this.rangeControlSetting$.pipe( + map(ctrl => ctrl?.rangeOrientation === "vertical") + ) + + private currentTemplateSize$ = this.store$.pipe( + fromRootStore.distinctATP(), + switchMap(({ atlas, template }) => + atlas && template + ? this.sapi.getSpace(atlas['@id'], template['@id']).getTemplateSize() + : NEVER), + ) + + private useMinimap$: Observable<EnumClassicalView> = this.maximisedPanelIndex$.pipe( + map(maximisedPanelIndex => { + if (maximisedPanelIndex === 0) return EnumClassicalView.SAGITTAL + if (maximisedPanelIndex === 1) return EnumClassicalView.CORONAL + if (maximisedPanelIndex === 2) return EnumClassicalView.CORONAL + return null + }) + ) + + + // this crazy hack is required since firefox support vertical-orient + // do not and -webkit-slider-thumb#apperance cannot be used to hide the thumb + public rangeInputStyle$ = this.rangeControlIsVertical$.pipe( + withLatestFrom(this.currentTemplateSize$, this.useMinimap$), + map(([ isVertical, templateSizes, useMinimap ]) => { + if (!isVertical) return {} + const { real } = templateSizes + const [ width, height ] = getDim(real, useMinimap) + const max = Math.max(width, height) + const useHeight = width/max*MAX_DIM + const useWidth = height/max*MAX_DIM + + const xformOriginVal = Math.min(useHeight, useWidth)/2 + const transformOrigin = `${xformOriginVal}px ${xformOriginVal}px` + + return { + height: `${useHeight}px`, + width: `${useWidth}px`, + transformOrigin, + } + }) + ) + + public rangeControlMinMaxValue$ = this.currentTemplateSize$.pipe( + switchMap(templateSize => { + return this.rangeControlSetting$.pipe( + switchMap(orientation => this.navPosition$.pipe( + take(1), + map(nav => { + if (!nav || !orientation || !templateSize) return null + + const { real: realPos } = nav + + const { anatomicalOrientation: anatOri } = orientation + const idx = anatOriToIdx[anatOri] + + const { real, transform } = templateSize + if (!transform || !transform[idx]) return null + const min = Math.round(transform[idx][3]) + const max = Math.round(real[idx] + transform[idx][3]) + + return { + min, max, value: realPos[idx] + } + }) + )) + ) + }), + ) + + public previewImageUrl$ = combineLatest([ + this.selectedTemplate$, + this.useMinimap$, + ]).pipe( + map(([template, view]) => { + const url = getScreenshotUrl(template, view) + if (!url) return null + return `assets/images/persp-view/${url}` + }) + ) + + public sliceviewIsNormal$ = this.store$.pipe( + select(atlasSelection.selectors.navigation), + map(navigation => { + // if navigation in store is nullish, assume starting position, ie slice view is normal + if (!navigation) return true + return [0, 0, 0, 1].every((v, idx) => floatEquality(navigation.orientation[idx], v, 1e-3))}) + ) + + public textToDisplay$ = combineLatest([ + this.sliceviewIsNormal$, + this.navPosition$, + this.maximisedPanelIndex$, + ]).pipe( + map(([ sliceviewIsNormal, nav, maximisedIdx ]) => { + if (!sliceviewIsNormal) return null + if (!(nav?.real) || (maximisedIdx === null)) return null + return `${(nav.real[maximisedIdx === 0? 1 : maximisedIdx === 1? 0 : 2] / 1e6).toFixed(3)}mm` + }) + ) + + public scrubberPosition$ = this.rangeControlMinMaxValue$.pipe( + switchMap(minmaxval => concat( + of(null), + this.minimapControl.valueChanges, + ).pipe( + map(newval => { + if (!minmaxval) return null + const { min, max, value } = minmaxval + if (min === null || max === null) return null + const useValue = newval ?? value + if (useValue === null) return null + const translate = 100 * (useValue - min) / (max - min) + return `translateX(${translate}%)` + }) + )) + ) + + public scrubberHighlighter$ = this.nehubaViewer$.pipe( + switchMap(viewer => combineLatest([ + // on component init, the viewerPositionChange would not have fired + // in this case. So we get the zoom from the store as the first value + concat( + this.store$.pipe( + select(atlasSelection.selectors.navigation), + take(1) + ), + viewer + ? viewer.viewerPositionChange + : NEVER, + ), + this.viewportSize$, + this.rangeControlSetting$, + this.currentTemplateSize$, + this.rangeControlIsVertical$, + ]).pipe( + map(([ navigation, viewportSize, ctrl, templateSize, isVertical ]) => { + if (!ctrl || !(templateSize?.real) || !navigation) return null + + const { zoom, position } = navigation + + let translate: number = null + const { sliceView } = ctrl + + const getTranslatePc = (idx: number) => { + const trueCenter = templateSize.real[idx] / 2 + const compensate = trueCenter + templateSize.transform[idx][3] + return (position[idx] - compensate) / templateSize.real[idx] + } + + let scale: number = 2 + const sliceviewDim = getDim(templateSize.real, sliceView) + if (!sliceviewDim) return null + + if (sliceView === EnumClassicalView.CORONAL) { + // minimap is sagittal view, so interested in superior-inferior axis + translate = getTranslatePc(2) + scale = Math.min(scale, viewportSize.height * zoom / sliceviewDim[1]) + } + + if (sliceView === EnumClassicalView.SAGITTAL) { + // minimap is coronal view, so interested in superior-inferior axis + translate = getTranslatePc(2) + scale = Math.min(scale, viewportSize.height * zoom / sliceviewDim[1]) + } + + if (sliceView === EnumClassicalView.AXIAL) { + // minimap is in coronal view, so interested in left-right axis + translate = getTranslatePc(0) * -1 + scale = Math.min(scale, viewportSize.width * zoom / sliceviewDim[0]) + } + + /** + * calculate scale + */ + const scaleString = `scaleY(${scale})` + + /** + * calculate translation + */ + const translateString = `translateY(${translate * -100}%)` + + return `${translateString} ${scaleString}` + }) + )) + ) + + constructor( + private store$: Store, + private sapi: SAPI, + @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaViewer$: Observable<NehubaViewerUnit>, + ) { + + this.subscriptions.push( + combineLatest([ + this.nehubaViewer$, + this.rangeControlSetting$, + ]).pipe( + switchMap(([ nehubaViewer, rangeCtrl ]) => this.minimapControl.valueChanges.pipe( + withLatestFrom(this.navPosition$.pipe( + map(value => value?.real) + )), + map(([newValue, currentPosition]) => ({ nehubaViewer, rangeCtrl, newValue, currentPosition })) + )) + ).subscribe(({ nehubaViewer, rangeCtrl, newValue, currentPosition }) => { + if (newValue === null) return + const { anatomicalOrientation } = rangeCtrl + if (!anatomicalOrientation) return + const idx = anatOriToIdx[anatomicalOrientation] + const newNavPosition = [...currentPosition] + newNavPosition[idx] = newValue + nehubaViewer.setNavigationState({ + position: newNavPosition, + positionReal: true + }) + }), + ) + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + resetSliceview() { + this.store$.dispatch( + atlasSelection.actions.navigateTo({ + animation: true, + navigation: { + orientation: [0, 0, 0, 1] + } + }) + ) + } + +} + +const spaceIdToPrefix = { + "minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2": "icbm152", + "minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992": "colin", + "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588": "bigbrain", + "minds/core/referencespace/v1.0.0/MEBRAINS": "monkey", + "minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9": "mouse", + "minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8": "rat" +} + +const viewToSuffix = { + [EnumClassicalView.SAGITTAL]: 'sag', + [EnumClassicalView.AXIAL]: 'axial', + [EnumClassicalView.CORONAL]: 'coronal', +} + +function getScreenshotUrl(space: SapiSpaceModel, requestedView: EnumClassicalView): string { + const prefix = spaceIdToPrefix[space?.['@id']] + if (!prefix) return null + const suffix = viewToSuffix[requestedView] + if (!suffix) return null + return `${prefix}-${suffix}.png` +} diff --git a/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.style.css b/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.style.css new file mode 100644 index 0000000000000000000000000000000000000000..bfd1bed6c8b4592410adbbec683998324635b60b --- /dev/null +++ b/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.style.css @@ -0,0 +1,100 @@ +:host +{ + display: inline-block; + background-size: cover; + position: relative; + overflow: hidden; + + border: 1px solid rgba(200, 200, 200, 0.5); + border-radius: 0.25rem; + + opacity: 0.7; + transition: all 160ms ease-in-out; +} + +:host:hover +{ + opacity: 1.0; +} + +img +{ + max-width: 200px; + max-height: 200px; + aspect-ratio: auto; +} + +.range-container, +.range-input-wrapper +{ + width: 100%; + height: 100%; +} + + +.anchored +{ + position: absolute; + left: 0; + top: 0; +} + +.perspective-slider-range-input +{ + appearance: none; + width: 100%; + height: 100%; + + background: transparent; +} + +.scrubber +{ + pointer-events: none; + width: 100%; + height: 100%; + display: inline-block; + + border-style: solid; + border-width: 0 0 0 0.5rem; + margin-left: -0.25rem; + + border-color: rgba(10, 10, 10, 0.5); +} + +:host-context([darktheme="true"]) .scrubber +{ + border-color: rgba(200, 200, 200, 0.5); +} + +.scrubber > .scrubber-highlight +{ + background-color: red; + display: inline-block; + height: 100%; + width: 0.5rem; + margin-left: -0.5rem; +} + + +.perspective-slider-range-input::-webkit-slider-thumb { + appearance: none; + height: 100%; + width: 1px; +} + +.perspective-slider-range-input::-moz-range-thumb { + appearance: none; + height: 100%; + width: 1px; +} +.perspective-slider-range-input:focus { + outline: none; +} + + +.range-value { + right: 10px; + bottom: 5px; +} + diff --git a/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.template.html b/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.template.html new file mode 100644 index 0000000000000000000000000000000000000000..a439a62f03872271b8eba33222c732bfcdec1b42 --- /dev/null +++ b/src/viewerModule/nehuba/viewerCtrl/perspectiveViewSlider/perspectiveViewSlider.template.html @@ -0,0 +1,46 @@ +<div class="range-container" + *ngIf="sliceviewIsNormal$ | async else resetOrientationTmpl"> + <img *ngIf="previewImageUrl$ | async as url" [src]="url"> + + <div class="range-input-wrapper anchored" + [style]="rangeInputStyle$ | async" + [ngClass]="{ + 'r-270': rangeControlIsVertical$ | async + }"> + + <input type="range" + *ngIf="rangeControlMinMaxValue$ | async as minMaxValue" + iav-window-resize + (iav-window-resize-event)="recalcViewportSize$.next($event)" + [min]="minMaxValue.min" + [max]="minMaxValue.max" + [value]="minMaxValue.value" + [formControl]="minimapControl" + class="anchored perspective-slider-range-input" + > + + <div *ngIf="scrubberPosition$ | async as transform" + [style.transform]="transform" + class="anchored scrubber"> + <div *ngIf="scrubberHighlighter$ | async as highlighter" + [style.transform]="highlighter" class="scrubber-highlight"> + </div> + </div> + </div> +</div> + +<ng-template #resetOrientationTmpl> + <div class="sxplr-custom-cmp text sxplr-p-2 bg"> + Minimap disabled until orientation is reset. + + <button mat-button + (click)="resetSliceview()"> + Reset Orientation + </button> + </div> +</ng-template> + +<div *ngIf="textToDisplay$ | async as textToDisplay" + class="position-absolute range-value sxplr-custom-cmp text"> + {{ textToDisplay }} +</div> diff --git a/src/viewerModule/nehuba/viewerCtrl/snapPerspectiveOrientation/snapPerspectiveOrientation.component.ts b/src/viewerModule/nehuba/viewerCtrl/snapPerspectiveOrientation/snapPerspectiveOrientation.component.ts index 6d936f0c3b2c62c21de377b7205676d56c2eee33..788341d055ef930840cbd23f39a5e324c0f9d58c 100644 --- a/src/viewerModule/nehuba/viewerCtrl/snapPerspectiveOrientation/snapPerspectiveOrientation.component.ts +++ b/src/viewerModule/nehuba/viewerCtrl/snapPerspectiveOrientation/snapPerspectiveOrientation.component.ts @@ -6,12 +6,7 @@ import { actions } from 'src/state/atlasSelection'; import { VALUES } from "common/constants" import { floatEquality } from "common/util" import { filter, map } from 'rxjs/operators'; - -enum EnumClassicalView { - CORONAL = "Coronal", - SAGITTAL = "Sagittal", - AXIAL = "Axial", -} +import { EnumClassicalView } from "src/atlasComponents/constants" const viewOrientations : Record<EnumClassicalView, number[][]> = { [EnumClassicalView.CORONAL]: [[0,-1 * VALUES.ROOT_2,VALUES.ROOT_2,0], [-1 * VALUES.ROOT_2,0,0,VALUES.ROOT_2]],