diff --git a/deploy/assets/images/atlas-selection/freesurfer.png b/deploy/assets/images/atlas-selection/freesurfer.png new file mode 100644 index 0000000000000000000000000000000000000000..3f85d87f42345c3487e8548fa2ab91f406b0c67c Binary files /dev/null and b/deploy/assets/images/atlas-selection/freesurfer.png differ diff --git a/deploy/atlas/index.js b/deploy/atlas/index.js index 08d79085e0bdde4e6c8db147853d297d4cc1fad7..eb96d883cff1593df8dfd59cdeeae8b235567342 100644 --- a/deploy/atlas/index.js +++ b/deploy/atlas/index.js @@ -55,6 +55,8 @@ const previewImageFIleNameMap = new Map([ ['minds/core/parcellationatlas/v1.0.0/2449a7f0-6dd0-4b5a-8f1e-aec0db03679d', 'waxholm-v2.png'], ['minds/core/parcellationatlas/v1.0.0/11017b35-7056-4593-baad-3934d211daba', 'waxholm-v1.png'], ['juelich/iav/atlas/v1.0.0/79cbeaa4ee96d5d3dfe2876e9f74b3dc3d3ffb84304fb9b965b1776563a1069c', 'short-bundle-hcp.png'], + ['julich/tmp/referencespace/freesurfer', 'freesurfer.png'], + ['julich/tmp/parcellation/freesurfer-test-parc', 'freesurfer.png'], ]) router.get('/preview', (req, res) => { diff --git a/deploy/csp/index.js b/deploy/csp/index.js index 8f664fae9c0b7f8d01e75bef88c610556791873f..3766830a9e7cf71087e9d92ed1976acf4c6e41fc 100644 --- a/deploy/csp/index.js +++ b/deploy/csp/index.js @@ -118,8 +118,9 @@ module.exports = (app) => { 'cdn.jsdelivr.net/npm/vue@2.5.16/', // plugin load external lib -> vue 2 'cdn.jsdelivr.net/npm/preact@8.4.2/', // plugin load external lib -> preact 'unpkg.com/react@16/umd/', // plugin load external lib -> react - 'unpkg.com/kg-dataset-previewer@1.1.5/', // preview component + 'unpkg.com/kg-dataset-previewer@1.2.0/', // preview component 'cdnjs.cloudflare.com/ajax/libs/mathjax/', // math jax + 'https://unpkg.com/three-surfer@0.0.1/dist/bundle.js', // for threeSurfer (freesurfer support in browser) (req, res) => res.locals.nonce ? `'nonce-${res.locals.nonce}'` : null, ...SCRIPT_SRC, ...WHITE_LIST_SRC, diff --git a/deploy/templates/query.js b/deploy/templates/query.js index b7f6f4589e91338cc95af93cd190fa414a9bda2e..9dbbafb4b1c9bbd3d6dc80b97d9128dec7b0532b 100644 --- a/deploy/templates/query.js +++ b/deploy/templates/query.js @@ -13,7 +13,8 @@ exports.getAllTemplates = () => new Promise((resolve, reject) => { 'colin', 'MNI152', 'waxholmRatV2_0', - 'allenMouse' + 'allenMouse', + 'freesurfer' ] resolve(templates) }) diff --git a/src/index.html b/src/index.html index 7cc1d11aea631bb48552a57baf1f12ae9769ecb7..5c9eccb2f215d24f7c1c8fd7d3328f17a9345845 100644 --- a/src/index.html +++ b/src/index.html @@ -14,6 +14,7 @@ <script src="https://unpkg.com/kg-dataset-previewer@1.2.0/dist/kg-dataset-previewer/kg-dataset-previewer.js" defer> </script> + <script src="https://unpkg.com/three-surfer@0.0.1/dist/bundle.js" defer></script> <title>Interactive Atlas Viewer</title> <script type="application/ld+json"> diff --git a/src/res/ext/atlas/atlas_multiLevelHuman.json b/src/res/ext/atlas/atlas_multiLevelHuman.json index 36d9d008da84e634321e63595bf02760502602ac..1922fe0af4ecf4259d9212580e09d25ce9920818 100644 --- a/src/res/ext/atlas/atlas_multiLevelHuman.json +++ b/src/res/ext/atlas/atlas_multiLevelHuman.json @@ -82,9 +82,27 @@ "@id": "juelich/iav/atlas/v1.0.0/4" } ] + }, + { + "@id": "julich/tmp/referencespace/freesurfer", + "name": "Freesurfer atlas test tmpl", + "availableIn": [ + { + "@id": "julich/tmp/parcellation/freesurfer-test-parc" + } + ] } ], "parcellations": [ + { + "@id": "julich/tmp/parcellation/freesurfer-test-parc", + "name": "Freesurfer atlas parc test", + "availableIn": [ + { + "@id": "julich/tmp/referencespace/freesurfer" + } + ] + }, { "@id": "juelich/iav/atlas/v1.0.0/8", "name": "Cytoarchitectonic Maps - v1.18", diff --git a/src/res/ext/freesurfer.json b/src/res/ext/freesurfer.json new file mode 100644 index 0000000000000000000000000000000000000000..3c3ef997570df51842400e6397a3952db945e1db --- /dev/null +++ b/src/res/ext/freesurfer.json @@ -0,0 +1,152 @@ +{ + "name": "Freesurfer Test Tmpl", + "@id": "julich/tmp/referencespace/freesurfer", + "@type": "julich/tmp/referencespace", + "useTheme": "dark", + "three-surfer": { + "@context": { + "rootUrl": "https://neuroglancer-dev.humanbrainproject.eu/precomputed/freesurfer/20210305", + "fsa_fsa": "/fsaverage/fsaverage", + "fsa_fsa6": "/fsaverage/fsaverage6", + "label_fsa_lh": "https://neuroglancer-dev.humanbrainproject.eu/precomputed/freesurfer/20210305/julichbrain_labels/lh.JulichBrain_MPMAtlas_l_N10_nlin2Stdicbm152asym2009c_publicDOI_83fb39b2811305777db0eb80a0fc8b53.allSub_RF_ANTs_MNI152_orig_to_fsaverage.gii", + "label_fsa_rh": "https://neuroglancer-dev.humanbrainproject.eu/precomputed/freesurfer/20210305/julichbrain_labels/rh.JulichBrain_MPMAtlas_r_N10_nlin2Stdicbm152asym2009c_publicDOI_172e93a5bec140c111ac862268f0d046.allSub_RF_ANTs_MNI152_orig_to_fsaverage.gii", + "label_fsa6_lh": "https://neuroglancer-dev.humanbrainproject.eu/precomputed/freesurfer/20210305/julichbrain_labels/lh.JulichBrain_MPMAtlas_l_N10_nlin2Stdicbm152asym2009c_publicDOI_83fb39b2811305777db0eb80a0fc8b53.BV_MNI152_orig_to_fsaverage6.gii", + "label_fsa6_rh": "https://neuroglancer-dev.humanbrainproject.eu/precomputed/freesurfer/20210305/julichbrain_labels/rh.JulichBrain_MPMAtlas_r_N10_nlin2Stdicbm152asym2009c_publicDOI_172e93a5bec140c111ac862268f0d046.BV_MNI152_orig_to_fsaverage6.gii", + "label_bv_hcp32k_lh": "https://neuroglancer-dev.humanbrainproject.eu/precomputed/freesurfer/20210305/julichbrain_labels/lh.JulichBrain_MPMAtlas_l_N10_nlin2Stdicbm152asym2009c_publicDOI_83fb39b2811305777db0eb80a0fc8b53.allSub_RF_ANTs_MNI152_orig_to_hcp32k.gii", + "label_bv_hcp32k_rh": "https://neuroglancer-dev.humanbrainproject.eu/precomputed/freesurfer/20210305/julichbrain_labels/rh.JulichBrain_MPMAtlas_r_N10_nlin2Stdicbm152asym2009c_publicDOI_172e93a5bec140c111ac862268f0d046.allSub_RF_ANTs_MNI152_orig_to_hcp32k.gii", + "label_ants_hcp32k_lh": "https://neuroglancer-dev.humanbrainproject.eu/precomputed/freesurfer/20210305/julichbrain_labels/lh.JulichBrain_MPMAtlas_l_N10_nlin2Stdicbm152asym2009c_publicDOI_83fb39b2811305777db0eb80a0fc8b53.BV_MNI152_orig_to_hcp32k.gii", + "label_ants_hcp32k_rh": "https://neuroglancer-dev.humanbrainproject.eu/precomputed/freesurfer/20210305/julichbrain_labels/rh.JulichBrain_MPMAtlas_r_N10_nlin2Stdicbm152asym2009c_publicDOI_172e93a5bec140c111ac862268f0d046.BV_MNI152_orig_to_hcp32k.gii" + }, + "modes": [ + { + "name": "fsaverage/fsaverage/pial", + "meshes": [ + { + "mesh": "rootUrl:fsa_fsa:/lh.pial.gii", + "colormap": "label_fsa_lh:" + }, + { + "mesh": "rootUrl:fsa_fsa:/rh.pial.gii", + "colormap": "label_fsa_rh:" + } + ] + }, + { + "name": "fsaverage/fsaverage/curv", + "meshes": [ + { + "mesh": "rootUrl:fsa_fsa:/lh.curv.gii", + "colormap": "label_fsa_lh:" + }, + { + "mesh": "rootUrl:fsa_fsa:/rh.curv.gii", + "colormap": "label_fsa_rh:" + } + ] + }, + { + "name": "fsaverage/fsaverage/white", + "meshes": [ + { + "mesh": "rootUrl:fsa_fsa:/lh.white.gii", + "colormap": "label_fsa_lh:" + }, + { + "mesh": "rootUrl:fsa_fsa:/rh.white.gii", + "colormap": "label_fsa_rh:" + } + ] + }, + { + "name": "fsaverage/fsaverage/inflated", + "meshes": [ + { + "mesh": "rootUrl:fsa_fsa:/lh.inflated.gii", + "colormap": "label_fsa_lh:" + }, + { + "mesh": "rootUrl:fsa_fsa:/rh.inflated.gii", + "colormap": "label_fsa_rh:" + } + ] + }, + { + "name": "fsaverage/fsaverage6/pial", + "meshes": [ + { + "mesh": "rootUrl:fsa_fsa6:/lh.pial.gii", + "colormap": "label_fsa6_lh:" + }, + { + "mesh": "rootUrl:fsa_fsa6:/rh.pial.gii", + "colormap": "label_fsa6_rh:" + } + ] + }, + { + "name": "fsaverage/fsaverage6/curv", + "meshes": [ + { + "mesh": "rootUrl:fsa_fsa6:/lh.curv.gii", + "colormap": "label_fsa6_lh:" + }, + { + "mesh": "rootUrl:fsa_fsa6:/rh.curv.gii", + "colormap": "label_fsa6_rh:" + } + ] + }, + { + "name": "fsaverage/fsaverage6/white", + "meshes": [ + { + "mesh": "rootUrl:fsa_fsa6:/lh.white.gii", + "colormap": "label_fsa6_lh:" + }, + { + "mesh": "rootUrl:fsa_fsa6:/rh.white.gii", + "colormap": "label_fsa6_rh:" + } + ] + }, + { + "name": "fsaverage/fsaverage6/inflated", + "meshes": [ + { + "mesh": "rootUrl:fsa_fsa6:/lh.inflated.gii", + "colormap": "label_fsa6_lh:" + }, + { + "mesh": "rootUrl:fsa_fsa6:/rh.inflated.gii", + "colormap": "label_fsa6_rh:" + } + ] + }, + { + "name": "fsaverage/fsaverage6/sphere", + "meshes": [ + { + "mesh": "rootUrl:fsa_fsa6:/lh.sphere.gii", + "colormap": "label_fsa6_lh:" + }, + { + "mesh": "rootUrl:fsa_fsa6:/rh.sphere.gii", + "colormap": "label_fsa6_rh:" + } + ] + } + ] + }, + "parcellations": [ + { + "@id": "julich/tmp/parcellation/freesurfer-test-parc", + "@type": "julich/tmp/parcellation", + "name": "Freesurfer Test Parc", + "properties": { + "name": "Freesurfer Test Parc", + "descrption": "This is a test parc for free surfer" + }, + "regions": [] + } + ] +} diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts index d8a4d64b05eaafe1de3f497b1f45ff5776b59545..8e588cd496e1375733a0d16847e7d4e15dc32f52 100644 --- a/src/util/pureConstant.service.ts +++ b/src/util/pureConstant.service.ts @@ -1,6 +1,6 @@ import { Injectable, OnDestroy } from "@angular/core"; import { Store, select } from "@ngrx/store"; -import { Observable, merge, Subscription, of, throwError, forkJoin, fromEvent, combineLatest, timer } from "rxjs"; +import { Observable, merge, Subscription, of, forkJoin, fromEvent, combineLatest, timer } from "rxjs"; import { viewerConfigSelectorUseMobileUi } from "src/services/state/viewerConfig.store.helper"; import { shareReplay, tap, scan, catchError, filter, switchMap, map, take, switchMapTo } from "rxjs/operators"; import { HttpClient } from "@angular/common/http"; @@ -34,17 +34,20 @@ export class PureContantService implements OnDestroy{ private fetchTemplate = (templateUrl) => this.http.get(`${this.backendUrl}${templateUrl}`, { responseType: 'json' }).pipe( switchMap((template: any) => { - if (template.nehubaConfig) { return of(template) } - if (template.nehubaConfigURL) { return this.http.get(`${this.backendUrl}${template.nehubaConfigURL}`, { responseType: 'json' }).pipe( - map(nehubaConfig => { - return { - ...template, - nehubaConfig, - } - }), - ) + if (template.nehubaConfig) { + return of(template) + } + if (template.nehubaConfigURL) { + return this.http.get(`${this.backendUrl}${template.nehubaConfigURL}`, { responseType: 'json' }).pipe( + map(nehubaConfig => { + return { + ...template, + nehubaConfig, + } + }), + ) } - throwError('neither nehubaConfig nor nehubaConfigURL defined') + return of(template) }), ) diff --git a/src/viewerModule/constants.ts b/src/viewerModule/constants.ts index 93d2e60eb3f72c1a70111b02b238953a847b41f6..d528f4e6212b3789d080b5641463b4a1b6c4d8a4 100644 --- a/src/viewerModule/constants.ts +++ b/src/viewerModule/constants.ts @@ -1,4 +1,6 @@ import { InjectionToken } from "@angular/core"; import { Observable } from "rxjs"; -export const VIEWERMODULE_DARKTHEME = new InjectionToken<Observable<boolean>>('VIEWERMODULE_DARKTHEME') \ No newline at end of file +export type TSupportedViewer = 'nehuba' | 'threeSurfer' | null + +export const VIEWERMODULE_DARKTHEME = new InjectionToken<Observable<boolean>>('VIEWERMODULE_DARKTHEME') diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 3c82d55e193ced3cf69a604be5ff681b1ae6b15d..cec938a068e649b42055bd79dd15509736610df1 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -15,6 +15,7 @@ import { TopMenuModule } from "src/ui/topMenu/module"; import { UtilModule } from "src/util"; import { VIEWERMODULE_DARKTHEME } from "./constants"; import { NehubaModule } from "./nehuba"; +import { ThreeSurferModule } from "./threeSurfer"; import { RegionAccordionTooltipTextPipe } from "./util/regionAccordionTooltipText.pipe"; import { ViewerCmp } from "./viewerCmp/viewerCmp.component"; @@ -22,6 +23,7 @@ import { ViewerCmp } from "./viewerCmp/viewerCmp.component"; imports: [ CommonModule, NehubaModule, + ThreeSurferModule, LayoutModule, DatabrowserModule, AtlasCmpUiSelectorsModule, diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 25861a93495e4fe93775e5610b2088cdd281d449..baaf1b2ad043fc8b622f7c231b627b1aa4c642a5 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, Inject, Input, OnChanges, OnDestroy, Optional, SimpleChanges, ViewChild } from "@angular/core"; +import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges, ViewChild } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { asyncScheduler, combineLatest, fromEvent, interval, merge, Observable, of, Subject, timer } from "rxjs"; import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerActionSetPerspOctantRemoval, ngViewerActionToggleMax } from "src/services/state/ngViewerState/actions"; @@ -52,11 +52,7 @@ export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{ @ViewChild(MouseHoverDirective, { static: true }) private mouseoverDirective: MouseHoverDirective - public viewerEvents$ = new Subject<TViewerEvent>() - public viewerLoaded$ = this.viewerEvents$.pipe( - filter(ev => ev.type === 'VIEWERLOADED'), - map(ev => ev.data) - ) + public viewerLoaded: boolean = false private onhoverSegments = [] private onDestroyCb: Function[] = [] @@ -259,6 +255,9 @@ export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{ })) } + @Output() + public viewerEvent = new EventEmitter<TViewerEvent>() + constructor( private store$: Store<any>, private el: ElementRef, @@ -267,7 +266,7 @@ export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{ @Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) setViewerHandle: TSetViewerHandle, @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaViewer$: Subject<NehubaViewerUnit>, ){ - this.viewerEvents$.next({ + this.viewerEvent.emit({ type: 'MOUSEOVER_ANNOTATION', data: {} }) @@ -696,10 +695,11 @@ export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{ } handleViewerLoadedEvent(flag: boolean) { - this.viewerEvents$.next({ + this.viewerEvent.emit({ type: 'VIEWERLOADED', data: flag }) + this.viewerLoaded = flag } private selectHoveredRegion(ev: any, next: Function){ diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html index ac4241ecfea6d528df7a71ed6fe7c83eea21152a..65057b2b5f6b841514f2619dc063b96fe136afe7 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html @@ -15,7 +15,7 @@ </div> </div> -<current-layout *ngIf="viewerLoaded$ | async" class="position-absolute w-100 h-100 d-block pe-none top-0 left-0"> +<current-layout *ngIf="viewerLoaded" class="position-absolute w-100 h-100 d-block pe-none top-0 left-0"> <div class="w-100 h-100 position-relative" cell-i> <ng-content *ngTemplateOutlet="ngPanelOverlayTmpl; context: { panelIndex: panelOrder$ | async | getNthElement : 0 | parseAsNumber }"></ng-content> </div> diff --git a/src/viewerModule/threeSurfer/index.ts b/src/viewerModule/threeSurfer/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..748c74827ac5b56cf13833e1133129b42da3d85c --- /dev/null +++ b/src/viewerModule/threeSurfer/index.ts @@ -0,0 +1 @@ +export { ThreeSurferModule } from './module' diff --git a/src/viewerModule/threeSurfer/module.ts b/src/viewerModule/threeSurfer/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcc19bc37b1cb77bf04e0749534c2e8730715a96 --- /dev/null +++ b/src/viewerModule/threeSurfer/module.ts @@ -0,0 +1,21 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; +import { ThreeSurferGlueCmp } from "./threeSurferGlue/threeSurfer.component"; +import { ThreeSurferViewerConfig } from "./tsViewerConfig/tsViewerConfig.component"; + +@NgModule({ + imports: [ + CommonModule, + AngularMaterialModule, + ], + declarations: [ + ThreeSurferGlueCmp, + ThreeSurferViewerConfig, + ], + exports: [ + ThreeSurferGlueCmp, + ] +}) + +export class ThreeSurferModule{} diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f47d5d37e52e0db185950173bba00f97c772040f --- /dev/null +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -0,0 +1,106 @@ +import { Component, Input, Output, EventEmitter, ElementRef, OnChanges, OnDestroy, AfterViewInit, ViewChild } from "@angular/core"; +import { IViewer, TViewerEvent } from "src/viewerModule/viewer.interface"; +import { TThreeSurferConfig, TThreeSurferMode } from "../types"; +import { IContext, parseContext } from "../util"; + +@Component({ + selector: 'three-surfer-glue-cmp', + templateUrl: './threeSurfer.template.html', + styleUrls: [ + './threeSurfer.style.css' + ] +}) + +export class ThreeSurferGlueCmp implements IViewer, OnChanges, AfterViewInit, OnDestroy { + + @Input() + selectedTemplate: any + + @Input() + selectedParcellation: any + + @Output() + viewerEvent = new EventEmitter<TViewerEvent>() + + private domEl: HTMLElement + private config: TThreeSurferConfig + public modes: TThreeSurferMode[] = [] + public selectedMode: string + + constructor( + private el: ElementRef + ){ + this.domEl = this.el.nativeElement + } + + tsRef: any + loadedMeshes: any[] = [] + + private unloadAllMeshes() { + while(this.loadedMeshes.length > 0) { + const m = this.loadedMeshes.pop() + this.tsRef.unloadMesh(m) + } + } + + public async loadMode(mode: TThreeSurferMode) { + + this.unloadAllMeshes() + + this.selectedMode = mode.name + const { meshes } = mode + for (const singleMesh of meshes) { + const { mesh, colormap } = singleMesh + + const tsM = await this.tsRef.loadMesh( + parseContext(mesh, [this.config['@context']]) + ) + this.loadedMeshes.push(tsM) + const tsC = await this.tsRef.loadColormap( + parseContext(colormap, [this.config['@context']]) + ) + const colorIdx = tsC[0].getData() + this.tsRef.applyColorMap(tsM, colorIdx) + } + } + + ngOnChanges(){ + if (this.selectedTemplate) { + + 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 + } + ) + } + // load mode0 by default + this.loadMode(this.config.modes[0]) + + this.viewerEvent.emit({ + type: 'VIEWERLOADED', + data: true + }) + } + } + ngAfterViewInit(){ + const customEvHandler = (ev: CustomEvent) => { + // console.log('ev listener', ev.detail.colormap?.verticesValue) + } + this.domEl.addEventListener((window as any).CUSTOM_EVENTNAME, customEvHandler) + this.onDestroyCb.push( + () => this.domEl.removeEventListener((window as any).CUSTOM_EVENTNAME, customEvHandler) + ) + } + + private onDestroyCb: (() => void) [] = [] + + ngOnDestroy() { + while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()() + } +} diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.style.css b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.style.css new file mode 100644 index 0000000000000000000000000000000000000000..ada443b1cc718f3681d2521af88812ef4924c8ce --- /dev/null +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.style.css @@ -0,0 +1,21 @@ +:host +{ + display: block; + height: 100%; + width: 100%; +} + +:host > div +{ + display: block; + height: 100%; + width: 100%; +} + +button[mat-icon-button] +{ + z-index: 1; + position: fixed; + bottom: 1rem; + right: 1rem; +} diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html new file mode 100644 index 0000000000000000000000000000000000000000..1023bb635ee52e510c6065b9b127cc6d962383c8 --- /dev/null +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html @@ -0,0 +1,21 @@ +<button mat-icon-button + color="primary" + class="pe-all" + [matMenuTriggerFor]="fsModeSelMenu"> + <i class="fas fa-bars"></i> +</button> + +<mat-menu #fsModeSelMenu="matMenu"> + <button *ngFor="let mode of modes" + mat-menu-item + (click)="loadMode(mode)" + color="primary"> + <mat-icon + fontSet="fas" + [fontIcon]="mode.name === selectedMode ? 'fa-circle' : 'fa-none'"> + </mat-icon> + <span> + {{ mode.name }} + </span> + </button> +</mat-menu> diff --git a/src/viewerModule/threeSurfer/tsViewerConfig/tsViewerConfig.component.ts b/src/viewerModule/threeSurfer/tsViewerConfig/tsViewerConfig.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d59b43509047e1853dca0e9be80c6753f8ba65b9 --- /dev/null +++ b/src/viewerModule/threeSurfer/tsViewerConfig/tsViewerConfig.component.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: 'three-surfer-viewer-config', + templateUrl: './tsViewerConfig.template.html', + styleUrls: [ + './tsViewerConfig.style.css' + ] +}) + +export class ThreeSurferViewerConfig { + +} diff --git a/src/viewerModule/threeSurfer/tsViewerConfig/tsViewerConfig.style.css b/src/viewerModule/threeSurfer/tsViewerConfig/tsViewerConfig.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/viewerModule/threeSurfer/tsViewerConfig/tsViewerConfig.template.html b/src/viewerModule/threeSurfer/tsViewerConfig/tsViewerConfig.template.html new file mode 100644 index 0000000000000000000000000000000000000000..578678e805ef42854e97e7d420f8146f584d0af5 --- /dev/null +++ b/src/viewerModule/threeSurfer/tsViewerConfig/tsViewerConfig.template.html @@ -0,0 +1 @@ +tsviewer config diff --git a/src/viewerModule/threeSurfer/types.ts b/src/viewerModule/threeSurfer/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..62fe903442c2764d824db6d0be0ef0e25bcd3245 --- /dev/null +++ b/src/viewerModule/threeSurfer/types.ts @@ -0,0 +1,16 @@ +import { IContext } from './util' + +export type TThreeSurferMesh = { + colormap: string + mesh: string +} + +export type TThreeSurferMode = { + name: string + meshes: TThreeSurferMesh[] +} + +export type TThreeSurferConfig = { + ['@context']: IContext + modes: TThreeSurferMode[] +} diff --git a/src/viewerModule/threeSurfer/util.ts b/src/viewerModule/threeSurfer/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..34a0d85065cf7104d57faf08790dd943804f7b26 --- /dev/null +++ b/src/viewerModule/threeSurfer/util.ts @@ -0,0 +1,14 @@ +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 4e4eada8942a3b563f036b4566b73e42be452d3e..fdd5600b3a67aafb26c06335c4ec93d5742c0bb5 100644 --- a/src/viewerModule/viewer.interface.ts +++ b/src/viewerModule/viewer.interface.ts @@ -1,4 +1,4 @@ -import { Observable } from "rxjs"; +import { EventEmitter } from "@angular/core"; type TLayersColorMap = Map<string, Map<number, { red: number, green: number, blue: number }>> @@ -44,5 +44,5 @@ export type IViewer = { selectedTemplate: any selectedParcellation: any viewerCtrlHandler?: IViewerCtrl - viewerEvents$: Observable<TViewerEvent> + viewerEvent: EventEmitter<TViewerEvent> } \ No newline at end of file diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 7d9e95ff855deb3f68647705a6cff34ad9de1487..6eda39c0b01bdec097d2c0abaa1345ebe160f96a 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -1,11 +1,9 @@ -import { AfterViewInit, Component, Inject, Input, OnDestroy, Optional, ViewChild } from "@angular/core"; +import { Component, Inject, Input, OnDestroy, Optional, ViewChild } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { combineLatest, Observable, Subject, Subscription } from "rxjs"; import { distinctUntilChanged, filter, map, startWith } from "rxjs/operators"; import { viewerStateHelperSelectParcellationWithId, viewerStateRemoveAdditionalLayer, viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; import { viewerStateContextedSelectedRegionsSelector, viewerStateGetOverlayingAdditionalParcellations, viewerStateParcVersionSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector, viewerStateStandAloneVolumes } from "src/services/state/viewerState/selectors" -import { NehubaGlueCmp } from "../nehuba"; -import { IViewer } from "../viewer.interface"; import { CONST, ARIA_LABELS } from 'common/constants' import { ngViewerActionClearView } from "src/services/state/ngViewerState/actions"; import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState/selectors"; @@ -13,6 +11,7 @@ import { uiActionHideAllDatasets, uiActionHideDatasetWithId } from "src/services import { REGION_OF_INTEREST } from "src/util/interfaces"; import { animate, state, style, transition, trigger } from "@angular/animations"; import { SwitchDirective } from "src/util/directives/switch.directive"; +import { TSupportedViewer } from "../constants"; @Component({ selector: 'iav-cmp-viewer-container', @@ -67,12 +66,11 @@ import { SwitchDirective } from "src/util/directives/switch.directive"; ] }) -export class ViewerCmp implements OnDestroy, AfterViewInit{ +export class ViewerCmp implements OnDestroy { public CONST = CONST public ARIA_LABELS = ARIA_LABELS - @ViewChild(NehubaGlueCmp) viewerCmp: IViewer @ViewChild('sideNavTopSwitch', { static: true }) private sidenavTopSwitch: SwitchDirective @@ -98,6 +96,15 @@ export class ViewerCmp implements OnDestroy, AfterViewInit{ distinctUntilChanged(), ) + public useViewer$: Observable<TSupportedViewer> = this.templateSelected$.pipe( + map(t => { + if (!t) return null + if (!!t['nehubaConfigURL'] || !!t['nehubaConfig']) return 'nehuba' + if (!!t['three-surfer']) return 'threeSurfer' + return null + }) + ) + public isStandaloneVolumes$ = this.store$.pipe( select(viewerStateStandAloneVolumes), map(v => v.length > 0) @@ -156,16 +163,6 @@ export class ViewerCmp implements OnDestroy, AfterViewInit{ ) } - ngAfterViewInit(){ - this.subscriptions.push( - this.viewerCmp.viewerEvents$.pipe( - filter(ev => ev.type === 'VIEWERLOADED'), - ).subscribe(ev => { - this.viewerLoaded = ev.data - }) - ) - } - ngOnDestroy() { while (this.subscriptions.length) this.subscriptions.pop().unsubscribe() } @@ -229,4 +226,4 @@ export class ViewerCmp implements OnDestroy, AfterViewInit{ : uiActionHideAllDatasets() ) } -} \ No newline at end of file +} diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 95052becdcfeccc8ba2e4d163b8ec643d81dc302..215f0459a933cc6b1833d639b734886414bab756 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -239,11 +239,26 @@ </ui-splashscreen> <div class="h-100 w-100 overflow-hidden position-relative" [ngClass]="{'pe-none': !viewerLoaded}"> - <iav-cmp-viewer-nehuba-glue class="d-block w-100 h-100 position-absolute left-0 top-0" - [selectedTemplate]="templateSelected$ | async" - [selectedParcellation]="parcellationSelected$ | async" - #iavCmpViewerNehubaGlue="iavCmpViewerNehubaGlue"> - </iav-cmp-viewer-nehuba-glue> + + <ng-container [ngSwitch]="useViewer$ | async"> + + <iav-cmp-viewer-nehuba-glue class="d-block w-100 h-100 position-absolute left-0 top-0" + *ngSwitchCase="'nehuba'" + (viewerEvent)="viewerLoaded = $event.data" + [selectedTemplate]="templateSelected$ | async" + [selectedParcellation]="parcellationSelected$ | async" + #iavCmpViewerNehubaGlue="iavCmpViewerNehubaGlue"> + </iav-cmp-viewer-nehuba-glue> + + <three-surfer-glue-cmp class="d-block w-100 h-100 position-absolute left-0 top-0" + *ngSwitchCase="'threeSurfer'" + (viewerEvent)="viewerLoaded = $event.data" + [selectedTemplate]="templateSelected$ | async" + [selectedParcellation]="parcellationSelected$ | async"> + </three-surfer-glue-cmp> + + <div *ngSwitchDefault>Template not supported by any of the viewers</div> + </ng-container> </div> </div>