From 4156a3034010c1eee714a75da5271001bf612e4b Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Thu, 22 Oct 2020 11:53:43 +0200
Subject: [PATCH] bugfix: viewer control not correctly setup

fixes #680
also relevant: #682
---
 common/constants.js                           |   1 +
 src/services/state/ngViewerState.store.ts     |  11 +-
 src/services/state/ngViewerState/selectors.ts |  15 ++
 src/ui/config/config.component.ts             |  13 +-
 .../currentLayout/currentLayout.component.ts  |   4 +-
 .../maximisePanelButton.component.ts          |   7 +-
 .../nehubaContainer.component.spec.ts         | 219 +++++++++++++++++-
 .../nehubaContainer.component.ts              |  22 +-
 .../nehubaContainer.template.html             |  15 +-
 .../nehubaViewerInterface.directive.ts        |   4 +-
 .../touchSideClass.directive.ts               |   4 +-
 src/util/pipes/getNthElement.pipe.ts          |  11 +
 src/util/pipes/parseAsNumber.pipe.ts          |  12 +
 src/util/util.module.ts                       |  10 +-
 14 files changed, 305 insertions(+), 43 deletions(-)
 create mode 100644 src/util/pipes/getNthElement.pipe.ts
 create mode 100644 src/util/pipes/parseAsNumber.pipe.ts

diff --git a/common/constants.js b/common/constants.js
index d94ab3a30..1849b731b 100644
--- a/common/constants.js
+++ b/common/constants.js
@@ -23,6 +23,7 @@
     // overlay/layout specific
     SELECT_ATLAS: 'Select a different atlas',
     CONTEXT_MENU: `Viewer context menu`,
+    TOGGLE_FRONTAL_OCTANT: `Toggle perspective view frontal octant`,
     ZOOM_IN: 'Zoom in',
     ZOOM_OUT: 'Zoom out',
     MAXIMISE_VIEW: 'Maximise this view',
diff --git a/src/services/state/ngViewerState.store.ts b/src/services/state/ngViewerState.store.ts
index 2f708a826..8a9136dc4 100644
--- a/src/services/state/ngViewerState.store.ts
+++ b/src/services/state/ngViewerState.store.ts
@@ -12,6 +12,7 @@ import { PureContantService } from 'src/util';
 import { PANELS } from './ngViewerState.store.helper'
 import { ngViewerActionToggleMax, ngViewerActionClearView, ngViewerActionSetPanelOrder, ngViewerActionSwitchPanelMode, ngViewerActionForceShowSegment, ngViewerActionNehubaReady } from './ngViewerState/actions';
 import { generalApplyState } from '../stateStore.helper';
