diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index 68f8a9a4adaae69284025d1a28a0b83fc60d1f9a..7a547e9fd937541502dea1aa7c0e61c3d974c78a 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -174,6 +174,7 @@ export interface InteractiveViewerInterface{ } export interface UserLandmark{ + name : string position : [number, number, number] id : string /* probably use the it to track and remove user landmarks */ highlight : boolean diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index b5800545b227d2969bf6707ce26546040e1f323c..f82a6f1da2cbc36e340407255164e83f4f44f96e 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -1,7 +1,7 @@ import { Component, HostBinding, ViewChild, ViewContainerRef, ComponentFactoryResolver, ComponentFactory, OnDestroy, ElementRef, Injector, ComponentRef, AfterViewInit, OnInit, TemplateRef, HostListener, Renderer2 } from "@angular/core"; import { Store, select } from "@ngrx/store"; -import { ViewerStateInterface, OPEN_SIDE_PANEL, CLOSE_SIDE_PANEL, isDefined,UNLOAD_DEDICATED_LAYER, FETCHED_SPATIAL_DATA, UPDATE_SPATIAL_DATA, TOGGLE_SIDE_PANEL, NgViewerStateInterface } from "../services/stateStore.service"; -import { Observable, Subscription } from "rxjs"; +import { ViewerStateInterface, OPEN_SIDE_PANEL, CLOSE_SIDE_PANEL, isDefined,UNLOAD_DEDICATED_LAYER, FETCHED_SPATIAL_DATA, UPDATE_SPATIAL_DATA, TOGGLE_SIDE_PANEL, NgViewerStateInterface, safeFilter } from "../services/stateStore.service"; +import { Observable, Subscription, combineLatest } from "rxjs"; import { map, filter, distinctUntilChanged, delay } from "rxjs/operators"; import { AtlasViewerDataService } from "./atlasViewer.dataService.service"; import { WidgetServices } from "./widgetUnit/widgetService.service"; @@ -39,7 +39,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @ViewChild('databrowser', { read: ElementRef }) databrowser: ElementRef @ViewChild('temporaryContainer', { read: ViewContainerRef }) temporaryContainer: ViewContainerRef @ViewChild('toastContainer', { read: ViewContainerRef }) toastContainer: ViewContainerRef - // @ViewChild('dedicatedViewerToast', { read: TemplateRef }) dedicatedViewerToast: TemplateRef<any> @ViewChild('floatingMouseContextualContainer', { read: ViewContainerRef }) floatingMouseContextualContainer: ViewContainerRef @ViewChild('pluginFactory', { read: ViewContainerRef }) pluginViewContainerRef: ViewContainerRef @ViewChild(LayoutMainSide) layoutMainSide: LayoutMainSide @@ -56,9 +55,12 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public sidePanelView$: Observable<string|null> private newViewer$: Observable<any> - public selectedRegions$: Observable<any[]> + + public selectedPOI$ : Observable<any[]> + public dedicatedView$: Observable<string | null> public onhoverSegment$: Observable<string> + public onhoverLandmark$ : Observable<string | null> private subscriptions: Subscription[] = [] /* handlers for nglayer */ @@ -95,10 +97,21 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { map(state => state.focusedSidePanel) ) - this.selectedRegions$ = this.store.pipe( - select('viewerState'), - filter(state=>isDefined(state)&&isDefined(state.regionsSelected)), - map(state=>state.regionsSelected) + this.selectedPOI$ = combineLatest( + this.store.pipe( + select('viewerState'), + filter(state=>isDefined(state)&&isDefined(state.regionsSelected)), + map(state=>state.regionsSelected), + distinctUntilChanged() + ), + this.store.pipe( + select('viewerState'), + filter(state => isDefined(state) && isDefined(state.landmarksSelected)), + map(state => state.landmarksSelected), + distinctUntilChanged() + ) + ).pipe( + map(results => [...results[0], ...results[1]]) ) this.newViewer$ = this.store.pipe( @@ -115,18 +128,46 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { distinctUntilChanged() ) - this.onhoverSegment$ = this.store.pipe( - select('uiState'), - /* cannot filter by state, as the template expects a default value, or it will throw ExpressionChangedAfterItHasBeenCheckedError */ - map(state => isDefined(state) ? - state.mouseOverSegment ? - state.mouseOverSegment.constructor === Number ? - state.mouseOverSegment.toString() : - state.mouseOverSegment.name : - '' : - ''), - distinctUntilChanged() + this.onhoverLandmark$ = combineLatest( + this.store.pipe( + select('uiState'), + map(state => state.mouseOverLandmark) + ), + this.store.pipe( + select('dataStore'), + safeFilter('fetchedSpatialData'), + map(state=>state.fetchedSpatialData) + ) + ).pipe( + map(([landmark, spatialDatas]) => { + if(landmark === null) + return landmark + const idx = Number(landmark.replace('label=','')) + if(isNaN(idx)) + return `Landmark index could not be parsed as a number: ${landmark}` + return spatialDatas[idx].name + }) + ) + + // TODO temporary hack. even though the front octant is hidden, it seems if a mesh is present, hover will select the said mesh + this.onhoverSegment$ = combineLatest( + this.store.pipe( + select('uiState'), + /* cannot filter by state, as the template expects a default value, or it will throw ExpressionChangedAfterItHasBeenCheckedError */ + map(state => isDefined(state) ? + state.mouseOverSegment ? + state.mouseOverSegment.constructor === Number ? + state.mouseOverSegment.toString() : + state.mouseOverSegment.name : + '' : + ''), + distinctUntilChanged() + ), + this.onhoverLandmark$ + ).pipe( + map(([segment, onhoverLandmark]) => onhoverLandmark ? '' : segment ) ) + } ngOnInit() { @@ -402,7 +443,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { ngLayersChangeHandler(){ this.ngLayers = (window['viewer'].layerManager.managedLayers as any[]) - .filter(obj => obj.sourceUrl && /precomputed|nifti/.test(obj.sourceUrl)) + // .filter(obj => obj.sourceUrl && /precomputed|nifti/.test(obj.sourceUrl)) .map(obj => ({ name : obj.name, type : obj.initialSpecification.type, diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts index 1295b188578881870bc4e958ad0600f3f81b0742..9f903988e6bad7a951c1d14cebf127f870f2222e 100644 --- a/src/atlasViewer/atlasViewer.constantService.service.ts +++ b/src/atlasViewer/atlasViewer.constantService.service.ts @@ -9,6 +9,15 @@ import { ViewerStateInterface, Property, FETCHED_METADATA } from "../services/st export class AtlasViewerConstantsServices{ + public ngLandmarkLayerName = 'spatial landmark layer' + + /* TODO to be replaced by @id: Landmark/UNIQUE_ID in KG in the future */ + public testLandmarksChanged : (prevLandmarks : any[], newLandmarks : any[]) => boolean = (prevLandmarks:any[], newLandmarks:any[]) => { + return prevLandmarks.every(lm => typeof lm.name !== 'undefined') && + newLandmarks.every(lm => typeof lm.name !== 'undefined') && + prevLandmarks.length === newLandmarks.length + } + /* to be provided by KG in future */ public templateUrls = [ // 'res/json/infant.json', diff --git a/src/atlasViewer/atlasViewer.dataService.service.ts b/src/atlasViewer/atlasViewer.dataService.service.ts index d1f504271661de4f7870d5aba823e27a4adc17ee..74d8f5d243aa6b38ebdc717900df675a3f82b52f 100644 --- a/src/atlasViewer/atlasViewer.dataService.service.ts +++ b/src/atlasViewer/atlasViewer.dataService.service.ts @@ -84,6 +84,25 @@ export class AtlasViewerDataService implements OnDestroy{ if(filterTemplateSpace){ url.searchParams.append('fq',filterTemplateSpace) + }else if (templateSpace === 'MNI 152 ICBM 2009c Nonlinear Asymmetric'){ + return Promise.all([ + fetch('res/json/***REMOVED***.json').then(res=>res.json()), + fetch('res/json/***REMOVED***.json').then(res=>res.json()) + ]) + .then(arr => { + this.store.dispatch({ + type : FETCHED_SPATIAL_DATA, + fetchedDataEntries : arr.reduce((acc,curr) => acc.concat(curr.map(obj => Object.assign({}, obj, { + position : obj.position.map(pos => pos/1e6), + properties : {} + }))), []) + }) + this.store.dispatch({ + type : UPDATE_SPATIAL_DATA, + totalResults : arr.reduce((acc,curr) => acc + curr.length, 0) + }) + }) + .catch(console.error) }else{ return } @@ -93,11 +112,13 @@ export class AtlasViewerDataService implements OnDestroy{ fetch(fetchUrl).then(r=>r.json()) .then((resp)=>{ const dataEntries = resp.response.docs.map(doc=>({ + name : doc['OID'][0], position : doc['geometry.coordinates'][0].split(',').map(string=>Number(string)), properties : { description : doc['OID'][0], publications : [] - } + }, + files:[] })) this.store.dispatch({ type : FETCHED_SPATIAL_DATA, diff --git a/src/atlasViewer/atlasViewer.style.css b/src/atlasViewer/atlasViewer.style.css index ceeaff428324886d98881a05a2adfc9aee099e6e..a3342d98d37d2b7ad06b97487055d218ff524562 100644 --- a/src/atlasViewer/atlasViewer.style.css +++ b/src/atlasViewer/atlasViewer.style.css @@ -132,7 +132,8 @@ div[contextualBlock] pointer-events: none; } -span.tabContainer > *:not(.pointer-events) +span.tabContainer > *:not(.pointer-events), +span.tabContainer > *:not(.pointer-events):before { pointer-events: none; } diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index c9306f06e37bf1f4ebb6980a5320e662692777cc..92b5f4e9d90f576110d9c0e362bf2f0878ee64d1 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -68,16 +68,17 @@ <div resizeSliver> <span + container = "body" placement = "left" - [tooltip] = "!(selectedRegions$ | async) ? '' : (((selectedRegions$ | async).length === 0 ? 'No' : (selectedRegions$ | async).length) + ' selected region' + ((selectedRegions$ | async).length > 1 ? 's' : ''))" + [tooltip] = "!(selectedPOI$ | async) ? '' : (((selectedPOI$ | async).length === 0 ? 'No' : (selectedPOI$ | async).length) + ' selected region' + ((selectedPOI$ | async).length > 1 ? 's' : '')) + ' of interest' " [ngClass] = "{'active-tab' : (sidePanelView$ | async) === 'dataBrowser'}" (click) = "toggleSidePanel('dataBrowser')" class = "tabContainer"> - <span [@newEvent] = "selectedRegions$ | async" class = "highlightContainer"> + <span [@newEvent] = "selectedPOI$ | async" class = "highlightContainer"> </span> <i class = "glyphicon glyphicon-search"></i> - <span class = "badge" *ngIf = "(selectedRegions$ | async) && (selectedRegions$ | async).length > 0"> - {{ (selectedRegions$ | async).length }} + <span class = "badge" *ngIf = "(selectedPOI$ | async) && (selectedPOI$ | async).length > 0"> + {{ (selectedPOI$ | async).length }} </span> </span> @@ -131,9 +132,14 @@ </ng-template> <div [style.transform] = "floatingMouseContextualContainerTransform" floatingMouseContextualContainer> + <div *ngIf = "onhoverLandmark$ | async" contextualInnerContainer> + <div contextualBlock> + {{ onhoverLandmark$ | async }} <i><small class = "mute-text">dbl click to toggle select</small></i> + </div> + </div> <div *ngIf = "onhoverSegment$ | async as onhoverSegment" contextualInnerContainer> <div *ngIf = "onhoverSegment !== ''" contextualBlock> - {{ onhoverSegment }} + {{ onhoverSegment }} <i><small class = "mute-text">dbl click to toggle select</small></i> </div> </div> </div> diff --git a/src/atlasViewer/atlasViewer.urlService.service.ts b/src/atlasViewer/atlasViewer.urlService.service.ts index 8fc0164d2ec5f5cd858cbd35710050bf74801cc8..eea96ba09d18dbc3e67f48f1fa447f66b92e72e6 100644 --- a/src/atlasViewer/atlasViewer.urlService.service.ts +++ b/src/atlasViewer/atlasViewer.urlService.service.ts @@ -2,7 +2,7 @@ import { Injectable } from "@angular/core"; import { Store, select } from "@ngrx/store"; import { ViewerStateInterface, isDefined, NEWVIEWER, getLabelIndexMap, SELECT_REGIONS, CHANGE_NAVIGATION, LOAD_DEDICATED_LAYER, ADD_NG_LAYER, PluginInitManifestInterface } from "../services/stateStore.service"; import { Observable,combineLatest } from "rxjs"; -import { filter, map, scan, take, distinctUntilChanged } from "rxjs/operators"; +import { filter, map, scan, take, distinctUntilChanged, bufferTime, debounceTime } from "rxjs/operators"; import { getActiveColorMapFragmentMain } from "../ui/nehubaContainer/nehubaContainer.component"; import { PluginServices } from "./atlasViewer.pluginService.service"; diff --git a/src/services/stateStore.service.ts b/src/services/stateStore.service.ts index 66d7a30487ebd26b52f7e7e694214120b8b29f12..6c45c411ba6837dcb7f0784ba02e7f2f4d3d2cca 100644 --- a/src/services/stateStore.service.ts +++ b/src/services/stateStore.service.ts @@ -7,6 +7,9 @@ export const NEWVIEWER = 'NEWVIEWER' export const FETCHED_TEMPLATES = 'FETCHED_TEMPLATES' export const SELECT_PARCELLATION = `SELECT_PARCELLATION` export const SELECT_REGIONS = `SELECT_REGIONS` +export const DESELECT_REGIONS = `DESELECT_REGIONS` +export const SELECT_LANDMARKS = `SELECT_LANDMARKS` +export const DESELECT_LANDMARKS = `DESELECT_LANDMARKS` export const USER_LANDMARKS = `USER_LANDMARKS` export const CHANGE_NAVIGATION = 'CHANGE_NAVIGATION' @@ -27,6 +30,7 @@ export const CLOSE_SIDE_PANEL = `CLOSE_SIDE_PANEL` export const OPEN_SIDE_PANEL = `OPEN_SIDE_PANEL` export const MOUSE_OVER_SEGMENT = `MOUSE_OVER_SEGMENT` +export const MOUSE_OVER_LANDMARK = `MOUSE_OVER_LANDMARK` export const SET_INIT_PLUGIN = `SET_INIT_PLUGIN` export const FETCHED_PLUGIN_MANIFESTS = `FETCHED_PLUGIN_MANIFESTS` @@ -50,6 +54,7 @@ export interface ViewerStateInterface{ parcellationSelected : any | null regionsSelected : any[] + landmarksSelected : any[] userLandmarks : UserLandmark[] navigation : any | null @@ -62,9 +67,11 @@ export interface AtlasAction extends Action{ selectTemplate? : any selectParcellation? : any selectRegions? : any[] + deselectRegions? : any[] dedicatedView? : string landmarks : UserLandmark[] + deselectLandmarks : UserLandmark[] navigation? : any } @@ -193,12 +200,16 @@ export function ngViewerState(prevState:NgViewerStateInterface = {layers:[], for } } -export function uiState(state:UIStateInterface = {mouseOverSegment:null, focusedSidePanel:null, sidePanelOpen: false},action:UIAction){ +export function uiState(state:UIStateInterface = {mouseOverSegment:null, mouseOverLandmark : null, focusedSidePanel:null, sidePanelOpen: false},action:UIAction){ switch(action.type){ case MOUSE_OVER_SEGMENT: return Object.assign({},state,{ mouseOverSegment : action.segment }) + case MOUSE_OVER_LANDMARK: + return Object.assign({}, state, { + mouseOverLandmark : action.landmark + }) case TOGGLE_SIDE_PANEL: return Object.assign({}, state, { focusedSidePanel : typeof action.focusedSidePanel === 'undefined' || state.focusedSidePanel === action.focusedSidePanel @@ -219,7 +230,7 @@ export function uiState(state:UIStateInterface = {mouseOverSegment:null, focused } } -export function viewerState(state:ViewerStateInterface,action:AtlasAction){ +export function viewerState(state:Partial<ViewerStateInterface> = {landmarksSelected : []},action:AtlasAction){ switch(action.type){ case LOAD_DEDICATED_LAYER: const dedicatedView = state.dedicatedView @@ -239,6 +250,7 @@ export function viewerState(state:ViewerStateInterface,action:AtlasAction){ templateSelected : action.selectTemplate, parcellationSelected : action.selectParcellation, regionsSelected : [], + landmarksSelected : [], navigation : {}, dedicatedView : null }) @@ -255,6 +267,11 @@ export function viewerState(state:ViewerStateInterface,action:AtlasAction){ regionsSelected : [] }) } + case DESELECT_REGIONS : { + return Object.assign({}, state, { + regionsSelected : state.regionsSelected.filter(re => action.deselectRegions.findIndex(dRe => dRe.name === re.name) < 0) + }) + } case SELECT_REGIONS : { return Object.assign({},state,{ regionsSelected : action.selectRegions.map(region=>Object.assign({},region,{ @@ -262,6 +279,16 @@ export function viewerState(state:ViewerStateInterface,action:AtlasAction){ })) }) } + case DESELECT_LANDMARKS : { + return Object.assign({}, state, { + landmarksSelected : state.landmarksSelected.filter(lm => action.deselectLandmarks.findIndex(dLm => dLm.name === lm.name) < 0) + }) + } + case SELECT_LANDMARKS : { + return Object.assign({}, state, { + landmarksSelected : action.landmarks + }) + } case USER_LANDMARKS : { return Object.assign({}, state, { userLandmarks : action.landmarks @@ -308,7 +335,7 @@ export interface SpatialDataStateInterface{ } const initSpatialDataState : SpatialDataStateInterface = { - spatialDataVisible : false, + spatialDataVisible : true, spatialSearchPagination : 0, spatialSearchTotalResults : 0 } @@ -409,11 +436,13 @@ export interface Publication{ export interface UIStateInterface{ sidePanelOpen : boolean mouseOverSegment : any | number + mouseOverLandmark : any focusedSidePanel : string | null } export interface UIAction extends Action{ segment : any | number + landmark : any focusedSidePanel? : string } diff --git a/src/ui/databrowser/databrowser.component.ts b/src/ui/databrowser/databrowser.component.ts index ed941a74af5496d577a93a23c7c8b795968dc259..126bf60267c0d1c9d2623a6b0845df497f004b04 100644 --- a/src/ui/databrowser/databrowser.component.ts +++ b/src/ui/databrowser/databrowser.component.ts @@ -1,14 +1,13 @@ import { Component, OnDestroy, ComponentFactoryResolver, ComponentFactory, OnInit, Injector } from "@angular/core"; import { Store, select } from "@ngrx/store"; -import { DataStateInterface, Property, safeFilter, DataEntry, File, SELECT_REGIONS, getLabelIndexMap, LOAD_DEDICATED_LAYER, UNLOAD_DEDICATED_LAYER, FETCHED_SPATIAL_DATA, isDefined, SPATIAL_GOTO_PAGE, CHANGE_NAVIGATION, UPDATE_SPATIAL_DATA_VISIBLE } from "../../services/stateStore.service"; +import { DataStateInterface, Property, safeFilter, DataEntry, File, SELECT_REGIONS, getLabelIndexMap, LOAD_DEDICATED_LAYER, UNLOAD_DEDICATED_LAYER, FETCHED_SPATIAL_DATA, isDefined, SPATIAL_GOTO_PAGE, CHANGE_NAVIGATION, UPDATE_SPATIAL_DATA_VISIBLE, DESELECT_REGIONS, DESELECT_LANDMARKS, SELECT_LANDMARKS } from "../../services/stateStore.service"; import { map, filter, take, distinctUntilChanged } from "rxjs/operators"; import { HasPathProperty } from "../../util/pipes/pathToNestedChildren.pipe"; import { TreeComponent } from "../../components/tree/tree.component"; -import { Observable, Subscription, merge } from "rxjs"; +import { Observable, Subscription, merge, combineLatest } from "rxjs"; import { FileViewer } from "../fileviewer/fileviewer.component"; import { WidgetServices } from "../../atlasViewer/widgetUnit/widgetService.service"; import { AtlasViewerConstantsServices } from "../../atlasViewer/atlasViewer.constantService.service"; -import { AtlasViewerDataService } from "../../atlasViewer/atlasViewer.dataService.service"; @Component({ selector : 'data-browser', @@ -25,8 +24,6 @@ export class DataBrowserUI implements OnDestroy,OnInit{ hitsPerPage : number = 15 currentPage : number = 0 - selectedRegions : any[] = [] - metadataMap : Map<string,Map<string,{properties:Property}>> dataEntries : DataEntry[] = [] spatialDataEntries : DataEntry[] = [] @@ -35,29 +32,27 @@ export class DataBrowserUI implements OnDestroy,OnInit{ hideDataTypes : Set<string> = new Set() private _spatialDataVisible : boolean = false - private spatialSearchObj : {center:[number,number,number],searchWidth:number,templateSpace : string,pageNo:number} dedicatedViewString : string | null private regionsLabelIndexMap : Map<number,any> = new Map() - private regionSelected$ : Observable<any> + public selectedRegions$ : Observable<any[]> + public selectedLandmarks$ : Observable<any[]> + public selectedPOI$ : Observable<any[]> + private metadataMap$ : Observable<any> private fetchedDataEntries$ : Observable<any> - private newViewer$ : Observable<any> private selectParcellation$ : Observable<any> private dedicatedViewString$ : Observable<string|null> private spatialDataEntries$ : Observable<any[]> private spatialPagination$ : Observable<{spatialSearchPagination:number,spatialSearchTotalResults:number}> - private debouncedNavigation$ : Observable<any> private subscriptions : Subscription[] = [] - private selectedTemplate : any constructor( private cfr : ComponentFactoryResolver, private store : Store<DataStateInterface>, - private atlasviewerDataService : AtlasViewerDataService, private constantService : AtlasViewerConstantsServices, private injector : Injector, private widgetServices : WidgetServices @@ -65,13 +60,25 @@ export class DataBrowserUI implements OnDestroy,OnInit{ this.fileViewerComponentFactory = this.cfr.resolveComponentFactory(FileViewer) - this.regionSelected$ = merge( - this.store.pipe( - select('viewerState'), - filter(state=>isDefined(state)&&isDefined(state.regionsSelected)), - map(state=>state.regionsSelected) - ) - ) + this.selectedRegions$ = this.store.pipe( + select('viewerState'), + safeFilter('regionsSelected'), + map(state=>state.regionsSelected) + ) + + this.selectedLandmarks$ = this.store.pipe( + select('viewerState'), + safeFilter('landmarksSelected'), + map(state => state.landmarksSelected) + ) + + this.selectedPOI$ = combineLatest( + this.selectedRegions$, + this.selectedLandmarks$ + ).pipe( + map(results => [...results[0], ...results[1]]) + ) + this.metadataMap$ = this.store.pipe( select('dataStore'), @@ -84,21 +91,6 @@ export class DataBrowserUI implements OnDestroy,OnInit{ safeFilter('fetchedDataEntries'), map(v=>v.fetchedDataEntries) ) - - this.newViewer$ = this.store.pipe( - select('viewerState'), - filter(state=>isDefined(state) && isDefined(state.templateSelected)), - filter(state=> - !isDefined(this.selectedTemplate) || - state.templateSelected.name !== this.selectedTemplate.name) - ) - - this.debouncedNavigation$ = this.store.pipe( - select('viewerState'), - filter(state=>isDefined(state) && isDefined(state.navigation)), - map(state=>state.navigation) - ) - this.selectParcellation$ = this.store.pipe( select('viewerState'), @@ -135,28 +127,6 @@ export class DataBrowserUI implements OnDestroy,OnInit{ ngOnInit(){ - this.subscriptions.push( - this.newViewer$.subscribe(state=>{ - this.selectedTemplate = state.templateSelected - this.handleParcellationSelection(state.parcellationSelected.regions) - this.store.dispatch({ - type : FETCHED_SPATIAL_DATA, - fetchedDataEntries : [] - }) - this.store.dispatch({ - type : SPATIAL_GOTO_PAGE, - pageNo : 0 - }) - }) - ) - - this.subscriptions.push( - this.debouncedNavigation$.subscribe(this.handleDebouncedNavigation.bind(this)) - ) - - this.subscriptions.push(this.regionSelected$ - .subscribe(rs=>this.selectedRegions = rs)) - this.subscriptions.push(this.metadataMap$.subscribe(map=>(this.metadataMap = map))) this.subscriptions.push(this.fetchedDataEntries$.subscribe(arr=>(this.dataEntries = arr))) @@ -193,9 +163,6 @@ export class DataBrowserUI implements OnDestroy,OnInit{ } toggleSpatialDataVisible(){ - /* disabling spatial data for now */ - return - //@ts-ignore this.store.dispatch({ type : UPDATE_SPATIAL_DATA_VISIBLE, visible : !this._spatialDataVisible @@ -206,9 +173,10 @@ export class DataBrowserUI implements OnDestroy,OnInit{ return this._spatialDataVisible } + // TODO deprecated? rethink how to implement displaying of spatial landmarks handleSpatialPaginationChange(state){ - if(isDefined (state.spatialSearchPagination) ) - this.spatialPagination = state.spatialSearchPagination + // if(isDefined (state.spatialSearchPagination) ) + // this.spatialPagination = state.spatialSearchPagination if(isDefined(state.spatialSearchTotalResults)) this.spatialTotalNo = state.spatialSearchTotalResults @@ -216,34 +184,15 @@ export class DataBrowserUI implements OnDestroy,OnInit{ if(isDefined(state.spatialDataVisible)) this._spatialDataVisible = state.spatialDataVisible - if(this._spatialDataVisible === false) - return + // if(this._spatialDataVisible === false) + // return - if(this.spatialPagination === this.spatialSearchObj.pageNo) - return - - this.spatialSearchObj.pageNo = this.spatialPagination - this.atlasviewerDataService.spatialSearch(this.spatialSearchObj) - } + // if(this.spatialPagination === this.spatialSearchObj.pageNo) + // return - handleDebouncedNavigation(navigation:any){ - if(!isDefined(navigation.position)) - return - const center = navigation.position.map(n=>n/1e6) - const searchWidth = this.constantService.spatialWidth / 4 * navigation.zoom / 1e6 - const templateSpace = this.selectedTemplate.name - const pageNo = this.spatialPagination - - this.spatialSearchObj = { - center, - searchWidth, - templateSpace, - pageNo - } - if(!this._spatialDataVisible) - return - console.log('spatial search') - this.atlasviewerDataService.spatialSearch(this.spatialSearchObj) + // console.log('pagination change') + // this.spatialSearchObj.pageNo = this.spatialPagination + // this.atlasviewerDataService.spatialSearch(this.spatialSearchObj) } handleSpatialDataEntries(datas){ @@ -300,17 +249,15 @@ export class DataBrowserUI implements OnDestroy,OnInit{ } } - get databrowserHeaderText() : string{ - return this.selectedRegions.length === 0 ? - `No regions selected.` : - `${this.selectedRegions.length} region${this.selectedRegions.length > 1 ? 's' : ''} selected.` - } - - clearAllRegions(){ + clearAllPOIs(){ this.store.dispatch({ type : SELECT_REGIONS, selectRegions : [] }) + this.store.dispatch({ + type : SELECT_LANDMARKS, + landmarks : [] + }) } typeVisible(type:string){ @@ -328,11 +275,6 @@ export class DataBrowserUI implements OnDestroy,OnInit{ ) } - regionSelected(region:any){ - const idx = this.selectedRegions.findIndex(re=>re.name===region.name) - return idx >= 0 - } - gothere(event:MouseEvent,position:any){ event.stopPropagation() event.preventDefault() @@ -348,13 +290,20 @@ export class DataBrowserUI implements OnDestroy,OnInit{ }) } - removeRegion(event:MouseEvent,region:any){ + removePOI(event:MouseEvent, region:any){ event.stopPropagation() event.preventDefault() - this.store.dispatch({ - type : SELECT_REGIONS, - selectRegions : this.selectedRegions.filter(re=>re.name!==region.name) - }) + if(region.spatialLandmark){ + this.store.dispatch({ + type : DESELECT_LANDMARKS, + deselectLandmarks : [region] + }) + }else{ + this.store.dispatch({ + type : DESELECT_REGIONS, + deselectRegions : [region] + }) + } } } diff --git a/src/ui/databrowser/databrowser.template.html b/src/ui/databrowser/databrowser.template.html index e031190ddb083b15803386acd53fa6eeb580b493..651cbdfa2823a799e5468c0a7de6e8544f8e9273 100644 --- a/src/ui/databrowser/databrowser.template.html +++ b/src/ui/databrowser/databrowser.template.html @@ -1,12 +1,12 @@ <!-- Header --> <div databrowserheader> <span> - {{ databrowserHeaderText }} + {{ !(selectedPOI$ | async) ? '' : (((selectedPOI$ | async).length === 0 ? 'No' : (selectedPOI$ | async).length) + ' selected region' + ((selectedPOI$ | async).length > 1 ? 's' : '')) + ' of interest' }} </span> <small - *ngIf = "selectedRegions.length > 0" + *ngIf = "(selectedPOI$ | async).length > 0" class = "btn btn-link" - (click) = "clearAllRegions()"> + (click) = "clearAllPOIs()"> clear all </small> </div> @@ -35,20 +35,20 @@ </small> </div> <div - class = "unclickable" + class = "clickable" (click) = "toggleSpatialDataVisible()"> <small> <i *ngIf = "spatialDataVisible" class = "glyphicon glyphicon-check"></i> <i *ngIf = "!spatialDataVisible" class = "glyphicon glyphicon-unchecked"></i> </small> - <small placement = "bottom" tooltip = "coming soon"> + <small> Spatial Search </small> </div> </div> <!-- Data --> -<div *ngIf = "spatialDataVisible"> +<div *ngIf = "spatialDataVisible && false"> <panel-component [collapseBody] = "true" [bodyCollapsable] = "true"> <div heading> <span> @@ -60,7 +60,12 @@ </div> <div body> <div *ngFor = "let data of spatialDataEntries" spatialSearchCell> - {{ data.properties.description }} + <div *ngIf = "data.name"> + {{ data.name }} + </div> + <div *ngIf = "data.properties"> + {{ data.properties.description }} + </div> </div> <pagination-component @@ -74,10 +79,10 @@ </div> </panel-component> </div> -<div *ngIf = "selectedRegions.length > 0; else noSelectedRegion"> +<div *ngIf = "(selectedPOI$ | async).length > 0; else noSelectedRegion"> <div - *ngFor = "let data of selectedRegions | searchResultPagination : currentPage : hitsPerPage | sortDataEntriesToRegion : dataEntries "> + *ngFor = "let data of (selectedRegions$ | async | sortDataEntriesToRegion : dataEntries).concat( selectedLandmarks$ | async | spatialLandmarksToDataBrowserItemPipe ) | searchResultPagination : currentPage : hitsPerPage "> <panel-component [collapseBody] = "true" [bodyCollapsable] = "true"> @@ -101,8 +106,7 @@ (click) = "gothere($event,data.region.position)" class = "glyphicon glyphicon-screenshot" > </i> - <i *ngIf = "regionSelected(data.region)" - (click) = "removeRegion($event,data.region)" + <i (click) = "removePOI($event,data.region)" class = "glyphicon glyphicon-remove-sign"> </i> </div> @@ -138,15 +142,15 @@ <ng-template #noSelectedRegion> <div noSelectedRegion> - Select a region to get started + Select a region / spatial landmark to get started </div> </ng-template> <!-- pagination --> <pagination-component - *ngIf = "selectedRegions.length > 0 && selectedRegions.length > hitsPerPage" + *ngIf = "(selectedPOI$ | async).length > 0 && (selectedPOI$ | async).length > hitsPerPage" [hitsPerPage]="hitsPerPage" - [total] = "selectedRegions.length" + [total] = "(selectedPOI$ | async).length" [currentPage]="currentPage" (paginationChange)="paginationChange($event)"> diff --git a/src/ui/datasetViewer/datasetViewer.component.ts b/src/ui/datasetViewer/datasetViewer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d0ee15a05e387b76b29dc1d7881288d782c680d --- /dev/null +++ b/src/ui/datasetViewer/datasetViewer.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from "@angular/core"; +import { DataEntry } from "../../services/stateStore.service"; + +@Component({ + selector : 'dataset-viewer', + templateUrl : './datasetViewer.template.html', + styleUrls : ['./datasetViewer.style.css'] +}) + +export class DatasetViewerComponent{ + @Input() dataset : DataEntry +} \ No newline at end of file diff --git a/src/ui/datasetViewer/datasetViewer.style.css b/src/ui/datasetViewer/datasetViewer.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/ui/datasetViewer/datasetViewer.template.html b/src/ui/datasetViewer/datasetViewer.template.html new file mode 100644 index 0000000000000000000000000000000000000000..357acc26b0c1a785537940f17c937c1e68aef0c6 --- /dev/null +++ b/src/ui/datasetViewer/datasetViewer.template.html @@ -0,0 +1,6 @@ +<div *ngIf = "dataset; else defaultDisplay"> + {{ dataset.name }} +</div> +<ng-template #defaultDisplay> + Nothing to display ... +</ng-template> \ No newline at end of file diff --git a/src/ui/fileviewer/fileviewer.style.css b/src/ui/fileviewer/fileviewer.style.css index ea626400867a3528f6cd14f4617d44bfd7785280..fb19d5e76b665d4aff9f7d89ce902a387d1a8040 100644 --- a/src/ui/fileviewer/fileviewer.style.css +++ b/src/ui/fileviewer/fileviewer.style.css @@ -31,4 +31,9 @@ kg-entry-viewer div[anchorContainer] { padding:0.2em 1em; +} + +div[mimetypeTextContainer] +{ + margin:1em; } \ No newline at end of file diff --git a/src/ui/fileviewer/fileviewer.template.html b/src/ui/fileviewer/fileviewer.template.html index ee8934f08169629a3cf6e1907e6778b79541166e..f73d5862eb404720ab5ae2eeb8a97b822ae9472f 100644 --- a/src/ui/fileviewer/fileviewer.template.html +++ b/src/ui/fileviewer/fileviewer.template.html @@ -52,6 +52,11 @@ The json file is not a chart. I mean, we could dump the json, but would it really help? </div> </div> + <div *ngSwitchCase = "'application/hibop'"> + <div mimetypeTextContainer> + You will need to install the HiBoP software on your computer, click the 'Download File' button and open the .hibop file. + </div> + </div> <div *ngSwitchCase = "'application/nifti'"> <dedicated-viewer [searchResultFile] = "searchResultFile"> @@ -66,7 +71,9 @@ </dedicated-view-controller> --> </div> <div *ngSwitchDefault> - The selected file with the mimetype {{ searchResultFile.mimetype }} could not be displayed. + <div mimetypeTextContainer> + The selected file with the mimetype {{ searchResultFile.mimetype }} could not be displayed. + </div> </div> </div> diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index 587fbc6e9f0b46a6f9cb4e7e5eccec5cb73a3087..aa71ad7a408349d93c67690b93f1dca2c5ca4eca 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -1,11 +1,15 @@ -import { Component, ViewChild, ViewContainerRef, ComponentFactoryResolver, ComponentFactory, ComponentRef, OnInit, OnDestroy, ElementRef } from "@angular/core"; +import { Component, ViewChild, ViewContainerRef, ComponentFactoryResolver, ComponentFactory, ComponentRef, OnInit, OnDestroy, ElementRef, Injector } from "@angular/core"; import { NehubaViewerUnit } from "./nehubaViewer/nehubaViewer.component"; import { Store, select } from "@ngrx/store"; -import { ViewerStateInterface, safeFilter, SELECT_REGIONS, getLabelIndexMap, DataEntry, CHANGE_NAVIGATION, isDefined, MOUSE_OVER_SEGMENT, USER_LANDMARKS, ADD_NG_LAYER, REMOVE_NG_LAYER, SHOW_NG_LAYER, NgViewerStateInterface, HIDE_NG_LAYER } from "../../services/stateStore.service"; -import { Observable, Subscription, fromEvent, combineLatest, merge } from "rxjs"; -import { filter,map, take, scan, debounceTime, distinctUntilChanged, switchMap, skip } from "rxjs/operators"; +import { ViewerStateInterface, safeFilter, SELECT_REGIONS, getLabelIndexMap, DataEntry, CHANGE_NAVIGATION, isDefined, MOUSE_OVER_SEGMENT, USER_LANDMARKS, ADD_NG_LAYER, REMOVE_NG_LAYER, SHOW_NG_LAYER, NgViewerStateInterface, HIDE_NG_LAYER, MOUSE_OVER_LANDMARK, SELECT_LANDMARKS } from "../../services/stateStore.service"; +import { Observable, Subscription, fromEvent, combineLatest, merge, of } from "rxjs"; +import { filter,map, take, scan, debounceTime, distinctUntilChanged, switchMap, skip, withLatestFrom, buffer } from "rxjs/operators"; import { AtlasViewerAPIServices, UserLandmark } from "../../atlasViewer/atlasViewer.apiService.service"; import { timedValues } from "../../util/generator"; +import { AtlasViewerDataService } from "../../atlasViewer/atlasViewer.dataService.service"; +import { AtlasViewerConstantsServices } from "../../atlasViewer/atlasViewer.constantService.service"; +import { DatasetViewerComponent } from "../datasetViewer/datasetViewer.component"; +import { WidgetServices } from "../../atlasViewer/widgetUnit/widgetService.service"; @Component({ selector : 'ui-nehuba-container', @@ -24,6 +28,7 @@ export class NehubaContainer implements OnInit, OnDestroy{ @ViewChild('[pos11]',{read:ElementRef}) bottomright : ElementRef private nehubaViewerFactory : ComponentFactory<NehubaViewerUnit> + private datasetViewerFactory : ComponentFactory<DatasetViewerComponent> public viewerLoaded : boolean = false @@ -35,12 +40,14 @@ export class NehubaContainer implements OnInit, OnDestroy{ private newViewer$ : Observable<any> private selectedParcellation$ : Observable<any> private selectedRegions$ : Observable<any[]> + private selectedLandmarks$ : Observable<any[]> private hideSegmentations$ : Observable<boolean> private fetchedSpatialDatasets$ : Observable<any[]> private userLandmarks$ : Observable<UserLandmark[]> public onHoverSegmentName$ : Observable<string> public onHoverSegment$ : Observable<any> + private onHoverLandmark$ : Observable<any|null> private navigationChanges$ : Observable<any> public spatialResultsVisible$ : Observable<boolean> @@ -58,6 +65,8 @@ export class NehubaContainer implements OnInit, OnDestroy{ private cr : ComponentRef<NehubaViewerUnit> public nehubaViewer : NehubaViewerUnit private regionsLabelIndexMap : Map<number,any> = new Map() + private landmarksLabelIndexMap : Map<number, any> = new Map() + private landmarksNameMap : Map<string,number> = new Map() private userLandmarks : UserLandmark[] = [] @@ -69,12 +78,15 @@ export class NehubaContainer implements OnInit, OnDestroy{ public combinedSpatialData$ : Observable<any[]> constructor( + private constantService : AtlasViewerConstantsServices, + private atlasViewerDataService : AtlasViewerDataService, private apiService :AtlasViewerAPIServices, private csf:ComponentFactoryResolver, private store : Store<ViewerStateInterface>, private elementRef : ElementRef ){ this.nehubaViewerFactory = this.csf.resolveComponentFactory(NehubaViewerUnit) + this.datasetViewerFactory = this.csf.resolveComponentFactory(DatasetViewerComponent) this.newViewer$ = this.store.pipe( select('viewerState'), @@ -97,11 +109,18 @@ export class NehubaContainer implements OnInit, OnDestroy{ map(state=>state.regionsSelected) ) + this.selectedLandmarks$ = this.store.pipe( + select('viewerState'), + safeFilter('landmarksSelected'), + map(state => state.landmarksSelected) + ) + this.fetchedSpatialDatasets$ = this.store.pipe( select('dataStore'), - safeFilter('fetchedSpatialData'), debounceTime(300), - map(state=>state.fetchedSpatialData) + safeFilter('fetchedSpatialData'), + map(state => state.fetchedSpatialData), + distinctUntilChanged(this.constantService.testLandmarksChanged) ) this.navigationChanges$ = this.store.pipe( @@ -150,17 +169,29 @@ export class NehubaContainer implements OnInit, OnDestroy{ distinctUntilChanged(segmentsUnchangedChanged) ) - this.onHoverSegmentName$ = this.store.pipe( + this.onHoverLandmark$ = this.store.pipe( select('uiState'), - filter(state=>isDefined(state)), - map(state=>state.mouseOverSegment ? - state.mouseOverSegment.constructor === Number ? - state.mouseOverSegment.toString() : - state.mouseOverSegment.name : - '' ), - distinctUntilChanged() + filter(state => isDefined(state)), + map(state => state.mouseOverLandmark) ) + // TODO hack, even though octant is hidden, it seems with VTK, one can highlight + this.onHoverSegmentName$ = combineLatest( + this.store.pipe( + select('uiState'), + filter(state=>isDefined(state)), + map(state=>state.mouseOverSegment ? + state.mouseOverSegment.constructor === Number ? + state.mouseOverSegment.toString() : + state.mouseOverSegment.name : + '' ), + distinctUntilChanged() + ), + this.onHoverLandmark$ + ).pipe( + map(results => results[1] === null ? results[0] : '') + ) + /* each time a new viewer is initialised, take the first event to get the translation function */ this.newViewer$.pipe( switchMap(() => fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent') @@ -236,7 +267,7 @@ export class NehubaContainer implements OnInit, OnDestroy{ .pipe( filter(event => isDefined(event) && isDefined((event as any).detail) && isDefined((event as any).detail.lastLoadedMeshId) ), map(event => { - + const e = (event as any) const lastLoadedIdString = e.detail.lastLoadedMeshId.split(',')[0] const lastLoadedIdNum = Number(lastLoadedIdString) @@ -279,17 +310,24 @@ export class NehubaContainer implements OnInit, OnDestroy{ ngOnInit(){ + this.subscriptions.push( + this.fetchedSpatialDatasets$.subscribe(datasets => { + this.landmarksLabelIndexMap = new Map(datasets.map((v,idx) => [idx, v]) as [number, any][]) + this.landmarksNameMap = new Map(datasets.map((v,idx) => [v.name, idx] as [string, number])) + }) + ) + this.subscriptions.push( combineLatest( this.fetchedSpatialDatasets$, this.spatialResultsVisible$ ).subscribe(([fetchedSpatialData,visible])=>{ - this.fetchedSpatialData = fetchedSpatialData - this.nehubaViewer.removeSpatialSearch3DLandmarks() if(visible) this.nehubaViewer.addSpatialSearch3DLandmarks(this.fetchedSpatialData.map((data:any)=>data.position)) + else + this.nehubaViewer.removeSpatialSearch3DLandmarks() }) ) @@ -447,15 +485,82 @@ export class NehubaContainer implements OnInit, OnDestroy{ positionReal : true }) this.nehubaViewer.initRegions = regions.map(re=>re.labelIndex) - - /* TODO what to do with init nfiti? */ }) this.subscriptions.push( this.navigationChanges$.subscribe(this.handleDispatchedNavigationChange.bind(this)) ) + + /* handler to open/select landmark */ + const clickObs$ = fromEvent(this.elementRef.nativeElement, 'click').pipe( + withLatestFrom(this.onHoverLandmark$), + filter(results => results[1] !== null), + map(results => results[1]), + withLatestFrom( + this.store.pipe( + select('dataStore'), + safeFilter('fetchedSpatialData'), + map(state => state.fetchedSpatialData) + ) + ) + ) + + this.subscriptions.push( + clickObs$.pipe( + buffer( + clickObs$.pipe( + debounceTime(200) + ) + ), + filter(arr => arr.length >= 2), + map(arr => [...arr].reverse()[0]), + withLatestFrom(this.selectedLandmarks$) + ) + .subscribe(([clickObs, selectedSpatialDatas]) => { + const [landmark, spatialDatas] = clickObs + const idx = Number(landmark.replace('label=','')) + if(isNaN(idx)){ + console.warn(`Landmark index could not be parsed as a number: ${landmark}`) + return + } + + const newSelectedSpatialDatas = selectedSpatialDatas.findIndex(data => data.name === spatialDatas[idx].name) >= 0 + ? selectedSpatialDatas.filter(v => v.name !== spatialDatas[idx].name) + : selectedSpatialDatas.concat(spatialDatas[idx]) + + this.store.dispatch({ + type : SELECT_LANDMARKS, + landmarks : newSelectedSpatialDatas + }) + // if(this.datasetViewerRegistry.has(spatialDatas[idx].name)){ + // return + // } + // this.datasetViewerRegistry.add(spatialDatas[idx].name) + // const comp = this.datasetViewerFactory.create(this.injector) + // comp.instance.dataset = spatialDatas[idx] + // comp.onDestroy(() => this.datasetViewerRegistry.delete(spatialDatas[idx].name)) + // this.widgetServices.addNewWidget(comp, { + // exitable : true, + // persistency : false, + // state : 'floating', + // title : `Spatial Dataset - ${spatialDatas[idx].name}` + // }) + }) + ) + + this.subscriptions.push( + this.selectedLandmarks$.pipe( + map(lms => lms.map(lm => this.landmarksNameMap.get(lm.name))) + ).subscribe(indices => { + const filteredIndices = indices.filter(v => typeof v !== 'undefined' && v !== null) + if(this.nehubaViewer) + this.nehubaViewer.spatialLandmarkSelectionChanged(filteredIndices) + }) + ) } + // datasetViewerRegistry : Set<string> = new Set() + ngOnDestroy(){ this.subscriptions.forEach(s=>s.unsubscribe()) } @@ -515,11 +620,37 @@ export class NehubaContainer implements OnInit, OnDestroy{ ) this.nehubaViewerSubscriptions.push( - this.nehubaViewer.mouseoverSegmentEmitter.subscribe(this.handleEmittedMouseoverSegment.bind(this)) + this.nehubaViewer.debouncedViewerPositionChange.pipe( + distinctUntilChanged((a,b) => + [0,1,2].every(idx => a.position[idx] === b.position[idx]) && a.zoom === b.zoom) + ).subscribe(this.handleNavigationPositionAndNavigationZoomChange.bind(this)) + ) + + this.nehubaViewerSubscriptions.push( + this.nehubaViewer.mouseoverSegmentEmitter.subscribe(emitted => { + this.store.dispatch({ + type : MOUSE_OVER_SEGMENT, + segment : emitted + }) + }) + ) + + this.nehubaViewerSubscriptions.push( + this.nehubaViewer.mouseoverLandmarkEmitter.subscribe(label => { + this.store.dispatch({ + type : MOUSE_OVER_LANDMARK, + landmark : label + }) + }) ) + // TODO hack, even though octant is hidden, it seems with vtk one can mouse on hover this.nehubaViewerSubscriptions.push( - this.nehubaViewer.regionSelectionEmitter.subscribe(region => { + this.nehubaViewer.regionSelectionEmitter.pipe( + withLatestFrom(this.onHoverLandmark$), + filter(results => results[1] === null), + map(results => results[0]) + ).subscribe((region:any) => { this.selectedRegionIndexSet.has(region.labelIndex) ? this.store.dispatch({ type : SELECT_REGIONS, @@ -625,10 +756,19 @@ export class NehubaContainer implements OnInit, OnDestroy{ } } - handleEmittedMouseoverSegment(emitted : any | number | null){ - this.store.dispatch({ - type : MOUSE_OVER_SEGMENT, - segment : emitted + handleNavigationPositionAndNavigationZoomChange(navigation){ + if(!navigation.position){ + return + } + + const center = navigation.position.map(n=>n/1e6) + const searchWidth = this.constantService.spatialWidth / 4 * navigation.zoom / 1e6 + const templateSpace = this.selectedTemplate.name + this.atlasViewerDataService.spatialSearch({ + center, + searchWidth, + templateSpace, + pageNo : 0 }) } @@ -654,7 +794,6 @@ export class NehubaContainer implements OnInit, OnDestroy{ if( !navigationChangedActively ) return - /* navigation changed actively (by user interaction with the viewer) probagate the changes to the store */ diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index 2d2c633e76a8e16d0b1035d416b3313ffffb4da2..7762891106e257db2ba7a1acb262f714b6b07807 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -7,7 +7,8 @@ <div landmarkMasterContainer> <div> - <layout-floating-container + <layout-floating-container + *ngIf = "false" pos00 landmarkContainer> <nehuba-2dlandmark-unit @@ -29,7 +30,8 @@ </layout-floating-container> </div> <div> - <layout-floating-container + <layout-floating-container + *ngIf = "false" pos01 landmarkContainer> <nehuba-2dlandmark-unit @@ -50,7 +52,8 @@ </layout-floating-container> </div> <div> - <layout-floating-container + <layout-floating-container + *ngIf = "false" pos10 landmarkContainer> <nehuba-2dlandmark-unit @@ -89,7 +92,7 @@ <div statusCard> <citations-component - *ngIf = "selectedParcellation.properties" + *ngIf = "selectedParcellation && selectedParcellation.properties" [properties] = "selectedParcellation.properties" citationContainer> diff --git a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts index 0876ffee9d8e72f1dd23c6076bf717b380a74afa..10492466bce64367f726d6b7e80a71a7bc9b53ab 100644 --- a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts +++ b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts @@ -4,7 +4,8 @@ import * as export_nehuba from 'third_party/export_nehuba/main.bundle.js' import 'third_party/export_nehuba/chunk_worker.bundle.js' import { fromEvent, interval } from 'rxjs' import { AtlasWorkerService } from "../../../atlasViewer/atlasViewer.workerService.service"; -import { buffer, map, filter } from "rxjs/operators"; +import { buffer, map, filter, debounceTime } from "rxjs/operators"; +import { AtlasViewerConstantsServices } from "../../../atlasViewer/atlasViewer.constantService.service"; @Component({ templateUrl : './nehubaViewer.template.html', @@ -17,6 +18,7 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ @Output() debouncedViewerPositionChange : EventEmitter<any> = new EventEmitter() @Output() mouseoverSegmentEmitter : EventEmitter<any | number | null> = new EventEmitter() + @Output() mouseoverLandmarkEmitter : EventEmitter<number | null> = new EventEmitter() @Output() regionSelectionEmitter : EventEmitter<any> = new EventEmitter() /* only used to set initial navigation state */ @@ -53,10 +55,47 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ constructor( public elementRef:ElementRef, private workerService : AtlasWorkerService, - private zone : NgZone + private zone : NgZone, + private constantService : AtlasViewerConstantsServices ){ this.patchNG() + this.ondestroySubscriptions.push( + fromEvent(this.workerService.worker, 'message').pipe( + filter((message:any) => { + + if(!message){ + // console.error('worker response message is undefined', message) + return false + } + if(!message.data){ + // console.error('worker response message.data is undefined', message.data) + return false + } + if(message.data.type !== 'ASSEMBLED_LANDMARK_VTK'){ + /* worker responded with not assembled landmark, no need to act */ + return false + } + if(!message.data.url){ + /* file url needs to be defined */ + return false + } + return true + }), + debounceTime(100), + map(e => e.data.url) + ).subscribe(url => { + this.removeSpatialSearch3DLandmarks() + const _ = {} + _[this.constantService.ngLandmarkLayerName] = { + type :'mesh', + source : `vtk://${url}`, + shader : FRAGMENT_MAIN_WHITE + } + this.loadLayer(_) + }) + ) + this.ondestroySubscriptions.push( fromEvent(this.workerService.worker,'message').pipe( @@ -143,6 +182,25 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ this.loadNewParcellation() } + spatialLandmarkSelectionChanged(labels:number[]){ + const getCondition = (label:number) => `if(label > ${label - 0.1} && label < ${label + 0.1} ){${FRAGMENT_EMIT_RED}}` + const newShader = `void main(){ ${labels.map(getCondition).join('else ')}else {${FRAGMENT_EMIT_WHITE}} }` + if(!this.nehubaViewer){ + console.warn('setting special landmark selection changed failed ... nehubaViewer is not yet defined') + return + } + const landmarkLayer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(this.constantService.ngLandmarkLayerName) + if(!landmarkLayer){ + console.warn('landmark layer could not be found ... will not update colour map') + return + } + if(labels.length === 0){ + landmarkLayer.layer.displayState.fragmentMain.restoreState(FRAGMENT_MAIN_WHITE) + }else{ + landmarkLayer.layer.displayState.fragmentMain.restoreState(newShader) + } + } + regionsLabelIndexMap : Map<number,any> navPosReal : [number,number,number] = [0,0,0] @@ -244,28 +302,16 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ public removeSpatialSearch3DLandmarks(){ this.removeLayer({ - name : /vtk-[0-9]/ + name : this.constantService.ngLandmarkLayerName }) } + //pos in mm public addSpatialSearch3DLandmarks(poss:[number,number,number][],scale?:number,type?:'icosahedron'){ - const _ = {} - poss.forEach((pos,idx)=>{ - - _[`vtk-${idx}`] = { - type : 'mesh', - source : `vtk://${ICOSAHEDRON_VTK_URL}`, - transform : [ - [2 ,0 ,0 , pos[0]*1e6], - [0 ,2 ,0 , pos[1]*1e6], - [0 ,0 ,2 , pos[2]*1e6], - [0 ,0 ,0 , 1 ], - ], - shader : FRAGMENT_MAIN_WHITE - } + this.workerService.worker.postMessage({ + type : 'GET_LANDMARK_VTK', + landmarks : poss.map(pos => pos.map(v => v * 1e6)) }) - /* load layer triggers navigation view event, results in infinite loop */ - this.loadLayer(_) } public setLayerVisibility(condition:{name:string|RegExp},visible:boolean){ @@ -402,7 +448,7 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ this.hideAllSeg() } - this._s8$ = this.nehubaViewer.mouseOver.segment.subscribe(({segment})=>{ + this._s8$ = this.nehubaViewer.mouseOver.segment.subscribe(({segment, ...rest})=>{ if( segment && segment < 65500 ) { const region = this.regionsLabelIndexMap.get(segment) this.mouseoverSegmentEmitter.emit(region ? region : segment) @@ -411,8 +457,31 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ } }) + // nehubaViewer.navigationState.all emits every time a new layer is added or removed from the viewer this._s3$ = this.nehubaViewer.navigationState.all .debounceTime(300) + .distinctUntilChanged((a, b) => { + const { + orientation: o1, + perspectiveOrientation: po1, + perspectiveZoom: pz1, + position: p1, + zoom: z1 + } = a + const { + orientation: o2, + perspectiveOrientation: po2, + perspectiveZoom: pz2, + position: p2, + zoom: z2 + } = b + + return [0,1,2,3].every(idx => o1[idx] === o2[idx]) && + [0,1,2,3].every(idx => po1[idx] === po2[idx]) && + pz1 === pz2 && + [0,1,2].every(idx => p1[idx] === p2[idx]) && + z1 === z2 + }) .subscribe(({ orientation, perspectiveOrientation, perspectiveZoom, position, zoom })=>{ this.viewerState = { orientation, @@ -422,6 +491,7 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ position, positionReal : false } + this.debouncedViewerPositionChange.emit({ orientation : Array.from(orientation), perspectiveOrientation : Array.from(perspectiveOrientation), @@ -432,6 +502,12 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ }) }) + this.ondestroySubscriptions.push( + this.nehubaViewer.mouseOver.layer + .filter(obj => obj.layer.name === this.constantService.ngLandmarkLayerName) + .subscribe(obj => this.mouseoverLandmarkEmitter.emit(obj.value)) + ) + this._s4$ = this.nehubaViewer.navigationState.position.inRealSpace .filter(v=>typeof v !== 'undefined' && v !== null) .subscribe(v=>this.navPosReal=v) @@ -614,4 +690,6 @@ declare const TextEncoder export const _encoder = new TextEncoder() export const ICOSAHEDRON_VTK_URL = URL.createObjectURL( new Blob([ _encoder.encode(ICOSAHEDRON) ],{type : 'application/octet-stream'} )) -export const FRAGMENT_MAIN_WHITE = `void main(){emitRGB(vec3(1.0,1.0,1.0));}` \ No newline at end of file +export const FRAGMENT_MAIN_WHITE = `void main(){emitRGB(vec3(1.0,1.0,1.0));}` +export const FRAGMENT_EMIT_WHITE = `emitRGB(vec3(1.0, 1.0, 1.0));` +export const FRAGMENT_EMIT_RED = `emitRGB(vec3(1.0, 0.1, 0.12));` \ No newline at end of file diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index e599b3263f81fb067a66e2c4ad2ce2ba9d8b605a..b1038fcbad031895709669f3a7a76decab3188f9 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -31,6 +31,8 @@ import { KgEntryViewer } from "./kgEntryViewer/kgentry.component"; import { SubjectViewer } from "./kgEntryViewer/subjectViewer/subjectViewer.component"; import { GetLayerNameFromDatasets } from "../util/pipes/getLayerNamePipe.pipe"; import { SortDataEntriesToRegion } from "../util/pipes/sortDataEntriesIntoRegion.pipe"; +import { DatasetViewerComponent } from "./datasetViewer/datasetViewer.component"; +import { SpatialLandmarksToDataBrowserItemPipe } from "../util/pipes/spatialLandmarksToDatabrowserItem.pipe"; @NgModule({ @@ -59,6 +61,7 @@ import { SortDataEntriesToRegion } from "../util/pipes/sortDataEntriesIntoRegion LayerBrowser, KgEntryViewer, SubjectViewer, + DatasetViewerComponent, /* pipes */ GroupDatasetByRegion, @@ -69,14 +72,16 @@ import { SortDataEntriesToRegion } from "../util/pipes/sortDataEntriesIntoRegion FilterDataEntriesbyType, SafeStylePipe, GetLayerNameFromDatasets, - SortDataEntriesToRegion + SortDataEntriesToRegion, + SpatialLandmarksToDataBrowserItemPipe ], entryComponents : [ /* dynamically created components needs to be declared here */ NehubaViewerUnit, FileViewer, - DataBrowserUI + DataBrowserUI, + DatasetViewerComponent ], exports : [ SubjectViewer, @@ -88,7 +93,8 @@ import { SortDataEntriesToRegion } from "../util/pipes/sortDataEntriesIntoRegion NehubaViewerUnit, DataBrowserUI, LayerBrowser, - FileViewer + FileViewer, + DatasetViewerComponent ] }) diff --git a/src/util/pipes/spatialLandmarksToDatabrowserItem.pipe.ts b/src/util/pipes/spatialLandmarksToDatabrowserItem.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ba40df5d61678179817ee66ac7007fed8c63d64 --- /dev/null +++ b/src/util/pipes/spatialLandmarksToDatabrowserItem.pipe.ts @@ -0,0 +1,23 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { DataEntry } from "../../services/stateStore.service"; + + +@Pipe({ + name : 'spatialLandmarksToDataBrowserItemPipe' +}) + +export class SpatialLandmarksToDataBrowserItemPipe implements PipeTransform{ + public transform(landmarks:any[]):{region:any, searchResults:Partial<DataEntry>[]}[]{ + return landmarks.map(landmark => ({ + region : Object.assign({}, landmark, { + position : landmark.position.map(v => v*1e6), + spatialLandmark : true + }), + searchResults : [{ + name : 'Associated dataset', + type : 'Associated dataset', + files : landmark.files + }] + })) + } +} \ No newline at end of file diff --git a/src/util/worker.js b/src/util/worker.js index c4081345976d5bf5067b4ca49a5621b4191243b2..c167cd81e9eb7cc72455558fa288c33bb6b375a8 100644 --- a/src/util/worker.js +++ b/src/util/worker.js @@ -1,5 +1,5 @@ -const validTypes = ['CHECK_MESHES'] -const validOutType = ['CHECKED_MESH'] +const validTypes = ['CHECK_MESHES', 'GET_LANDMARK_VTK'] +const validOutType = ['CHECKED_MESH', 'ASSEMBLED_LANDMARK_VTK'] const checkMeshes = (action) => { @@ -33,6 +33,142 @@ const checkMeshes = (action) => { }) } +const vtkHeader = `# vtk DataFile Version 2.0 +Created by worker thread at https://github.com/HumanBrainProject/interactive-viewer +ASCII +DATASET POLYDATA` + +const getVertexHeader = (numLandmarks) => `POINTS ${12 * numLandmarks} float` + +const getPolyHeader = (numLandmarks) => `POLYGONS ${20 * numLandmarks} ${80 * numLandmarks}` + +const getLabelHeader = (numLandmarks) => `POINT_DATA ${numLandmarks * 12} +SCALARS label unsigned_char 1 +LOOKUP_TABLE none` + +//pos in nm +const getIcoVertex = (pos, scale) => `-525731.0 0.0 850651.0 +525731.0 0.0 850651.0 +-525731.0 0.0 -850651.0 +525731.0 0.0 -850651.0 +0.0 850651.0 525731.0 +0.0 850651.0 -525731.0 +0.0 -850651.0 525731.0 +0.0 -850651.0 -525731.0 +850651.0 525731.0 0.0 +-850651.0 525731.0 0.0 +850651.0 -525731.0 0.0 +-850651.0 -525731.0 0.0` + .split('\n') + .map(line => + line + .split(' ') + .map((string, idx) => (Number(string) * (scale ? scale : 1) + pos[idx]).toString() ) + .join(' ') + ) + .join('\n') + +const getIcoPoly = (idx2) => `3 1 4 0 +3 4 9 0 +3 4 5 9 +3 8 5 4 +3 1 8 4 +3 1 10 8 +3 10 3 8 +3 8 3 5 +3 3 2 5 +3 3 7 2 +3 3 10 7 +3 10 6 7 +3 6 11 7 +3 6 0 11 +3 6 1 0 +3 10 1 6 +3 11 0 9 +3 2 11 9 +3 5 2 9 +3 11 2 7` + .split('\n') + .map((line) => + line + .split(' ') + .map((v,idx) => idx === 0 ? v : (Number(v) + idx2 * 12).toString() ) + .join(' ') + ) + .join('\n') + +const getLabelScalar = (idx) => [...Array(12)].map(() => idx.toString()).join('\n') + +const ICOSAHEDRON = `# vtk DataFile Version 2.0 +Converted using https://github.com/HumanBrainProject/neuroglancer-scripts +ASCII +DATASET POLYDATA +POINTS 12 float +-525731.0 0.0 850651.0 +525731.0 0.0 850651.0 +-525731.0 0.0 -850651.0 +525731.0 0.0 -850651.0 +0.0 850651.0 525731.0 +0.0 850651.0 -525731.0 +0.0 -850651.0 525731.0 +0.0 -850651.0 -525731.0 +850651.0 525731.0 0.0 +-850651.0 525731.0 0.0 +850651.0 -525731.0 0.0 +-850651.0 -525731.0 0.0 +POLYGONS 20 80 +3 1 4 0 +3 4 9 0 +3 4 5 9 +3 8 5 4 +3 1 8 4 +3 1 10 8 +3 10 3 8 +3 8 3 5 +3 3 2 5 +3 3 7 2 +3 3 10 7 +3 10 6 7 +3 6 11 7 +3 6 0 11 +3 6 1 0 +3 10 1 6 +3 11 0 9 +3 2 11 9 +3 5 2 9 +3 11 2 7` + +let landmarkVtkUrl + +const encoder = new TextEncoder() +const getLandmarkVtk = (action) => { + + // landmarks are array of triples in nm (array of array of numbers) + const landmarks = action.landmarks + const vtk = vtkHeader + .concat('\n') + .concat(getVertexHeader(landmarks.length)) + .concat('\n') + .concat(landmarks.map(landmark => getIcoVertex(landmark, 2.8)).join('\n')) + .concat('\n') + .concat(getPolyHeader(landmarks.length)) + .concat('\n') + .concat(landmarks.map((_, idx) => getIcoPoly(idx)).join('\n')) + .concat('\n') + .concat(getLabelHeader(landmarks.length)) + .concat('\n') + .concat(landmarks.map((_, idx) => getLabelScalar(idx)).join('\n')) + + // when new set of landmarks are to be displayed, the old landmarks will be discarded + if(landmarkVtkUrl) + URL.revokeObjectURL(landmarkVtkUrl) + landmarkVtkUrl = URL.createObjectURL(new Blob( [encoder.encode(vtk)], {type : 'application/octet-stream'} )) + postMessage({ + type : 'ASSEMBLED_LANDMARK_VTK', + url : landmarkVtkUrl + }) +} + onmessage = (message) => { if(validTypes.findIndex(type => type === message.data.type)>=0){ @@ -40,6 +176,9 @@ onmessage = (message) => { case 'CHECK_MESHES': checkMeshes(message.data) return; + case 'GET_LANDMARK_VTK': + getLandmarkVtk(message.data) + return; default: console.warn('unhandled worker action') }