diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 3648dc0b6f518b7bf147b15fb809e954faff8375..196567eb092c133b22002002e2aa3f1a15fc944f 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -13,6 +13,7 @@ import { SAPIFeature } from "./features"; import { environment } from "src/environments/environment" import { FeatureType, PathReturn, RouteParam, SapiRoute } from "./typeV3"; import { BoundingBox, SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate, VoiFeature, Feature } from "./sxplrTypes"; +import { atlasAppearance } from "src/state"; export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version' @@ -444,7 +445,7 @@ export class SAPI{ bbox: JSON.stringify([bbox.minpoint, bbox.maxpoint]), } }).pipe( - switchMap(v => Promise.all(v.items.map(item => translateV3Entities.translateVoi(item)))) + switchMap(v => Promise.all(v.items.map(item => translateV3Entities.translateVoiFeature(item)))) ) } @@ -459,6 +460,29 @@ export class SAPI{ }).toPromise() } + public useViewer(template: SxplrTemplate) { + return forkJoin({ + voxel: this.getVoxelTemplateImage(template), + surface: this.getSurfaceTemplateImage(template) + }).pipe( + map(vols => { + if (!vols) return null + const { voxel, surface } = vols + if (voxel.length > 0 && surface.length > 0) { + console.error(`both voxel and surface length are > 0, this should not happen.`) + return atlasAppearance.const.useViewer.NOT_SUPPORTED + } + if (voxel.length > 0) { + return atlasAppearance.const.useViewer.NEHUBA + } + if (surface.length > 0) { + return atlasAppearance.const.useViewer.THREESURFER + } + return atlasAppearance.const.useViewer.NOT_SUPPORTED + }) + ) + } + public getVoxelTemplateImage(template: SxplrTemplate) { return from(translateV3Entities.translateSpaceToVolumeImage(template)) } diff --git a/src/atlasComponents/sapi/sxplrTypes.ts b/src/atlasComponents/sapi/sxplrTypes.ts index 53320ec5645f56806c0098f88266c3020efed002..c9c2cb1bb2a1225c7b8b780a282e5d281f65441e 100644 --- a/src/atlasComponents/sapi/sxplrTypes.ts +++ b/src/atlasComponents/sapi/sxplrTypes.ts @@ -47,7 +47,7 @@ export type AdditionalInfo = { } type Location = { - space: SxplrTemplate + readonly space: SxplrTemplate } type LocTuple = [number, number, number] @@ -104,6 +104,11 @@ type DataFrame = { export type VoiFeature = { bbox: BoundingBox + ngVolume: { + url: string + transform: number[][] + info: Record<string, any> + } } & Feature type CorticalDataType = number diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index 82584e1193b23c56d4066786b2050d8af49c57c7..d0334963b910833bf380b5a7fcd72d5b0c7dc15f 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -1,5 +1,5 @@ import { - SxplrAtlas, SxplrParcellation, SxplrTemplate, SxplrRegion, NgLayerSpec, NgPrecompMeshSpec, NgSegLayerSpec, VoiFeature, Point, TemplateDefaultImage, TThreeSurferMesh, TThreeMesh, LabelledMap, CorticalFeature, Feature, TabularFeature, GenericInfo + SxplrAtlas, SxplrParcellation, SxplrTemplate, SxplrRegion, NgLayerSpec, NgPrecompMeshSpec, NgSegLayerSpec, VoiFeature, Point, TThreeMesh, LabelledMap, CorticalFeature, Feature, TabularFeature, GenericInfo, BoundingBox } from "./sxplrTypes" import { PathReturn } from "./typeV3" import { hexToRgb } from 'common/util' @@ -50,16 +50,20 @@ class TranslateV3 { } #templateMap: Map<string, PathReturn<"/spaces/{space_id}">> = new Map() + #sxplrTmplMap: Map<string, SxplrTemplate> = new Map() retrieveTemplate(template:SxplrTemplate): PathReturn<"/spaces/{space_id}"> { return this.#templateMap.get(template.id) } async translateTemplate(template:PathReturn<"/spaces/{space_id}">): Promise<SxplrTemplate> { + this.#templateMap.set(template["@id"], template) - return { + const tmpl = { id: template["@id"], name: template.fullName, - type: "SxplrTemplate" + type: "SxplrTemplate" as const } + this.#sxplrTmplMap.set(tmpl.id, tmpl) + return tmpl } /** @@ -93,6 +97,41 @@ class TranslateV3 { } } + + #hasNoFragment(input: Record<string, unknown>): input is Record<string, string> { + for (const key in input) { + if (typeof input[key] !== 'string') return false + } + return true + } + async #extractNgPrecompUnfrag(input: Record<string, unknown>) { + if (!this.#hasNoFragment(input)) { + throw new Error(`#extractNgPrecompUnfrag can only handle unfragmented volume`) + } + + const returnObj: Record<string, { + url: string, + transform: number[][], + info: Record<string, any> + }> = {} + for (const key in input) { + if (key !== 'neuroglancer/precomputed') { + continue + } + const url = input[key] + const [ transform, info ] = await Promise.all([ + fetch(`${url}/transform.json`).then(res => res.json()) as Promise<number[][]>, + fetch(`${url}/info`).then(res => res.json()) as Promise<Record<string, any>>, + ]) + returnObj[key] = { + url: input[key], + transform: transform, + info: info, + } + } + return returnObj + } + async translateSpaceToVolumeImage(template: SxplrTemplate): Promise<NgLayerSpec[]> { if (!template) return [] const space = this.retrieveTemplate(template) @@ -103,42 +142,20 @@ class TranslateV3 { for (const defaultImage of validImages) { const { providedVolumes } = defaultImage - - const { ['neuroglancer/precomputed']: precomputedVol } = providedVolumes + const { "neuroglancer/precomputed": precomputedVol, ...rest } = await this.#extractNgPrecompUnfrag(providedVolumes) + if (!precomputedVol) { console.error(`neuroglancer/precomputed data source has not been found!`) continue } - if (typeof precomputedVol === "object") { - console.error(`template default image cannot have fragment`) - continue + const { transform, info: _info, url } = precomputedVol + const { resolution, size } = _info.scales[0] + const info = { + voxel: size as [number, number, number], + real: [0, 1, 2].map(idx => resolution[idx] * size[idx]) as [number, number, number] } - const [transform, info] = await Promise.all([ - (async () => { - const resp = await fetch(`${precomputedVol}/transform.json`) - if (resp.status >= 400) { - console.error(`cannot retrieve transform: ${resp.status}`) - return null - } - const transform: number[][] = await resp.json() - return transform - })(), - (async () => { - const resp = await fetch(`${precomputedVol}/info`) - if (resp.status >= 400) { - console.error(`cannot retrieve transform: ${resp.status}`) - return null - } - const info = await resp.json() - const { resolution, size } = info.scales[0] - return { - voxel: info.scales[0].size as [number, number, number], - real: [0, 1, 2].map(idx => resolution[idx] * size[idx]) as [number, number, number], - } - })() - ]) returnObj.push({ - source: `precomputed://${precomputedVol}`, + source: `precomputed://${url}`, transform, info, }) @@ -250,7 +267,7 @@ class TranslateV3 { if (url in nglayerSpecMap){ segLayerSpec = nglayerSpecMap[url] } else { - const resp = await fetch(`${url}/transform.json`) + const resp = await this.cFetch(`${url}/transform.json`) const transform = await resp.json() segLayerSpec = { layer: { @@ -301,6 +318,36 @@ class TranslateV3 { return nglayerSpecMap } + #cFetchCache = new Map<string, string>() + /** + * Cached fetch + * + * Since translate v3 has no dependency on any angular components. + * We couldn't cache the response. This is a monkey patch to allow for caching of queries. + * @param url: string + * @returns { status: number, json: () => Promise<unknown> } + */ + async cFetch(url: string): Promise<{ status: number, json?: () => Promise<any> }> { + + if (!this.#cFetchCache.has(url)) { + const resp = await fetch(url) + if (resp.status >= 400) { + return { + status: resp.status, + } + } + const text = await resp.text() + this.#cFetchCache.set(url, text) + } + const cachedText = this.#cFetchCache.get(url) + return { + status: 200, + json() { + return Promise.resolve(JSON.parse(cachedText)) + } + } + } + async translateSpaceToAuxMesh(template: SxplrTemplate): Promise<NgPrecompMeshSpec[]>{ if (!template) return [] const space = this.retrieveTemplate(template) @@ -327,7 +374,7 @@ class TranslateV3 { console.error(`Expecting exactly two fragments by splitting precompmeshvol, but got ${splitPrecompMeshVol.length}`) continue } - const resp = await fetch(`${splitPrecompMeshVol[0]}/transform.json`) + const resp = await this.cFetch(`${splitPrecompMeshVol[0]}/transform.json`) if (resp.status >= 400) { console.error(`cannot retrieve transform: ${resp.status}`) continue @@ -345,30 +392,15 @@ class TranslateV3 { return returnObj } - async translatePoint(point: components["schemas"]["CoordinatePointModel"]): Promise<Point> { - const sapiSpace = this.#templateMap.get(point.coordinateSpace['@id']) - const space = await this.translateTemplate(sapiSpace) - return { - space, - loc: point.coordinates.map(v => v.value) as [number, number, number] + async #translatePoint(point: components["schemas"]["CoordinatePointModel"]): Promise<Point> { + const getTmpl = (id: string) => { + return this.#sxplrTmplMap.get(id) } - } - - async translateVoi(voi: PathReturn<"/feature/Image/{feature_id}">): Promise<VoiFeature> { - const { boundingbox } = voi - const { loc: center, space } = await this.translatePoint(boundingbox.center) - const { loc: maxpoint } = await this.translatePoint(boundingbox.maxpoint) - const { loc: minpoint } = await this.translatePoint(boundingbox.minpoint) return { - bbox: { - center, - maxpoint, - minpoint, - space - }, - name: voi.name, - desc: voi.description, - id: voi.id + loc: point.coordinates.map(v => v.value) as [number, number, number], + get space() { + return getTmpl(point.coordinateSpace['@id']) + } } } @@ -376,6 +408,10 @@ class TranslateV3 { if (this.#isTabular(feat)) { return await this.translateTabularFeature(feat) } + if (this.#isVoi(feat)) { + return await this.translateVoiFeature(feat) + } + return await this.translateBaseFeature(feat) } @@ -395,6 +431,35 @@ class TranslateV3 { } } + #isVoi(feat: unknown): feat is PathReturn<"/feature/Image/{feature_id}"> { + return feat['@type'].includes("feature/volume_of_interest") + } + + async translateVoiFeature(feat: PathReturn<"/feature/Image/{feature_id}">): Promise<VoiFeature> { + const [superObj, { loc: center }, { loc: maxpoint }, { loc: minpoint }, { "neuroglancer/precomputed": precomputedVol }] = await Promise.all([ + this.translateBaseFeature(feat), + this.#translatePoint(feat.boundingbox.center), + this.#translatePoint(feat.boundingbox.maxpoint), + this.#translatePoint(feat.boundingbox.minpoint), + await this.#extractNgPrecompUnfrag(feat.volume.providedVolumes), + ]) + const { ['@id']: spaceId } = feat.boundingbox.space + const getSpace = (id: string) => this.#sxplrTmplMap.get(id) + const bbox: BoundingBox = { + center, + maxpoint, + minpoint, + get space() { + return getSpace(spaceId) + } + } + return { + ...superObj, + bbox, + ngVolume: precomputedVol + } + } + #isTabular(feat: unknown): feat is PathReturn<"/feature/Tabular/{feature_id}"> { return feat["@type"].includes("feature/tabular") } diff --git a/src/features/feature-view/feature-view.component.html b/src/features/feature-view/feature-view.component.html index b91b00901deb47df55e74bf79da97ee14aaba437..7bbe648781733121949f45fcd7e6d2741f9c9f6f 100644 --- a/src/features/feature-view/feature-view.component.html +++ b/src/features/feature-view/feature-view.component.html @@ -66,3 +66,15 @@ <tr mat-row *matRowDef="let row; columns: columns$ | async;"></tr> </table> </ng-template> + + +<!-- voi special view --> +<ng-template [ngIf]="voi$ | async" let-voi> + <ng-layer-ctl + [ng-layer-ctl-name]="voi.ngVolume.url" + [ng-layer-ctl-src]="voi.ngVolume.url" + [ng-layer-ctl-transform]="voi.ngVolume.transform" + [ng-layer-ctl-info]="voi.ngVolume.info" + [ng-layer-ctl-opacity]="1.0"> + </ng-layer-ctl> +</ng-template> diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index 7c66b0270412086ae7b154581bd9a0d8632a45ec..9cd37f0ac72e1b127d72c03663b222cbaaf9d438 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -2,12 +2,16 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@a import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi/sapi.service'; -import { Feature, TabularFeature } from 'src/atlasComponents/sapi/sxplrTypes'; +import { Feature, TabularFeature, VoiFeature } from 'src/atlasComponents/sapi/sxplrTypes'; function isTabularData(feature: unknown): feature is TabularFeature<number|string|number[]> { return !!feature['index'] && !!feature['columns'] } +function isVoiData(feature: unknown): feature is VoiFeature { + return !!feature['bbox'] +} + @Component({ selector: 'sxplr-feature-view', templateUrl: './feature-view.component.html', @@ -20,8 +24,9 @@ export class FeatureViewComponent implements OnChanges { feature: Feature busy$ = new BehaviorSubject<boolean>(false) - - tabular$: BehaviorSubject<TabularFeature<number|string|number[]>> = new BehaviorSubject(null) + + tabular$ = new BehaviorSubject<TabularFeature<number|string|number[]>>(null) + voi$ = new BehaviorSubject<VoiFeature>(null) columns$: Observable<string[]> = this.tabular$.pipe( map(data => data ? ['index', ...data.columns] @@ -30,8 +35,11 @@ export class FeatureViewComponent implements OnChanges { constructor(private sapi: SAPI) { } ngOnChanges(): void { + + this.voi$.next(null) this.tabular$.next(null) this.busy$.next(true) + this.sapi.getV3FeatureDetailWithId(this.feature.id).subscribe( val => { this.busy$.next(false) @@ -39,6 +47,9 @@ export class FeatureViewComponent implements OnChanges { if (isTabularData(val)) { this.tabular$.next(val) } + if (isVoiData(val)) { + this.voi$.next(val) + } }, () => this.busy$.next(false) ) diff --git a/src/features/module.ts b/src/features/module.ts index 1aedc1d8a8695cf13cb2b9830d05438129837f1b..3795cf74814af3283e23d2b3cd22c39af8419d64 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -21,6 +21,7 @@ import { MarkdownModule } from "src/components/markdown"; import { MatTableModule } from "@angular/material/table"; import { FeatureViewComponent } from "./feature-view/feature-view.component"; import { TransformPdToDsPipe } from "./transform-pd-to-ds.pipe"; +import { NgLayerCtlModule } from "src/viewerModule/nehuba/ngLayerCtlModule/module"; @NgModule({ imports: [ @@ -39,6 +40,7 @@ import { TransformPdToDsPipe } from "./transform-pd-to-ds.pipe"; MatDividerModule, MarkdownModule, MatTableModule, + NgLayerCtlModule, ], declarations: [ EntryComponent, diff --git a/src/routerModule/routeStateTransform.service.ts b/src/routerModule/routeStateTransform.service.ts index 7f8a71a0611ecd91d4d7640597e924f10dc479af..deab0f8b9733a703c9279a57c49a299c966a8bce 100644 --- a/src/routerModule/routeStateTransform.service.ts +++ b/src/routerModule/routeStateTransform.service.ts @@ -57,6 +57,7 @@ export class RouteStateTransformSvc { this.sapi.getParcRegions(selectedParcellationId).toPromise(), ]) + const userViewer = await this.sapi.useViewer(selectedTemplate).toPromise() const selectedRegions = await (async () => { if (!selectedRegionIds) return [] @@ -127,12 +128,12 @@ export class RouteStateTransformSvc { selectedTemplate, selectedParcellation, selectedRegions, - allParcellationRegions + allParcellationRegions, + userViewer } } async cvtRouteToState(fullPath: UrlTree) { - const returnState: MainState = structuredClone(defaultState) const pathFragments: UrlSegment[] = fullPath.root.hasChildren() ? fullPath.root.children['primary'].segments @@ -217,13 +218,15 @@ export class RouteStateTransformSvc { } try { - const { selectedAtlas, selectedParcellation, selectedRegions = [], selectedTemplate, allParcellationRegions } = await this.getATPR(returnObj as TUrlPathObj<string[], TUrlAtlas<string[]>>) + const { selectedAtlas, selectedParcellation, selectedRegions = [], selectedTemplate, allParcellationRegions, userViewer } = await this.getATPR(returnObj as TUrlPathObj<string[], TUrlAtlas<string[]>>) returnState["[state.atlasSelection]"].selectedAtlas = selectedAtlas returnState["[state.atlasSelection]"].selectedParcellation = selectedParcellation returnState["[state.atlasSelection]"].selectedTemplate = selectedTemplate + returnState["[state.atlasSelection]"].selectedRegions = selectedRegions || [] returnState["[state.atlasSelection]"].selectedParcellationAllRegions = allParcellationRegions || [] returnState["[state.atlasSelection]"].navigation = parsedNavObj + returnState["[state.atlasAppearance]"].useViewer = userViewer } catch (e) { // if error, show error on UI? console.error(`parse template, parc, region error`, e) diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts index 7a2047489958038b0d4dcd36d2759ec7ffdc7e4f..37d6caec7354d1343031f1760f48f99124ec4409 100644 --- a/src/routerModule/router.service.ts +++ b/src/routerModule/router.service.ts @@ -5,7 +5,7 @@ import { NavigationEnd, Router } from '@angular/router' import { Store } from "@ngrx/store"; import { catchError, debounceTime, distinctUntilChanged, filter, map, mapTo, shareReplay, startWith, switchMap, switchMapTo, take, withLatestFrom } from "rxjs/operators"; import { encodeCustomState, decodeCustomState, verifyCustomState } from "./util"; -import { BehaviorSubject, combineLatest, concat, from, merge, Observable, of, timer } from 'rxjs' +import { BehaviorSubject, combineLatest, concat, forkJoin, from, merge, Observable, of, timer } from 'rxjs' import { scan } from 'rxjs/operators' import { RouteStateTransformSvc } from "./routeStateTransform.service"; import { SAPI } from "src/atlasComponents/sapi"; @@ -133,31 +133,33 @@ export class RouterService { switchMap(() => navEnd$), map(navEv => navEv.urlAfterRedirects), switchMap(url => - routeToStateTransformSvc.cvtRouteToState( - router.parseUrl( - url - ) - ).then(stateFromRoute => { - return { - url, - stateFromRoute - } - }) + forkJoin([ + routeToStateTransformSvc.cvtRouteToState( + router.parseUrl( + url + ) + ).then(stateFromRoute => { + return { + url, + stateFromRoute + } + }), + store$.pipe( + switchMap(state => + from(routeToStateTransformSvc.cvtStateToRoute(state)).pipe( + catchError(() => of(``)) + ) + ) + ), + ]), ), withLatestFrom( - store$.pipe( - switchMap(state => - from(routeToStateTransformSvc.cvtStateToRoute(state)).pipe( - catchError(() => of(``)) - ) - ) - ), this.customRoute$.pipe( startWith({}) ) ) ).subscribe(arg => { - const [{ stateFromRoute, url }, _routeFromState, customRoutes] = arg + const [[{ stateFromRoute, url }, _routeFromState ], customRoutes] = arg const fullPath = url let routeFromState = _routeFromState for (const key in customRoutes) { diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 96cdcfe3911b20d5c08f963a3d338dda26d834f6..8d2e4006fcb1d99787f8a9517c6751d6d3980209 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -31,6 +31,7 @@ import { LeapModule } from "./leap/module"; import { environment } from "src/environments/environment" import { ATPSelectorModule } from "src/atlasComponents/sapiViews/core/rich/ATPSelector"; import { FeatureModule } from "src/features"; +import { NgLayerCtlModule } from "./nehuba/ngLayerCtlModule/module"; @NgModule({ imports: [ @@ -54,6 +55,7 @@ import { FeatureModule } from "src/features"; ShareModule, ATPSelectorModule, FeatureModule, + NgLayerCtlModule, ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : []) ], declarations: [ diff --git a/src/viewerModule/nehuba/constants.ts b/src/viewerModule/nehuba/constants.ts index 03e242246e76227e2e0d0687afa6019b611e298f..667c5b7147246a276e3a02f59cd436851db83932 100644 --- a/src/viewerModule/nehuba/constants.ts +++ b/src/viewerModule/nehuba/constants.ts @@ -9,52 +9,6 @@ export interface IRegion { rgb?: [number, number, number] } -export function getMultiNgIdsRegionsLabelIndexMap(parcellation: any = {}, inheritAttrsOpt: any = { ngId: 'root' }): Map<string, Map<number, IRegion>> { - const map: Map<string, Map<number, any>> = new Map() - - const inheritAttrs = Object.keys(inheritAttrsOpt) - if (inheritAttrs.indexOf('children') >=0 ) throw new Error(`children attr cannot be inherited`) - - const processRegion = (region: any) => { - const { ngId: rNgId } = region - const labelIndex = Number(region.labelIndex) - if (labelIndex && rNgId) { - const existingMap = map.get(rNgId) - if (!existingMap) { - const newMap = new Map() - newMap.set(labelIndex, region) - map.set(rNgId, newMap) - } else { - existingMap.set(labelIndex, region) - } - } - - if (region.children && Array.isArray(region.children)) { - for (const r of region.children) { - const copiedRegion = { ...r } - for (const attr of inheritAttrs){ - copiedRegion[attr] = copiedRegion[attr] || region[attr] || parcellation[attr] - } - processRegion(copiedRegion) - } - } - } - - if (!parcellation) throw new Error(`parcellation needs to be defined`) - if (!parcellation.regions) throw new Error(`parcellation.regions needs to be defined`) - if (!Array.isArray(parcellation.regions)) throw new Error(`parcellation.regions needs to be an array`) - - for (const region of parcellation.regions){ - const copiedregion = { ...region } - for (const attr of inheritAttrs){ - copiedregion[attr] = copiedregion[attr] || parcellation[attr] - } - processRegion(copiedregion) - } - - return map -} - export interface IMeshesToLoad { labelIndicies: number[] layer: { @@ -62,6 +16,22 @@ export interface IMeshesToLoad { } } +export type TVec4 = number[] +export type TVec3 = number[] + +export interface INavObj { + position: TVec3 + orientation: TVec4 + perspectiveOrientation: TVec4 + perspectiveZoom: number + zoom: number +} + +export type TNehubaViewerUnit = { + viewerPositionChange: Observable<INavObj> + setNavigationState(nav: Partial<INavObj> & { positionReal?: boolean }): void +} + export const SET_MESHES_TO_LOAD = new InjectionToken<Observable<IMeshesToLoad>>('SET_MESHES_TO_LOAD') export const PMAP_LAYER_NAME = 'regional-pmap' diff --git a/src/viewerModule/nehuba/index.ts b/src/viewerModule/nehuba/index.ts index 65cb900d6d7293034929898b2515f02082d04b37..7ea67235487ac5d9209ca9bceccc27e9517807e7 100644 --- a/src/viewerModule/nehuba/index.ts +++ b/src/viewerModule/nehuba/index.ts @@ -2,3 +2,4 @@ export { NehubaGlueCmp } from "./nehubaViewerGlue/nehubaViewerGlue.component" export { NehubaViewerTouchDirective } from "./nehubaViewerInterface/nehubaViewerTouch.directive" export { NehubaModule } from "./module" export { NehubaViewerUnit } from "./nehubaViewer/nehubaViewer.component" +export { NEHUBA_INSTANCE_INJTKN } from "./util" diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts index 20288e1d0ad406f02646aceba89e26ae530ac226..5c5a85fd3a6b143a805fbf2cf3017a089de3eeed 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts @@ -3,12 +3,8 @@ import { createEffect } from "@ngrx/effects"; import { select, Store } from "@ngrx/store"; import { forkJoin, from, of } from "rxjs"; import { switchMap, withLatestFrom, filter, catchError, map, debounceTime, shareReplay, distinctUntilChanged, startWith, pairwise, tap } from "rxjs/operators"; -import { Feature, NgSegLayerSpec, SxplrAtlas, SxplrParcellation, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; +import { Feature, NgSegLayerSpec, SxplrAtlas, SxplrParcellation, SxplrTemplate, VoiFeature } from "src/atlasComponents/sapi/sxplrTypes"; import { SAPI } from "src/atlasComponents/sapi" -import { - SapiFeatureModel, - SapiSpatialFeatureModel, -} from "src/atlasComponents/sapi/typeV3"; import { atlasAppearance, atlasSelection, userInteraction } from "src/state"; import { arrayEqual } from "src/util/array"; import { EnumColorMapName } from "src/util/colorMaps"; @@ -19,16 +15,13 @@ import { getParcNgId } from "../config.service"; @Injectable() export class LayerCtrlEffects { - static TransformVolumeModel(volumeModel: SapiSpatialFeatureModel['volume']): atlasAppearance.const.NgLayerCustomLayer[] { - /** - * TODO implement - */ - throw new Error(`IMPLEMENT ME`) - // for (const volumeFormat in volumeModel.providedVolumes) { - - // } - - return [] + static TransformVolumeModel(volumeModel: VoiFeature['ngVolume']): atlasAppearance.const.NgLayerCustomLayer[] { + return [{ + clType: "customlayer/nglayer", + id: volumeModel.url, + source: `precomputed://${volumeModel.url}`, + transform: volumeModel.transform, + }] } #onATP$ = this.store.pipe( @@ -101,16 +94,21 @@ export class LayerCtrlEffects { map(([ prev, curr ]) => { const removeLayers: atlasAppearance.const.NgLayerCustomLayer[] = [] const addLayers: atlasAppearance.const.NgLayerCustomLayer[] = [] - if (prev?.["@type"]?.includes("feature/volume_of_interest")) { - const prevVoi = prev as SapiSpatialFeatureModel + + /** + * TODO: use proper guard functions + */ + if (!!prev?.['bbox']) { + const prevVoi = prev as VoiFeature + prevVoi.bbox removeLayers.push( - ...LayerCtrlEffects.TransformVolumeModel(prevVoi.volume) + ...LayerCtrlEffects.TransformVolumeModel(prevVoi.ngVolume) ) } - if (curr?.["@type"]?.includes("feature/volume_of_interest")) { - const currVoi = curr as SapiSpatialFeatureModel + if (!!curr?.['bbox']) { + const currVoi = curr as VoiFeature addLayers.push( - ...LayerCtrlEffects.TransformVolumeModel(currVoi.volume) + ...LayerCtrlEffects.TransformVolumeModel(currVoi.ngVolume) ) } return { removeLayers, addLayers } diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index b1c82b156dfa5888c6b828b944641c66bde8d7b6..971f599b69ece635aab52fc9290c3ae4b954703d 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -378,14 +378,11 @@ export class NehubaLayerControlService implements OnDestroy{ this.customLayers$.pipe( map(cl => { const otherColormapExist = cl.filter(l => l.clType === "customlayer/colormap").length > 0 - const pmapExist = cl.filter(l => l.clType === "customlayer/nglayer").length > 0 - return pmapExist && !otherColormapExist + const otherLayerNames = cl.filter(l => l.clType === "customlayer/nglayer").map(l => l.id) + return otherColormapExist + ? [] + : otherLayerNames }), - distinctUntilChanged(), - map(flag => flag - ? [ PMAP_LAYER_NAME ] - : [] - ) ) ]).pipe( map(([ expectedLayerNames, customLayerNames, pmapName ]) => [...expectedLayerNames, ...customLayerNames, ...pmapName, ...AnnotationLayer.Map.keys()]) diff --git a/src/viewerModule/nehuba/module.ts b/src/viewerModule/nehuba/module.ts index e3cc3aeb2bdead46e359b35d21ad773849411301..8243707dae27438a851e0ccd38cf39fa428519ff 100644 --- a/src/viewerModule/nehuba/module.ts +++ b/src/viewerModule/nehuba/module.ts @@ -21,7 +21,6 @@ import { StateModule } from "src/state"; import { AuthModule } from "src/auth"; import {QuickTourModule} from "src/ui/quickTour/module"; import { WindowResizeModule } from "src/util/windowResize"; -import { NgLayerCtrlCmp } from "./ngLayerCtl/ngLayerCtrl.component"; import { EffectsModule } from "@ngrx/effects"; import { MeshEffects } from "./mesh.effects/mesh.effects"; import { NehubaLayoutOverlayModule } from "./layoutOverlay"; @@ -66,7 +65,6 @@ import { NehubaUserLayerModule } from "./userLayers"; NehubaViewerTouchDirective, NehubaGlueCmp, StatusCardComponent, - NgLayerCtrlCmp, NehubaViewerContainer, ], exports: [ @@ -74,7 +72,6 @@ import { NehubaUserLayerModule } from "./userLayers"; NehubaViewerTouchDirective, NehubaGlueCmp, StatusCardComponent, - NgLayerCtrlCmp, ], providers: [ @@ -94,9 +91,6 @@ import { NehubaUserLayerModule } from "./userLayers"; deps: [ NgAnnotationService ] }, NgAnnotationService - ], - schemas: [ - CUSTOM_ELEMENTS_SCHEMA ] }) diff --git a/src/viewerModule/nehuba/navigation.service/index.ts b/src/viewerModule/nehuba/navigation.service/index.ts index ac47740ee69a641d7ef23ed05e27eeb80a945824..8b137891791fe96927ad78e64b0aad7bded08bdc 100644 --- a/src/viewerModule/nehuba/navigation.service/index.ts +++ b/src/viewerModule/nehuba/navigation.service/index.ts @@ -1,7 +1 @@ -export { - NehubaNavigationService -} from './navigation.service' -export { - INavObj -} from './navigation.util' diff --git a/src/viewerModule/nehuba/navigation.service/navigation.base.service.ts b/src/viewerModule/nehuba/navigation.service/navigation.base.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd82454761ad41d4ccb18f8a37ea3abbabe3c6f5 --- /dev/null +++ b/src/viewerModule/nehuba/navigation.service/navigation.base.service.ts @@ -0,0 +1,46 @@ +import { Inject, Injectable, Optional } from "@angular/core"; +import { concat, EMPTY, NEVER, Observable, of } from "rxjs"; +import { delay, exhaustMap, shareReplay, switchMap, take, tap } from "rxjs/operators"; +import { TNehubaViewerUnit } from "../constants"; +import { NEHUBA_INSTANCE_INJTKN } from "../util"; + +@Injectable({ + providedIn: 'root' +}) +export class NavigationBaseSvc{ + + public nehubaViewerUnit$ = this.nehubaInst$ + ? this.nehubaInst$.pipe( + switchMap(val => val ? of(val): EMPTY) + ) + : NEVER + + public viewerNavLock$: Observable<boolean> = this.nehubaViewerUnit$.pipe( + switchMap(nvUnit => + nvUnit.viewerPositionChange.pipe( + exhaustMap(() => concat( + of(true), + concat( + /** + * in the event that viewerPositionChange only emits once (such is the case on init) + */ + of(false), + nvUnit.viewerPositionChange, + ).pipe( + switchMap(() => + of(false).pipe( + delay(160) + ) + ), + take(1) + ), + )) + ) + ), + shareReplay(1), + ) + constructor( + @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaInst$: Observable<TNehubaViewerUnit>, + ){ + } +} diff --git a/src/viewerModule/nehuba/navigation.service/navigation.effects.ts b/src/viewerModule/nehuba/navigation.service/navigation.effects.ts index fd615fb7e25ba2141e534a71b836b2d923ff9753..a303c4526490b224cc80c5215b06a1fdaa27ea08 100644 --- a/src/viewerModule/nehuba/navigation.service/navigation.effects.ts +++ b/src/viewerModule/nehuba/navigation.service/navigation.effects.ts @@ -1,21 +1,18 @@ -import { Inject, Injectable, OnDestroy } from "@angular/core"; +import { Injectable, OnDestroy } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; import { select, Store } from "@ngrx/store"; -import { combineLatest, Observable, Subscription } from "rxjs"; -import { filter, map, mapTo, tap, withLatestFrom } from "rxjs/operators"; +import { combineLatest, NEVER, of, Subscription } from "rxjs"; +import { debounce, distinctUntilChanged, filter, map, mapTo, skipWhile, switchMap, tap, withLatestFrom } from "rxjs/operators"; import { atlasSelection, MainState, userInterface, userPreference } from "src/state" import { CYCLE_PANEL_MESSAGE } from "src/util/constants"; import { timedValues } from "src/util/generator"; -import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; -import { NEHUBA_INSTANCE_INJTKN } from "../util"; -import { navAdd, navMul } from "./navigation.util"; +import { NavigationBaseSvc } from "./navigation.base.service"; +import { navAdd, navMul, navObjEqual } from "./navigation.util"; @Injectable() export class NehubaNavigationEffects implements OnDestroy{ private subscription: Subscription[] = [] - private nehubaInst: NehubaViewerUnit - private rafRef: number /** * This is an implementation which reconciles local state with the global navigation state. @@ -27,64 +24,56 @@ export class NehubaNavigationEffects implements OnDestroy{ * and update global state accordingly. * - This effect updates the internal navigation state. It should leave reporting any diff to the local viewer's native implementation. */ - onNavigateTo = createEffect(() => this.action.pipe( - ofType(atlasSelection.actions.navigateTo), - filter(() => !!this.nehubaInst), - withLatestFrom( - this.store.pipe( - select(userPreference.selectors.useAnimation) + onNavigateTo = createEffect(() => this.baseSvc.nehubaViewerUnit$.pipe( + switchMap(nehubaInst => this.action.pipe( + ofType(atlasSelection.actions.navigateTo), + withLatestFrom( + this.store.pipe( + select(userPreference.selectors.useAnimation) + ), + this.store.pipe( + select(atlasSelection.selectors.navigation) + ) ), - this.store.pipe( - select(atlasSelection.selectors.navigation) - ) - ), - tap(([{ navigation, animation, physical }, globalAnimationFlag, currentNavigation]) => { - if (!animation || !globalAnimationFlag) { - this.nehubaInst.setNavigationState({ - ...navigation, - positionReal: physical - }) - return - } - - const gen = timedValues() - const src = currentNavigation - - const dest = { - ...src, - ...navigation - } - - const delta = navAdd(dest, navMul(src, -1)) - - const animate = () => { - - /** - * if nehubaInst becomes nullish whilst animation is running - */ - if (!this.nehubaInst) { - this.rafRef = null + tap(([{ navigation, animation, physical }, globalAnimationFlag, currentNavigation]) => { + if (!animation || !globalAnimationFlag) { + nehubaInst.setNavigationState({ + ...navigation, + positionReal: physical + }) return } - - const next = gen.next() - const d = next.value - - const n = navAdd(src, navMul(delta, d)) - this.nehubaInst.setNavigationState({ - ...n, - positionReal: physical - }) - - if ( !next.done ) { - this.rafRef = requestAnimationFrame(() => animate()) - } else { - this.rafRef = null + + const gen = timedValues() + const src = currentNavigation + + const dest = { + ...src, + ...navigation } - } - this.rafRef = requestAnimationFrame(() => animate()) - - }) + + const delta = navAdd(dest, navMul(src, -1)) + + const animate = () => { + + + const next = gen.next() + const d = next.value + + const n = navAdd(src, navMul(delta, d)) + nehubaInst.setNavigationState({ + ...n, + positionReal: physical + }) + + if ( !next.done ) { + requestAnimationFrame(() => animate()) + } + } + requestAnimationFrame(() => animate()) + + }) + )), ), { dispatch: false }) onMaximise = createEffect(() => combineLatest([ @@ -104,14 +93,66 @@ export class NehubaNavigationEffects implements OnDestroy{ ) )) + onStoreNavigationUpdate = createEffect(() => this.store.pipe( + select(atlasSelection.selectors.navigation), + distinctUntilChanged((o, n) => navObjEqual(o, n)), + withLatestFrom( + this.baseSvc.viewerNavLock$, + /** + * n.b. if NEHUBA_INSTANCE_INJTKN is not provided, this obs will never emit + * which, semantically is the correct behaviour + */ + this.baseSvc.nehubaViewerUnit$, + this.baseSvc.nehubaViewerUnit$.pipe( + switchMap(nvUnit => nvUnit.viewerPositionChange) + ) + ), + skipWhile(([nav, lock, _nvUnit, viewerNav]) => lock || navObjEqual(nav, viewerNav)), + tap(([nav, _lock, nvUnit, _viewerNav]) => { + nvUnit.setNavigationState(nav) + }) + ), { dispatch: false }) + + onViewerNavigationUpdate = createEffect(() => this.baseSvc.nehubaViewerUnit$.pipe( + switchMap(nvUnit => + nvUnit.viewerPositionChange.pipe( + debounce(() => this.baseSvc.viewerNavLock$.pipe( + filter(lock => !lock), + )), + withLatestFrom( + this.store.pipe( + select(atlasSelection.selectors.navigation) + ) + ), + switchMap(([ val, storedNav ]) => { + const { zoom, perspectiveZoom, position } = val + const roundedZoom = Math.round(zoom) + const roundedPz = Math.round(perspectiveZoom) + const roundedPosition = position.map(v => Math.round(v)) as [number, number, number] + const roundedNav = { + ...val, + zoom: roundedZoom, + perspectiveZoom: roundedPz, + position: roundedPosition, + } + if (navObjEqual(roundedNav, storedNav)) { + return NEVER + } + return of( + atlasSelection.actions.setNavigation({ + navigation:roundedNav + }) + ) + }) + ) + ) + )) + constructor( private action: Actions, private store: Store<MainState>, - @Inject(NEHUBA_INSTANCE_INJTKN) nehubaInst$: Observable<NehubaViewerUnit>, + private baseSvc: NavigationBaseSvc, ){ - this.subscription.push( - nehubaInst$.subscribe(val => this.nehubaInst = val), - ) } ngOnDestroy(): void { diff --git a/src/viewerModule/nehuba/navigation.service/navigation.service.spec.ts b/src/viewerModule/nehuba/navigation.service/navigation.service.spec.ts deleted file mode 100644 index 9180048b03a28473737f81fa49eb4d1800fcb87b..0000000000000000000000000000000000000000 --- a/src/viewerModule/nehuba/navigation.service/navigation.service.spec.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing' -import { MockStore, provideMockStore } from '@ngrx/store/testing' -import { BehaviorSubject, of, Subject } from 'rxjs' -import * as NavUtil from './navigation.util' -import { NehubaViewerUnit } from '../nehubaViewer/nehubaViewer.component' -import { NEHUBA_INSTANCE_INJTKN } from '../util' -import { NehubaNavigationService } from './navigation.service' -import { userPreference, atlasSelection } from "src/state" - -const nav1 = { - position: [1,2,3], - orientation: [0, 0, 0, 1], - perspectiveOrientation: [1, 0, 0, 0], - perspectiveZoom: 100, - zoom: -12 -} - -const nav1x2 = { - position: [2,4,6], - orientation: [0, 0, 0, 2], - perspectiveOrientation: [2, 0, 0, 0], - perspectiveZoom: 200, - zoom: -24 -} - -const nav2 = { - position: [5, 1, -3], - orientation: [0, 0, 1, 0], - perspectiveOrientation: [-3, 0, 0, 0], - perspectiveZoom: 150, - zoom: -60 -} - -const nav1p2 = { - position: [6, 3, 0], - orientation: [0, 0, 1, 1], - perspectiveOrientation: [-2, 0, 0, 0], - perspectiveZoom: 250, - zoom: -72 -} -describe('> navigation.service.ts', () => { - - describe('> NehubaNavigationService', () => { - let nehubaInst$: BehaviorSubject<NehubaViewerUnit> - let nehubaInst: Partial<NehubaViewerUnit> - let service: NehubaNavigationService - beforeEach(() => { - nehubaInst$ = new BehaviorSubject(null) - TestBed.configureTestingModule({ - imports: [ - - ], - providers: [ - provideMockStore(), - { - provide: NEHUBA_INSTANCE_INJTKN, - useValue: nehubaInst$ - }, - NehubaNavigationService - ] - }) - - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector( - atlasSelection.selectors.navigation, - nav1 - ) - mockStore.overrideSelector( - userPreference.selectors.useAnimation, - true - ) - }) - - it('> on new emit null on nehubaInst, clearViewSub is called, but setupviewersub is not called', () => { - - service = TestBed.inject(NehubaNavigationService) - const clearviewSpy = spyOn(service, 'clearViewerSub').and.callThrough() - const setupViewSpy = spyOn(service, 'setupViewerSub').and.callThrough() - nehubaInst$.next(null) - expect(clearviewSpy).toHaveBeenCalled() - expect(setupViewSpy).not.toHaveBeenCalled() - }) - - it('> on new emit with viewer, clear view sub and setupviewers are both called', () => { - - service = TestBed.inject(NehubaNavigationService) - const clearviewSpy = spyOn(service, 'clearViewerSub').and.callThrough() - const setupViewSpy = spyOn(service, 'setupViewerSub').and.callThrough() - nehubaInst = { - viewerPositionChange: of(nav1) as any, - setNavigationState: jasmine.createSpy() - } - nehubaInst$.next(nehubaInst as NehubaViewerUnit) - expect(clearviewSpy).toHaveBeenCalled() - expect(setupViewSpy).toHaveBeenCalled() - }) - - describe('> #setupViewerSub', () => { - let dispatchSpy: jasmine.Spy - beforeEach(() => { - nehubaInst = { - viewerPositionChange: new Subject() as any, - setNavigationState: jasmine.createSpy(), - } - - service = TestBed.inject(NehubaNavigationService) - service['nehubaViewerInstance'] = nehubaInst as NehubaViewerUnit - - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(atlasSelection.selectors.navigation, nav1) - dispatchSpy = spyOn(mockStore, 'dispatch').and.callFake(() => {}) - }) - - describe('> on viewerPosition change multiple times', () => { - beforeEach(() => { - service.setupViewerSub() - }) - it('> viewerNav set to last value', fakeAsync(() => { - - nehubaInst.viewerPositionChange.next(nav2) - nehubaInst.viewerPositionChange.next(nav1x2) - expect( - service.viewerNav - ).toEqual(nav1x2 as any) - discardPeriodicTasks() - })) - - it('> dispatch does not get called immediately', fakeAsync(() => { - - nehubaInst.viewerPositionChange.next(nav2) - nehubaInst.viewerPositionChange.next(nav1x2) - expect(dispatchSpy).not.toHaveBeenCalled() - discardPeriodicTasks() - })) - - it('> dispatch called after 160 debounce', fakeAsync(() => { - - // next/'ing cannot be done in beforeEach - // or this test will fail - nehubaInst.viewerPositionChange.next(nav2) - nehubaInst.viewerPositionChange.next(nav1x2) - tick(160) - expect(dispatchSpy).toHaveBeenCalled() - })) - }) - }) - - describe('> on storeNavigation update', () => { - let navEqlSpy: jasmine.Spy - beforeEach(() => { - nehubaInst = { - setNavigationState: jasmine.createSpy(), - viewerPositionChange: new Subject() as any, - } - nehubaInst$.next(nehubaInst as NehubaViewerUnit) - navEqlSpy = spyOnProperty(NavUtil, 'navObjEqual') - }) - it('> if navEq returnt true, do not setNav', () => { - navEqlSpy.and.returnValue(() => true) - service = TestBed.inject(NehubaNavigationService) - expect(nehubaInst.setNavigationState).not.toHaveBeenCalled() - }) - it('> if navEq return false, call setNav', () => { - navEqlSpy.and.returnValue(() => false) - service = TestBed.inject(NehubaNavigationService) - expect(nehubaInst.setNavigationState).toHaveBeenCalled() - }) - }) - }) -}) diff --git a/src/viewerModule/nehuba/navigation.service/navigation.service.ts b/src/viewerModule/nehuba/navigation.service/navigation.service.ts deleted file mode 100644 index e06ddd27fe488f686aec8dcd9c803491df1fad60..0000000000000000000000000000000000000000 --- a/src/viewerModule/nehuba/navigation.service/navigation.service.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Inject, Injectable, OnDestroy, Optional } from "@angular/core"; -import { select, Store } from "@ngrx/store"; -import { Observable, ReplaySubject, Subscription } from "rxjs"; -import { debounceTime } from "rxjs/operators"; -import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; -import { NEHUBA_INSTANCE_INJTKN } from "../util"; -import { INavObj, navObjEqual } from './navigation.util' -import { actions } from "src/state/atlasSelection"; -import { atlasSelection, userPreference } from "src/state"; - -@Injectable() -export class NehubaNavigationService implements OnDestroy{ - - private subscriptions: Subscription[] = [] - private viewerInstanceSubscriptions: Subscription[] = [] - - private nehubaViewerInstance: NehubaViewerUnit - public storeNav: INavObj - public viewerNav: INavObj - public viewerNav$ = new ReplaySubject<INavObj>(1) - - // if set, ignores store attempt to update nav - private viewerNavLock: boolean = false - - private globalAnimationFlag = true - private rafRef: number - - constructor( - private store$: Store<any>, - @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaInst$: Observable<NehubaViewerUnit>, - ){ - this.subscriptions.push( - this.store$.pipe( - select(userPreference.selectors.useAnimation) - ).subscribe(flag => this.globalAnimationFlag = flag) - ) - - if (nehubaInst$) { - this.subscriptions.push( - nehubaInst$.subscribe(val => { - this.clearViewerSub() - this.nehubaViewerInstance = val - if (this.nehubaViewerInstance) { - this.setupViewerSub() - } - }) - ) - } - - this.subscriptions.push( - // realtime state nav state - this.store$.pipe( - select(atlasSelection.selectors.navigation) - ).subscribe(v => { - this.storeNav = v - // if stored nav differs from viewerNav - if (!this.viewerNavLock && this.nehubaViewerInstance) { - const navEql = navObjEqual(this.storeNav, this.viewerNav) - if (!navEql) { - this.navigateViewer(this.storeNav) - } - } - }) - ) - } - - navigateViewer(navigation: INavObj): void { - if (!navigation) return - // TODO - // readd consider how to do animation - this.nehubaViewerInstance.setNavigationState(navigation) - } - - setupViewerSub(): void { - this.viewerInstanceSubscriptions.push( - // realtime viewer nav state - this.nehubaViewerInstance.viewerPositionChange.subscribe( - (val: INavObj) => { - this.viewerNav = val - this.viewerNav$.next(val) - this.viewerNavLock = true - } - ), - // debounced viewer nav state - this.nehubaViewerInstance.viewerPositionChange.pipe( - debounceTime(160) - ).subscribe((val: INavObj) => { - this.viewerNavLock = false - - const { zoom, perspectiveZoom, position } = val - const roundedZoom = Math.round(zoom) - const roundedPz = Math.round(perspectiveZoom) - const roundedPosition = position.map(v => Math.round(v)) as [number, number, number] - const roundedNav = { - ...val, - zoom: roundedZoom, - perspectiveZoom: roundedPz, - position: roundedPosition, - } - const navEql = navObjEqual(roundedNav, this.storeNav) - - if (!navEql) { - this.store$.dispatch( - actions.setNavigation({ - navigation: roundedNav - }) - ) - } - }) - ) - } - - clearViewerSub(): void { - while (this.viewerInstanceSubscriptions.length > 0) this.viewerInstanceSubscriptions.pop().unsubscribe() - } - - ngOnDestroy(): void { - this.clearViewerSub() - while (this.subscriptions.length > 0) this.subscriptions.pop().unsubscribe() - } -} diff --git a/src/viewerModule/nehuba/navigation.service/navigation.util.ts b/src/viewerModule/nehuba/navigation.service/navigation.util.ts index 9ae2df4760f4216b0e9f98e4958750882b791c21..835e6ed94d852d7120019e3cc3a190accf38b81c 100644 --- a/src/viewerModule/nehuba/navigation.service/navigation.util.ts +++ b/src/viewerModule/nehuba/navigation.service/navigation.util.ts @@ -1,13 +1,6 @@ -import { TVec3, TVec4 } from "src/messaging/types"; +import { TVec3, TVec4, INavObj } from "../constants"; import { arrayOfPrimitiveEqual } from "src/util/fn"; -export interface INavObj { - position: TVec3 - orientation: TVec4 - perspectiveOrientation: TVec4 - perspectiveZoom: number - zoom: number -} export function navMul(nav: INavObj, scalar: number): INavObj { return { diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 53c0afb97bb0933a327c68ff675d773cc4f008c2..746a3363a857b01a3f5bd0f06def5c6f9145c834 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -697,7 +697,7 @@ export class NehubaViewerUnit implements OnDestroy { private setLayerTransparency(layerName: string, alpha: number) { const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName) - if (!layer) return + if (!(layer?.layer)) return /** * for segmentation layer diff --git a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.spec.ts b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.spec.ts index 9b7a7b09d88e0ad801477b90e13f1c4a5f6e60bb..19314d6737ebfd0c3229b41f3fb62055d9b0bcb6 100644 --- a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.spec.ts @@ -6,7 +6,6 @@ import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component" import { NehubaViewerContainerDirective } from "./nehubaViewerInterface.directive" import { NEVER, of, pipe, Subject } from "rxjs" import { userPreference, atlasSelection, atlasAppearance } from "src/state" -import { NehubaNavigationService } from "../navigation.service" import { LayerCtrlEffects } from "../layerCtrl.service/layerCtrl.effects" import { mapTo } from "rxjs/operators" @@ -31,13 +30,6 @@ describe('> nehubaViewerInterface.directive.ts', () => { ], providers: [ provideMockStore(), - { - provide: NehubaNavigationService, - useValue: { - viewerNav$: NEVER, - storeNav: null - } - }, { provide: LayerCtrlEffects, useValue: { diff --git a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts index 6454ea378c353eab8487b686f2009759eb95e819..aee39e6f48641ebdb9b5b8051f23c6c023a91545 100644 --- a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts +++ b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts @@ -6,13 +6,14 @@ import { distinctUntilChanged, filter, debounceTime, scan, map, throttleTime, sw import { serializeSegment } from "../util"; import { LoggingService } from "src/logging"; import { arrayOfPrimitiveEqual } from 'src/util/fn' -import { INavObj, NehubaNavigationService } from "../navigation.service"; +import { INavObj } from "../constants" import { NehubaConfig, defaultNehubaConfig, getNehubaConfig } from "../config.service"; import { atlasAppearance, atlasSelection, userPreference } from "src/state"; import { SxplrAtlas, SxplrParcellation, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; import { arrayEqual } from "src/util/array"; import { cvtNavigationObjToNehubaConfig } from "../config.service/util"; import { LayerCtrlEffects } from "../layerCtrl.service/layerCtrl.effects"; +import { NavigationBaseSvc } from "../navigation.service/navigation.base.service"; const determineProtocol = (url: string) => { @@ -134,7 +135,6 @@ const accumulatorFn: ( @Directive({ selector: '[iav-nehuba-viewer-container]', exportAs: 'iavNehubaViewerContainer', - providers: [ NehubaNavigationService ] }) export class NehubaViewerContainerDirective implements OnDestroy{ @@ -150,19 +150,16 @@ export class NehubaViewerContainerDirective implements OnDestroy{ @Output() public iavNehubaViewerContainerViewerLoading: EventEmitter<boolean> = new EventEmitter() - private componentFactory: ComponentFactory<NehubaViewerUnit> private cr: ComponentRef<NehubaViewerUnit> private navigation: atlasSelection.AtlasSelectionState['navigation'] constructor( private el: ViewContainerRef, private store$: Store<any>, - private navService: NehubaNavigationService, + private navBaseSvc: NavigationBaseSvc, private effect: LayerCtrlEffects, private cdr: ChangeDetectorRef, - cfr: ComponentFactoryResolver, @Optional() private log: LoggingService, ){ - this.componentFactory = cfr.resolveComponentFactory(NehubaViewerUnit) this.cdr.detach() this.subscriptions.push( @@ -247,9 +244,9 @@ export class NehubaViewerContainerDirective implements OnDestroy{ this.nehubaViewerInstance.applyGpuLimit(limit) } }), - this.navService.viewerNav$.subscribe(v => { - this.navigationEmitter.emit(v) - }), + this.navBaseSvc.nehubaViewerUnit$.pipe( + switchMap(nvUnit => nvUnit.viewerPositionChange) + ).subscribe(v => this.navigationEmitter.emit(v)), this.store$.pipe( select(atlasSelection.selectors.navigation) ).subscribe(nav => this.navigation = nav) @@ -295,14 +292,8 @@ export class NehubaViewerContainerDirective implements OnDestroy{ await new Promise(rs => setTimeout(rs, 0)) this.iavNehubaViewerContainerViewerLoading.emit(true) - this.cr = this.el.createComponent(this.componentFactory) + this.cr = this.el.createComponent(NehubaViewerUnit) - if (this.navService.storeNav) { - this.nehubaViewerInstance.initNav = { - ...this.navService.storeNav, - positionReal: true - } - } if (this.gpuLimit) { const initialNgState = nehubaConfig && nehubaConfig.dataset && nehubaConfig.dataset.initialNgState diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.style.css b/src/viewerModule/nehuba/ngLayerCtlModule/index.ts similarity index 100% rename from src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.style.css rename to src/viewerModule/nehuba/ngLayerCtlModule/index.ts diff --git a/src/viewerModule/nehuba/ngLayerCtlModule/module.ts b/src/viewerModule/nehuba/ngLayerCtlModule/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbdf56167ba1073daaa8743fb034ae6f0d2db7b4 --- /dev/null +++ b/src/viewerModule/nehuba/ngLayerCtlModule/module.ts @@ -0,0 +1,26 @@ +import { CommonModule } from "@angular/common"; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; +import { MatButtonModule } from "@angular/material/button"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { NgLayerCtrlCmp } from "./ngLayerCtl/ngLayerCtrl.component"; + +@NgModule({ + imports: [ + CommonModule, + MatTooltipModule, + MatButtonModule, + + ], + declarations: [ + NgLayerCtrlCmp + ], + exports: [ + NgLayerCtrlCmp + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] +}) +export class NgLayerCtlModule{ + +} \ No newline at end of file diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts b/src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.component.ts similarity index 81% rename from src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts rename to src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.component.ts index cce89a171435f72d6c17f112548f92477ad47dc1..c39f0955038506f38623f7eafb07e09f578189f9 100644 --- a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts +++ b/src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.component.ts @@ -4,8 +4,7 @@ import { isMat4 } from "common/util" import { CONST } from "common/constants" import { Observable } from "rxjs"; import { atlasAppearance, atlasSelection } from "src/state"; -import { NehubaViewerUnit } from ".."; -import { NEHUBA_INSTANCE_INJTKN } from "../util"; +import { NehubaViewerUnit, NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba"; import { getExportNehuba } from "src/util/fn"; type Vec4 = [number, number, number, number] @@ -60,7 +59,7 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{ transform: Mat4 = idMat4 @Input('ng-layer-ctl-transform') - set _transform(xform: string | Mat4) { + set _transform(xform: string | Mat4 | number[][]) { const parsedResult = typeof xform === "string" ? JSON.parse(xform) : xform @@ -70,6 +69,9 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{ this.transform = parsedResult as Mat4 } + @Input('ng-layer-ctl-info') + info: Record<string, any> + visible: boolean = true private viewer: NehubaViewerUnit @@ -84,7 +86,10 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{ () => sub.unsubscribe() ) - getExportNehuba().then(exportNehuba => this.exportNehuba = exportNehuba) + getExportNehuba().then(exportNehuba => { + this.exportNehuba = exportNehuba + this.setOrientation() + }) } ngOnDestroy(): void { @@ -136,10 +141,23 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{ const scaledM = mat4.scale(mat4.create(), incM, vec3.inverse(vec3.create(), scale)) const q = mat4.getRotation(quat.create(0), scaledM) + let position: number[] + if (this.info) { + const { scales } = this.info + const sizeInNm = [0, 1, 2].map(idx => scales[0].size[idx] * scales[0].resolution[idx]) + const start = vec3.transformMat4(vec3.create(), vec3.fromValues(0, 0, 0), incM) + const end = vec3.transformMat4(vec3.create(), vec3.fromValues(...sizeInNm), incM) + const final = vec3.add(vec3.create(), start, end) + vec3.scale(final, final, 0.5) + position = Array.from(final) + } + + this.store.dispatch( atlasSelection.actions.navigateTo({ navigation: { - orientation: Array.from(q) + orientation: Array.from(q), + position, }, animation: true }) diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.stories.ts b/src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.stories.ts similarity index 95% rename from src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.stories.ts rename to src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.stories.ts index b716f7d93824fd1e223450fde56655398aa91d0b..ff1fb3cd5f97eaf74b17751d28b5bd245707f9ba 100644 --- a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.stories.ts +++ b/src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.stories.ts @@ -2,7 +2,7 @@ import { idMat4, NgLayerCtrlCmp } from "./ngLayerCtrl.component" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { CommonModule } from "@angular/common" import { MatButtonModule } from "@angular/material/button" -import { NEHUBA_INSTANCE_INJTKN } from "../util" +import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util" import { NEVER } from "rxjs" import { action } from "@storybook/addon-actions" import { MatTooltipModule } from "@angular/material/tooltip" diff --git a/src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.style.css b/src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.style.css new file mode 100644 index 0000000000000000000000000000000000000000..07e52b8e10c90b947671b91e17b5347eb894094d --- /dev/null +++ b/src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.style.css @@ -0,0 +1,22 @@ +:host +{ + padding: 0.5rem 0; +} + +.container +{ + display: flex; + width: 100%; + align-items: center; +} + +button +{ + flex: 0 0 auto; +} + +.layer-name +{ + flex: 1 1 0px; + overflow: hidden; +} \ No newline at end of file diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html b/src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.template.html similarity index 67% rename from src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html rename to src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.template.html index b53ec540ad7fd2bcf7df004327d9cc3f2b3ba51b..4f5c45c9fd2baa02a8852a49203636fa52922f84 100644 --- a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html +++ b/src/viewerModule/nehuba/ngLayerCtlModule/ngLayerCtl/ngLayerCtrl.template.html @@ -1,4 +1,4 @@ -<div [ngClass]="{ 'text-muted': !visible }"> +<div class="container" [ngClass]="{ 'text-muted': !visible }"> <button mat-icon-button [matTooltip]="CONST.TOGGLE_LAYER_VISILITY" @@ -6,7 +6,7 @@ <i [ngClass]="visible ? 'fa-eye' : 'fa-eye-slash'" class="far"></i> </button> - <span> + <span class="layer-name"> {{ name }} </span> @@ -24,11 +24,12 @@ <i class="fas fa-cog"></i> </button> - <ng-template [ngIf]="showOpacityCtrl"> - <ng-layer-tune - [ngLayerName]="name" - [hideCtrl]="hideNgTuneCtrl" - [opacity]="defaultOpacity"> - </ng-layer-tune> - </ng-template> </div> + +<ng-template [ngIf]="showOpacityCtrl"> + <ng-layer-tune + [ngLayerName]="name" + [hideCtrl]="hideNgTuneCtrl" + [opacity]="defaultOpacity"> + </ng-layer-tune> +</ng-template> \ No newline at end of file diff --git a/src/viewerModule/nehuba/types.ts b/src/viewerModule/nehuba/types.ts index 88b6095c5c1a4fa7b653a655cd8d21347c04e15d..c7684e637dd9a156c722db3ca806b7f381171bab 100644 --- a/src/viewerModule/nehuba/types.ts +++ b/src/viewerModule/nehuba/types.ts @@ -1,5 +1,5 @@ import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes"; -import { INavObj } from "./navigation.service"; +import { INavObj } from "./constants"; export type TNehubaContextInfo = { nav: INavObj diff --git a/src/viewerModule/viewer.common.effects.ts b/src/viewerModule/viewer.common.effects.ts index cc888f129935050edd49a5238764a672f3d00d66..4aa169bb256ca4f14c8d2cd28a0ba971575f0d44 100644 --- a/src/viewerModule/viewer.common.effects.ts +++ b/src/viewerModule/viewer.common.effects.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { createEffect } from "@ngrx/effects"; import { select, Store } from "@ngrx/store"; -import { forkJoin, of } from "rxjs"; +import { of } from "rxjs"; import { distinctUntilChanged, map, switchMap } from "rxjs/operators"; import * as atlasSelection from "src/state/atlasSelection"; import * as atlasAppearance from "src/state/atlasAppearance" @@ -17,31 +17,7 @@ export class ViewerCommonEffects { : this.store.pipe( select(atlasSelection.selectors.selectedTemplate), distinctUntilChanged((o, n) => o?.id === n?.id), - switchMap(template => { - if (!template) { - return of(null as atlasAppearance.const.UseViewer) - } - return forkJoin({ - voxel: this.sapi.getVoxelTemplateImage(template), - surface: this.sapi.getSurfaceTemplateImage(template) - }).pipe( - map(vols => { - if (!vols) return null - const { voxel, surface } = vols - if (voxel.length > 0 && surface.length > 0) { - console.error(`both voxel and surface length are > 0, this should not happen.`) - return atlasAppearance.const.useViewer.NOT_SUPPORTED - } - if (voxel.length > 0) { - return atlasAppearance.const.useViewer.NEHUBA - } - if (surface.length > 0) { - return atlasAppearance.const.useViewer.THREESURFER - } - return atlasAppearance.const.useViewer.NOT_SUPPORTED - }) - ) - }) + switchMap(template => this.sapi.useViewer(template)) ) ), map(viewer => atlasAppearance.actions.setUseViewer({ viewer })) diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 25c57644e627082617ed60ce3d5262def63be2d8..8961b4f440ae0298602daed8efd50ad01afdeb6d 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -66,7 +66,6 @@ export class ViewerCmp implements OnDestroy { public CONST = CONST public ARIA_LABELS = ARIA_LABELS - public VOI_QUERY_FLAG = environment.EXPERIMENTAL_FEATURE_FLAG @ViewChild('genericInfoVCR', { read: ViewContainerRef }) genericInfoVCR: ViewContainerRef diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 2c7d4b7089a4d6752d667eed3594cafe6c84e567..72c4cfdac0921c2ea38b7079e3e015b48ef2baa7 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -231,11 +231,9 @@ <ng-container *ngTemplateOutlet="autocompleteTmpl; context: { showTour: true }"> </ng-container> - <ng-template [ngIf]="VOI_QUERY_FLAG"> - <div *ngIf="!((selectedRegions$ | async)[0])" class="sxplr-p-1 w-100"> - <ng-container *ngTemplateOutlet="spatialFeatureListTmpl"></ng-container> - </div> - </ng-template> + <div *ngIf="!((selectedRegions$ | async)[0])" class="sxplr-p-1 w-100"> + <ng-container *ngTemplateOutlet="spatialFeatureListTmpl"></ng-container> + </div> </div> <!-- such a gross implementation -->