From 589ef9646d38fb36f6a8134ba68f552d5c7df9cd Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Thu, 31 Mar 2022 21:30:54 +0200
Subject: [PATCH] feat: added axisalignedboundingbox to annotation feat: added
 onhover function to annotation layer chore: formalised spatialFeatureBBox ->
 voiQuery + bbox chore: moved floating ui from atlas-viewer -> viewercmp
 chore: removed floating container bugfix: do not remove baselayer/colormap in
 nehuba effects bugfix: nehuba also show custom layers

---
 .../annotations/annotation.service.ts         |  58 +++++-
 src/atlasComponents/annotations/index.ts      |   2 +-
 .../spatialFeatureBBox.directive.ts           | 115 ------------
 src/atlasComponents/sapi/index.ts             |   1 -
 src/atlasComponents/sapi/module.ts            |   3 -
 src/atlasComponents/sapi/type.ts              |   1 +
 .../core/space/boundingBox.directive.ts       |  81 +++++++++
 .../sapiViews/core/space/module.ts            |   3 +
 .../sapiViews/features/module.ts              |   4 +
 .../sapiViews/features/voi/index.ts           |   2 +
 .../sapiViews/features/voi/module.ts          |  19 ++
 .../features/voi/voiQuery.directive.ts        | 169 ++++++++++++++++++
 .../userAnnotations/tools/service.ts          |  40 ++---
 src/atlasViewer/atlasViewer.component.ts      |   1 -
 src/atlasViewer/atlasViewer.style.css         |  58 ------
 src/atlasViewer/atlasViewer.template.html     |  57 +-----
 src/layouts/floating/floating.component.ts    |  15 --
 src/layouts/floating/floating.style.css       |  15 --
 src/layouts/floating/floating.template.html   |   2 -
 src/layouts/layout.module.ts                  |   3 -
 src/main.module.ts                            |   6 -
 .../logoContainer/logoContainer.component.ts  |   2 +-
 src/ui/ui.module.ts                           |   5 -
 .../directives/floatingContainer.directive.ts |  15 --
 src/viewerModule/module.ts                    |   6 +
 .../layerCtrl.service/layerCtrl.effects.ts    |  62 ++++++-
 .../layerCtrl.service/layerCtrl.service.ts    |  54 +++---
 .../nehuba.layoutOverlay.template.html        |   9 +-
 .../viewerCmp/viewerCmp.component.ts          |   4 +-
 .../viewerCmp/viewerCmp.style.css             |  29 +++
 .../viewerCmp/viewerCmp.template.html         |  99 ++++++++--
 31 files changed, 552 insertions(+), 388 deletions(-)
 delete mode 100644 src/atlasComponents/sapi/directives/spatialFeatureBBox.directive.ts
 create mode 100644 src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts
 create mode 100644 src/atlasComponents/sapiViews/features/voi/index.ts
 create mode 100644 src/atlasComponents/sapiViews/features/voi/module.ts
 create mode 100644 src/atlasComponents/sapiViews/features/voi/voiQuery.directive.ts
 delete mode 100644 src/layouts/floating/floating.component.ts
 delete mode 100644 src/layouts/floating/floating.style.css
 delete mode 100644 src/layouts/floating/floating.template.html
 delete mode 100644 src/util/directives/floatingContainer.directive.ts

diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts
index 471e628bb..bf9d18fd9 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 651584158..da271a636 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 bdd15c9b2..000000000
--- 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 7e00877e9..4c890621d 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 32701a9c5..a64cc8bc8 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 97519d6fd..27574a962 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 000000000..3d592f397
--- /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 a55902c11..35b96ce90 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 6ee7b852c..df02a0492 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 000000000..55b2fbc01
--- /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 000000000..e9817aaca
--- /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 000000000..21f0225f9
--- /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 1381520da..e998c9c53 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 7cf6952ae..055079aa1 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 0f938ac66..24a3efe3d 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 0e1a5b245..e62408b36 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 bb8ec5176..000000000
--- 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 0cb15ee43..000000000
--- 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 d7b4509bd..000000000
--- 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 0923ccb50..40cedc4f1 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 de2ee0525..c468717ba 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 105e07262..f07321c93 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 64536c949..6e66aeefe 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 4ff9eb3b1..000000000
--- 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 a485a1157..179053b1d 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 934f5ba8d..33e41b96c 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 2f86c53b2..4792776ad 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 31fab82cd..bd018eafd 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 4d9e1e3e7..e9f6ac785 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 4d6407b91..671fef08c 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 7e1a9b889..95c9168e3 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>
-- 
GitLab