diff --git a/src/atlasViewer/atlasViewer.dataService.service.ts b/src/atlasViewer/atlasViewer.dataService.service.ts index 74d8f5d243aa6b38ebdc717900df675a3f82b52f..38c2ed3a7f2c9409d9efa50799edecf1fdb40dae 100644 --- a/src/atlasViewer/atlasViewer.dataService.service.ts +++ b/src/atlasViewer/atlasViewer.dataService.service.ts @@ -93,7 +93,6 @@ export class AtlasViewerDataService implements OnDestroy{ 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 : {} }))), []) }) @@ -103,6 +102,20 @@ export class AtlasViewerDataService implements OnDestroy{ }) }) .catch(console.error) + }else if (templateSpace === 'Allen Mouse'){ + return fetch('res/json/allenTestPlane.json') + .then(res => res.json()) + .then(arr => { + this.store.dispatch({ + type : FETCHED_SPATIAL_DATA, + fetchedDataEntries : arr.map(item => Object.assign({}, item, { properties : {} })) + }) + this.store.dispatch({ + type : UPDATE_SPATIAL_DATA, + totalResults : arr.length + }) + }) + .catch(console.error) }else{ return } diff --git a/src/res/cvtPtsToSpatial.js b/src/res/cvtPtsToSpatial.js index b232bfdb9b479f08650c9cecafc70a3412ff2e8e..6765f731841cfbdbdb78313cf9824ab8d8b2fd71 100644 --- a/src/res/cvtPtsToSpatial.js +++ b/src/res/cvtPtsToSpatial.js @@ -21,11 +21,16 @@ filenames.map(filename => { return { type : 'iEEG Recording Site', name : filename.replace('_MNI.pts', '').concat(`_${name.replace(/^.p/g,(s) => s.slice(0,1).concat('\''))}`), - position : [ - Number(x) * 1e6, - Number(y) * 1e6, - Number(z) * 1e6 - ], + templateSpace : 'MNI 152 ICBM 2009c Nonlinear Asymmetric', + geometry : { + type : 'point', + space : 'real', + position : [ + Number(x), + Number(y), + Number(z) + ], + }, properties : { description : descContactPts, publications : [] diff --git a/src/res/ext/allenTestPlane.json b/src/res/ext/allenTestPlane.json new file mode 100644 index 0000000000000000000000000000000000000000..bbf92f741739b54173dfcaf95b85d21162898f88 --- /dev/null +++ b/src/res/ext/allenTestPlane.json @@ -0,0 +1 @@ +[{"type":"Test Allen plane spatial anchor","name":"Plane 01","templateSpace":"Allen Mouse","geometry":{"type":"plane","corners":[[2.689,-0.8875,3.3125],[2.689,-5.2675,2.8925],[-1.711,-5.2675,2.8925],[-1.711,-0.8875,3.3125]]},"properties":{"description":"This spatial plane represent an observation plane of the mouse.","publications":[]},"files":[{"filename":"DATA1/DATA1_FILE1","name":"DATA1_FILE1","mimetype":"application/raw","url":"http://about:blank","properties":{"description":"This is the description for the DATA1 FILE1","publications":[]}},{"filename":"DATA1/DATA1_FILE2","name":"DATA1 FILE2","mimetype":"application/raw","url":"http://about:blank","properties":{"description":"This is the description for the DATA1 FILE2","publications":[]}},{"filename":"DATA2/DATA2_FILE1","name":"DATA2_FILE1","mimetype":"application/raw","url":"http://about:blank","properties":{"description":"This is the description for the DATA2 FILE1","publications":[]}},{"filename":"DATA2/DATA2_FILE2","name":"DATA2_FILE2","mimetype":"application/raw","url":"http://about:blank","properties":{"description":"This is the description for the DATA2 FILE2","publications":[]}}],"originalJson":{"reference_atlas":"CCF v3","name":"hbp-00552_GCaMP6.json","HBP_Storage_path":"bp00sp01/Pavone_SGA1_1.3.2/hbp-00552/GCaMP6/","Primary region(s)":"Anterior cingulate area, dorsal part; Anterolateral visual area; Posteromedial visual area; Primary motor area; Primary somatosensory area, barrel field; Primary somatosensory area, lower limb; Primary somatosensory area, nose; Primary somatosensory area, trunk; Primary somatosensory area, unassigned; Primary somatosensory area, upper limb; Primary visual area; Retrosplenial area, dorsal part; Retrosplenial area, lateral agranular part; Retrosplenial area, ventral part; Rostrolateral area; Secondary motor area","Landmarks":[{"name":"upper left corner","x":230,"y":294,"z":322.5},{"name":"upper right corner","x":54.8,"y":277.2,"z":322.5},{"name":"lower right corner","x":54.8,"y":277.2,"z":146.5},{"name":"lower left corner","x":230,"y":294,"z":146.5}]},"processedData":{"transformedCorners":[[230,294,322.5],[54.8,277.2,322.5],[54.8,277.2,146.5],[230,294,146.5]]}}] \ No newline at end of file diff --git a/src/services/stateStore.service.ts b/src/services/stateStore.service.ts index 6c45c411ba6837dcb7f0784ba02e7f2f4d3d2cca..7d49498576165b98c3e70426ccfd0a67519c1f41 100644 --- a/src/services/stateStore.service.ts +++ b/src/services/stateStore.service.ts @@ -446,6 +446,30 @@ export interface UIAction extends Action{ focusedSidePanel? : string } +export interface Landmark{ + type : string //e.g. sEEG recording site, etc + name : string + templateSpace : string // possibily inherited from LandmarkBundle (?) + geometry : PointLandmarkGeometry | PlaneLandmarkGeometry + properties : Property + files : File[] +} + +export interface LandmarkGeometry{ + type : 'point' | 'plane' + space? : 'voxel' | 'real' +} + +export interface PointLandmarkGeometry extends LandmarkGeometry{ + position : [number, number, number] +} + +export interface PlaneLandmarkGeometry extends LandmarkGeometry{ + // corners have to be CW or CCW (no zigzag) + corners : [[number, number, number],[number, number, number],[number, number, number],[number, number, number]] +} + + export function isDefined(obj){ return typeof obj !== 'undefined' && obj !== null } \ No newline at end of file diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index aa71ad7a408349d93c67690b93f1dca2c5ca4eca..e83f390949bdd867c51e77bd8cb87a4f35cd5e65 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -1,15 +1,13 @@ 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, MOUSE_OVER_LANDMARK, SELECT_LANDMARKS } from "../../services/stateStore.service"; +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, Landmark, PointLandmarkGeometry, PlaneLandmarkGeometry } 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', @@ -28,7 +26,6 @@ export class NehubaContainer implements OnInit, OnDestroy{ @ViewChild('[pos11]',{read:ElementRef}) bottomright : ElementRef private nehubaViewerFactory : ComponentFactory<NehubaViewerUnit> - private datasetViewerFactory : ComponentFactory<DatasetViewerComponent> public viewerLoaded : boolean = false @@ -43,7 +40,7 @@ export class NehubaContainer implements OnInit, OnDestroy{ private selectedLandmarks$ : Observable<any[]> private hideSegmentations$ : Observable<boolean> - private fetchedSpatialDatasets$ : Observable<any[]> + private fetchedSpatialDatasets$ : Observable<Landmark[]> private userLandmarks$ : Observable<UserLandmark[]> public onHoverSegmentName$ : Observable<string> public onHoverSegment$ : Observable<any> @@ -55,7 +52,7 @@ export class NehubaContainer implements OnInit, OnDestroy{ private selectedTemplate : any | null private selectedRegionIndexSet : Set<number> = new Set() - public fetchedSpatialData : DataEntry[] = [] + public fetchedSpatialData : Landmark[] = [] private ngLayersRegister : NgViewerStateInterface = {layers : [], forceShowSegment: null} private ngLayers$ : Observable<NgViewerStateInterface> @@ -86,7 +83,6 @@ export class NehubaContainer implements OnInit, OnDestroy{ private elementRef : ElementRef ){ this.nehubaViewerFactory = this.csf.resolveComponentFactory(NehubaViewerUnit) - this.datasetViewerFactory = this.csf.resolveComponentFactory(DatasetViewerComponent) this.newViewer$ = this.store.pipe( select('viewerState'), @@ -117,10 +113,10 @@ export class NehubaContainer implements OnInit, OnDestroy{ this.fetchedSpatialDatasets$ = this.store.pipe( select('dataStore'), - debounceTime(300), safeFilter('fetchedSpatialData'), map(state => state.fetchedSpatialData), - distinctUntilChanged(this.constantService.testLandmarksChanged) + distinctUntilChanged(this.constantService.testLandmarksChanged), + debounceTime(300), ) this.navigationChanges$ = this.store.pipe( @@ -290,12 +286,12 @@ export class NehubaContainer implements OnInit, OnDestroy{ ) .pipe( map(([datas,visible, ...rest]) => visible ? datas : []), - map(arr => arr.map(v => Object.assign({}, v, {type : 'spatialSearchLandmark'}))) + // map(arr => arr.map(v => Object.assign({}, v, {type : 'spatialSearchLandmark'}))) ), this.userLandmarks$.pipe( - map(arr => arr.map(v => Object.assign({}, v, {type : 'userLandmark'}))) + // map(arr => arr.map(v => Object.assign({}, v, {type : 'userLandmark'}))) ) - ).pipe(map(arr => arr[0].concat(arr[1]))) + ).pipe(map(arr => [...arr[0], ...arr[1]])) this.ngLayers$ = this.store.pipe( select('ngViewerState') @@ -324,10 +320,21 @@ export class NehubaContainer implements OnInit, OnDestroy{ ).subscribe(([fetchedSpatialData,visible])=>{ this.fetchedSpatialData = fetchedSpatialData - if(visible) - this.nehubaViewer.addSpatialSearch3DLandmarks(this.fetchedSpatialData.map((data:any)=>data.position)) - else + if(visible && this.fetchedSpatialData && this.fetchedSpatialData.length > 0){ + this.nehubaViewer.addSpatialSearch3DLandmarks( + this.fetchedSpatialData + .map(data=> data.geometry.type === 'point' + ? (data.geometry as PointLandmarkGeometry).position + : data.geometry.type === 'plane' + ? [ + (data.geometry as PlaneLandmarkGeometry).corners, + [[0,1,2], [0,2,3]] + ] + : null) + ) + }else{ this.nehubaViewer.removeSpatialSearch3DLandmarks() + } }) ) diff --git a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts index 10492466bce64367f726d6b7e80a71a7bc9b53ab..a07f85932460235dc138339c9aa64414d31817da 100644 --- a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts +++ b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts @@ -72,7 +72,7 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ // console.error('worker response message.data is undefined', message.data) return false } - if(message.data.type !== 'ASSEMBLED_LANDMARK_VTK'){ + if(message.data.type !== 'ASSEMBLED_LANDMARKS_VTK'){ /* worker responded with not assembled landmark, no need to act */ return false } @@ -307,10 +307,17 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ } //pos in mm - public addSpatialSearch3DLandmarks(poss:[number,number,number][],scale?:number,type?:'icosahedron'){ + public addSpatialSearch3DLandmarks(geometries: any[],scale?:number,type?:'icosahedron'){ this.workerService.worker.postMessage({ - type : 'GET_LANDMARK_VTK', - landmarks : poss.map(pos => pos.map(v => v * 1e6)) + type : 'GET_LANDMARKS_VTK', + landmarks : geometries.map(geometry => + geometry === null + ? null + //gemoetry : [number,number,number] | [ [number,number,number][], [number,number,number][] ] + : isNaN(geometry[0]) + ? [geometry[0].map(triplets => triplets.map(coord => (console.log(coord), coord * 1e6))), geometry[1]] + : geometry.map(coord => coord * 1e6) + ) }) } diff --git a/src/util/pipes/spatialLandmarksToDatabrowserItem.pipe.ts b/src/util/pipes/spatialLandmarksToDatabrowserItem.pipe.ts index 9ba40df5d61678179817ee66ac7007fed8c63d64..4b5316976e9eefe521d571c28999ee09eef8a9d4 100644 --- a/src/util/pipes/spatialLandmarksToDatabrowserItem.pipe.ts +++ b/src/util/pipes/spatialLandmarksToDatabrowserItem.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from "@angular/core"; -import { DataEntry } from "../../services/stateStore.service"; +import { DataEntry, Landmark, PointLandmarkGeometry, PlaneLandmarkGeometry } from "../../services/stateStore.service"; @Pipe({ @@ -7,12 +7,19 @@ import { DataEntry } from "../../services/stateStore.service"; }) export class SpatialLandmarksToDataBrowserItemPipe implements PipeTransform{ - public transform(landmarks:any[]):{region:any, searchResults:Partial<DataEntry>[]}[]{ + public transform(landmarks:Landmark[]):{region:any, searchResults:Partial<DataEntry>[]}[]{ return landmarks.map(landmark => ({ region : Object.assign({}, landmark, { - position : landmark.position.map(v => v*1e6), spatialLandmark : true - }), + }, landmark.geometry.type === 'point' + ? { + position : (landmark.geometry as PointLandmarkGeometry).position.map(v => v*1e6), + } + : landmark.geometry.type === 'plane' + ? { + POIs : (landmark.geometry as PlaneLandmarkGeometry).corners.map(corner => corner.map(coord => coord * 1e6)) + } + : {}), searchResults : [{ name : 'Associated dataset', type : 'Associated dataset', diff --git a/src/util/worker.js b/src/util/worker.js index c167cd81e9eb7cc72455558fa288c33bb6b375a8..4f92bfc1f78a624fb5ebbda9907201cfafa64dfb 100644 --- a/src/util/worker.js +++ b/src/util/worker.js @@ -1,5 +1,5 @@ -const validTypes = ['CHECK_MESHES', 'GET_LANDMARK_VTK'] -const validOutType = ['CHECKED_MESH', 'ASSEMBLED_LANDMARK_VTK'] +const validTypes = ['CHECK_MESHES', 'GET_LANDMARKS_VTK'] +const validOutType = ['CHECKED_MESH', 'ASSEMBLED_LANDMARKS_VTK'] const checkMeshes = (action) => { @@ -38,11 +38,11 @@ Created by worker thread at https://github.com/HumanBrainProject/interactive-vie ASCII DATASET POLYDATA` -const getVertexHeader = (numLandmarks) => `POINTS ${12 * numLandmarks} float` +const getVertexHeader = (numVertex) => `POINTS ${numVertex} float` -const getPolyHeader = (numLandmarks) => `POLYGONS ${20 * numLandmarks} ${80 * numLandmarks}` +const getPolyHeader = (numPoly) => `POLYGONS ${numPoly} ${4 * numPoly}` -const getLabelHeader = (numLandmarks) => `POINT_DATA ${numLandmarks * 12} +const getLabelHeader = (numVertex) => `POINT_DATA ${numVertex} SCALARS label unsigned_char 1 LOOKUP_TABLE none` @@ -68,7 +68,8 @@ const getIcoVertex = (pos, scale) => `-525731.0 0.0 850651.0 ) .join('\n') -const getIcoPoly = (idx2) => `3 1 4 0 + +const getIcoPoly = (startingIdx) => `3 1 4 0 3 4 9 0 3 4 5 9 3 8 5 4 @@ -92,79 +93,90 @@ const getIcoPoly = (idx2) => `3 1 4 0 .map((line) => line .split(' ') - .map((v,idx) => idx === 0 ? v : (Number(v) + idx2 * 12).toString() ) + .map((v,idx) => idx === 0 ? v : (Number(v) + startingIdx).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` +const getMeshVertex = (vertices) => (console.log(vertices), vertices.map(vertex => vertex.join(' ')).join('\n')) +const getMeshPoly = (polyIndices, currentIdx) => polyIndices.map(triplet => + '3 '.concat(triplet.map(index => + index + currentIdx + ).join(' ')) +).join('\n') let landmarkVtkUrl const encoder = new TextEncoder() -const getLandmarkVtk = (action) => { +const getLandmarksVtk = (action) => { + console.log('getin vtk') // landmarks are array of triples in nm (array of array of numbers) const landmarks = action.landmarks + + console.log({landmarks}) + + const reduce = landmarks.reduce((acc,curr,idx) => { + //curr : null | [number,number,number] | [ [number,number,number], [number,number,number], [number,number,number] ][] + if(curr === null) + return acc + if(!isNaN(curr[0])) + return { + currentVertexIndex : acc.currentVertexIndex + 12, + vertexString : acc.vertexString.concat(getIcoVertex(curr, 2.8)), + polyCount : acc.polyCount + 20, + polyString : acc.polyString.concat(getIcoPoly(acc.currentVertexIndex)), + labelString : acc.labelString.concat(Array(12).fill(idx.toString()).join('\n')) + } + else{ + //curr[0] : [number,number,number][] vertices + //curr[1] : [number,number,number][] indices for the vertices that poly forms + + const vertices = curr[0] + const polyIndices = curr[1] + + return { + currentVertexIndex : acc.currentVertexIndex + vertices.length, + vertexString : acc.vertexString.concat(getMeshVertex(vertices)), + polyCount : acc.currentVertexIndex + polyIndices.length, + polyString : acc.polyString.concat(getMeshPoly(polyIndices, acc.currentVertexIndex)), + labelString : acc.labelString.concat(Array(vertices.length).fill(idx.toString()).join('\n')) + } + } + }, { + currentVertexIndex : 0, + vertexString : [], + polyCount : 0, + polyString: [], + labelString : [], + }) + + // if no vertices are been rendered, do not replace old + if(reduce.currentVertexIndex === 0) + return + const vtk = vtkHeader .concat('\n') - .concat(getVertexHeader(landmarks.length)) + .concat(getVertexHeader(reduce.currentVertexIndex)) .concat('\n') - .concat(landmarks.map(landmark => getIcoVertex(landmark, 2.8)).join('\n')) + .concat(reduce.vertexString.join('\n')) .concat('\n') - .concat(getPolyHeader(landmarks.length)) + .concat(getPolyHeader(reduce.polyCount)) .concat('\n') - .concat(landmarks.map((_, idx) => getIcoPoly(idx)).join('\n')) + .concat(reduce.polyString.join('\n')) .concat('\n') - .concat(getLabelHeader(landmarks.length)) + .concat(getLabelHeader(reduce.currentVertexIndex)) .concat('\n') - .concat(landmarks.map((_, idx) => getLabelScalar(idx)).join('\n')) + .concat(reduce.labelString.join('\n')) + console.log({vtk}) // 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', + type : 'ASSEMBLED_LANDMARKS_VTK', url : landmarkVtkUrl }) } @@ -175,12 +187,12 @@ onmessage = (message) => { switch(message.data.type){ case 'CHECK_MESHES': checkMeshes(message.data) - return; - case 'GET_LANDMARK_VTK': - getLandmarkVtk(message.data) - return; + return + case 'GET_LANDMARKS_VTK': + getLandmarksVtk(message.data) + return default: - console.warn('unhandled worker action') + console.warn('unhandled worker action', message) } } }