diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts index 471e628bbff4e137d91d4bd5c3fb2633d25a6dd9..bf9d18fd97d9d438255e03e57a9478f55e0c0fae 100644 --- a/src/atlasComponents/annotations/annotation.service.ts +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -1,5 +1,23 @@ +import { BehaviorSubject, Observable } from "rxjs"; +import { distinctUntilChanged } from "rxjs/operators"; import { getUuid } from "src/util/fn"; +export type TNgAnnotationEv = { + pickedAnnotationId: string + pickedOffset: number +} + +/** + * axis aligned bounding box + */ +export type TNgAnnotationAABBox = { + type: 'aabbox' + pointA: [number, number, number] + pointB: [number, number, number] + id: string + description?: string +} + export type TNgAnnotationLine = { type: 'line' pointA: [number, number, number] @@ -15,7 +33,7 @@ export type TNgAnnotationPoint = { description?: string } -export type AnnotationSpec = TNgAnnotationLine | TNgAnnotationPoint +export type AnnotationSpec = TNgAnnotationLine | TNgAnnotationPoint | TNgAnnotationAABBox type _AnnotationSpec = Omit<AnnotationSpec, 'type'> & { type: number } type AnnotationRef = {} @@ -37,7 +55,21 @@ interface NgAnnotationLayer { } export class AnnotationLayer { + static Map = new Map<string, AnnotationLayer>() + static Get(name: string, color: string){ + if (AnnotationLayer.Map.has(name)) return AnnotationLayer.Map.get(name) + const layer = new AnnotationLayer(name, color) + AnnotationLayer.Map.set(name, layer) + return layer + } + + private _onHover = new BehaviorSubject<{ id: string, offset: number }>(null) + public onHover: Observable<{ id: string, offset: number }> = this._onHover.asObservable().pipe( + distinctUntilChanged((o, n) => o?.id === n?.id) + ) + private onDestroyCb: (() => void)[] = [] private nglayer: NgAnnotationLayer + private idset = new Set<string>() constructor( private name: string = getUuid(), private color="#ffffff" @@ -58,14 +90,32 @@ export class AnnotationLayer { } ) this.nglayer = this.viewer.layerManager.addManagedLayer(layerSpec) + const mouseState = this.viewer.mouseState + const res: () => void = mouseState.changed.add(() => { + const payload = mouseState.active + && !!mouseState.pickedAnnotationId + && this.idset.has(mouseState.pickedAnnotationId) + ? { + id: mouseState.pickedAnnotationId, + offset: mouseState.pickedOffset + } + : null + this._onHover.next(payload) + }) + this.onDestroyCb.push(res) + this.nglayer.layer.registerDisposer(() => { - this.nglayer = null + this.dispose() }) } setVisible(flag: boolean){ this.nglayer.setVisible(flag) } dispose() { + this.nglayer = null + AnnotationLayer.Map.delete(this.name) + this._onHover.complete() + while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() try { this.viewer.layerManager.removeManagedLayer(this.nglayer) } catch (e) { @@ -75,6 +125,7 @@ export class AnnotationLayer { addAnnotation(spec: AnnotationSpec){ const localAnnotations = this.nglayer.layer.localAnnotations + this.idset.add(spec.id) const annSpec = this.parseNgSpecType(spec) localAnnotations.add( annSpec @@ -82,6 +133,7 @@ export class AnnotationLayer { } removeAnnotation(spec: { id: string }) { const { localAnnotations } = this.nglayer.layer + this.idset.delete(spec.id) const ref = localAnnotations.references.get(spec.id) if (ref) { localAnnotations.delete(ref) @@ -98,6 +150,7 @@ export class AnnotationLayer { _spec ) } else { + this.idset.add(_spec.id) localAnnotations.add(_spec) } } @@ -111,6 +164,7 @@ export class AnnotationLayer { let overwritingType = null if (spec.type === 'point') overwritingType = 0 if (spec.type === 'line') overwritingType = 1 + if (spec.type === "aabbox") overwritingType = 2 if (overwritingType === null) throw new Error(`overwrite type lookup failed for ${spec.type}`) return { ...spec, diff --git a/src/atlasComponents/annotations/index.ts b/src/atlasComponents/annotations/index.ts index 651584158e28cbeb2ea72a5f3cbb954e4e402310..da271a6363a9f8e34165466841eba3dda31135f8 100644 --- a/src/atlasComponents/annotations/index.ts +++ b/src/atlasComponents/annotations/index.ts @@ -1 +1 @@ -export { AnnotationLayer, TNgAnnotationPoint } from "./annotation.service" \ No newline at end of file +export { TNgAnnotationAABBox, AnnotationLayer, TNgAnnotationPoint, TNgAnnotationLine } from "./annotation.service" \ No newline at end of file diff --git a/src/atlasComponents/sapi/directives/spatialFeatureBBox.directive.ts b/src/atlasComponents/sapi/directives/spatialFeatureBBox.directive.ts deleted file mode 100644 index bdd15c9b221c01dc699d1959de3eade353c416c9..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapi/directives/spatialFeatureBBox.directive.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { combineLatest, Observable, BehaviorSubject, Subject, Subscription, of, merge } from 'rxjs'; -import { debounceTime, map, distinctUntilChanged, switchMap, tap, startWith, filter } from 'rxjs/operators'; -import { Directive, EventEmitter, Input, OnDestroy, Output } from "@angular/core"; -import { BoundingBoxConcept, SapiSpatialFeatureModel, SapiVOIDataResponse } from '../type' -import { SAPI } from '../sapi.service' -import { environment } from "src/environments/environment" - -function validateBbox(input: any): boolean { - if (!Array.isArray(input)) return false - if (input.length !== 2) return false - return input.every(el => Array.isArray(el) && el.length === 3 && el.every(val => typeof val === "number")) -} - -@Directive({ - selector: '[sii-xp-spatial-feat-bbox]', - exportAs: 'siiXpSpatialFeatBbox' -}) -export class SpatialFeatureBBox implements OnDestroy{ - - static FEATURE_NAME = "VolumeOfInterest" - private EXPERIMENTAL_FEATURE_FLAG = environment.EXPERIMENTAL_FEATURE_FLAG - - private atlasId$ = new Subject<string>() - @Input('sii-xp-spatial-feat-bbox-atlas-id') - set atlasId(val: string) { - this.atlasId$.next(val) - } - - private spaceId$ = new Subject<string>() - @Input('sii-xp-spatial-feat-bbox-space-id') - set spaceId(val: string) { - this.spaceId$.next(val) - } - - public bbox$ = new BehaviorSubject<BoundingBoxConcept>(null) - @Input('sii-xp-spatial-feat-bbox-bbox-spec') - set bbox(val: string | BoundingBoxConcept) { - if (typeof val === "string") { - try { - const [min, max] = JSON.parse(val) - this.bbox$.next([min, max]) - } catch (e) { - console.warn(`Parse bbox input error`) - } - return - } - if (!validateBbox(val)) { - console.warn(`Bbox is not string, and validate error`) - return - } - this.bbox$.next(val) - } - - @Output('sii-xp-spatial-feat-bbox-features') - featureOutput = new EventEmitter<SapiVOIDataResponse[]>() - features$ = new BehaviorSubject<SapiVOIDataResponse[]>([]) - - @Output('sii-xp-spatial-feat-bbox-busy') - busy$ = new EventEmitter<boolean>() - - private spatialFeatureSpec$: Observable<{ - atlasId: string - spaceId: string - bbox: BoundingBoxConcept - }> = combineLatest([ - this.atlasId$, - this.spaceId$, - this.bbox$, - ]).pipe( - map(([ atlasId, spaceId, bbox ]) => ({ atlasId, spaceId, bbox })), - ) - - private subscription: Subscription[] = [] - - constructor(private svc: SAPI){ - this.subscription.push( - this.spatialFeatureSpec$.pipe( - // experimental feature - // remove to enable in prod - filter(() => this.EXPERIMENTAL_FEATURE_FLAG), - distinctUntilChanged( - (prev, curr) => prev.atlasId === curr.atlasId - && prev.spaceId === curr.spaceId - && JSON.stringify(prev.bbox) === JSON.stringify(curr.bbox) - ), - tap(() => { - this.busy$.emit(true) - this.featureOutput.emit([]) - this.features$.next([]) - }), - debounceTime(160), - switchMap(({ - atlasId, - spaceId, - bbox, - }) => { - if (!atlasId || !spaceId || !bbox) { - this.busy$.emit(false) - return of([] as SapiSpatialFeatureModel[]) - } - const space = this.svc.getSpace(atlasId, spaceId) - return space.getFeatures(SpatialFeatureBBox.FEATURE_NAME, { bbox: JSON.stringify(bbox) }) - }) - ).subscribe((results: SapiVOIDataResponse[]) => { - this.featureOutput.emit(results) - this.features$.next(results) - this.busy$.emit(false) - }) - ) - } - - ngOnDestroy(): void { - while(this.subscription.length) this.subscription.pop().unsubscribe() - } -} diff --git a/src/atlasComponents/sapi/index.ts b/src/atlasComponents/sapi/index.ts index 7e00877e930cd38bd040751ccd4b5dadd5ee723b..4c890621dad7016da25ca192919b8e881ffe784a 100644 --- a/src/atlasComponents/sapi/index.ts +++ b/src/atlasComponents/sapi/index.ts @@ -1,5 +1,4 @@ export { SAPIModule } from './module' -export { SpatialFeatureBBox } from './directives/spatialFeatureBBox.directive' export { SapiAtlasModel, diff --git a/src/atlasComponents/sapi/module.ts b/src/atlasComponents/sapi/module.ts index 32701a9c5e86af9becbcbcbf23261d796f754f4a..a64cc8bc817f05c801cde40d58a95d93c1d198a1 100644 --- a/src/atlasComponents/sapi/module.ts +++ b/src/atlasComponents/sapi/module.ts @@ -1,6 +1,5 @@ import { NgModule } from "@angular/core"; import { SAPI } from "./sapi.service"; -import { SpatialFeatureBBox } from "./directives/spatialFeatureBBox.directive" import { CommonModule } from "@angular/common"; import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; import { PriorityHttpInterceptor } from "src/util/priority"; @@ -13,10 +12,8 @@ import { MatSnackBarModule } from "@angular/material/snack-bar"; MatSnackBarModule, ], declarations: [ - SpatialFeatureBBox, ], exports: [ - SpatialFeatureBBox, ], providers: [ SAPI, diff --git a/src/atlasComponents/sapi/type.ts b/src/atlasComponents/sapi/type.ts index 97519d6fd6fcf91e68880e7f8e359f508897815b..27574a962ebb33410adb62e6cfac93bff5f4f256 100644 --- a/src/atlasComponents/sapi/type.ts +++ b/src/atlasComponents/sapi/type.ts @@ -15,6 +15,7 @@ export type SapiAtlasModel = components["schemas"]["SapiAtlasModel"] export type SapiSpaceModel = components["schemas"]["SapiSpaceModel"] export type SapiParcellationModel = components["schemas"]["SapiParcellationModel"] export type SapiRegionModel = components["schemas"]["siibra__openminds__SANDS__v3__atlas__parcellationEntityVersion__Model"] +export type OpenMINDSCoordinatePoint = components['schemas']['siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Model'] export type SapiRegionMapInfoModel = components["schemas"]["NiiMetadataModel"] export type SapiVOIDataResponse = components["schemas"]["VOIDataModel"] diff --git a/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts b/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d592f3974c5db9b91fb6172fdf05df516b8ad63 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts @@ -0,0 +1,81 @@ +import { Directive, Input, OnChanges, SimpleChanges } from "@angular/core"; +import { BehaviorSubject, Observable } from "rxjs"; +import { distinctUntilChanged } from "rxjs/operators"; +import { BoundingBoxConcept, SapiAtlasModel, SapiSpaceModel } from "src/atlasComponents/sapi/type"; + +function validateBbox(input: any): boolean { + if (!Array.isArray(input)) return false + if (input.length !== 2) return false + return input.every(el => Array.isArray(el) && el.length === 3 && el.every(val => typeof val === "number")) +} + +@Directive({ + selector: '[sxplr-sapiviews-core-space-boundingbox]', + exportAs: 'sxplrSapiViewsCoreSpaceBoundingBox' +}) + +export class SapiViewsCoreSpaceBoundingBox implements OnChanges{ + @Input('sxplr-sapiviews-core-space-boundingbox-atlas') + atlas: SapiAtlasModel + + @Input('sxplr-sapiviews-core-space-boundingbox-space') + space: SapiSpaceModel + + private _bbox: BoundingBoxConcept + @Input('sxplr-sapiviews-core-space-boundingbox-spec') + set bbox(val: string | BoundingBoxConcept ) { + + if (typeof val === "string") { + try { + const [min, max] = JSON.parse(val) + this._bbox = [min, max] + } catch (e) { + console.warn(`Parse bbox input error`) + } + return + } + if (!validateBbox(val)) { + // console.warn(`Bbox is not string, and validate error`) + return + } + this._bbox = val + } + get bbox(): BoundingBoxConcept { + return this._bbox + } + + private _bbox$: BehaviorSubject<{ + atlas: SapiAtlasModel + space: SapiSpaceModel + bbox: BoundingBoxConcept + }> = new BehaviorSubject({ + atlas: null, + space: null, + bbox: null + }) + + public bbox$: Observable<{ + atlas: SapiAtlasModel + space: SapiSpaceModel + bbox: BoundingBoxConcept + }> = this._bbox$.asObservable().pipe( + distinctUntilChanged( + (prev, curr) => prev.atlas?.["@id"] === curr.atlas?.['@id'] + && prev.space?.["@id"] === curr.space?.["@id"] + && JSON.stringify(prev.bbox) === JSON.stringify(curr.bbox) + ) + ) + + ngOnChanges(): void { + const { + atlas, + space, + bbox + } = this + this._bbox$.next({ + atlas, + space, + bbox + }) + } +} diff --git a/src/atlasComponents/sapiViews/core/space/module.ts b/src/atlasComponents/sapiViews/core/space/module.ts index a55902c111b8a2de12a53062f23d7d61e31047ab..35b96ce90dd005580407c05411d923986e2ad9d4 100644 --- a/src/atlasComponents/sapiViews/core/space/module.ts +++ b/src/atlasComponents/sapiViews/core/space/module.ts @@ -1,6 +1,7 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { ComponentsModule } from "src/components"; +import { SapiViewsCoreSpaceBoundingBox } from "./boundingBox.directive"; import { PreviewSpaceUrlPipe } from "./previewSpaceUrl.pipe"; import { SapiViewsCoreSpaceSpaceTile } from "./tile/space.tile.component"; @@ -12,9 +13,11 @@ import { SapiViewsCoreSpaceSpaceTile } from "./tile/space.tile.component"; declarations: [ SapiViewsCoreSpaceSpaceTile, PreviewSpaceUrlPipe, + SapiViewsCoreSpaceBoundingBox, ], exports: [ SapiViewsCoreSpaceSpaceTile, + SapiViewsCoreSpaceBoundingBox, ] }) diff --git a/src/atlasComponents/sapiViews/features/module.ts b/src/atlasComponents/sapiViews/features/module.ts index 6ee7b852c0400d3bb9b6af371f8f930c4bfb2ceb..df02a04923073ef565de75b17ed7259fab635ee2 100644 --- a/src/atlasComponents/sapiViews/features/module.ts +++ b/src/atlasComponents/sapiViews/features/module.ts @@ -9,6 +9,7 @@ import { FeatureBadgeFlagPipe } from "./featureBadgeFlag.pipe" import { FeatureBadgeNamePipe } from "./featureBadgeName.pipe" import * as ieeg from "./ieeg" import * as receptor from "./receptors" +import * as voi from "./voi" const { SxplrSapiViewsFeaturesIeegModule @@ -16,6 +17,7 @@ const { const { ReceptorViewModule } = receptor +const { SapiViewsFeaturesVoiModule } = voi @NgModule({ imports: [ @@ -23,6 +25,7 @@ const { ReceptorViewModule, SxplrSapiViewsFeaturesIeegModule, AngularMaterialModule, + SapiViewsFeaturesVoiModule, ], declarations: [ FeatureEntryCmp, @@ -41,6 +44,7 @@ const { exports: [ FeatureEntryCmp, SapiViewsFeaturesEntryListItem, + SapiViewsFeaturesVoiModule, ] }) export class SapiViewsFeaturesModule{} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/voi/index.ts b/src/atlasComponents/sapiViews/features/voi/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..55b2fbc01ff6805ae01ef334e88d52db40ded951 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/voi/index.ts @@ -0,0 +1,2 @@ +export { SapiViewsFeaturesVoiModule } from "./module" +export { SapiViewsFeaturesVoiQuery } from "./voiQuery.directive" diff --git a/src/atlasComponents/sapiViews/features/voi/module.ts b/src/atlasComponents/sapiViews/features/voi/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9817aacad60533082cbf4be70c62831d50727cd --- /dev/null +++ b/src/atlasComponents/sapiViews/features/voi/module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { SAPIModule } from "src/atlasComponents/sapi/module"; +import { SapiViewsFeaturesVoiQuery } from "./voiQuery.directive"; + +@NgModule({ + imports: [ + CommonModule, + SAPIModule, + ], + declarations: [ + SapiViewsFeaturesVoiQuery, + ], + exports: [ + SapiViewsFeaturesVoiQuery + ] +}) + +export class SapiViewsFeaturesVoiModule{} diff --git a/src/atlasComponents/sapiViews/features/voi/voiQuery.directive.ts b/src/atlasComponents/sapiViews/features/voi/voiQuery.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..21f0225f9456170bc9b7d9eee6527b0822e0e2a5 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/voi/voiQuery.directive.ts @@ -0,0 +1,169 @@ +import { Directive, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output } from "@angular/core"; +import { merge, Observable, of, Subject, Subscription } from "rxjs"; +import { debounceTime, pairwise, shareReplay, startWith, switchMap, tap } from "rxjs/operators"; +import { AnnotationLayer, TNgAnnotationPoint, TNgAnnotationAABBox } from "src/atlasComponents/annotations"; +import { SAPI } from "src/atlasComponents/sapi/sapi.service"; +import { BoundingBoxConcept, SapiAtlasModel, SapiSpaceModel, SapiVOIDataResponse, OpenMINDSCoordinatePoint } from "src/atlasComponents/sapi/type"; +import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; + +@Directive({ + selector: '[sxplr-sapiviews-features-voi-query]', + exportAs: 'sxplrSapiViewsFeaturesVoiQuery' +}) + +export class SapiViewsFeaturesVoiQuery implements OnChanges, OnDestroy{ + + static VOI_LAYER_NAME = 'voi-annotation-layer' + static VOI_ANNOTATION_COLOR = "#ffff00" + private voiQuerySpec = new Subject<{ + atlas: SapiAtlasModel + space: SapiSpaceModel + bbox: BoundingBoxConcept + }>() + + private canFetchVoi(){ + return !!this.atlas && !!this.space && !!this.bbox + } + + @Input('sxplr-sapiviews-features-voi-query-atlas') + atlas: SapiAtlasModel + + @Input('sxplr-sapiviews-features-voi-query-space') + space: SapiSpaceModel + + @Input('sxplr-sapiviews-features-voi-query-bbox') + bbox: BoundingBoxConcept + + @Output('sxplr-sapiviews-features-voi-query-onhover') + onhover = new EventEmitter<SapiVOIDataResponse>() + + @Output('sxplr-sapiviews-features-voi-query-onclick') + onclick = new EventEmitter<SapiVOIDataResponse>() + + public busy$ = new EventEmitter<boolean>() + public features$: Observable<SapiVOIDataResponse[]> = this.voiQuerySpec.pipe( + debounceTime(160), + tap(() => this.busy$.emit(true)), + switchMap(({ atlas, bbox, space }) => { + if (!this.canFetchVoi()) { + return of([]) + } + return merge( + of([]), + this.sapi.getSpace(atlas["@id"], space["@id"]).getFeatures({ bbox: JSON.stringify(bbox) }).pipe( + tap(val => { + this.busy$.emit(false) + }) + ) + ) + }), + startWith([]), + shareReplay(1) + ) + + private hoveredFeat: SapiVOIDataResponse + private onDestroyCb: (() => void)[] = [] + private subscription: Subscription[] = [] + ngOnChanges(): void { + const { + atlas, + space, + bbox + } = this + this.voiQuerySpec.next({ atlas, space, bbox }) + } + ngOnDestroy(): void { + if (this.voiBBoxSvc) this.voiBBoxSvc.dispose() + while (this.subscription.length > 0) this.subscription.pop().unsubscribe() + while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() + } + + handleOnHoverFeature(id: string){ + const ann = this.annotationIdToFeature.get(id) + this.hoveredFeat = ann + this.onhover.emit(ann) + } + + private _voiBBoxSvc: AnnotationLayer + get voiBBoxSvc(): AnnotationLayer { + if (this._voiBBoxSvc) return this._voiBBoxSvc + try { + const layer = AnnotationLayer.Get( + SapiViewsFeaturesVoiQuery.VOI_LAYER_NAME, + SapiViewsFeaturesVoiQuery.VOI_ANNOTATION_COLOR + ) + this._voiBBoxSvc = layer + this.subscription.push( + layer.onHover.subscribe(val => this.handleOnHoverFeature(val?.id)) + ) + return layer + } catch (e) { + return null + } + } + + constructor( + private sapi: SAPI, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, + ){ + const handle = () => { + if (!this.hoveredFeat) return true + this.onclick.emit(this.hoveredFeat) + return false + } + this.onDestroyCb.push( + () => clickInterceptor.deregister(handle) + ) + clickInterceptor.register(handle) + this.subscription.push( + this.features$.pipe( + startWith([] as SapiVOIDataResponse[]), + pairwise() + ).subscribe(([ prev, curr ]) => { + for (const v of prev) { + const box = this.pointsToAABB(v.location.maxpoint, v.location.minpoint) + const point = this.pointToPoint(v.location.center) + this.annotationIdToFeature.delete(box.id) + this.annotationIdToFeature.delete(point.id) + if (!this.voiBBoxSvc) continue + for (const ann of [box, point]) { + this.voiBBoxSvc.removeAnnotation({ + id: ann.id + }) + } + } + for (const v of curr) { + const box = this.pointsToAABB(v.location.maxpoint, v.location.minpoint) + const point = this.pointToPoint(v.location.center) + this.annotationIdToFeature.set(box.id, v) + this.annotationIdToFeature.set(point.id, v) + if (!this.voiBBoxSvc) { + throw new Error(`annotation is expected to be added, but annotation layer cannot be instantiated.`) + } + for (const ann of [box, point]) { + this.voiBBoxSvc.updateAnnotation(ann) + } + } + if (this.voiBBoxSvc) this.voiBBoxSvc.setVisible(true) + }) + ) + } + + private annotationIdToFeature = new Map<string, SapiVOIDataResponse>() + + private pointsToAABB(pointA: OpenMINDSCoordinatePoint, pointB: OpenMINDSCoordinatePoint): TNgAnnotationAABBox{ + return { + id: `${SapiViewsFeaturesVoiQuery.VOI_LAYER_NAME}:${pointA["@id"]}:${pointB["@id"]}`, + pointA: pointA.coordinates.map(v => v.value * 1e6) as [number, number, number], + pointB: pointB.coordinates.map(v => v.value * 1e6) as [number, number, number], + type: "aabbox" + } + } + private pointToPoint(point: OpenMINDSCoordinatePoint): TNgAnnotationPoint { + return { + id: `${SapiViewsFeaturesVoiQuery.VOI_LAYER_NAME}:${point["@id"]}`, + point: point.coordinates.map(v => v.value * 1e6) as [number, number, number], + type: "point" + } + } +} diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index 1381520da9c9a7fae35e5203c9e40a5790c7aa07..e998c9c53a65be4965b8cad62e84d1b45df3c3f3 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -327,35 +327,6 @@ export class ModularUserAnnotationToolService implements OnDestroy{ }) ) - /** - * on new nehubaViewer, listen to mouseState - */ - let cb: () => void - this.subscription.push( - nehubaViewer$.pipe( - switchMap(switchMapWaitFor({ - condition: nv => !!(nv?.nehubaViewer), - })) - ).subscribe(nehubaViewer => { - if (cb) cb() - if (nehubaViewer) { - const mouseState = nehubaViewer.nehubaViewer.ngviewer.mouseState - cb = mouseState.changed.add(() => { - const payload: IAnnotationEvents['hoverAnnotation'] = mouseState.active && !!mouseState.pickedAnnotationId - ? { - pickedAnnotationId: mouseState.pickedAnnotationId, - pickedOffset: mouseState.pickedOffset - } - : null - this.annotnEvSubj.next({ - type: 'hoverAnnotation', - detail: payload - }) - }) - } - }) - ) - /** * get mouse real position */ @@ -497,6 +468,17 @@ export class ModularUserAnnotationToolService implements OnDestroy{ ModularUserAnnotationToolService.ANNOTATION_LAYER_NAME, ModularUserAnnotationToolService.USER_ANNOTATION_LAYER_SPEC.annotationColor ) + this.annotationLayer.onHover.subscribe(val => { + this.annotnEvSubj.next({ + type: 'hoverAnnotation', + detail: val + ? { + pickedAnnotationId: val.id, + pickedOffset: val.offset + } + : null + }) + }) /** * on template changes, the layer gets lost * force redraw annotations if layer needs to be recreated diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 7cf6952ae0c87594b243aa328f424e8731a15f44..055079aa1ff7667645c1c71d2dd84283530b8290 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -48,7 +48,6 @@ const compareFn = (it, item) => it.name === item.name export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public CONST = CONST - public CONTEXT_MENU_ARIA_LABEL = ARIA_LABELS.CONTEXT_MENU public compareFn = compareFn @ViewChild('cookieAgreementComponent', {read: TemplateRef}) public cookieAgreementComponent: TemplateRef<any> diff --git a/src/atlasViewer/atlasViewer.style.css b/src/atlasViewer/atlasViewer.style.css index 0f938ac6632a634a35ee793318a408c0e70248fd..24a3efe3d00e05055d78b7625eb53e7e37ba5986 100644 --- a/src/atlasViewer/atlasViewer.style.css +++ b/src/atlasViewer/atlasViewer.style.css @@ -11,66 +11,8 @@ display: block; } -ui-nehuba-container -{ - position:absolute; - top:0; - left:0; - width:100%; - height:100%; -} - -layout-floating-container -{ - width:100%; - height:100%; - overflow:hidden; -} - -layout-floating-container > * -{ - position: absolute; - left: 0; - top: 0; -} - -mat-list[dense].contextual-block -{ - display: inline-block; - background-color:rgba(200,200,200,0.8); -} - -:host-context([darktheme="true"]) mat-list[dense].contextual-block -{ - background-color : rgba(30,30,30,0.8); -} - -[fixedMouseContextualContainerDirective] -{ - max-width: 100%; -} div.displayCard { opacity: 0.8; } - -mat-sidenav { - box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); -} - -region-menu -{ - display:inline-block; -} - -.floating-container -{ - max-width: 350px; -} - -logo-container -{ - height: 2rem; - opacity: 0.2; -} diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 0e1a5b2450c1f945d1487ba79b57c495ed225728..e62408b366a8dac2a41fcc00ff7d5e6eeb1d1c53 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -29,71 +29,21 @@ <!-- atlas template --> <ng-template #viewerBody> <div class="w-100 h-100" - iav-media-query quick-tour [quick-tour-position]="quickTourFinale.position" [quick-tour-description]="quickTourFinale.description" [quick-tour-description-md]="quickTourFinale.descriptionMd" [quick-tour-order]="quickTourFinale.order" [quick-tour-overwrite-arrow]="emptyArrowTmpl" - quick-tour-severity="low" - #media="iavMediaQuery"> + quick-tour-severity="low"> <!-- prevent default is required so that user do not zoom in on UI or scroll on mobile UI --> <iav-cmp-viewer-container class="w-100 h-100 d-block" - [ismobile]="(media.mediaBreakPoint$ | async) > 3" iav-captureClickListenerDirective [iav-captureClickListenerDirective-captureDocument]="true" (iav-captureClickListenerDirective-onUnmovedClick)="mouseClickDocument($event)"> </iav-cmp-viewer-container> - <!-- TODO move to viewerCmp.template.html --> - <layout-floating-container - zIndex="13" - #floatingOverlayContainer> - <div floatingContainerDirective> - </div> - - <div *ngIf="(media.mediaBreakPoint$ | async) < 3" - class="fixed-bottom pe-none mb-2 d-flex justify-content-center"> - <ng-container *ngTemplateOutlet="logoTmpl"> - </ng-container> - </div> - - <div *ngIf="!ismobile" floatingMouseContextualContainerDirective> - - <div class="h-0" - iav-mouse-hover - #iavMouseHoverContextualBlock="iavMouseHover"> - </div> - <mat-list dense class="contextual-block"> - - <mat-list-item *ngFor="let cvtOutput of iavMouseHoverContextualBlock.currentOnHoverObs$ | async | mouseoverCvt" - class="h-auto"> - - <mat-icon - [fontSet]="cvtOutput.icon.fontSet" - [fontIcon]="cvtOutput.icon.fontIcon" - mat-list-icon> - </mat-icon> - - <div matLine>{{ cvtOutput.text }}</div> - - </mat-list-item> - </mat-list> - <!-- TODO Potentially implementing plugin contextual info --> - </div> - - <div class="floating-container" - [attr.aria-label]="CONTEXT_MENU_ARIA_LABEL" - fixedMouseContextualContainerDirective - #fixedContainer="iavFixedMouseCtxContainer"> - - <!-- mouse on click context menu, currently not used --> - - </div> - - </layout-floating-container> </div> </ng-template> @@ -102,11 +52,6 @@ <not-supported-component></not-supported-component> </ng-template> -<!-- logo tmpl --> -<ng-template #logoTmpl> - <logo-container></logo-container> -</ng-template> - <ng-template #idleOverlay> <tryme-component></tryme-component> </ng-template> diff --git a/src/layouts/floating/floating.component.ts b/src/layouts/floating/floating.component.ts deleted file mode 100644 index bb8ec5176576e4d6c0b2c06db51aea133237a5b9..0000000000000000000000000000000000000000 --- a/src/layouts/floating/floating.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, HostBinding, Input } from "@angular/core"; - -@Component({ - selector : 'layout-floating-container', - templateUrl : './floating.template.html', - styleUrls : [ - `./floating.style.css`, - ], -}) - -export class FloatingLayoutContainer { - @HostBinding('style.z-index') - @Input() - public zIndex: number = 5 -} diff --git a/src/layouts/floating/floating.style.css b/src/layouts/floating/floating.style.css deleted file mode 100644 index 0cb15ee43e183a4d6bb193608840da0b4886934f..0000000000000000000000000000000000000000 --- a/src/layouts/floating/floating.style.css +++ /dev/null @@ -1,15 +0,0 @@ -:host -{ - position: absolute; - top:0; - left:0; - width:100%; - height:100%; - display:block; - pointer-events: none; -} - -:host * -{ - pointer-events: all; -} \ No newline at end of file diff --git a/src/layouts/floating/floating.template.html b/src/layouts/floating/floating.template.html deleted file mode 100644 index d7b4509bdd7e7b9ac0c84063fdc18c606fb96a9f..0000000000000000000000000000000000000000 --- a/src/layouts/floating/floating.template.html +++ /dev/null @@ -1,2 +0,0 @@ -<ng-content> -</ng-content> \ No newline at end of file diff --git a/src/layouts/layout.module.ts b/src/layouts/layout.module.ts index 0923ccb50102bfcde21c45a8f8bafe250ba31863..40cedc4f12736cadd766c749d55ab3e1139b4a43 100644 --- a/src/layouts/layout.module.ts +++ b/src/layouts/layout.module.ts @@ -3,7 +3,6 @@ import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { ComponentsModule } from "../components/components.module"; import { CurrentLayout } from "./currentLayout/currentLayout.component"; -import { FloatingLayoutContainer } from "./floating/floating.component"; import { FourCornersCmp } from "./fourCorners/fourCorners.component"; import { FourPanelLayout } from "./layouts/fourPanel/fourPanel.component"; import { HorizontalOneThree } from "./layouts/h13/h13.component"; @@ -17,7 +16,6 @@ import { VerticalOneThree } from "./layouts/v13/v13.component"; ComponentsModule, ], declarations : [ - FloatingLayoutContainer, FourCornersCmp, CurrentLayout, @@ -28,7 +26,6 @@ import { VerticalOneThree } from "./layouts/v13/v13.component"; ], exports : [ BrowserAnimationsModule, - FloatingLayoutContainer, FourCornersCmp, CurrentLayout, FourPanelLayout, diff --git a/src/main.module.ts b/src/main.module.ts index de2ee0525e1e189c01f619314f01534c69f1d377..c468717badc2a083494f6394acfb456ca1f2224c 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -17,8 +17,6 @@ import { ConfirmDialogComponent } from "./components/confirmDialog/confirmDialog import { DialogComponent } from "./components/dialog/dialog.component"; import { DialogService } from "./services/dialogService.service"; import { UIService } from "./services/uiService.service"; -import { FloatingContainerDirective } from "./util/directives/floatingContainer.directive"; -import { FloatingMouseContextualContainerDirective } from "./util/directives/floatingMouseContextualContainer.directive"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR, PureContantService, UtilModule } from "src/util"; import { SpotLightModule } from 'src/spotlight/spot-light.module' import { TryMeComponent } from "./ui/tryme/tryme.component"; @@ -35,7 +33,6 @@ import { MesssagingModule } from './messaging/module'; import { ViewerModule, VIEWERMODULE_DARKTHEME } from './viewerModule'; import { CookieModule } from './ui/cookieAgreement/module'; import { KgTosModule } from './ui/kgtos/module'; -import { MouseoverModule } from './mouseoverModule/mouseover.module'; import { AtlasViewerRouterModule } from './routerModule'; import { MessagingGlue } from './messagingGlue'; import { BS_ENDPOINT } from './util/constants'; @@ -77,7 +74,6 @@ import { CONST } from "common/constants" SpotLightModule, CookieModule, KgTosModule, - MouseoverModule, AtlasViewerRouterModule, QuickTourModule, @@ -95,8 +91,6 @@ import { CONST } from "common/constants" TryMeComponent, /* directives */ - FloatingContainerDirective, - FloatingMouseContextualContainerDirective, ], entryComponents : [ diff --git a/src/ui/logoContainer/logoContainer.component.ts b/src/ui/logoContainer/logoContainer.component.ts index 105e0726251a75522bc2d747767c0fe7b3eb50c2..f07321c93b938ece21e32359b53ae8a3b75a0b40 100644 --- a/src/ui/logoContainer/logoContainer.component.ts +++ b/src/ui/logoContainer/logoContainer.component.ts @@ -24,7 +24,7 @@ export class LogoContainer { private subscriptions: Subscription[] = [] constructor( - private pureConstantService: PureContantService + pureConstantService: PureContantService ){ this.subscriptions.push( pureConstantService.darktheme$.pipe( diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 64536c949719e4dffae27b1c24a9c02ac0cdf5a6..6e66aeefeb560e99b10a9e27bed492f4387a18b0 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -10,7 +10,6 @@ import { AngularMaterialModule } from 'src/sharedModules' import { UtilModule } from "src/util"; import { DownloadDirective } from "../util/directives/download.directive"; -import { LogoContainer } from "./logoContainer/logoContainer.component"; import { MobileOverlay } from "./nehubaContainer/mobileOverlay/mobileOverlay.component"; import { HumanReadableFileSizePipe } from "src/util/pipes/humanReadableFileSize.pipe"; @@ -44,8 +43,6 @@ import { HANDLE_SCREENSHOT_PROMISE, TypeHandleScrnShotPromise } from "../screens Landmark2DModule, ], declarations : [ - - LogoContainer, MobileOverlay, ActionDialog, @@ -127,8 +124,6 @@ import { HANDLE_SCREENSHOT_PROMISE, TypeHandleScrnShotPromise } from "../screens ], exports : [ // NehubaContainer, - - LogoContainer, MobileOverlay, // StatusCardComponent, diff --git a/src/util/directives/floatingContainer.directive.ts b/src/util/directives/floatingContainer.directive.ts deleted file mode 100644 index 4ff9eb3b102668138f6a0ab8ed3eeacf02296fbf..0000000000000000000000000000000000000000 --- a/src/util/directives/floatingContainer.directive.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Directive, ViewContainerRef } from "@angular/core"; -import { WidgetServices } from "src/widget"; - -@Directive({ - selector: '[floatingContainerDirective]', -}) - -export class FloatingContainerDirective { - constructor( - widgetService: WidgetServices, - viewContainerRef: ViewContainerRef, - ) { - widgetService.floatingContainer = viewContainerRef - } -} diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index a485a115773c2953b3396c72a0d054d925fa85f6..179053b1d0bb1424c208d1d39631b43718334490 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -24,6 +24,9 @@ import { SAPIModule } from 'src/atlasComponents/sapi'; import { NehubaVCtxToBbox } from "./pipes/nehubaVCtxToBbox.pipe"; import { SapiViewsModule, SapiViewsUtilModule } from "src/atlasComponents/sapiViews"; import { DialogModule } from "src/ui/dialogInfo/module"; +import { MouseoverModule } from "src/mouseoverModule"; +import { LogoContainer } from "src/ui/logoContainer/logoContainer.component"; +import { FloatingMouseContextualContainerDirective } from "src/util/directives/floatingMouseContextualContainer.directive"; @NgModule({ imports: [ @@ -43,10 +46,13 @@ import { DialogModule } from "src/ui/dialogInfo/module"; SapiViewsModule, SapiViewsUtilModule, DialogModule, + MouseoverModule, ], declarations: [ ViewerCmp, NehubaVCtxToBbox, + LogoContainer, + FloatingMouseContextualContainerDirective, ], providers: [ { diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts index 934f5ba8d3147fba7292b51436c2bdc1357f8417..33e41b96c7e75862e1c226775ecb42d15dc89f35 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts @@ -2,10 +2,10 @@ import { Injectable } from "@angular/core"; import { createEffect } from "@ngrx/effects"; import { select, Store } from "@ngrx/store"; import { forkJoin, of } from "rxjs"; -import { mapTo, switchMap, withLatestFrom, filter, catchError, map, debounceTime, shareReplay, distinctUntilChanged } from "rxjs/operators"; -import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi"; -import { atlasAppearance, atlasSelection } from "src/state"; -import { NgLayerCustomLayer } from "src/state/atlasAppearance"; +import { mapTo, switchMap, withLatestFrom, filter, catchError, map, debounceTime, shareReplay, distinctUntilChanged, startWith, pairwise } from "rxjs/operators"; +import { SAPI, SapiAtlasModel, SapiFeatureModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi"; +import { SapiVOIDataResponse } from "src/atlasComponents/sapi/type"; +import { atlasAppearance, atlasSelection, userInteraction } from "src/state"; import { arrayEqual } from "src/util/array"; import { EnumColorMapName } from "src/util/colorMaps"; import { getShader } from "src/util/constants"; @@ -67,12 +67,62 @@ export class LayerCtrlEffects { map(val => val as { atlas: SapiAtlasModel, parcellation: SapiParcellationModel, template: SapiSpaceModel }) ) + onShownFeature = createEffect(() => this.store.pipe( + select(userInteraction.selectors.selectedFeature), + startWith(null as SapiFeatureModel), + pairwise(), + map(([ prev, curr ]) => { + const removeLayers: atlasAppearance.NgLayerCustomLayer[] = [] + const addLayers: atlasAppearance.NgLayerCustomLayer[] = [] + if (prev?.["@type"] === "siibra/features/voi") { + removeLayers.push( + ...(prev as SapiVOIDataResponse).volumes.map(v => { + return { + id: v.metadata.fullName, + clType: "customlayer/nglayer", + source: v.data.url, + transform: v.data.detail['neuroglancer/precomputed']['transform'], + opacity: 1.0, + visible: true, + shader: v.data.detail['neuroglancer/precomputed']['shader'] || getShader() + } as atlasAppearance.NgLayerCustomLayer + }) + ) + } + if (curr?.["@type"] === "siibra/features/voi") { + addLayers.push( + ...(curr as SapiVOIDataResponse).volumes.map(v => { + return { + id: v.metadata.fullName, + clType: "customlayer/nglayer", + source: `precomputed://${v.data.url}`, + transform: v.data.detail['neuroglancer/precomputed']['transform'], + opacity: v.data.detail['neuroglancer/precomputed']['opacity'] || 1.0, + visible: true, + shader: v.data.detail['neuroglancer/precomputed']['shader'] || getShader() + } as atlasAppearance.NgLayerCustomLayer + }) + ) + } + return { removeLayers, addLayers } + }), + filter(({ removeLayers, addLayers }) => removeLayers.length !== 0 || addLayers.length !== 0), + switchMap(({ removeLayers, addLayers }) => of(...[ + ...removeLayers.map( + l => atlasAppearance.actions.removeCustomLayer({ id: l.id }) + ), + ...addLayers.map( + l => atlasAppearance.actions.addCustomLayer({ customLayer: l }) + ) + ])) + )) + onATPClearBaseLayers = createEffect(() => this.onATP$.pipe( withLatestFrom( this.store.pipe( select(atlasAppearance.selectors.customLayers), map( - cl => cl.filter(layer => layer.clType === "baselayer/nglayer" || layer.clType === "baselayer/colormap") + cl => cl.filter(layer => layer.clType === "baselayer/nglayer" || "customlayer/nglayer") ) ) ), @@ -178,7 +228,7 @@ export class LayerCtrlEffects { switchMap(ngLayers => { const { parcNgLayers, tmplAuxNgLayers, tmplNgLayers } = ngLayers - const customBaseLayers: NgLayerCustomLayer[] = [] + const customBaseLayers: atlasAppearance.NgLayerCustomLayer[] = [] for (const layers of [parcNgLayers, tmplAuxNgLayers, tmplNgLayers]) { for (const key in layers) { const { source, transform, opacity, visible } = layers[key] diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index 2f86c53b2a37395d57f7805a6df835025c018398..4792776add9128e8128ba17eb08cab2dc4976db6 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -1,7 +1,7 @@ import { Injectable, OnDestroy } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { BehaviorSubject, combineLatest, merge, Observable, Subject, Subscription } from "rxjs"; -import { debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, tap, withLatestFrom } from "rxjs/operators"; +import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators"; import { IColorMap, INgLayerCtrl, TNgLayerCtrl } from "./layerCtrl.util"; import { SAPIRegion } from "src/atlasComponents/sapi/core"; import { getParcNgId } from "../config.service" @@ -191,30 +191,6 @@ export class NehubaLayerControlService implements OnDestroy{ }) ) - public visibleLayer$: Observable<string[]> = combineLatest([ - this.expectedLayerNames$.pipe( - map(expectedLayerNames => { - const ngIdSet = new Set<string>([...expectedLayerNames]) - return Array.from(ngIdSet) - }) - ), - this.store$.pipe( - select(atlasAppearance.selectors.customLayers), - 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 - }), - distinctUntilChanged(), - map(flag => flag - ? [ NehubaLayerControlService.PMAP_LAYER_NAME ] - : [] - ) - ) - ]).pipe( - map(([ expectedLayerNames, pmapLayer ]) => [...expectedLayerNames, ...pmapLayer]) - ) - /** * define when shown segments should be updated */ @@ -318,4 +294,32 @@ export class NehubaLayerControlService implements OnDestroy{ this.manualNgLayersControl$, ).pipe( ) + + public visibleLayer$: Observable<string[]> = combineLatest([ + this.expectedLayerNames$.pipe( + map(expectedLayerNames => { + const ngIdSet = new Set<string>([...expectedLayerNames]) + return Array.from(ngIdSet) + }) + ), + this.ngLayers$.pipe( + map(({ customLayers }) => customLayers), + startWith([] as atlasAppearance.NgLayerCustomLayer[]) + ), + this.store$.pipe( + select(atlasAppearance.selectors.customLayers), + 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 + }), + distinctUntilChanged(), + map(flag => flag + ? [ NehubaLayerControlService.PMAP_LAYER_NAME ] + : [] + ) + ) + ]).pipe( + map(([ expectedLayerNames, customLayers, pmapLayer ]) => [...expectedLayerNames, ...customLayers.map(l => l.id), ...pmapLayer]) + ) } diff --git a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html index 31fab82cddab4c072488a108e0d8391d5f1145b0..bd018eafdbd00f4234f60ea51be7ec67e7a9a668 100644 --- a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html +++ b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html @@ -32,18 +32,14 @@ <!-- perspective view tmpl --> <ng-template #overlayPerspectiveTmpl> - <layout-floating-container> - + <!-- mesh loading is still weird --> <!-- if the precomputed server does not have the necessary fragment file, then the numberws will not collate --> <!-- TODO --> - </layout-floating-container> </ng-template> <iav-layout-fourcorners class="w-100 h-100 d-block"> - <layout-floating-container *ngIf="panelIndex < 3; else overlayPerspectiveTmpl" - class="overflow-hidden" - iavLayoutFourCornersContent> + <!-- TODO add landmarks here --> @@ -58,7 +54,6 @@ [positionZ]="getPositionZ(panelIndex, lm)"> </landmark-2d-flat-cmp> --> - </layout-floating-container> <!-- panel controller --> <div iavLayoutFourCornersBottomRight class="position-relative honing"> diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 4d9e1e3e7fad53ad4dab257e88b412d1f77e76e8..e9f6ac785123cdeb2c6f34597c585677fa043514 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -94,8 +94,6 @@ export class ViewerCmp implements OnDestroy { description: QUICKTOUR_DESC.ATLAS_SELECTOR, } - @Input() ismobile = false - private subscriptions: Subscription[] = [] private onDestroyCb: (() => void)[] = [] public viewerLoaded: boolean = false @@ -385,7 +383,7 @@ export class ViewerCmp implements OnDestroy { atlasSelection.actions.navigateTo({ navigation: { orientation: [0, 0, 0, 1], - position: feature.location.center.coordinates.map(v => (v.unit as number) * 1e6) + position: feature.location.center.coordinates.map(v => v.value * 1e6) }, animation: true }) diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css index 4d6407b910bb98bb8791bdebfcb2321bac7c835b..671fef08c4faf6d72e1cfd3c9664208fb60261b7 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.style.css +++ b/src/viewerModule/viewerCmp/viewerCmp.style.css @@ -89,3 +89,32 @@ sxplr-sapiviews-core-region-region-chip [prefix] padding-right: 0.5rem; margin-top: -1rem; } + +.floating-ui +{ + display: block; + z-index: 5; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + pointer-events: none; +} + +logo-container +{ + height: 2rem; + opacity: 0.2; +} + +mat-list[dense].contextual-block +{ + display: inline-block; + background-color:rgba(200,200,200,0.8); +} + +:host-context([darktheme="true"]) mat-list[dense].contextual-block +{ + background-color : rgba(30,30,30,0.8); +} diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 7e1a9b889ed43ced89ec318b6b38143e944ca9d0..95c9168e308e5283de8a81818fa77e913c4d5816 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -1,6 +1,59 @@ -<div class="position-absolute w-100 h-100"> +<div iav-media-query class="position-absolute w-100 h-100" #media="iavMediaQuery"> <ng-container *ngTemplateOutlet="viewerTmpl"> </ng-container> + + <div class="floating-ui"> + + <div *ngIf="(media.mediaBreakPoint$ | async) < 3" + class="fixed-bottom pe-none mb-2 d-flex justify-content-center"> + <logo-container></logo-container> + </div> + + <div *ngIf="(media.mediaBreakPoint$ | async) < 3" floatingMouseContextualContainerDirective> + + <div class="h-0" + iav-mouse-hover + #iavMouseHoverContextualBlock="iavMouseHover"> + </div> + <mat-list dense class="contextual-block"> + <mat-list-item *ngFor="let cvtOutput of iavMouseHoverContextualBlock.currentOnHoverObs$ | async | mouseoverCvt" + class="h-auto"> + + <mat-icon + [fontSet]="cvtOutput.icon.fontSet" + [fontIcon]="cvtOutput.icon.fontIcon" + mat-list-icon> + </mat-icon> + + <div matLine>{{ cvtOutput.text }}</div> + + </mat-list-item> + + <ng-template [ngIf]="voiFeatures.onhover | async" let-feat> + <mat-list-item> + <mat-icon + fontSet="fas" + fontIcon="fa-database" + mat-list-icon> + </mat-icon> + <div matLine>{{ feat?.metadata?.fullName || 'Feature' }}</div> + </mat-list-item> + </ng-template> + </mat-list> + <!-- TODO Potentially implementing plugin contextual info --> + </div> + + <!-- mouse on click context menu, currently not used --> + <!-- <div class="floating-container" + [attr.aria-label]="CONTEXT_MENU_ARIA_LABEL" + fixedMouseContextualContainerDirective + #fixedContainer="iavFixedMouseCtxContainer"> + + + + </div> --> + + </div> </div> @@ -238,7 +291,7 @@ isOpen: minTrayVisSwitch.switchState$ | async, regionSelected: selectedRegions$ | async, click: minTrayVisSwitch.toggle.bind(minTrayVisSwitch), - badge: (spatialFeatureBbox.features$ | async).length || null + badge: (voiFeatures.features$ | async).length || null }"> </ng-container> </div> @@ -330,7 +383,7 @@ <!-- signin banner at top right corner --> <top-menu-cmp class="mt-3 mr-2 d-inline-block" - [ismobile]="ismobile" + [ismobile]="(media.mediaBreakPoint$ | async) > 3" [viewerLoaded]="viewerLoaded"> </top-menu-cmp> @@ -933,29 +986,30 @@ }"> </ng-container> + <ng-layer-ctl *ngFor="let vol of feature.volumes" + class="d-block" + [ng-layer-ctl-name]="vol.metadata.fullName" + [ng-layer-ctl-src]="vol.data.url" + [ng-layer-ctl-transform]="vol.data | getProperty : 'detail' | getProperty: 'neuroglancer/precomputed' | getProperty : 'transform'"> + </ng-layer-ctl> <ng-template #sapiVOITmpl> - <ng-layer-ctl *ngFor="let vol of feature.volumes" - class="d-block" - [ng-layer-ctl-name]="vol.name" - [ng-layer-ctl-src]="vol.url" - [ng-layer-ctl-transform]="vol | getProperty : 'detail' | getProperty: 'neuroglancer/precomputed' | getProperty : 'transform'"> - </ng-layer-ctl> </ng-template> </ng-template> <ng-template #spatialFeatureListViewTmpl> - <div *ngIf="spatialFeatureBbox.busy$ | async; else notBusyTmpl" class="fs-200"> + <div *ngIf="voiFeatures.busy$ | async; else notBusyTmpl" class="fs-200"> <spinner-cmp></spinner-cmp> </div> <ng-template #notBusyTmpl> - <mat-card *ngIf="(spatialFeatureBbox.features$ | async).length > 0" class="pe-all mat-elevation-z4"> + <mat-card *ngIf="(voiFeatures.features$ | async).length > 0" class="pe-all mat-elevation-z4"> <mat-card-title> Volumes of interest </mat-card-title> <mat-card-subtitle class="overflow-hidden"> - <ng-template let-bbox [ngIf]="spatialFeatureBbox.bbox$ | async" [ngIfElse]="bboxFallbackTmpl"> + <!-- TODO in future, perhaps encapsulate this as a component? seems like a nature fit in sapiView/space/boundingbox --> + <ng-template let-bbox [ngIf]="bbox.bbox$ | async | getProperty : 'bbox'" [ngIfElse]="bboxFallbackTmpl"> Bounding box: {{ bbox[0] | numbers | json }} - {{ bbox[1] | numbers | json }} mm </ng-template> <ng-template #bboxFallbackTmpl> @@ -966,20 +1020,27 @@ <mat-divider></mat-divider> - <div *ngFor="let feature of spatialFeatureBbox.features$ | async" + <div *ngFor="let feature of voiFeatures.features$ | async" mat-ripple (click)="showDataset(feature)" class="sxplr-custom-cmp hoverable w-100 overflow-hidden text-overflow-ellipses"> - {{ feature.name }} + {{ feature.metadata.fullName }} </div> </mat-card> </ng-template> </ng-template> <div class="d-none" - sii-xp-spatial-feat-bbox - [sii-xp-spatial-feat-bbox-atlas-id]="selectedAtlas$ | async | getProperty : '@id'" - [sii-xp-spatial-feat-bbox-space-id]="templateSelected$ | async | getProperty : '@id'" - [sii-xp-spatial-feat-bbox-bbox-spec]="viewerCtx$ | async | nehubaVCtxToBbox" - #spatialFeatureBbox="siiXpSpatialFeatBbox"> + sxplr-sapiviews-core-space-boundingbox + [sxplr-sapiviews-core-space-boundingbox-atlas]="selectedAtlas$ | async" + [sxplr-sapiviews-core-space-boundingbox-space]="templateSelected$ | async" + [sxplr-sapiviews-core-space-boundingbox-spec]="viewerCtx$ | async | nehubaVCtxToBbox" + #bbox="sxplrSapiViewsCoreSpaceBoundingBox" + sxplr-sapiviews-features-voi-query + [sxplr-sapiviews-features-voi-query-atlas]="selectedAtlas$ | async" + [sxplr-sapiviews-features-voi-query-space]="templateSelected$ | async" + [sxplr-sapiviews-features-voi-query-bbox]="bbox.bbox$ | async | getProperty : 'bbox'" + (sxplr-sapiviews-features-voi-query-onclick)="showDataset($event)" + #voiFeatures="sxplrSapiViewsFeaturesVoiQuery"> + </div>