diff --git a/docs/releases/v2.4.0.md b/docs/releases/v2.4.0.md index c5a0899369bfe492c04435051a7e234a5be989b0..d10152cc2e9b0d91fb59a774864db361172c286c 100644 --- a/docs/releases/v2.4.0.md +++ b/docs/releases/v2.4.0.md @@ -9,6 +9,9 @@ ## New features - plugins will now follow a permission model, if they would like to access external resources +- only top level mesh(es) will be loaded (#890). This should drastically improve atlases with hierarchical surface meshes (e.g. Allen CCF v3). +- Preliminary support for displaying of SWC (#907) +- Preliminary support for freesurfer (#900) ## Under the hood stuff diff --git a/src/res/ext/allenMouse.json b/src/res/ext/allenMouse.json index 9c9a5ec855bb7c5bf3a9da191f287ff7100219ce..d8ae3d5a9f14014796c79e453aaec4a4a937269e 100644 --- a/src/res/ext/allenMouse.json +++ b/src/res/ext/allenMouse.json @@ -18,6 +18,9 @@ "name": "Allen Mouse Common Coordinate Framework v3 2017", "ngData": null, "type": "parcellation", + "auxillaryMeshIndices": [ + 997 + ], "regions": [ { "ngId": "v3_2017", @@ -19372,6 +19375,9 @@ "name": "Allen Mouse Common Coordinate Framework v3 2015", "ngData": null, "type": "parcellation", + "auxillaryMeshIndices": [ + 997 + ], "regions": [ { "ngId": "atlas", @@ -19387,6 +19393,7 @@ ], "name": "root", "labelIndex": 997, + "unselectable": true, "relatedAreas": [ { "name": "Whole brain", diff --git a/src/viewerModule/nehuba/constants.ts b/src/viewerModule/nehuba/constants.ts index 5dd2cd2c51eb1146d8769bfe0025d93644d33c6c..81b4f89cf14e6ecdc98431ea1f4fe35ab902f575 100644 --- a/src/viewerModule/nehuba/constants.ts +++ b/src/viewerModule/nehuba/constants.ts @@ -1,3 +1,6 @@ +import { InjectionToken } from '@angular/core' +import { Observable } from 'rxjs' + export { getNgIds } from 'src/util/fn' export const NEHUBA_VIEWER_FEATURE_KEY = 'ngViewerFeature' @@ -57,3 +60,12 @@ export function getMultiNgIdsRegionsLabelIndexMap(parcellation: any = {}, inheri return map } + +export interface IMeshesToLoad { + labelIndicies: number[] + layer: { + name: string + } +} + +export const SET_MESHES_TO_LOAD = new InjectionToken<Observable<IMeshesToLoad>>('SET_MESHES_TO_LOAD') diff --git a/src/viewerModule/nehuba/mesh.service.spec.ts b/src/viewerModule/nehuba/mesh.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa5d5002bdaa17613a897d65cdff68221d3228a0 --- /dev/null +++ b/src/viewerModule/nehuba/mesh.service.spec.ts @@ -0,0 +1,156 @@ +import { TestBed } from "@angular/core/testing" +import { MockStore, provideMockStore } from "@ngrx/store/testing" +import { hot } from "jasmine-marbles" +import { viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors" +import { getLayerNameIndiciesFromParcRs, collateLayerNameIndicies, findFirstChildrenWithLabelIndex, NehubaMeshService } from "./mesh.service" + + +const fits1 = { + ngId: 'foobar', + labelIndex: 123, + children: [] +} + +const fits1_1 = { + ngId: 'foobar', + labelIndex: 5, + children: [] +} + +const fits2 = { + ngId: 'helloworld', + labelIndex: 567, + children: [] +} + +const fits2_1 = { + ngId: 'helloworld', + labelIndex: 11, + children: [] +} + +const nofit1 = { + ngId: 'bazz', + children: [] +} + +const nofit2 = { + ngId: 'but', + children: [] +} + +describe('> mesh.server.ts', () => { + describe('> findFirstChildrenWithLabelIndex', () => { + it('> if root fits, return root', () => { + const result = findFirstChildrenWithLabelIndex({ + ...fits1, + children: [fits2] + }) + + expect(result).toEqual([{ + ...fits1, + children: [fits2] + }]) + }) + + it('> if root doesnt fit, will try to find the next node, until one fits', () => { + const result = findFirstChildrenWithLabelIndex({ + ...nofit1, + children: [fits1, fits2] + }) + expect(result).toEqual([fits1, fits2]) + }) + + it('> if notthings fits, will return empty array', () => { + const result = findFirstChildrenWithLabelIndex({ + ...nofit1, + children: [nofit1, nofit2] + }) + expect(result).toEqual([]) + }) + }) + + describe('> collateLayerNameIndicies', () => { + it('> collates same ngIds', () => { + const result = collateLayerNameIndicies([ + fits1_1, fits1, fits2, fits2_1 + ]) + expect(result).toEqual({ + [fits1.ngId]: [fits1_1.labelIndex, fits1.labelIndex], + [fits2.ngId]: [fits2.labelIndex, fits2_1.labelIndex] + }) + }) + }) + + describe('> getLayerNameIndiciesFromParcRs', () => { + const root = { + ...fits1, + children: [ + { + ...nofit1, + children: [ + { + ...fits1_1, + children: [ + fits2, fits2_1 + ] + } + ] + } + ] + } + const parc = { + regions: [ root ] + } + it('> if selectedRegion.length === 0, selects top most regions with labelIndex', () => { + const result = getLayerNameIndiciesFromParcRs(parc, []) + expect(result).toEqual({ + [root.ngId]: [root.labelIndex] + }) + }) + + it('> if selReg.length !== 0, select region ngId & labelIndex', () => { + const result = getLayerNameIndiciesFromParcRs(parc, [ fits1_1 ]) + expect(result).toEqual({ + [fits1_1.ngId]: [fits1_1.labelIndex] + }) + }) + }) + + describe('> NehubaMeshService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideMockStore(), + NehubaMeshService, + ] + }) + }) + + it('> can be init', () => { + const service = TestBed.inject(NehubaMeshService) + expect(service).toBeTruthy() + }) + + it('> mixes in auxillaryMeshIndices', () => { + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector(viewerStateSelectedParcellationSelector, { + auxillaryMeshIndices: [11, 22] + }) + mockStore.overrideSelector(viewerStateSelectedRegionsSelector, [ fits1 ]) + const service = TestBed.inject(NehubaMeshService) + expect( + service.loadMeshes$ + ).toBeObservable( + hot('a', { + a: { + layer: { + name: fits1.ngId + }, + labelIndicies: [ fits1.labelIndex, 11, 22 ] + } + }) + ) + }) + }) +}) diff --git a/src/viewerModule/nehuba/mesh.service.ts b/src/viewerModule/nehuba/mesh.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..007aedaa559a035306d1e73ed71f34a580a19d28 --- /dev/null +++ b/src/viewerModule/nehuba/mesh.service.ts @@ -0,0 +1,101 @@ +import { Injectable, OnDestroy } from "@angular/core"; +import { select, Store } from "@ngrx/store"; +import { combineLatest, Observable, of } from "rxjs"; +import { switchMap } from "rxjs/operators"; +import { viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors"; +import { IMeshesToLoad } from './constants' +import { flattenReducer } from 'common/util' + +interface IRegion { + ngId?: string + labelIndex?: number + children: IRegion[] +} + +interface IParc { + ngId?: string + regions: IRegion[] +} + +type TCollatedLayerNameIdx = { + [key: string]: number[] +} + +export function findFirstChildrenWithLabelIndex(region: IRegion): IRegion[]{ + if (region.ngId && region.labelIndex) { + return [ region ] + } + return region.children + .map(findFirstChildrenWithLabelIndex) + .reduce(flattenReducer, []) +} + +export function collateLayerNameIndicies(regions: IRegion[]){ + const returnObj: TCollatedLayerNameIdx = {} + for (const r of regions) { + if (returnObj[r.ngId]) { + returnObj[r.ngId].push(r.labelIndex) + } else { + returnObj[r.ngId] = [r.labelIndex] + } + } + return returnObj +} + +export function getLayerNameIndiciesFromParcRs(parc: IParc, rs: IRegion[]): TCollatedLayerNameIdx { + + const arrOfRegions = (rs.length === 0 ? parc.regions : rs) + .map(findFirstChildrenWithLabelIndex) + .reduce(flattenReducer, []) as IRegion[] + + return collateLayerNameIndicies(arrOfRegions) +} + +/** + * control mesh loading etc + */ + +@Injectable() +export class NehubaMeshService implements OnDestroy { + + private onDestroyCb: (() => void)[] = [] + + constructor( + private store$: Store<any> + ){ + + } + + ngOnDestroy(){ + while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() + } + + private selectedRegions$ = this.store$.pipe( + select(viewerStateSelectedRegionsSelector) + ) + + private selectedParc$ = this.store$.pipe( + select(viewerStateSelectedParcellationSelector) + ) + + public loadMeshes$: Observable<IMeshesToLoad> = combineLatest([ + this.selectedParc$, + this.selectedRegions$, + ]).pipe( + switchMap(([parc, selRegions]) => { + const obj = getLayerNameIndiciesFromParcRs(parc, selRegions) + const { auxillaryMeshIndices = [] } = parc + const arr: IMeshesToLoad[] = [] + for (const key in obj) { + const labelIndicies = Array.from(new Set([...obj[key], ...auxillaryMeshIndices])) + arr.push({ + layer: { + name: key + }, + labelIndicies + }) + } + return of(...arr) + }), + ) +} diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts index f24c61fc2d74b143ee4554871bd29032dc2ab27e..451dcbdd7af2a58560cdb978096f3b636f3ec9a3 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts @@ -5,10 +5,13 @@ import { importNehubaFactory } from "../util" import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service" import { LoggingModule } from "src/logging" import { APPEND_SCRIPT_TOKEN, appendScriptFactory } from "src/util/constants" - +import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants" +import { Subject } from "rxjs" describe('nehubaViewer.component,ts', () => { describe('NehubaViewerUnit', () => { + let provideSetMeshToLoadCtrl = false + const setMeshToLoadCtl$ = new Subject<IMeshesToLoad>() beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -29,7 +32,13 @@ describe('nehubaViewer.component,ts', () => { useFactory: appendScriptFactory, deps: [ DOCUMENT ] }, - AtlasWorkerService + { + provide: SET_MESHES_TO_LOAD, + useFactory: () => provideSetMeshToLoadCtrl + ? setMeshToLoadCtl$ + : null + }, + AtlasWorkerService, ] }).compileComponents() })) @@ -76,19 +85,62 @@ describe('nehubaViewer.component,ts', () => { }) describe('> loading meshes', () => { - it('> on loadMeshes$ emit, calls nehubaViewer.setMeshesToLoad', fakeAsync(() => { + describe('> native', () => { + beforeAll(() => { + provideSetMeshToLoadCtrl = false + }) + it('> on loadMeshes$ emit, calls nehubaViewer.setMeshesToLoad', fakeAsync(() => { - const fixture = TestBed.createComponent(NehubaViewerUnit) - fixture.componentInstance.nehubaViewer = { - setMeshesToLoad: jasmine.createSpy('setMeshesToLoad').and.returnValue(null), - dispose: () => {} - } + const fixture = TestBed.createComponent(NehubaViewerUnit) + fixture.componentInstance.nehubaViewer = { + setMeshesToLoad: jasmine.createSpy('setMeshesToLoad').and.returnValue(null), + dispose: () => {} + } + + fixture.detectChanges() + fixture.componentInstance['loadMeshes']([1,2,3], { name: 'foo-bar' }) + tick(1000) + expect(fixture.componentInstance.nehubaViewer.setMeshesToLoad).toHaveBeenCalledWith([1,2,3], { name: 'foo-bar' }) + })) + }) - fixture.detectChanges() - fixture.componentInstance['loadMeshes']([1,2,3], { name: 'foo-bar' }) - tick(1000) - expect(fixture.componentInstance.nehubaViewer.setMeshesToLoad).toHaveBeenCalledWith([1,2,3], { name: 'foo-bar' }) - })) + describe('> injecting SET_MESHES_TO_LOAD', () => { + beforeAll(() => { + provideSetMeshToLoadCtrl = true + }) + it('> navtive loadMeshes method will not trigger loadMesh call',fakeAsync(() => { + + const fixture = TestBed.createComponent(NehubaViewerUnit) + fixture.componentInstance.nehubaViewer = { + setMeshesToLoad: jasmine.createSpy('setMeshesToLoad').and.returnValue(null), + dispose: () => {} + } + + fixture.detectChanges() + fixture.componentInstance['loadMeshes']([1,2,3], { name: 'foo-bar' }) + tick(1000) + expect(fixture.componentInstance.nehubaViewer.setMeshesToLoad).not.toHaveBeenCalledWith([1,2,3], { name: 'foo-bar' }) + })) + + it('> when injected obs emits, will trigger loadMesh call', fakeAsync(() => { + + const fixture = TestBed.createComponent(NehubaViewerUnit) + fixture.componentInstance.nehubaViewer = { + setMeshesToLoad: jasmine.createSpy('setMeshesToLoad').and.returnValue(null), + dispose: () => {} + } + + fixture.detectChanges() + setMeshToLoadCtl$.next({ + labelIndicies: [1,2,3], + layer: { + name: 'foo-bar' + } + }) + tick(1000) + expect(fixture.componentInstance.nehubaViewer.setMeshesToLoad).toHaveBeenCalledWith([1,2,3], { name: 'foo-bar' }) + })) + }) }) }) }) diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 9fb2ce89790bda6c23d4ac26259585067c5c0990..e6b84b64cfbbfa06f4b9ac2a381b67f85406a268 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -1,6 +1,6 @@ import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, Inject, Optional } from "@angular/core"; import { fromEvent, Subscription, ReplaySubject, BehaviorSubject, Observable, race, timer, Subject } from 'rxjs' -import { debounceTime, filter, map, scan, startWith, mapTo, switchMap, take, skip } from "rxjs/operators"; +import { debounceTime, filter, map, scan, startWith, mapTo, switchMap, take, skip, tap } from "rxjs/operators"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { StateInterface as ViewerConfiguration } from "src/services/state/viewerConfig.store"; @@ -11,6 +11,7 @@ import '!!file-loader?context=third_party&name=main.bundle.js!export-nehuba/dist import '!!file-loader?context=third_party&name=chunk_worker.bundle.js!export-nehuba/dist/min/chunk_worker.bundle.js' import { NEHUBA_INSTANCE_INJTKN, scanSliceViewRenderFn } from "../util"; import { strToRgb, deserialiseParcRegionId } from 'common/util' +import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants"; const NG_LANDMARK_LAYER_NAME = 'spatial landmark layer' const NG_USER_LANDMARK_LAYER_NAME = 'user landmark layer' @@ -155,6 +156,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { private log: LoggingService, @Inject(IMPORT_NEHUBA_INJECT_TOKEN) getImportNehubaPr: () => Promise<any>, @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaViewer$: Subject<NehubaViewerUnit>, + @Optional() @Inject(SET_MESHES_TO_LOAD) private injSetMeshesToLoad$: Observable<IMeshesToLoad>, ) { if (this.nehubaViewer$) { @@ -429,7 +431,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { ) this.subscriptions.push( - this.loadMeshes$.pipe( + (this.injSetMeshesToLoad$ || this.loadMeshes$).pipe( scan(scanFn, []), debounceTime(100), switchMap(layerLabelIdx => @@ -644,8 +646,6 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { } public hideAllSeg() { - // console.log('hideallseg') - // debugger if (!this.nehubaViewer) { return } Array.from(this.multiNgIdsLabelIndexMap.keys()).forEach(ngId => { @@ -939,8 +939,9 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { ...Array.from(this.multiNgIdsLabelIndexMap.get(id).keys()), ...this.auxilaryMeshIndices, ] - - this.loadMeshes(indicies, { name: id }) + if (!this.injSetMeshesToLoad$) { + this.loadMeshes(indicies, { name: id }) + } }) const obj = Array.from(this.multiNgIdsLabelIndexMap.keys()).map(ngId => { @@ -948,10 +949,12 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { ngId, new Map(Array.from( [ - ...this.multiNgIdsLabelIndexMap.get(ngId).entries(), + // set aux mesh first + // as sometimes, existing rgb can overwrite the rgb prop of aux mesh ...this.auxilaryMeshIndices.map(val => { return [val, {}] }), + ...this.multiNgIdsLabelIndexMap.get(ngId).entries(), ], ).map((val: [number, any]) => ([val[0], this.getRgb(val[0], { ngId, rgb: val[1].rgb})])) as any), ] diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 430782c05f95c1ff8b42dcbbc1eff7814afffaf6..af1bbefe8592dc18f515a9606a74461e7264404b 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -13,13 +13,14 @@ import { ARIA_LABELS, IDS } from 'common/constants' import { PANELS } from "src/services/state/ngViewerState/constants"; import { LoggingService } from "src/logging"; -import { getNgIds, getMultiNgIdsRegionsLabelIndexMap } from "../constants"; +import { getNgIds, getMultiNgIdsRegionsLabelIndexMap, SET_MESHES_TO_LOAD } from "../constants"; import { IViewer, TViewerEvent } from "../../viewer.interface"; import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; import { NehubaViewerContainerDirective } from "../nehubaViewerInterface/nehubaViewerInterface.directive"; import { cvtNavigationObjToNehubaConfig, getFourPanel, getHorizontalOneThree, getSinglePanel, getVerticalOneThree, NEHUBA_INSTANCE_INJTKN, scanSliceViewRenderFn, takeOnePipe } from "../util"; import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service"; import { MouseHoverDirective } from "src/mouseoverModule"; +import { NehubaMeshService } from "../mesh.service"; interface INgLayerInterface { name: string // displayName @@ -38,7 +39,15 @@ interface INgLayerInterface { styleUrls: [ './nehubaViewerGlue.style.css' ], - exportAs: 'iavCmpViewerNehubaGlue' + exportAs: 'iavCmpViewerNehubaGlue', + providers: [ + { + provide: SET_MESHES_TO_LOAD, + useFactory: (meshService: NehubaMeshService) => meshService.loadMeshes$, + deps: [ NehubaMeshService ] + }, + NehubaMeshService + ] }) export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{