+import { ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from './ngViewerState/selectors';
 
 export function mixNgLayers(oldLayers: INgLayerInterface[], newLayers: INgLayerInterface|INgLayerInterface[]): INgLayerInterface[] {
   if (newLayers instanceof Array) {
@@ -233,14 +234,12 @@ export class NgViewerUseEffect implements OnDestroy {
     )
 
     this.panelOrder$ = this.store$.pipe(
-      select('ngViewerState'),
-      select('panelOrder'),
+      select(ngViewerSelectorPanelOrder),
       distinctUntilChanged(),
     )
 
     this.panelMode$ = this.store$.pipe(
-      select('ngViewerState'),
-      select('panelMode'),
+      select(ngViewerSelectorPanelMode),
       distinctUntilChanged(),
     )
 
@@ -258,10 +257,10 @@ export class NgViewerUseEffect implements OnDestroy {
 
     this.maximiseOrder$ = toggleMaxmimise$.pipe(
       withLatestFrom(
-        combineLatest(
+        combineLatest([
           this.panelOrder$,
           this.panelMode$,
-        ),
+        ]),
       ),
       filter(([_action, [_panelOrder, panelMode]]) => panelMode !== PANELS.SINGLE_PANEL),
       map(([ action, [ oldPanelOrder ] ]) => {
diff --git a/src/services/state/ngViewerState/selectors.ts b/src/services/state/ngViewerState/selectors.ts
index 85b992c44..ddfa9f306 100644
--- a/src/services/state/ngViewerState/selectors.ts
+++ b/src/services/state/ngViewerState/selectors.ts
@@ -15,3 +15,18 @@ export const ngViewerSelectorClearView = createSelector(
   ngViewerSelectorClearViewEntries,
   keys => keys.length > 0
 )
+
+export const ngViewerSelectorPanelOrder = createSelector(
+  state => state['ngViewerState'],
+  ngViewerState => ngViewerState.panelOrder
+)
+
+export const ngViewerSelectorPanelMode = createSelector(
+  state => state['ngViewerState'],
+  ngViewerState => ngViewerState.panelMode
+)
+
+export const ngViewerSelectorOctantRemoval = createSelector(
+  state => state['ngViewerState'],
+  ngViewerState => ngViewerState.octantRemoval
+)
diff --git a/src/ui/config/config.component.ts b/src/ui/config/config.component.ts
index 756c15d1a..36f9e9e0d 100644
--- a/src/ui/config/config.component.ts
+++ b/src/ui/config/config.component.ts
@@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
 import { select, Store } from '@ngrx/store';
 import { combineLatest, Observable, Subscription } from 'rxjs';
 import { debounceTime, distinctUntilChanged, map, startWith } from 'rxjs/operators';
-import { NG_VIEWER_ACTION_TYPES, SUPPORTED_PANEL_MODES } from 'src/services/state/ngViewerState.store';
+import { SUPPORTED_PANEL_MODES } from 'src/services/state/ngViewerState.store';
 import { ngViewerActionSetPanelOrder } from 'src/services/state/ngViewerState.store.helper';
 import { VIEWER_CONFIG_ACTION_TYPES, StateInterface as ViewerConfiguration } from 'src/services/state/viewerConfig.store'
 import { IavRootStoreInterface } from 'src/services/stateStore.service';
@@ -11,6 +11,7 @@ import {MatSlideToggleChange} from "@angular/material/slide-toggle";
 import {MatSliderChange} from "@angular/material/slider";
 import { PureContantService } from 'src/util';
 import { ngViewerActionSwitchPanelMode } from 'src/services/state/ngViewerState/actions';
+import { ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from 'src/services/state/ngViewerState/selectors';
 
 const GPU_TOOLTIP = `Higher GPU usage can cause crashes on lower end machines`
 const ANIMATION_TOOLTIP = `Animation can cause slowdowns in lower end machines`
@@ -73,14 +74,12 @@ export class ConfigComponent implements OnInit, OnDestroy {
     )
 
     this.panelMode$ = this.store.pipe(
-      select('ngViewerState'),
-      select('panelMode'),
+      select(ngViewerSelectorPanelMode),
       startWith(SUPPORTED_PANEL_MODES[0]),
     )
 
     this.panelOrder$ = this.store.pipe(
-      select('ngViewerState'),
-      select('panelOrder'),
+      select(ngViewerSelectorPanelOrder),
     )
 
     this.viewerObliqueRotated$ = this.store.pipe(
@@ -93,12 +92,12 @@ export class ConfigComponent implements OnInit, OnDestroy {
       distinctUntilChanged(),
     )
 
-    this.panelTexts$ = combineLatest(
+    this.panelTexts$ = combineLatest([
       this.panelOrder$.pipe(
         map(string => string.split('').map(s => Number(s))),
       ),
       this.viewerObliqueRotated$,
-    ).pipe(
+    ]).pipe(
       map(([arr, isObliqueRotated]) => arr.map(idx => (isObliqueRotated ? OBLIQUE_ROOT_TEXT_ORDER : ROOT_TEXT_ORDER)[idx]) as [string, string, string, string]),
       startWith(ROOT_TEXT_ORDER),
     )
diff --git a/src/ui/config/currentLayout/currentLayout.component.ts b/src/ui/config/currentLayout/currentLayout.component.ts
index 40e8e8c49..313f1cd93 100644
--- a/src/ui/config/currentLayout/currentLayout.component.ts
+++ b/src/ui/config/currentLayout/currentLayout.component.ts
@@ -3,6 +3,7 @@ import { select, Store } from "@ngrx/store";
 import { Observable } from "rxjs";
 import { startWith } from "rxjs/operators";
 import { SUPPORTED_PANEL_MODES } from "src/services/state/ngViewerState.store";
+import { ngViewerSelectorPanelMode } from "src/services/state/ngViewerState/selectors";
 import { IavRootStoreInterface } from "src/services/stateStore.service";
 
 @Component({
@@ -22,8 +23,7 @@ export class CurrentLayout {
     private store$: Store<IavRootStoreInterface>,
   ) {
     this.panelMode$ = this.store$.pipe(
-      select('ngViewerState'),
-      select('panelMode'),
+      select(ngViewerSelectorPanelMode),
       startWith(SUPPORTED_PANEL_MODES[0]),
     )
   }
diff --git a/src/ui/nehubaContainer/maximisePanelButton/maximisePanelButton.component.ts b/src/ui/nehubaContainer/maximisePanelButton/maximisePanelButton.component.ts
index 36b2d9e88..b3e5745f9 100644
--- a/src/ui/nehubaContainer/maximisePanelButton/maximisePanelButton.component.ts
+++ b/src/ui/nehubaContainer/maximisePanelButton/maximisePanelButton.component.ts
@@ -4,6 +4,7 @@ import { Observable } from "rxjs";
 import { distinctUntilChanged, map } from "rxjs/operators";
 import { PANELS } from 'src/services/state/ngViewerState.store.helper'
 import { ARIA_LABELS } from 'common/constants'
+import { ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from "src/services/state/ngViewerState/selectors";
 
 const {
   MAXIMISE_VIEW,
@@ -34,14 +35,12 @@ export class MaximmisePanelButton {
     private store$: Store<any>,
   ) {
     this.panelMode$ = this.store$.pipe(
-      select('ngViewerState'),
-      select('panelMode'),
+      select(ngViewerSelectorPanelMode),
       distinctUntilChanged(),
     )
 
     this.panelOrder$ = this.store$.pipe(
-      select('ngViewerState'),
-      select('panelOrder'),
+      select(ngViewerSelectorPanelOrder),
       distinctUntilChanged(),
     )
 
diff --git a/src/ui/nehubaContainer/nehubaContainer.component.spec.ts b/src/ui/nehubaContainer/nehubaContainer.component.spec.ts
index 0d3f098c0..966569fe9 100644
--- a/src/ui/nehubaContainer/nehubaContainer.component.spec.ts
+++ b/src/ui/nehubaContainer/nehubaContainer.component.spec.ts
@@ -1,5 +1,5 @@
-import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
-import { TestBed, async } from "@angular/core/testing"
+import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'
+import { TestBed, async, ComponentFixture, fakeAsync, tick, flush, discardPeriodicTasks } from "@angular/core/testing"
 import { NehubaContainer } from "./nehubaContainer.component"
 import { provideMockStore, MockStore } from "@ngrx/store/testing"
 import { defaultRootState } from 'src/services/stateStore.service'
@@ -39,11 +39,16 @@ import { ARIA_LABELS } from 'common/constants'
 import { NoopAnimationsModule } from '@angular/platform-browser/animations'
 import { RegionAccordionTooltipTextPipe } from '../util'
 import { hot } from 'jasmine-marbles'
+import { ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from 'src/services/state/ngViewerState/selectors'
+import { PANELS } from 'src/services/state/ngViewerState/constants'
 
 const { 
   TOGGLE_SIDE_PANEL,
   EXPAND,
-  COLLAPSE
+  COLLAPSE,
+  ZOOM_IN,
+  ZOOM_OUT,
+  TOGGLE_FRONTAL_OCTANT
 } = ARIA_LABELS
 
 const _bigbrainJson = require('!json-loader!src/res/ext/bigbrain.json')
@@ -420,5 +425,211 @@ describe('> nehubaContainer.component.ts', () => {
         it('> if something (region features/connectivity) exists, placeh holder text should be hdiden')
       })
     })
+  
+    describe('> panelCtrl', () => {
+      let fixture: ComponentFixture<NehubaContainer>
+      const setViewerLoaded = () => {
+        fixture.componentInstance.viewerLoaded = true
+      }
+      const ctrlElementIsVisible = (el: DebugElement) => {
+        const visible = (el.nativeElement as HTMLElement).getAttribute('data-viewer-controller-visible')
+        return visible === 'true'
+      }
+      beforeEach(() => {
+        fixture = TestBed.createComponent(NehubaContainer)
+      })
+      it('> on start, all four ctrl panels exists', () => {
+        fixture.detectChanges()
+        setViewerLoaded()
+        fixture.detectChanges()
+        for (const idx of [0, 1, 2, 3]) {
+          const el = fixture.debugElement.query(
+            By.css(`[data-viewer-controller-index="${idx}"]`)
+          )
+          expect(el).toBeTruthy()
+        }
+      })
+
+      it('> on start all four ctrl panels are invisible', () => {
+        
+        fixture.detectChanges()
+        setViewerLoaded()
+        fixture.detectChanges()
+        for (const idx of [0, 1, 2, 3]) {
+          const el = fixture.debugElement.query(
+            By.css(`[data-viewer-controller-index="${idx}"]`)
+          )
+          expect(ctrlElementIsVisible(el)).toBeFalsy()
+        }
+      })
+
+      describe('> on hover, only the hovered panel have ctrl shown', () => {
+
+        for (const idx of [0, 1, 2, 3]) {
+          
+          it(`> on hoveredPanelIndices$ emit ${idx}, the panel index ${idx} ctrl becomes visible`, fakeAsync(() => {
+            fixture.detectChanges()
+            const findPanelIndexSpy = spyOn<any>(fixture.componentInstance, 'findPanelIndex').and.callFake(() => {
+              return idx
+            })
+            setViewerLoaded()
+            fixture.detectChanges()
+            const nativeElement = fixture.componentInstance['elementRef'].nativeElement
+            nativeElement.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
+  
+            /**
+             * assert findPanelIndex called with event.target, i.e. native element in thsi case
+             */
+            expect(findPanelIndexSpy).toHaveBeenCalledWith(nativeElement)
+            tick(200)
+            fixture.detectChanges()
+            
+            /**
+             * every panel index should be non visible
+             * only when idx matches, it can be visible
+             * n.b. this does not test visual visibility (which is controlled by extra-style.css)
+             * (which is also affected by global [ismobile] configuration)
+             * 
+             * this merely test the unit logic, and sets the flag appropriately
+             */
+            for (const iterativeIdx of [0, 1, 2, 3]) {
+              const el = fixture.debugElement.query(
+                By.css(`[data-viewer-controller-index="${iterativeIdx}"]`)
+              )
+              if (iterativeIdx === idx) {
+                expect(ctrlElementIsVisible(el)).toBeTruthy()
+              } else {
+                expect(ctrlElementIsVisible(el)).toBeFalsy()
+              }
+            }
+            discardPeriodicTasks()
+          }))
+        }
+  
+      })
+
+      describe('> on maximise top right slice panel (idx 1)', () => {
+        beforeEach(() => {
+          const mockStore = TestBed.inject(MockStore)
+          mockStore.overrideSelector(ngViewerSelectorPanelMode, PANELS.SINGLE_PANEL)
+          mockStore.overrideSelector(ngViewerSelectorPanelOrder, '1230')
+
+          fixture.detectChanges()
+          setViewerLoaded()
+          fixture.detectChanges()
+        })
+        it('> toggle front octant btn not visible', () => {
+
+          const toggleBtn = fixture.debugElement.query(
+            By.css(`[cell-i] [aria-label="${TOGGLE_FRONTAL_OCTANT}"]`)
+          )
+          expect(toggleBtn).toBeFalsy()
+        })
+
+        it('> zoom in and out btns are visible', () => {
+
+          const zoomInBtn = fixture.debugElement.query(
+            By.css(`[cell-i] [aria-label="${ZOOM_IN}"]`)
+          )
+
+          const zoomOutBtn = fixture.debugElement.query(
+            By.css(`[cell-i] [aria-label="${ZOOM_OUT}"]`)
+          )
+
+          expect(zoomInBtn).toBeTruthy()
+          expect(zoomOutBtn).toBeTruthy()
+        })
+
+        it('> zoom in btn calls fn with right param', () => {
+          const zoomViewSpy = spyOn(fixture.componentInstance, 'zoomNgView')
+
+          const zoomInBtn = fixture.debugElement.query(
+            By.css(`[cell-i] [aria-label="${ZOOM_IN}"]`)
+          )
+          zoomInBtn.triggerEventHandler('click', null)
+          expect(zoomViewSpy).toHaveBeenCalled()
+          const { args } = zoomViewSpy.calls.first()
+          expect(args[0]).toEqual(1)
+          /**
+           * zoom in < 1
+           */
+          expect(args[1]).toBeLessThan(1)
+        })
+        it('> zoom out btn calls fn with right param', () => {
+          const zoomViewSpy = spyOn(fixture.componentInstance, 'zoomNgView')
+
+          const zoomOutBtn = fixture.debugElement.query(
+            By.css(`[cell-i] [aria-label="${ZOOM_OUT}"]`)
+          )
+          zoomOutBtn.triggerEventHandler('click', null)
+          expect(zoomViewSpy).toHaveBeenCalled()
+          const { args } = zoomViewSpy.calls.first()
+          expect(args[0]).toEqual(1)
+          /**
+           * zoom out > 1
+           */
+          expect(args[1]).toBeGreaterThan(1)
+        })
+      })
+      describe('> on maximise perspective panel', () => {
+        beforeEach(() => {
+          const mockStore = TestBed.inject(MockStore)
+          mockStore.overrideSelector(ngViewerSelectorPanelMode, PANELS.SINGLE_PANEL)
+          mockStore.overrideSelector(ngViewerSelectorPanelOrder, '3012')
+
+          fixture.detectChanges()
+          setViewerLoaded()
+          fixture.detectChanges()
+        })
+        it('> toggle octant btn visible and functional', () => {
+          const setOctantRemovalSpy = spyOn(fixture.componentInstance, 'setOctantRemoval')
+
+          const toggleBtn = fixture.debugElement.query(
+            By.css(`[cell-i] [aria-label="${TOGGLE_FRONTAL_OCTANT}"]`)
+          )
+          expect(toggleBtn).toBeTruthy()
+          toggleBtn.nativeElement.dispatchEvent(
+            new MouseEvent('click', { bubbles: true })
+          )
+          expect(setOctantRemovalSpy).toHaveBeenCalled()
+        })
+
+        it('> zoom in btn visible and functional', () => {
+          const zoomViewSpy = spyOn(fixture.componentInstance, 'zoomNgView')
+
+          const zoomInBtn = fixture.debugElement.query(
+            By.css(`[cell-i] [aria-label="${ZOOM_IN}"]`)
+          )
+          expect(zoomInBtn).toBeTruthy()
+
+          zoomInBtn.triggerEventHandler('click', null)
+          expect(zoomViewSpy).toHaveBeenCalled()
+          const { args } = zoomViewSpy.calls.first()
+          expect(args[0]).toEqual(3)
+          /**
+           * zoom in < 1
+           */
+          expect(args[1]).toBeLessThan(1)
+        })
+        it('> zoom out btn visible and functional', () => {
+          const zoomViewSpy = spyOn(fixture.componentInstance, 'zoomNgView')
+
+          const zoomOutBtn = fixture.debugElement.query(
+            By.css(`[cell-i] [aria-label="${ZOOM_OUT}"]`)
+          )
+          expect(zoomOutBtn).toBeTruthy()
+
+          zoomOutBtn.triggerEventHandler('click', null)
+          expect(zoomViewSpy).toHaveBeenCalled()
+          const { args } = zoomViewSpy.calls.first()
+          expect(args[0]).toEqual(3)
+          /**
+           * zoom in < 1
+           */
+          expect(args[1]).toBeGreaterThan(1)
+        })
+      
+      })
+    })
   })
-})
\ No newline at end of file
+})
diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts
index 7f0e469ac..319d2be8b 100644
--- a/src/ui/nehubaContainer/nehubaContainer.component.ts
+++ b/src/ui/nehubaContainer/nehubaContainer.component.ts
@@ -38,6 +38,7 @@ import { NehubaViewerContainerDirective } from "./nehubaViewerInterface/nehubaVi
 import { ITunableProp } from "./mobileOverlay/mobileOverlay.component";
 import {ConnectivityBrowserComponent} from "src/ui/connectivityBrowser/connectivityBrowser.component";
 import { viewerStateMouseOverCustomLandmark } from "src/services/state/viewerState/actions";
+import { ngViewerSelectorOctantRemoval, ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from "src/services/state/ngViewerState/selectors";
 
 const { MESH_LOADING_STATUS } = IDS
 
@@ -97,10 +98,11 @@ const sortByFreshness: (acc: any[], curr: any[]) => any[] = (acc, curr) => {
 const {
   ZOOM_IN,
   ZOOM_OUT,
+  TOGGLE_FRONTAL_OCTANT,
   TOGGLE_SIDE_PANEL,
   EXPAND,
   COLLAPSE,
-  ADDITIONAL_VOLUME_CONTROL
+  ADDITIONAL_VOLUME_CONTROL,
 } = ARIA_LABELS
 
 @Component({
@@ -149,6 +151,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy {
   public CONST = CONST
   public ARIA_LABEL_ZOOM_IN = ZOOM_IN
   public ARIA_LABEL_ZOOM_OUT = ZOOM_OUT
+  public ARIA_LABEL_TOGGLE_FRONTAL_OCTANT = TOGGLE_FRONTAL_OCTANT
   public ARIA_LABEL_TOGGLE_SIDE_PANEL = TOGGLE_SIDE_PANEL
   public ARIA_LABEL_EXPAND = EXPAND
   public ARIA_LABEL_COLLAPSE = COLLAPSE
@@ -299,20 +302,17 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy {
     this.useMobileUI$ = this.pureConstantService.useTouchUI$
 
     this.nehubaViewerPerspectiveOctantRemoval$ = this.store.pipe(
-      select('ngViewerState'),
-      select('octantRemoval')
+      select(ngViewerSelectorOctantRemoval),
     )
 
     this.panelMode$ = this.store.pipe(
-      select('ngViewerState'),
-      select('panelMode'),
+      select(ngViewerSelectorPanelMode),
       distinctUntilChanged(),
       shareReplay(1),
     )
 
     this.panelOrder$ = this.store.pipe(
-      select('ngViewerState'),
-      select('panelOrder'),
+      select(ngViewerSelectorPanelOrder),
       distinctUntilChanged(),
       shareReplay(1),
     )
@@ -322,10 +322,10 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy {
       select('nehubaReady'),
       distinctUntilChanged(),
       filter(v => !!v),
-      switchMapTo(combineLatest(
+      switchMapTo(combineLatest([
         this.panelMode$,
         this.panelOrder$,
-      )),
+      ])),
     )
 
     this.selectedLandmarks$ = this.store.pipe(
@@ -661,7 +661,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy {
 
     this.subscriptions.push(
 
-      combineLatest(
+      combineLatest([
         this.selectedRegions$.pipe(
           distinctUntilChanged(),
         ),
@@ -678,7 +678,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy {
           select('overwrittenColorMap'),
           distinctUntilChanged()
         )
-      ).pipe(
+      ]).pipe(
         delayWhen(() => timer())
       ).subscribe(([regions, hideSegmentFlag, forceShowSegment, selectedParcellation, overwrittenColorMap]) => {
         if (!this.nehubaViewer) { return }
diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html
index 6aee27181..364093d9b 100644
--- a/src/ui/nehubaContainer/nehubaContainer.template.html
+++ b/src/ui/nehubaContainer/nehubaContainer.template.html
@@ -459,7 +459,10 @@
     </div>
 
     <!-- maximise/minimise button -->
-    <ng-container *ngTemplateOutlet="panelCtrlTmpl; context: { panelIndex: 3, visible: (panelOrder$ | async | reorderPanelIndexPipe : ( hoveredPanelIndices$ | async  )) === 3 }">
+    <ng-container *ngTemplateOutlet="panelCtrlTmpl; context: {
+      panelIndex: panelOrder$ | async | getNthElement : 3 | parseAsNumber,
+      visible: (panelOrder$ | async | reorderPanelIndexPipe : ( hoveredPanelIndices$ | async )) === 3
+    }">
     </ng-container>
     
     <!-- mesh loading is still weird -->
@@ -505,7 +508,10 @@
     </nehuba-2dlandmark-unit>
 
     <!-- maximise/minimise button -->
-    <ng-container *ngTemplateOutlet="panelCtrlTmpl; context: { panelIndex: panelIndex, visible: (panelOrder$ | async | reorderPanelIndexPipe : ( hoveredPanelIndices$ | async  )) === panelIndex }">
+    <ng-container *ngTemplateOutlet="panelCtrlTmpl; context: {
+      panelIndex: panelOrder$ | async | getNthElement : panelIndex | parseAsNumber,
+      visible: (panelOrder$ | async | reorderPanelIndexPipe : ( hoveredPanelIndices$ | async )) === panelIndex
+    }">
     </ng-container>
 
     <div *ngIf="(sliceViewLoadingMain$ | async)[panelIndex]" class="loadingIndicator">
@@ -523,7 +529,9 @@
   let-visible="visible">
 
   <div class="opacity-crossfade always-show-touchdevice pe-all overlay-btn-container"
-    [ngClass]="{ onHover: visible }">
+    [ngClass]="{ onHover: visible }"
+    [attr.data-viewer-controller-visible]="visible"
+    [attr.data-viewer-controller-index]="panelIndex">
 
     <!-- perspective specific control -->
     <ng-container *ngIf="panelIndex === 3">
@@ -559,6 +567,7 @@
   <button
     (click)="setOctantRemoval(!state)"
     mat-icon-button
+    [attr.aria-label]="ARIA_LABEL_TOGGLE_FRONTAL_OCTANT"
     color="primary">
   
     <!-- octant removal is true -->
diff --git a/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts b/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts
index 74d8f4019..c4748e090 100644
--- a/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts
+++ b/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts
@@ -13,6 +13,7 @@ import { takeOnePipe } from "../nehubaContainer.component";
 import { ngViewerActionNehubaReady } from "src/services/state/ngViewerState/actions";
 import { viewerStateMouseOverCustomLandmarkInPerspectiveView } from "src/services/state/viewerState/actions";
 import { viewerStateStandAloneVolumes } from "src/services/state/viewerState/selectors";
+import { ngViewerSelectorOctantRemoval } from "src/services/state/ngViewerState/selectors";
 
 const defaultNehubaConfig = {
   "configName": "",
@@ -295,8 +296,7 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{
     )
 
     this.nehubaViewerPerspectiveOctantRemoval$ = this.store$.pipe(
-      select('ngViewerState'),
-      select('octantRemoval')
+      select(ngViewerSelectorOctantRemoval),
     )
   }
 
diff --git a/src/ui/nehubaContainer/touchSideClass.directive.ts b/src/ui/nehubaContainer/touchSideClass.directive.ts
index f10e11d71..edaa9823f 100644
--- a/src/ui/nehubaContainer/touchSideClass.directive.ts
+++ b/src/ui/nehubaContainer/touchSideClass.directive.ts
@@ -2,6 +2,7 @@ import { Directive, ElementRef, Input, OnDestroy, OnInit } from "@angular/core";
 import { select, Store } from "@ngrx/store";
 import { Observable, Subscription } from "rxjs";
 import { distinctUntilChanged, tap } from "rxjs/operators";
+import { ngViewerSelectorPanelMode } from "src/services/state/ngViewerState/selectors";
 import { IavRootStoreInterface } from "src/services/stateStore.service";
 import { addTouchSideClasses, removeTouchSideClasses } from "./util";
 
@@ -25,8 +26,7 @@ export class TouchSideClass implements OnDestroy, OnInit {
   ) {
 
     this.panelMode$ = this.store$.pipe(
-      select('ngViewerState'),
-      select('panelMode'),
+      select(ngViewerSelectorPanelMode),
       distinctUntilChanged(),
       tap(mode => this.panelMode = mode),
     )
diff --git a/src/util/pipes/getNthElement.pipe.ts b/src/util/pipes/getNthElement.pipe.ts
new file mode 100644
index 000000000..d4b27720b
--- /dev/null
+++ b/src/util/pipes/getNthElement.pipe.ts
@@ -0,0 +1,11 @@
+import { Pipe, PipeTransform } from "@angular/core";
+
+@Pipe({
+  name: 'getNthElement'
+})
+export class GetNthElementPipe<T> implements PipeTransform{
+  public transform(array: T[], idx: number): T{
+    if (!array[idx]) throw new Error(`[GetNthElementPipe] accessor error`)
+    return array[idx]
+  }
+}
diff --git a/src/util/pipes/parseAsNumber.pipe.ts b/src/util/pipes/parseAsNumber.pipe.ts
new file mode 100644
index 000000000..40688b6b9
--- /dev/null
+++ b/src/util/pipes/parseAsNumber.pipe.ts
@@ -0,0 +1,12 @@
+import { Pipe, PipeTransform } from "@angular/core";
+
+@Pipe({
+  name: 'parseAsNumber'
+})
+
+export class ParseAsNumberPipe implements PipeTransform{
+  public transform(input: string | string[]): number | number[]{
+    if (input instanceof Array) return input.map(v => Number(v))
+    return Number(input)
+  }
+}
diff --git a/src/util/util.module.ts b/src/util/util.module.ts
index 4c8bf1ce5..f29aa368e 100644
--- a/src/util/util.module.ts
+++ b/src/util/util.module.ts
@@ -16,6 +16,8 @@ import { LayoutModule } from "@angular/cdk/layout";
 import { MapToPropertyPipe } from "./pipes/mapToProperty.pipe";
 import {ClickOutsideDirective} from "src/util/directives/clickOutside.directive";
 import { CounterDirective } from "./directives/counter.directive";
+import { GetNthElementPipe } from "./pipes/getNthElement.pipe";
+import { ParseAsNumberPipe } from "./pipes/parseAsNumber.pipe";
 
 @NgModule({
   imports:[
@@ -36,7 +38,9 @@ import { CounterDirective } from "./directives/counter.directive";
     MediaQueryDirective,
     MapToPropertyPipe,
     ClickOutsideDirective,
-    CounterDirective
+    CounterDirective,
+    GetNthElementPipe,
+    ParseAsNumberPipe,
   ],
   exports: [
     FilterNullPipe,
@@ -53,7 +57,9 @@ import { CounterDirective } from "./directives/counter.directive";
     MediaQueryDirective,
     MapToPropertyPipe,
     ClickOutsideDirective,
-    CounterDirective
+    CounterDirective,
+    GetNthElementPipe,
+    ParseAsNumberPipe,
   ]
 })
 
-- 
GitLab