From 2f0c0331806d134803f1c4e0e966ba43fcbc813f Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Fri, 16 Aug 2019 16:41:32 +0200
Subject: [PATCH] experimental: redesigned viewer state controller

---
 src/components/components.module.ts           |   6 +
 .../confirmDialog/confirmDialog.component.ts  |  24 ++
 .../confirmDialog/confirmDialog.style.css     |   0
 .../confirmDialog/confirmDialog.template.html |  16 +
 src/components/dialog/dialog.component.ts     |  70 ++++
 src/components/dialog/dialog.style.css        |   0
 src/components/dialog/dialog.template.html    |  38 ++
 src/components/flatTree/flatTree.component.ts |   6 +
 .../flatTree/flatTree.template.html           |   4 +-
 src/components/sleightOfHand/soh.component.ts |  26 +-
 src/components/sleightOfHand/soh.style.css    |  10 +-
 src/main.module.ts                            |  16 +-
 src/res/css/extra_styles.css                  |  23 +-
 src/res/ext/colinNehubaConfig.json            | 180 +++++++++-
 src/services/dialogService.service.ts         |  62 ++++
 src/services/effect/effect.ts                 | 124 +++++--
 src/services/state/userConfigState.store.ts   | 340 ++++++++++++++++++
 src/services/state/viewerState.store.ts       |  11 +-
 src/services/stateStore.service.ts            |   5 +
 src/ui/layerbrowser/layerbrowser.component.ts |  59 ++-
 .../layerbrowser/layerbrowser.template.html   |  96 +++--
 src/ui/menuicons/menuicons.component.ts       | 106 +-----
 src/ui/menuicons/menuicons.style.css          |  37 +-
 src/ui/menuicons/menuicons.template.html      | 116 ++----
 .../regionHierarchy.component.ts              |  67 +---
 .../sharedModules/angularMaterial.module.ts   |  49 ++-
 .../signinBanner/signinBanner.components.ts   | 249 +------------
 .../signinBanner/signinBanner.template.html   |  56 ---
 src/ui/ui.module.ts                           |  26 +-
 .../regionHierachy/filterNameBySearch.pipe.ts |   0
 .../regionHierarchy.component.ts              | 214 +++++++++++
 .../regionHierachy/regionHierarchy.style.css  |  17 +-
 .../regionHierarchy.template.html             |  21 +-
 .../regionSearch/regionSearch.component.ts    | 149 ++++++++
 .../regionSearch/regionSearch.style.css       |   4 +
 .../regionSearch/regionSearch.template.html   |  45 +++
 .../viewerState.component.ts                  | 304 ++++++++++++++++
 .../viewerState.pipes.ts                      |  38 ++
 .../viewerState.style.css                     |  35 ++
 .../viewerState.template.html                 | 202 +++++++++++
 .../viewerState.useEffect.ts                  | 180 ++++++++++
 src/util/pipes/filterNgLayer.pipe.ts          |  18 -
 src/util/pipes/getFileExt.pipe.ts             |  36 ++
 .../pipes/getFileNameFromPathName.pipe.ts     |  12 -
 src/util/pipes/getFilename.pipe.ts            |  14 +
 45 files changed, 2375 insertions(+), 736 deletions(-)
 create mode 100644 src/components/confirmDialog/confirmDialog.component.ts
 create mode 100644 src/components/confirmDialog/confirmDialog.style.css
 create mode 100644 src/components/confirmDialog/confirmDialog.template.html
 create mode 100644 src/components/dialog/dialog.component.ts
 create mode 100644 src/components/dialog/dialog.style.css
 create mode 100644 src/components/dialog/dialog.template.html
 create mode 100644 src/services/dialogService.service.ts
 create mode 100644 src/services/state/userConfigState.store.ts
 rename src/ui/{ => viewerStateController}/regionHierachy/filterNameBySearch.pipe.ts (100%)
 create mode 100644 src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts
 rename src/ui/{ => viewerStateController}/regionHierachy/regionHierarchy.style.css (86%)
 rename src/ui/{ => viewerStateController}/regionHierachy/regionHierarchy.template.html (72%)
 create mode 100644 src/ui/viewerStateController/regionSearch/regionSearch.component.ts
 create mode 100644 src/ui/viewerStateController/regionSearch/regionSearch.style.css
 create mode 100644 src/ui/viewerStateController/regionSearch/regionSearch.template.html
 create mode 100644 src/ui/viewerStateController/viewerState.component.ts
 create mode 100644 src/ui/viewerStateController/viewerState.pipes.ts
 create mode 100644 src/ui/viewerStateController/viewerState.style.css
 create mode 100644 src/ui/viewerStateController/viewerState.template.html
 create mode 100644 src/ui/viewerStateController/viewerState.useEffect.ts
 delete mode 100644 src/util/pipes/filterNgLayer.pipe.ts
 create mode 100644 src/util/pipes/getFileExt.pipe.ts
 delete mode 100644 src/util/pipes/getFileNameFromPathName.pipe.ts
 create mode 100644 src/util/pipes/getFilename.pipe.ts

diff --git a/src/components/components.module.ts b/src/components/components.module.ts
index f6114ed86..d944ca40e 100644
--- a/src/components/components.module.ts
+++ b/src/components/components.module.ts
@@ -30,6 +30,8 @@ import { RadioList } from './radiolist/radiolist.component';
 import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module';
 import { FilterCollapsePipe } from './flatTree/filterCollapse.pipe';
 import { SleightOfHand } from './sleightOfHand/soh.component';
+import { DialogComponent } from './dialog/dialog.component';
+import { ConfirmDialogComponent } from './confirmDialog/confirmDialog.component';
 
 
 @NgModule({
@@ -54,6 +56,8 @@ import { SleightOfHand } from './sleightOfHand/soh.component';
     PillComponent,
     RadioList,
     SleightOfHand,
+    DialogComponent,
+    ConfirmDialogComponent,
 
     /* directive */
     HoverableBlockDirective,
@@ -86,6 +90,8 @@ import { SleightOfHand } from './sleightOfHand/soh.component';
     PillComponent,
     RadioList,
     SleightOfHand,
+    DialogComponent,
+    ConfirmDialogComponent,
 
     SearchResultPaginationPipe,
     TreeSearchPipe,
diff --git a/src/components/confirmDialog/confirmDialog.component.ts b/src/components/confirmDialog/confirmDialog.component.ts
new file mode 100644
index 000000000..6c16434ac
--- /dev/null
+++ b/src/components/confirmDialog/confirmDialog.component.ts
@@ -0,0 +1,24 @@
+import { Component, Inject, Input } from "@angular/core";
+import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material";
+
+@Component({
+  selector: 'confirm-dialog-component',
+  templateUrl: './confirmDialog.template.html',
+  styleUrls: [
+    './confirmDialog.style.css'
+  ]
+})
+export class ConfirmDialogComponent{
+
+  @Input()
+  public title: string = 'Confirm'
+
+  @Input()
+  public message: string = 'Would you like to proceed?'
+
+  constructor(@Inject(MAT_DIALOG_DATA) data: any){
+    const { title = null, message  = null} = data || {}
+    if (title) this.title = title
+    if (message) this.message = message
+  }
+}
\ No newline at end of file
diff --git a/src/components/confirmDialog/confirmDialog.style.css b/src/components/confirmDialog/confirmDialog.style.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/components/confirmDialog/confirmDialog.template.html b/src/components/confirmDialog/confirmDialog.template.html
new file mode 100644
index 000000000..eb0c76fff
--- /dev/null
+++ b/src/components/confirmDialog/confirmDialog.template.html
@@ -0,0 +1,16 @@
+<h1 mat-dialog-title>
+  {{ title }}
+</h1>
+
+<mat-dialog-content>
+  <p>
+    {{ message }}
+  </p>
+</mat-dialog-content>
+
+<mat-divider></mat-divider>
+
+<mat-dialog-actions class="justify-content-start flex-row-reverse">
+    <button [mat-dialog-close]="true" mat-raised-button color="primary">OK</button>
+  <button [mat-dialog-close]="false" mat-button>Cancel</button>
+</mat-dialog-actions>
\ No newline at end of file
diff --git a/src/components/dialog/dialog.component.ts b/src/components/dialog/dialog.component.ts
new file mode 100644
index 000000000..ac9607865
--- /dev/null
+++ b/src/components/dialog/dialog.component.ts
@@ -0,0 +1,70 @@
+import { Component, Input, ChangeDetectionStrategy, ViewChild, ElementRef, OnInit, OnDestroy, Inject } from "@angular/core";
+import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material";
+import { Subscription, Observable, fromEvent } from "rxjs";
+import { filter, share } from "rxjs/operators";
+
+@Component({
+  selector: 'dialog-component',
+  templateUrl: './dialog.template.html',
+  styleUrls: [
+    './dialog.style.css'
+  ],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+
+export class DialogComponent implements OnInit, OnDestroy {
+
+  private subscrptions: Subscription[] = []
+  
+  @Input() title: string = 'Message'
+  @Input() placeholder: string = "Type your response here"
+  @Input() defaultValue: string = ''
+  @Input() message: string = ''
+  @ViewChild('inputField', {read: ElementRef}) private inputField: ElementRef
+
+  private value: string = ''
+  private keyListener$: Observable<any>
+
+  constructor(
+    @Inject(MAT_DIALOG_DATA) public data:any,
+    private dialogRef: MatDialogRef<DialogComponent>
+  ){
+    const { title, placeholder, defaultValue, message } = this.data
+    if (title) this.title = title
+    if (placeholder) this.placeholder = placeholder
+    if (defaultValue) this.value = defaultValue
+    if (message) this.message = message
+  }
+
+  ngOnInit(){
+
+    this.keyListener$ = fromEvent(this.inputField.nativeElement, 'keyup').pipe(
+      filter((ev: KeyboardEvent) => ev.key === 'Enter' || ev.key === 'Esc' || ev.key === 'Escape'),
+      share()
+    )
+    this.subscrptions.push(
+      this.keyListener$.subscribe(ev => {
+        if (ev.key === 'Enter') {
+          this.dialogRef.close(this.value)
+        }
+        if (ev.key === 'Esc' || ev.key === 'Escape') {
+          this.dialogRef.close(null)
+        }
+      })
+    )
+  }
+
+  confirm(){
+    this.dialogRef.close(this.value)
+  }
+
+  cancel(){
+    this.dialogRef.close(null)
+  }
+
+  ngOnDestroy(){
+    while(this.subscrptions.length > 0) {
+      this.subscrptions.pop().unsubscribe()
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/components/dialog/dialog.style.css b/src/components/dialog/dialog.style.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/components/dialog/dialog.template.html b/src/components/dialog/dialog.template.html
new file mode 100644
index 000000000..311b6f30f
--- /dev/null
+++ b/src/components/dialog/dialog.template.html
@@ -0,0 +1,38 @@
+<h1 mat-dialog-title>
+  {{ title }}
+</h1>
+
+<div>
+  {{ message }}
+</div>
+
+<div mat-dialog-content>
+  <mat-form-field>
+    <input
+      tabindex="0"
+      [(ngModel)]="value"
+      matInput
+      [placeholder]="placeholder"
+      #inputField>
+  </mat-form-field>
+</div>
+
+<mat-divider></mat-divider>
+
+<div class="mt-2 d-flex flex-row justify-content-end">
+  <button 
+    (click)="cancel()"
+    color="primary"
+    mat-button>
+    Cancel
+  </button>
+  
+  <button
+    (click)="confirm()"
+    class="ml-1"
+    mat-raised-button
+    color="primary">
+    <i class="fas fa-save mr-1"></i>
+    Confirm
+  </button>
+</div>
\ No newline at end of file
diff --git a/src/components/flatTree/flatTree.component.ts b/src/components/flatTree/flatTree.component.ts
index d8cd1d832..5d565cd48 100644
--- a/src/components/flatTree/flatTree.component.ts
+++ b/src/components/flatTree/flatTree.component.ts
@@ -92,4 +92,10 @@ export class FlatTreeComponent implements AfterViewChecked {
       .some(id => this.isCollapsedById(id))
   }
 
+  handleTreeNodeClick(event:MouseEvent, inputItem: any){
+    this.treeNodeClick.emit({
+      event,
+      inputItem
+    })
+  }
 }
\ No newline at end of file
diff --git a/src/components/flatTree/flatTree.template.html b/src/components/flatTree/flatTree.template.html
index 48ff87a25..893c539a9 100644
--- a/src/components/flatTree/flatTree.template.html
+++ b/src/components/flatTree/flatTree.template.html
@@ -27,7 +27,7 @@
       <i [ngClass]="isCollapsed(flattenedItem) ? 'r-270' : ''" class="fas fa-chevron-down"></i>
     </span>
     <span
-      (click)="treeNodeClick.emit({event:$event,inputItem:flattenedItem})"
+      (click)="handleTreeNodeClick($event, flattenedItem)"
       class="render-node-text"
       [innerHtml]="flattenedItem | renderPipe : renderNode ">
     </span>
@@ -63,7 +63,7 @@
         <i [ngClass]="isCollapsed(flattenedItem) ? 'r-270' : ''" class="fas fa-chevron-down"></i>
       </span>
       <span
-        (click)="treeNodeClick.emit({event:$event,inputItem:flattenedItem})"
+        (click)="handleTreeNodeClick($event, flattenedItem)"
         class="render-node-text"
         [innerHtml]="flattenedItem | renderPipe : renderNode ">
       </span>
diff --git a/src/components/sleightOfHand/soh.component.ts b/src/components/sleightOfHand/soh.component.ts
index ed7d8cfab..4b6fbe5fd 100644
--- a/src/components/sleightOfHand/soh.component.ts
+++ b/src/components/sleightOfHand/soh.component.ts
@@ -1,13 +1,33 @@
-import { Component } from "@angular/core";
+import { Component, Input, HostBinding, ChangeDetectionStrategy, HostListener } from "@angular/core";
 
 @Component({
   selector: 'sleight-of-hand',
   templateUrl: './soh.template.html',
   styleUrls: [
     './soh.style.css'
-  ]
+  ],
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
 
 export class SleightOfHand{
-  
+
+  @HostBinding('class.do-not-close')
+  get doNotCloseClass(){
+    return this.doNotClose || this.focusInStatus
+  }
+
+  @HostListener('focusin')
+  focusInHandler(){
+    this.focusInStatus = true
+  }
+
+  @HostListener('focusout')
+  focusOutHandler(){
+    this.focusInStatus = false
+  }
+
+  private focusInStatus: boolean = false
+
+  @Input()
+  doNotClose: boolean = false
 }
\ No newline at end of file
diff --git a/src/components/sleightOfHand/soh.style.css b/src/components/sleightOfHand/soh.style.css
index b862eb721..2c4f61aa2 100644
--- a/src/components/sleightOfHand/soh.style.css
+++ b/src/components/sleightOfHand/soh.style.css
@@ -1,10 +1,6 @@
-:host:not(:hover) > .sleight-of-hand-back
-{
-  opacity: 0;
-  pointer-events: none;
-}
-
-:host:hover > .sleight-of-hand-front
+:host:not(.do-not-close):not(:hover) > .sleight-of-hand-back,
+:host:not(.do-not-close):hover > .sleight-of-hand-front,
+:host-context(.do-not-close) > .sleight-of-hand-front
 {
   opacity: 0;
   pointer-events: none;
diff --git a/src/main.module.ts b/src/main.module.ts
index 1fb66d55c..3820e2b00 100644
--- a/src/main.module.ts
+++ b/src/main.module.ts
@@ -5,7 +5,7 @@ import { UIModule } from "./ui/ui.module";
 import { LayoutModule } from "./layouts/layout.module";
 import { AtlasViewer } from "./atlasViewer/atlasViewer.component";
 import { StoreModule, Store, select } from "@ngrx/store";
-import { viewerState, dataStore,spatialSearchState,uiState, ngViewerState, pluginState, viewerConfigState } from "./services/stateStore.service";
+import { viewerState, dataStore,spatialSearchState,uiState, ngViewerState, pluginState, viewerConfigState, userConfigState, UserConfigStateUseEffect } from "./services/stateStore.service";
 import { GetNamesPipe } from "./util/pipes/getNames.pipe";
 import { CommonModule } from "@angular/common";
 import { GetNamePipe } from "./util/pipes/getName.pipe";
@@ -22,7 +22,6 @@ import { ModalModule } from 'ngx-bootstrap/modal'
 import { ModalUnit } from "./atlasViewer/modalUnit/modalUnit.component";
 import { AtlasViewerURLService } from "./atlasViewer/atlasViewer.urlService.service";
 import { ToastComponent } from "./components/toast/toast.component";
-import { GetFilenameFromPathnamePipe } from "./util/pipes/getFileNameFromPathName.pipe";
 import { AtlasViewerAPIServices } from "./atlasViewer/atlasViewer.apiService.service";
 import { PluginUnit } from "./atlasViewer/pluginUnit/pluginUnit.component";
 import { NewViewerDisctinctViewToLayer } from "./util/pipes/newViewerDistinctViewToLayer.pipe";
@@ -42,6 +41,10 @@ import { TransformOnhoverSegmentPipe } from "./atlasViewer/onhoverSegment.pipe";
 import {HttpClientModule} from "@angular/common/http";
 import { EffectsModule } from "@ngrx/effects";
 import { UseEffects } from "./services/effect/effect";
+import { DialogService } from "./services/dialogService.service";
+import { DialogComponent } from "./components/dialog/dialog.component";
+import { ViewerStateControllerUseEffect } from "./ui/viewerStateController/viewerState.useEffect";
+import { ConfirmDialogComponent } from "./components/confirmDialog/confirmDialog.component";
 
 @NgModule({
   imports : [
@@ -57,7 +60,9 @@ import { UseEffects } from "./services/effect/effect";
     TooltipModule.forRoot(),
     TabsModule.forRoot(),
     EffectsModule.forRoot([
-      UseEffects
+      UseEffects,
+      UserConfigStateUseEffect,
+      ViewerStateControllerUseEffect
     ]),
     StoreModule.forRoot({
       pluginState,
@@ -67,6 +72,7 @@ import { UseEffects } from "./services/effect/effect";
       dataStore,
       spatialSearchState,
       uiState,
+      userConfigState
     }),
     HttpClientModule
   ],
@@ -96,7 +102,6 @@ import { UseEffects } from "./services/effect/effect";
     GetNamesPipe,
     GetNamePipe,
     TransformOnhoverSegmentPipe,
-    GetFilenameFromPathnamePipe,
     NewViewerDisctinctViewToLayer
   ],
   entryComponents : [
@@ -104,6 +109,8 @@ import { UseEffects } from "./services/effect/effect";
     ModalUnit,
     ToastComponent,
     PluginUnit,
+    DialogComponent,
+    ConfirmDialogComponent,
   ],
   providers : [
     AtlasViewerDataService,
@@ -113,6 +120,7 @@ import { UseEffects } from "./services/effect/effect";
     ToastService,
     AtlasWorkerService,
     AuthService,
+    DialogService,
     
     /**
      * TODO
diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css
index bef52b792..c197b4d65 100644
--- a/src/res/css/extra_styles.css
+++ b/src/res/css/extra_styles.css
@@ -301,11 +301,26 @@ markdown-dom pre code
   max-width: 60%!important;
 }
 
+.mw-20em
+{
+  max-width: 20em!important;
+}
+
+.w-20em
+{
+  width: 20em!important;
+}
+
 .mh-20em
 {
   max-height: 20em;
 }
 
+.mh-10em
+{
+  max-height: 10em;
+}
+
 .pe-all
 {
   pointer-events: all;
@@ -333,7 +348,7 @@ markdown-dom pre code
 
 .overflow-x-hidden
 {
-  overflow-x:hidden;
+  overflow-x:hidden!important;
 }
 
 .muted
@@ -360,4 +375,10 @@ markdown-dom pre code
 .bs-content-box
 {
   box-sizing: content-box;
+}
+
+/* required to hide  */
+.cdk-global-scrollblock
+{
+  overflow-y:hidden !important;
 }
\ No newline at end of file
diff --git a/src/res/ext/colinNehubaConfig.json b/src/res/ext/colinNehubaConfig.json
index 7b40174f1..6028ab6b9 100644
--- a/src/res/ext/colinNehubaConfig.json
+++ b/src/res/ext/colinNehubaConfig.json
@@ -1 +1,179 @@
-{"globals":{"hideNullImageValues":true,"useNehubaLayout":true,"useNehubaMeshLayer":true,"useCustomSegmentColors":true},"zoomWithoutCtrl":true,"hideNeuroglancerUI":true,"rightClickWithCtrl":true,"rotateAtViewCentre":true,"zoomAtViewCentre":true,"enableMeshLoadingControl":true,"layout":{"useNehubaPerspective":{"fixedZoomPerspectiveSlices":{"sliceViewportWidth":300,"sliceViewportHeight":300,"sliceZoom":724698.1843689409,"sliceViewportSizeMultiplier":2},"centerToOrigin":true,"mesh":{"removeBasedOnNavigation":true,"flipRemovedOctant":true,"surfaceParcellation":false},"removePerspectiveSlicesBackground":{"mode":"=="},"waitForMesh":false,"drawSubstrates":{"color":[0.5,0.5,1,0.2]},"drawZoomLevels":{"cutOff":150000},"restrictZoomLevel":{"minZoom":2500000,"maxZoom":3500000}}},"dataset":{"imageBackground":[0,0,0,1],"initialNgState":{"showDefaultAnnotations":false,"layers":{"colin":{"type":"image","visible":true,"source":"precomputed://https://neuroglancer.humanbrainproject.org/precomputed/JuBrain/v2.2c/colin27_seg","transform":[[1,0,0,-75500000],[0,1,0,-111500000],[0,0,1,-67500000],[0,0,0,1]]},"jubrain v2_2c":{"type":"segmentation","source":"precomputed://https://neuroglancer.humanbrainproject.org/precomputed/JuBrain/v2.2c/MPM","transform":[[1,0,0,-75500000],[0,1,0,-111500000],[0,0,1,-67500000],[0,0,0,1]]},"jubrain colin v17 left":{"type":"segmentation","visible":true,"source":"precomputed://https://neuroglancer.humanbrainproject.org/precomputed/JuBrain/17/colin27/left","transform":[[1,0,0,-128500000],[0,1,0,-148500000],[0,0,1,-110500000],[0,0,0,1]]},"jubrain colin v17 right":{"type":"segmentation","visible":true,"source":"precomputed://https://neuroglancer.humanbrainproject.org/precomputed/JuBrain/17/colin27/right","transform":[[1,0,0,-128500000],[0,1,0,-148500000],[0,0,1,-110500000],[0,0,0,1]]}},"navigation":{"pose":{"position":{"voxelSize":[1000000,1000000,1000000],"voxelCoordinates":[0,-32,0]}},"zoomFactor":1000000},"perspectiveOrientation":[-0.2753947079181671,0.6631333827972412,-0.6360703706741333,0.2825356423854828],"perspectiveZoom":3000000}}}
\ No newline at end of file
+{
+  "globals": {
+    "hideNullImageValues": true,
+    "useNehubaLayout": true,
+    "useNehubaMeshLayer": true,
+    "useCustomSegmentColors": true
+  },
+  "zoomWithoutCtrl": true,
+  "hideNeuroglancerUI": true,
+  "rightClickWithCtrl": true,
+  "rotateAtViewCentre": true,
+  "zoomAtViewCentre": true,
+  "enableMeshLoadingControl": true,
+  "layout": {
+    "useNehubaPerspective": {
+      "fixedZoomPerspectiveSlices": {
+        "sliceViewportWidth": 300,
+        "sliceViewportHeight": 300,
+        "sliceZoom": 724698.1843689409,
+        "sliceViewportSizeMultiplier": 2
+      },
+      "centerToOrigin": true,
+      "mesh": {
+        "removeBasedOnNavigation": true,
+        "flipRemovedOctant": true,
+        "surfaceParcellation": false
+      },
+      "removePerspectiveSlicesBackground": {
+        "mode": "=="
+      },
+      "waitForMesh": false,
+      "drawSubstrates": {
+        "color": [
+          0.5,
+          0.5,
+          1,
+          0.2
+        ]
+      },
+      "drawZoomLevels": {
+        "cutOff": 150000
+      },
+      "restrictZoomLevel": {
+        "minZoom": 2500000,
+        "maxZoom": 3500000
+      }
+    }
+  },
+  "dataset": {
+    "imageBackground": [
+      0,
+      0,
+      0,
+      1
+    ],
+    "initialNgState": {
+      "showDefaultAnnotations": false,
+      "layers": {
+        "colin": {
+          "type": "image",
+          "visible": true,
+          "source": "precomputed://https://neuroglancer.humanbrainproject.org/precomputed/JuBrain/v2.2c/colin27_seg",
+          "transform": [
+            [
+              1,
+              0,
+              0,
+              -75500000
+            ],
+            [
+              0,
+              1,
+              0,
+              -111500000
+            ],
+            [
+              0,
+              0,
+              1,
+              -67500000
+            ],
+            [
+              0,
+              0,
+              0,
+              1
+            ]
+          ]
+        },
+        "jubrain colin v17 left": {
+          "type": "segmentation",
+          "visible": true,
+          "source": "precomputed://https://neuroglancer.humanbrainproject.org/precomputed/JuBrain/17/colin27/left",
+          "transform": [
+            [
+              1,
+              0,
+              0,
+              -128500000
+            ],
+            [
+              0,
+              1,
+              0,
+              -148500000
+            ],
+            [
+              0,
+              0,
+              1,
+              -110500000
+            ],
+            [
+              0,
+              0,
+              0,
+              1
+            ]
+          ]
+        },
+        "jubrain colin v17 right": {
+          "type": "segmentation",
+          "visible": true,
+          "source": "precomputed://https://neuroglancer.humanbrainproject.org/precomputed/JuBrain/17/colin27/right",
+          "transform": [
+            [
+              1,
+              0,
+              0,
+              -128500000
+            ],
+            [
+              0,
+              1,
+              0,
+              -148500000
+            ],
+            [
+              0,
+              0,
+              1,
+              -110500000
+            ],
+            [
+              0,
+              0,
+              0,
+              1
+            ]
+          ]
+        }
+      },
+      "navigation": {
+        "pose": {
+          "position": {
+            "voxelSize": [
+              1000000,
+              1000000,
+              1000000
+            ],
+            "voxelCoordinates": [
+              0,
+              -32,
+              0
+            ]
+          }
+        },
+        "zoomFactor": 1000000
+      },
+      "perspectiveOrientation": [
+        -0.2753947079181671,
+        0.6631333827972412,
+        -0.6360703706741333,
+        0.2825356423854828
+      ],
+      "perspectiveZoom": 3000000
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/services/dialogService.service.ts b/src/services/dialogService.service.ts
new file mode 100644
index 000000000..110edf666
--- /dev/null
+++ b/src/services/dialogService.service.ts
@@ -0,0 +1,62 @@
+import { Injectable } from "@angular/core";
+import { MatDialog, MatDialogRef } from "@angular/material";
+import { DialogComponent } from "src/components/dialog/dialog.component";
+import { ConfirmDialogComponent } from "src/components/confirmDialog/confirmDialog.component";
+
+
+@Injectable({
+  providedIn: 'root'
+})
+
+export class DialogService{
+
+  private dialogRef: MatDialogRef<DialogComponent>
+  private confirmDialogRef: MatDialogRef<ConfirmDialogComponent>
+
+  constructor(private dialog:MatDialog){
+
+  }
+
+  public getUserConfirm(config: Partial<DialogConfig> = {}): Promise<string>{
+    this.confirmDialogRef = this.dialog.open(ConfirmDialogComponent, {
+      data: config
+    })
+    return new Promise((resolve, reject) => this.confirmDialogRef.afterClosed()
+      .subscribe(val => {
+        if (val) resolve()
+        else reject('User cancelled')
+      },
+      reject,
+      () => this.confirmDialogRef = null))
+  }
+
+  public getUserInput(config: Partial<DialogConfig> = {}):Promise<string>{
+    const { defaultValue = '', placeholder = 'Type your response here', title = 'Message', message = '' } = config
+    this.dialogRef = this.dialog.open(DialogComponent, {
+      data: {
+        title,
+        placeholder,
+        defaultValue,
+        message
+      }
+    })
+    return new Promise((resolve, reject) => {
+      /**
+       * nb: one one value is ever emitted, then the subscription ends
+       * Should not result in leak
+       */
+      this.dialogRef.afterClosed().subscribe(value => {
+        if (value) resolve(value)
+        else reject('User cancelled input')
+        this.dialogRef = null
+      })
+    })
+  }
+}
+
+export interface DialogConfig{
+  title: string
+  placeholder: string
+  defaultValue: string
+  message: string
+}
\ No newline at end of file
diff --git a/src/services/effect/effect.ts b/src/services/effect/effect.ts
index 7ab1195be..603ed7be8 100644
--- a/src/services/effect/effect.ts
+++ b/src/services/effect/effect.ts
@@ -1,9 +1,9 @@
 import { Injectable, OnDestroy } from "@angular/core";
 import { Effect, Actions, ofType } from "@ngrx/effects";
 import { Subscription, merge, fromEvent, combineLatest, Observable } from "rxjs";
-import { withLatestFrom, map, filter, shareReplay } from "rxjs/operators";
+import { withLatestFrom, map, filter, shareReplay, tap, switchMap, take } from "rxjs/operators";
 import { Store, select } from "@ngrx/store";
-import { SELECT_PARCELLATION, SELECT_REGIONS, NEWVIEWER, UPDATE_PARCELLATION, SELECT_REGIONS_WITH_ID, DESELECT_REGIONS } from "../state/viewerState.store";
+import { SELECT_PARCELLATION, SELECT_REGIONS, NEWVIEWER, UPDATE_PARCELLATION, SELECT_REGIONS_WITH_ID, DESELECT_REGIONS, ADD_TO_REGIONS_SELECTION_WITH_IDS } from "../state/viewerState.store";
 import { worker } from 'src/atlasViewer/atlasViewer.workerService.service'
 import { getNgIdLabelIndexFromId, generateLabelIndexId, recursiveFindRegionWithLabelIndexId } from '../stateStore.service';
 
@@ -44,6 +44,27 @@ export class UseEffects implements OnDestroy{
         }
       })
     )
+
+    this.addToSelectedRegions$ = this.actions$.pipe(
+      ofType(ADD_TO_REGIONS_SELECTION_WITH_IDS),
+      map(action => {
+        const { selectRegionIds } = action
+        return selectRegionIds
+      }),
+      switchMap(selectRegionIds => this.updatedParcellation$.pipe(
+        filter(p => !!p),
+        take(1),
+        map(p => [selectRegionIds, p])
+      )),
+      map(this.convertRegionIdsToRegion),
+      withLatestFrom(this.regionsSelected$),
+      map(([ selectedRegions, alreadySelectedRegions ]) => {
+        return {
+          type: SELECT_REGIONS,
+          selectRegions: this.removeDuplicatedRegions(selectedRegions, alreadySelectedRegions)
+        }
+      })
+    )
   }
 
   private regionsSelected$: Observable<any[]>
@@ -75,48 +96,76 @@ export class UseEffects implements OnDestroy{
   private updatedParcellation$ = this.store$.pipe(
     select('viewerState'),
     select('parcellationSelected'),
-    filter(p => !!p && !!p.regions)
+    map(p => p.updated ? p : null),
+    shareReplay(1)
   )
 
   @Effect()
   onDeselectRegions: Observable<any> 
 
+  private convertRegionIdsToRegion = ([selectRegionIds, parcellation]) => {
+    const { ngId: defaultNgId } = parcellation
+    return (<any[]>selectRegionIds)
+      .map(labelIndexId => getNgIdLabelIndexFromId({ labelIndexId }))
+      .map(({ ngId, labelIndex }) => {
+        return {
+          labelIndexId: generateLabelIndexId({
+            ngId: ngId || defaultNgId,
+            labelIndex 
+          })
+        }
+      })
+      .map(({ labelIndexId }) => {
+        return recursiveFindRegionWithLabelIndexId({ 
+          regions: parcellation.regions,
+          labelIndexId,
+          inheritedNgId: defaultNgId
+        })
+      })
+      .filter(v => {
+        if (!v) {
+          console.log(`SELECT_REGIONS_WITH_ID, some ids cannot be parsed intto label index`)
+        }
+        return !!v
+      })
+  }
+
+  private removeDuplicatedRegions = (...args) => {
+    const set = new Set()
+    const returnArr = []
+    for (const regions of args){
+      for (const region of regions){
+        if (!set.has(region.name)) {
+          returnArr.push(region)
+          set.add(region.name)
+        }
+      }
+    }
+    return returnArr
+  }
+
+  @Effect()
+  addToSelectedRegions$: Observable<any>
+  
+
   /**
    * for backwards compatibility.
    * older versions of atlas viewer may only have labelIndex as region identifier
    */
   @Effect()
-  onSelectRegionWithId = combineLatest(
-    this.actions$.pipe(
-      ofType(SELECT_REGIONS_WITH_ID)
-    ),
-    this.updatedParcellation$
-  ).pipe(
-    map(([action, parcellation]) => {
+  onSelectRegionWithId = this.actions$.pipe(
+    ofType(SELECT_REGIONS_WITH_ID),
+    map(action => {
       const { selectRegionIds } = action
-      const { ngId: defaultNgId } = parcellation
-
-      const selectRegions = (<any[]>selectRegionIds)
-        .map(labelIndexId => getNgIdLabelIndexFromId({ labelIndexId }))
-        .map(({ ngId, labelIndex }) => {
-          return {
-            labelIndexId: generateLabelIndexId({
-              ngId: ngId || defaultNgId,
-              labelIndex 
-            })
-          }
-        })
-        .map(({ labelIndexId }) => {
-          return recursiveFindRegionWithLabelIndexId({ 
-            regions: parcellation.regions,
-            labelIndexId,
-            inheritedNgId: defaultNgId
-          })
-        })
-        .filter(v => {
-          if (!v) console.log(`SELECT_REGIONS_WITH_ID, some ids cannot be parsed intto label index`)
-          return !!v
-        })
+      return selectRegionIds
+    }),
+    switchMap(selectRegionIds => this.updatedParcellation$.pipe(
+      filter(p => !!p),
+      take(1),
+      map(parcellation => [selectRegionIds, parcellation])
+    )),
+    map(this.convertRegionIdsToRegion),
+    map(selectRegions => {
       return {
         type: SELECT_REGIONS,
         selectRegions
@@ -143,7 +192,14 @@ export class UseEffects implements OnDestroy{
     filter((message: MessageEvent) => message && message.data && message.data.type === 'UPDATE_PARCELLATION_REGIONS'),
     map(({data}) => data.parcellation),
     withLatestFrom(this.newParcellationSelected$),
-    filter(([ propagatedP, selectedP ] : [any, any]) => propagatedP.name === selectedP.name),
+    filter(([ propagatedP, selectedP ] : [any, any]) => {
+      /**
+       * TODO 
+       * use id
+       * but jubrain may have same id for different template spaces
+       */
+      return propagatedP.name === selectedP.name
+    }),
     map(([ propagatedP, _ ]) => propagatedP),
     map(parcellation => ({
       type: UPDATE_PARCELLATION,
diff --git a/src/services/state/userConfigState.store.ts b/src/services/state/userConfigState.store.ts
new file mode 100644
index 000000000..a4af6c342
--- /dev/null
+++ b/src/services/state/userConfigState.store.ts
@@ -0,0 +1,340 @@
+import { Action, Store, select } from "@ngrx/store";
+import { Injectable, OnDestroy } from "@angular/core";
+import { Actions, Effect, ofType } from "@ngrx/effects";
+import { Observable, combineLatest, Subscription, from, of } from "rxjs";
+import { shareReplay, withLatestFrom, map, distinctUntilChanged, filter, take, tap, switchMap, catchError, share } from "rxjs/operators";
+import { generateLabelIndexId, recursiveFindRegionWithLabelIndexId } from "../stateStore.service";
+import { SELECT_REGIONS, NEWVIEWER, SELECT_PARCELLATION } from "./viewerState.store";
+import { DialogService } from "../dialogService.service";
+
+interface UserConfigState{
+  savedRegionsSelection: RegionSelection[]
+}
+
+export interface RegionSelection{
+  templateSelected: any
+  parcellationSelected: any
+  regionsSelected: any[]
+  name: string
+  id: string
+}
+
+/**
+ * for serialisation into local storage/database
+ */
+interface SimpleRegionSelection{
+  id: string,
+  name: string,
+  tName: string,
+  pName: string,
+  rSelected: string[]
+}
+
+interface UserConfigAction extends Action{
+  config?: Partial<UserConfigState>
+  payload?: any
+}
+
+const defaultUserConfigState: UserConfigState = {
+  savedRegionsSelection: []
+}
+
+const ACTION_TYPES = {
+  UPDATE_REGIONS_SELECTIONS: `UPDATE_REGIONS_SELECTIONS`,
+  UPDATE_REGIONS_SELECTION:'UPDATE_REGIONS_SELECTION',
+  SAVE_REGIONS_SELECTION: `SAVE_REGIONS_SELECTIONN`,
+  DELETE_REGIONS_SELECTION: 'DELETE_REGIONS_SELECTION',
+
+  LOAD_REGIONS_SELECTION: 'LOAD_REGIONS_SELECTION'
+}
+
+export const USER_CONFIG_ACTION_TYPES = ACTION_TYPES
+
+export function userConfigState(prevState: UserConfigState = defaultUserConfigState, action: UserConfigAction) {
+  switch(action.type) {
+    case ACTION_TYPES.UPDATE_REGIONS_SELECTIONS:
+      const { config = {} } = action
+      const { savedRegionsSelection } = config
+      return {
+        ...prevState,
+        savedRegionsSelection
+      }
+    default:
+      return {
+        ...prevState
+      }
+  }
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class UserConfigStateUseEffect implements OnDestroy{
+
+  private subscriptions: Subscription[] = []
+
+  constructor(
+    private actions$: Actions,
+    private store$: Store<any>,
+    private dialogService: DialogService
+  ){
+    const viewerState$ = this.store$.pipe(
+      select('viewerState'),
+      shareReplay(1)
+    )
+
+    this.parcellationSelected$ = viewerState$.pipe(
+      select('parcellationSelected'),
+      distinctUntilChanged(),
+      share()
+    )
+
+    this.tprSelected$ = combineLatest(
+      viewerState$.pipe(
+        select('templateSelected'),
+        distinctUntilChanged()
+      ),
+      this.parcellationSelected$,
+      viewerState$.pipe(
+        select('regionsSelected'),
+        /**
+         * TODO
+         * distinct selectedRegions
+         */
+      )
+    ).pipe(
+      map(([ templateSelected, parcellationSelected, regionsSelected ]) => {
+        return {
+          templateSelected, parcellationSelected, regionsSelected
+        }
+      }),
+      shareReplay(1)
+    )
+
+    this.savedRegionsSelections$ = this.store$.pipe(
+      select('userConfigState'),
+      select('savedRegionsSelection'),
+      shareReplay(1)
+    )
+
+    this.onSaveRegionsSelection$ = this.actions$.pipe(
+      ofType(ACTION_TYPES.SAVE_REGIONS_SELECTION),
+      withLatestFrom(this.tprSelected$),
+      withLatestFrom(this.savedRegionsSelections$),
+      
+      map(([[action, tprSelected], savedRegionsSelection]) => {
+        const { payload = {} } = action as UserConfigAction
+        const { name = 'Untitled' } = payload
+
+        const { templateSelected, parcellationSelected, regionsSelected } = tprSelected
+        const newSavedRegionSelection: RegionSelection = {
+          id: Date.now().toString(),
+          name,
+          templateSelected,
+          parcellationSelected,
+          regionsSelected
+        }
+        return {
+          type: ACTION_TYPES.UPDATE_REGIONS_SELECTIONS,
+          config: {
+            savedRegionsSelection: savedRegionsSelection.concat([newSavedRegionSelection])
+          }
+        } as UserConfigAction
+      })
+    )
+
+    this.onDeleteRegionsSelection$ = this.actions$.pipe(
+      ofType(ACTION_TYPES.DELETE_REGIONS_SELECTION),
+      withLatestFrom(this.savedRegionsSelections$),
+      map(([ action, savedRegionsSelection ]) => {
+        const { payload = {} } = action as UserConfigAction
+        const { id } = payload
+        return {
+          type: ACTION_TYPES.UPDATE_REGIONS_SELECTIONS,
+          config: {
+            savedRegionsSelection: savedRegionsSelection.filter(srs => srs.id !== id)
+          }
+        }
+      })
+    )
+
+    this.onUpdateRegionsSelection$ = this.actions$.pipe(
+      ofType(ACTION_TYPES.UPDATE_REGIONS_SELECTION),
+      withLatestFrom(this.savedRegionsSelections$),
+      map(([ action, savedRegionsSelection]) => {
+        const { payload = {} } = action as UserConfigAction
+        const { id, ...rest } = payload
+        return {
+          type: ACTION_TYPES.UPDATE_REGIONS_SELECTIONS,
+          config: {
+            savedRegionsSelection: savedRegionsSelection
+              .map(srs => srs.id === id
+                ? { ...srs, ...rest }
+                : { ...srs })
+          }
+        }
+      })
+    )
+
+    this.subscriptions.push(
+      this.actions$.pipe(
+        ofType(ACTION_TYPES.LOAD_REGIONS_SELECTION),
+        map(action => {
+          const { payload = {}} = action as UserConfigAction
+          const { savedRegionsSelection } : {savedRegionsSelection : RegionSelection} = payload
+          return savedRegionsSelection
+        }),
+        filter(val => !!val),
+        withLatestFrom(this.tprSelected$),
+        switchMap(([savedRegionsSelection, { parcellationSelected, templateSelected, regionsSelected }]) => 
+          from(this.dialogService.getUserConfirm({
+            title: `Load region selection: ${savedRegionsSelection.name}`,
+            message: `This action would cause the viewer to navigate away from the current view. Proceed?`
+          })).pipe(
+            catchError((e, obs) => of(null)),
+            map(() => {
+              return {
+                savedRegionsSelection,
+                parcellationSelected,
+                templateSelected,
+                regionsSelected
+              }
+            }),
+            filter(val => !!val)
+          )
+        ),
+        switchMap(({ savedRegionsSelection, parcellationSelected, templateSelected, regionsSelected }) => {
+          if (templateSelected.name !== savedRegionsSelection.templateSelected.name ) {
+            /**
+             * template different, dispatch NEWVIEWER
+             */
+            this.store$.dispatch({
+              type: NEWVIEWER,
+              selectParcellation: savedRegionsSelection.parcellationSelected,
+              selectTemplate: savedRegionsSelection.templateSelected
+            })
+            return this.parcellationSelected$.pipe(
+              filter(p => p.updated),
+              take(1),
+              map(() => {
+                return {
+                  regionsSelected: savedRegionsSelection.regionsSelected
+                }
+              })
+            )
+          }
+  
+          if (parcellationSelected.name !== savedRegionsSelection.parcellationSelected.name) {
+            /**
+             * parcellation different, dispatch SELECT_PARCELLATION
+             */
+  
+             this.store$.dispatch({
+               type: SELECT_PARCELLATION,
+               selectParcellation: savedRegionsSelection.parcellationSelected
+             })
+            return this.parcellationSelected$.pipe(
+              filter(p => p.updated),
+              take(1),
+              map(() => {
+                return {
+                  regionsSelected: savedRegionsSelection.regionsSelected
+                }
+              })
+            )
+          }
+
+          return of({ 
+            regionsSelected: savedRegionsSelection.regionsSelected
+          })
+        })
+      ).subscribe(({ regionsSelected }) => {
+        this.store$.dispatch({
+          type: SELECT_REGIONS,
+          selectRegions: regionsSelected
+        })
+      })
+    )
+
+    this.subscriptions.push(
+      this.actions$.pipe(
+        ofType(ACTION_TYPES.UPDATE_REGIONS_SELECTIONS)
+      ).subscribe(action => {
+        const { config = {} } = action as UserConfigAction
+        const { savedRegionsSelection } = config
+        const simpleSRSs = savedRegionsSelection.map(({ id, name, templateSelected, parcellationSelected, regionsSelected }) => {
+          return {
+            id,
+            name,
+            tName: templateSelected.name,
+            pName: parcellationSelected.name,
+            rSelected: regionsSelected.map(({ ngId, labelIndex }) => generateLabelIndexId({ ngId, labelIndex }))
+          } as SimpleRegionSelection
+        })
+
+        /**
+         * TODO save server side on per user basis
+         */
+        window.localStorage.setItem(LOCAL_STORAGE_KEY.SAVED_REGION_SELECTIONS, JSON.stringify(simpleSRSs))
+      })
+    )
+
+    const savedSRSsString = window.localStorage.getItem(LOCAL_STORAGE_KEY.SAVED_REGION_SELECTIONS)
+    const savedSRSs:SimpleRegionSelection[] = savedSRSsString && JSON.parse(savedSRSsString)
+
+    this.restoreSRSsFromStorage$ = viewerState$.pipe(
+      filter(() => !!savedSRSs),
+      select('fetchedTemplates'),
+      distinctUntilChanged(),
+      map(fetchedTemplates => savedSRSs.map(({ id, name, tName, pName, rSelected }) => {
+        const templateSelected = fetchedTemplates.find(t => t.name === tName)
+        const parcellationSelected = templateSelected && templateSelected.parcellations.find(p => p.name === pName)
+        const regionsSelected = parcellationSelected && rSelected.map(labelIndexId => recursiveFindRegionWithLabelIndexId({ regions: parcellationSelected.regions, labelIndexId, inheritedNgId: parcellationSelected.ngId }))
+        return {
+          templateSelected,
+          parcellationSelected,
+          id,
+          name,
+          regionsSelected
+        } as RegionSelection
+      })),
+      filter(RSs => RSs.every(rs => rs.regionsSelected && rs.regionsSelected.every(r => !!r))),
+      take(1),
+      map(savedRegionsSelection => {
+        return {
+          type: ACTION_TYPES.UPDATE_REGIONS_SELECTIONS,
+          config: { savedRegionsSelection }
+        }
+      })
+    )
+  }
+
+  ngOnDestroy(){
+    while(this.subscriptions.length > 0) {
+      this.subscriptions.pop().unsubscribe()
+    }
+  }
+
+  /**
+   * Temmplate Parcellation Regions selected
+   */
+  private tprSelected$: Observable<{templateSelected:any, parcellationSelected: any, regionsSelected: any[]}>
+  private savedRegionsSelections$: Observable<any[]>
+  private parcellationSelected$: Observable<any>
+
+  @Effect()
+  public onSaveRegionsSelection$: Observable<any>
+
+  @Effect()
+  public onDeleteRegionsSelection$: Observable<any>
+
+  @Effect()
+  public onUpdateRegionsSelection$: Observable<any>
+
+  @Effect()
+  public restoreSRSsFromStorage$: Observable<any>
+}
+
+const LOCAL_STORAGE_KEY = {
+  SAVED_REGION_SELECTIONS: 'fzj.xg.iv.SAVED_REGION_SELECTIONS'
+}
\ No newline at end of file
diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts
index 874b8e311..2448818ec 100644
--- a/src/services/state/viewerState.store.ts
+++ b/src/services/state/viewerState.store.ts
@@ -107,7 +107,10 @@ export function viewerState(
       const { updatedParcellation } = action
       return {
         ...state,
-        parcellationSelected: updatedParcellation
+        parcellationSelected: {
+          ...updatedParcellation,
+          updated: true
+        }
       }
     }
     case SELECT_REGIONS:
@@ -134,6 +137,10 @@ export function viewerState(
         userLandmarks: action.landmarks
       } 
     }
+    /**
+     * TODO
+     * duplicated with ngViewerState.layers ?
+     */
     case NEHUBA_LAYER_CHANGED: {
       if (!window['viewer']) {
         return {
@@ -175,4 +182,6 @@ export const SELECT_LANDMARKS = `SELECT_LANDMARKS`
 export const DESELECT_LANDMARKS = `DESELECT_LANDMARKS`
 export const USER_LANDMARKS = `USER_LANDMARKS`
 
+export const ADD_TO_REGIONS_SELECTION_WITH_IDS = `ADD_TO_REGIONS_SELECTION_WITH_IDS`
+
 export const NEHUBA_LAYER_CHANGED = `NEHUBA_LAYER_CHANGED`
diff --git a/src/services/stateStore.service.ts b/src/services/stateStore.service.ts
index 96048ec21..842499c18 100644
--- a/src/services/stateStore.service.ts
+++ b/src/services/stateStore.service.ts
@@ -7,6 +7,11 @@ export { CHANGE_NAVIGATION, AtlasAction, DESELECT_LANDMARKS, FETCHED_TEMPLATE, N
 export { DataEntry, ParcellationRegion, DataStateInterface, DatasetAction, FETCHED_DATAENTRIES, FETCHED_SPATIAL_DATA, Landmark, OtherLandmarkGeometry, PlaneLandmarkGeometry, PointLandmarkGeometry, Property, Publication, ReferenceSpace, dataStore, File, FileSupplementData } from './state/dataStore.store'
 export { CLOSE_SIDE_PANEL, MOUSE_OVER_LANDMARK, MOUSE_OVER_SEGMENT, OPEN_SIDE_PANEL, TOGGLE_SIDE_PANEL, UIAction, UIStateInterface, uiState } from './state/uiState.store'
 export { SPATIAL_GOTO_PAGE, SpatialDataEntries, SpatialDataStateInterface, UPDATE_SPATIAL_DATA, spatialSearchState } from './state/spatialSearchState.store'
+export { userConfigState, UserConfigStateUseEffect, USER_CONFIG_ACTION_TYPES } from './state/userConfigState.store'
+
+export const GENERAL_ACTION_TYPES = {
+  ERROR: 'ERROR'
+}
 
 export function safeFilter(key:string){
   return filter((state:any)=>
diff --git a/src/ui/layerbrowser/layerbrowser.component.ts b/src/ui/layerbrowser/layerbrowser.component.ts
index 18c2cb75b..35178d86e 100644
--- a/src/ui/layerbrowser/layerbrowser.component.ts
+++ b/src/ui/layerbrowser/layerbrowser.component.ts
@@ -1,9 +1,9 @@
-import { Component,  OnDestroy } from "@angular/core";
+import { Component,  OnDestroy, Input, Pipe, PipeTransform } from "@angular/core";
 import { NgLayerInterface } from "../../atlasViewer/atlasViewer.component";
 import { Store, select } from "@ngrx/store";
 import { ViewerStateInterface, isDefined, REMOVE_NG_LAYER, FORCE_SHOW_SEGMENT, safeFilter, getNgIds } from "../../services/stateStore.service";
-import { Subscription, Observable } from "rxjs";
-import { filter, map } from "rxjs/operators";
+import { Subscription, Observable, combineLatest } from "rxjs";
+import { filter, map, shareReplay, tap } from "rxjs/operators";
 import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service";
 
 @Component({
@@ -20,13 +20,15 @@ export class LayerBrowser implements OnDestroy{
   /**
    * TODO make untangle nglayernames and its dependency on ng
    */
-  loadedNgLayers$: Observable<NgLayerInterface[]>
-  lockedLayers : string[] = []
+  public loadedNgLayers$: Observable<NgLayerInterface[]>
+  public lockedLayers : string[] = []
+
+  public nonBaseNgLayers$: Observable<NgLayerInterface[]>
 
   public forceShowSegmentCurrentState : boolean | null = null
   public forceShowSegment$ : Observable<boolean|null>
   
-  public ngLayers$: Observable<any>
+  public ngLayers$: Observable<string[]>
   public advancedMode: boolean = false
 
   private subscriptions : Subscription[] = []
@@ -35,6 +37,11 @@ export class LayerBrowser implements OnDestroy{
   /* TODO temporary measure. when datasetID can be used, will use  */
   public fetchedDataEntries$ : Observable<any>
 
+  @Input()
+  showPlaceholder: boolean = true
+
+  darktheme$: Observable<boolean>
+
   constructor(
     private store : Store<ViewerStateInterface>,
     private constantsService: AtlasViewerConstantsServices){
@@ -64,6 +71,22 @@ export class LayerBrowser implements OnDestroy{
        */
       map(arr => arr.filter(v => !!v))
     )
+
+    this.loadedNgLayers$ = this.store.pipe(
+      select('viewerState'),
+      select('loadedNgLayers')
+    )
+
+    this.nonBaseNgLayers$ = combineLatest(
+      this.ngLayers$,
+      this.loadedNgLayers$
+    ).pipe(
+      map(([baseNgLayerNames, loadedNgLayers]) => {
+        const baseNameSet = new Set(baseNgLayerNames)
+        return loadedNgLayers.filter(l => !baseNameSet.has(l.name))
+      })
+    )
+
     /**
      * TODO
      * this is no longer populated
@@ -80,9 +103,9 @@ export class LayerBrowser implements OnDestroy{
       map(state => state.forceShowSegment)
     )
 
-    this.loadedNgLayers$ = this.store.pipe(
-      select('viewerState'),
-      select('loadedNgLayers')
+
+    this.darktheme$ = this.constantsService.darktheme$.pipe(
+      shareReplay(1)
     )
 
     this.subscriptions.push(
@@ -128,6 +151,9 @@ export class LayerBrowser implements OnDestroy{
       return
     }
 
+    /**
+     * TODO perhaps useEffects ?
+     */
     this.store.dispatch({
       type : FORCE_SHOW_SEGMENT,
       forceShowSegment : this.forceShowSegmentCurrentState === null
@@ -151,6 +177,9 @@ export class LayerBrowser implements OnDestroy{
     })
   }
 
+  /**
+   * TODO use observable and pipe to make this more perf
+   */
   segmentationTooltip(){
     return `toggle segments visibility: 
     ${this.forceShowSegmentCurrentState === true ? 'always show' : this.forceShowSegmentCurrentState === false ? 'always hide' : 'auto'}`
@@ -169,4 +198,16 @@ export class LayerBrowser implements OnDestroy{
   get isMobile(){
     return this.constantsService.mobile
   }
+
+  public matTooltipPosition: string = 'below'
 }
+
+@Pipe({
+  name: 'lockedLayerBtnClsPipe'
+})
+
+export class LockedLayerBtnClsPipe implements PipeTransform{
+  public transform(ngLayer:NgLayerInterface, lockedLayers?: string[]): boolean{
+    return (lockedLayers && new Set(lockedLayers).has(ngLayer.name)) || false
+  }
+}
\ No newline at end of file
diff --git a/src/ui/layerbrowser/layerbrowser.template.html b/src/ui/layerbrowser/layerbrowser.template.html
index 6ac015c16..37e52eb05 100644
--- a/src/ui/layerbrowser/layerbrowser.template.html
+++ b/src/ui/layerbrowser/layerbrowser.template.html
@@ -1,71 +1,61 @@
-<ng-container *ngIf="ngLayers$ | async | filterNgLayer : (loadedNgLayers$ | async) as filteredNgLayers; else noLayerPlaceHolder">
-  <ng-container *ngIf="filteredNgLayers.length > 0; else noLayerPlaceHolder">
+<ng-container *ngIf="nonBaseNgLayers$ | async as nonBaseNgLayers; else noLayerPlaceHolder">
+  <ng-container *ngIf="nonBaseNgLayers.length > 0; else noLayerPlaceHolder">
     <div
       class="layerContainer overflow-hidden"
-      *ngFor = "let ngLayer of filteredNgLayers">
+      *ngFor = "let ngLayer of nonBaseNgLayers">
     
       <!-- toggle visibility -->
-      <div class="btnWrapper">
-        <div
-          container = "body"
-          placement = "bottom"
-          [tooltip] = "checkLocked(ngLayer) ? 'base layer cannot be hidden' : 'toggle visibility'"
-          (click) = "checkLocked(ngLayer) ? null : toggleVisibility(ngLayer)"
-          class="btn btn-sm btn-outline-secondary rounded-circle">
-          <i [ngClass] = "checkLocked(ngLayer) ? 'fas fa-lock muted' :ngLayer.visible ? 'far fa-eye' : 'far fa-eye-slash'">
-          </i>
-        </div>
-      </div>
+      
+      <button
+        [matTooltipPosition]="matTooltipPosition"
+        [matTooltip]="(ngLayer | lockedLayerBtnClsPipe : lockedLayers) ? 'base layer cannot be hidden' : 'toggle visibility'"
+        (click)="toggleVisibility(ngLayer)"
+        mat-icon-button
+        [disabled]="ngLayer | lockedLayerBtnClsPipe : lockedLayers"
+        [color]="ngLayer.visible ? 'primary' : null">
+        <i [ngClass]="(ngLayer | lockedLayerBtnClsPipe : lockedLayers) ? 'fas fa-lock muted' : ngLayer.visible ? 'far fa-eye' : 'far fa-eye-slash'">
+        </i>
+      </button>
 
       <!-- advanced mode only: toggle force show segmentation -->
-      <div class="btnWrapper">
-        <div
-          *ngIf="advancedMode"
-          container="body"
-          placement="bottom"
-          [tooltip]="ngLayer.type === 'segmentation' ? segmentationTooltip() : 'only segmentation layer can hide/show segments'"
-          #forceSegment="bs-tooltip"
-          (click)="forceSegment.hide();toggleForceShowSegment(ngLayer)"
-          class="btn btn-sm btn-outline-secondary rounded-circle">
-          <i 
-            class="fas" 
-            [ngClass]="ngLayer.type === 'segmentation' ? ('fa-th-large ' + segmentationAdditionalClass) : 'fa-lock muted' ">
-      
-          </i>
-        </div>
-      </div>
+      <button
+        *ngIf="advancedMode"
+        [matTooltipPosition]="matTooltipPosition"
+        [matTooltip]="ngLayer.type === 'segmentation' ? segmentationTooltip() : 'only segmentation layer can hide/show segments'"
+        (click)="toggleForceShowSegment(ngLayer)"
+        mat-icon-button>
+        <i 
+          class="fas" 
+          [ngClass]="ngLayer.type === 'segmentation' ? ('fa-th-large ' + segmentationAdditionalClass) : 'fa-lock muted' ">
+    
+        </i>
+      </button>
 
       <!-- remove layer -->
-      <div class="btnWrapper">
-        <div
-          container="body"
-          placement="bottom"
-          [tooltip]="checkLocked(ngLayer) ? 'base layers cannot be removed' : 'remove layer'"
-          (click)="removeLayer(ngLayer)"
-          class="btn btn-sm btn-outline-secondary rounded-circle">
-          <i [ngClass]="checkLocked(ngLayer) ? 'fas fa-lock muted' : 'far fa-times-circle'">
-          </i>
-        </div>
-      </div>
+      <button
+        color="warn"
+        mat-icon-button
+        (click)="removeLayer(ngLayer)"
+        [disabled]="ngLayer | lockedLayerBtnClsPipe : lockedLayers"
+        [matTooltip]="(ngLayer | lockedLayerBtnClsPipe : lockedLayers) ? 'base layers cannot be removed' : 'remove layer'">
+        <i [class]="(ngLayer | lockedLayerBtnClsPipe : lockedLayers) ? 'fas fa-lock muted' : 'fas fa-trash'">
+        </i>
+      </button>
 
       <!-- layer description -->
-      <panel-component [ngClass]="{'muted-text muted' : !classVisible(ngLayer)}">
-    
-        <div heading>
-          {{ ngLayer.name | getLayerNameFromDatasets : (fetchedDataEntries$ | async) }}
-        </div>
-    
-        <div bodyy>
-          {{ ngLayer.source }}
-        </div>
-      </panel-component>
+      <div
+        [matTooltipPosition]="matTooltipPosition"
+        [matTooltip]="ngLayer.name | getFilenamePipe "
+        [class]="((darktheme$ | async) ? 'text-light' : 'text-dark') + ' text-truncate'">
+        {{ ngLayer.name | getFilenamePipe | getFileExtension }}
+      </div>
     </div>
   </ng-container>
 </ng-container>
 
 <!-- fall back when no layers are showing -->
 <ng-template #noLayerPlaceHolder>
-  <h5 class="noLayerPlaceHolder text-muted">
+  <small *ngIf="showPlaceholder" class="noLayerPlaceHolder text-muted">
     No additional layers added.
-  </h5>
+  </small>
 </ng-template>
\ No newline at end of file
diff --git a/src/ui/menuicons/menuicons.component.ts b/src/ui/menuicons/menuicons.component.ts
index 1ccf9a21b..8acb577d7 100644
--- a/src/ui/menuicons/menuicons.component.ts
+++ b/src/ui/menuicons/menuicons.component.ts
@@ -2,17 +2,14 @@ import { Component, ComponentRef, Injector, ComponentFactory, ComponentFactoryRe
 
 import { WidgetServices } from "src/atlasViewer/widgetUnit/widgetService.service";
 import { WidgetUnit } from "src/atlasViewer/widgetUnit/widgetUnit.component";
-import { LayerBrowser } from "src/ui/layerbrowser/layerbrowser.component";
 import { DataBrowser } from "src/ui/databrowserModule/databrowser/databrowser.component";
 import { PluginBannerUI } from "../pluginBanner/pluginBanner.component";
 import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service";
 import { DatabrowserService } from "../databrowserModule/databrowser.service";
 import { PluginServices, PluginManifest } from "src/atlasViewer/atlasViewer.pluginService.service";
 import { Store, select } from "@ngrx/store";
-import { Observable, BehaviorSubject, combineLatest, merge, of } from "rxjs";
-import { map, shareReplay } from "rxjs/operators";
-import { DESELECT_REGIONS, SELECT_REGIONS, CHANGE_NAVIGATION } from "src/services/state/viewerState.store";
-import { MatSnackBar } from "@angular/material";
+import { Observable, combineLatest, merge, of } from "rxjs";
+import { map, shareReplay, startWith } from "rxjs/operators";
 import { ToastService } from "src/services/toastService.service";
 
 @Component({
@@ -35,13 +32,6 @@ export class MenuIconsBar{
   dataBrowser: ComponentRef<DataBrowser> = null
   dbWidget: ComponentRef<WidgetUnit> = null
 
-  /**
-   * layerBrowser
-   */
-  lbcf: ComponentFactory<LayerBrowser>
-  layerBrowser: ComponentRef<LayerBrowser> = null
-  lbWidget: ComponentRef<WidgetUnit> = null
-
   /**
    * pluginBrowser
    */
@@ -57,9 +47,6 @@ export class MenuIconsBar{
   public themedBtnClass$: Observable<string>
 
   public skeletonBtnClass$: Observable<string>
-  
-  private layerBrowserExists$: BehaviorSubject<boolean> = new BehaviorSubject(false)
-  public layerBrowserBtnClass$: Observable<string> 
 
   public toolBtnClass$: Observable<string>
   public getKgSearchBtnCls$: Observable<[Set<WidgetUnit>, string]>
@@ -88,7 +75,6 @@ export class MenuIconsBar{
     this.dbService.createDatabrowser = this.clickSearch.bind(this)
 
     this.dbcf = cfr.resolveComponentFactory(DataBrowser)
-    this.lbcf = cfr.resolveComponentFactory(LayerBrowser)
     this.pbcf = cfr.resolveComponentFactory(PluginBannerUI)
 
     this.selectedTemplate$ = store.pipe(
@@ -96,13 +82,10 @@ export class MenuIconsBar{
       select('templateSelected')
     )
 
-    this.selectedRegions$ = merge(
-      of([]),
-      store.pipe(
-        select('viewerState'),
-        select('regionsSelected')
-      )
-    ).pipe(
+    this.selectedRegions$ = store.pipe(
+      select('viewerState'),
+      select('regionsSelected'),
+      startWith([]),
       shareReplay(1)
     )
 
@@ -116,13 +99,6 @@ export class MenuIconsBar{
       shareReplay(1)
     )
 
-    this.layerBrowserBtnClass$ = combineLatest(
-      this.layerBrowserExists$,
-      this.themedBtnClass$
-    ).pipe(
-      map(([flag,themedBtnClass]) => `${this.mobileRespBtnClass} ${flag ? 'btn-primary' : themedBtnClass}`)
-    )
-
     this.launchedPlugins$ = this.pluginServices.launchedPlugins$.pipe(
       map(set => Array.from(set))
     )
@@ -170,37 +146,6 @@ export class MenuIconsBar{
   public catchError(e) {
     this.constantService.catchError(e)
   }
-
-  public clickLayer(event: MouseEvent){
-
-    if (this.lbWidget) {
-      this.lbWidget.destroy()
-      this.lbWidget = null
-      return
-    }
-    this.layerBrowser = this.lbcf.create(this.injector)
-    this.lbWidget = this.widgetServices.addNewWidget(this.layerBrowser, {
-      exitable: true,
-      persistency: true,
-      state: 'floating',
-      title: 'Layer Browser',
-      titleHTML: '<i class="fas fa-layer-group"></i> Layer Browser'
-    })
-
-    this.layerBrowserExists$.next(true)
-
-    this.lbWidget.onDestroy(() => {
-      this.layerBrowserExists$.next(false)
-      this.layerBrowser = null
-      this.lbWidget = null
-    })
-
-    const el = event.currentTarget as HTMLElement
-    const top = el.offsetTop
-    const left = el.offsetLeft + 50
-    this.lbWidget.instance.position = [left, top]
-  }
-
   public clickPlugins(event: MouseEvent){
     if(this.pbWidget) {
       this.pbWidget.destroy()
@@ -245,45 +190,6 @@ export class MenuIconsBar{
     this.widgetServices.exitWidget(wu)
   }
 
-  public deselectRegion(event: MouseEvent, region: any){
-    event.stopPropagation()
-    
-    this.store.dispatch({
-      type: DESELECT_REGIONS,
-      deselectRegions: [region]
-    })
-  }
-
-  public deselectAllRegions(event: MouseEvent){
-    event.stopPropagation()
-    this.store.dispatch({
-      type: SELECT_REGIONS,
-      selectRegions: []
-    })
-  }
-
-  public gotoRegion(event: MouseEvent, region:any){
-    event.stopPropagation()
-
-    if (region.position) {
-      this.store.dispatch({
-        type: CHANGE_NAVIGATION,
-        navigation: {
-          position: region.position,
-          animation: {}
-        }
-      })
-    } else {
-      /**
-       * TODO convert to snack bar
-       */
-      this.toastService.showToast(`${region.name} does not have a position defined`, {
-        timeout: 5000,
-        dismissable: true
-      })
-    }
-  }
-
   public renameKgSearchWidget(event:MouseEvent, wu: WidgetUnit) {
     event.stopPropagation()
   }
diff --git a/src/ui/menuicons/menuicons.style.css b/src/ui/menuicons/menuicons.style.css
index 1873b453d..c76c706c7 100644
--- a/src/ui/menuicons/menuicons.style.css
+++ b/src/ui/menuicons/menuicons.style.css
@@ -32,38 +32,7 @@
   margin-top: 0.1em;
 }
 
-.virtual-scroll-viewport-container
+layer-browser
 {
-  height: 20em;
-  width: 20em;
-  overflow: hidden;
-}
-
-.virtual-scroll-viewport-container > cdk-virtual-scroll-viewport
-{
-  width: 100%;
-  height: 100%;
-  box-sizing: content-box;
-  padding-right: 3em;
-}
-
-.virtual-scroll-row
-{
-  width: 20em;
-}
-
-/* required to match virtual scroll itemSize property */
-.virtual-scroll-unit
-{
-  height: 26px
-}
-
-.selected-region-container
-{
-  flex: 1 1 auto;
-}
-
-.selected-region-actionbtn
-{
-  flex: 0 0 auto;
-}
+  max-width: 20em;
+}
\ No newline at end of file
diff --git a/src/ui/menuicons/menuicons.template.html b/src/ui/menuicons/menuicons.template.html
index 796fe0890..7e3140dbe 100644
--- a/src/ui/menuicons/menuicons.template.html
+++ b/src/ui/menuicons/menuicons.template.html
@@ -5,16 +5,31 @@
 <ng-template [ngIf]="selectedTemplate$ | async">
 
   <!-- layer browser -->
-  <div
-    matTooltip="Layer"
-    matTooltipPosition="right"
-    (click)="clickLayer($event)"
-    [class]="layerBrowserBtnClass$ | async"
-    class="btn">
-    <i class="fas fa-layer-group">
-    </i>
-  </div>
+  <sleight-of-hand>
+    <div sleight-of-hand-front>
+      <div [ngClass]="(skeletonBtnClass$ | async) + ' btn'">
+        <i class="fas fa-layer-group"></i>
+      </div>
+    </div>
+    <div class="d-flex flex-row align-items-start soh-row pe-none"
+      sleight-of-hand-back>
 
+      <div [ngClass]="(skeletonBtnClass$ | async) + ' btn muted pe-all'">
+        <i class="fas fa-layer-group"></i>
+      </div>
+
+      <div [class]="((darktheme$ | async) ? 'bg-dark' : 'bg-light') + ' card pe-all'">
+        <layer-browser #layerBrowser [showPlaceholder]="false">
+        </layer-browser>
+      </div>
+      <ng-container *ngIf="(layerBrowser.nonBaseNgLayers$ | async).length === 0" #noNonBaseNgLayerTemplate>
+        <small [class]="((darktheme$ | async) ? 'bg-dark text-light' : 'bg-light text-dark') + ' muted pl-2 pr-2 p-1 text-nowrap'">
+          No additional layers added
+        </small>
+      </ng-container>
+    </div>
+  </sleight-of-hand>
+  
   <!-- tools -->
   <sleight-of-hand>
 
@@ -29,7 +44,7 @@
 
     <!-- shown after mouse over -->
     <div
-      class="d-flex flex-row soh-row"
+      class="d-flex flex-row soh-row align-items-start"
       sleight-of-hand-back>
 
       <!-- placeholder icon -->
@@ -77,11 +92,11 @@
     <!-- shown after mouse over -->
     <div
       sleight-of-hand-back
-      class="d-flex flex-row align-items-center soh-row">
+      class="d-flex flex-row align-items-center soh-row pe-none">
 
       <!-- placeholder icon -->
       <div
-        [ngClass]="(skeletonBtnClass$ | async) + ' btn muted'"
+        [ngClass]="(skeletonBtnClass$ | async) + ' btn muted pe-all'"
         [matBadgePosition]="badgetPosition"
         [matBadge]="dbService.instantiatedWidgetUnits.length > 0 ? dbService.instantiatedWidgetUnits.length : null">
         <i class="fas fa-search"></i>
@@ -90,7 +105,7 @@
       <!-- only renders if there is at least one search result -->
       <div
         *ngIf="dbService.instantiatedWidgetUnits.length > 0; else noKgSearchTemplate"
-        class="position-relative">
+        class="position-relative pe-all">
 
         <div class="position-absolute d-flex flex-column soh-column">
 
@@ -194,7 +209,8 @@
   </sleight-of-hand>
 
   <!-- selected regions -->
-  <sleight-of-hand>
+  <sleight-of-hand
+    [doNotClose]="viewerStateController.focused">
 
     <!-- shown prior to mouse over -->
     <div
@@ -220,76 +236,12 @@
         <i class="fas fa-brain"></i>
       </div>
 
-      <div
-        *ngIf="(selectedRegions$ | async).length > 0; else noBrainRegionSelected"
-        class="position-relative">
+      <div class="position-relative">
 
-        <!-- rendering all the selected regions -->
-        <div [class]="((darktheme$ | async) ? 'text-light bg-dark' : 'text-dark bg-light') + ' position-absolute'">
-          <div class="m-1 position-relative">
-            <button
-              mat-raised-button
-              (click)="deselectAllRegions($event)"
-              class="virtual-scroll-row"
-              color="warn">
-              <i class="fas fa-trash"></i>
-              Deselect all {{ (selectedRegions$ | async).length }} region{{ (selectedRegions$ | async).length > 1 ? 's' : '' }}
-            </button>
-          </div>
-          <mat-divider></mat-divider>
-          <div class="virtual-scroll-viewport-container">
-            <cdk-virtual-scroll-viewport
-              class="d-flex-flex-column soh-column"
-              itemSize="26">
-              <div
-                *cdkVirtualFor="let region of (selectedRegions$ | async)"
-                class="virtual-scroll-unit">
-                <sleight-of-hand>
-                  <!-- prior to mouseover -->
-                  <div
-                    [class]="((darktheme$ | async) ? 'text-light bg-dark' : 'text-dark bg-light') + ' d-flex flex-row align-items-center virtual-scroll-row'"
-                    sleight-of-hand-front>
-                    <div class="ml-2 cursor-default text-nowrap">
-                      {{ region.name }}
-                    </div>
-      
-                    <!-- place holder icon to support height  -->
-                    <div [class]="(skeletonBtnClass$ | async) + ' invisible w-0 pe-none'">
-                      <i class="fas fa-trash"></i>
-                    </div>
-                  </div>
-      
-                  <!-- on mouse over -->
-                  <div
-                    [class]="((darktheme$ | async) ? 'text-light bg-dark' : 'text-dark bg-light') + ' d-flex flex-row align-items-center virtual-scroll-row'"
-                    sleight-of-hand-back>
-                    <div class="text-truncate selected-region-container ml-2 cursor-default text-nowrap">
-                      {{ region.name }}
-                    </div>
-                    <div
-                      *ngIf="region.position"
-                      (click)="gotoRegion($event, region)"
-                      matTooltip="Goto"
-                      matTooltipPosition="below"
-                      [class]="(skeletonBtnClass$ | async) + ' selected-region-actionbtn'">
-                      <i class="fas fa-map-marked-alt"></i>
-                    </div>
-                    <div
-                      #trashBtn="matTooltip"
-                      (mousedown)="trashBtn.hide()"
-                      (click)="deselectRegion($event, region)"
-                      matTooltip="Deselect"
-                      matTooltipPosition="below"
-                      [class]="(skeletonBtnClass$ | async) + ' selected-region-actionbtn'">
-                      <i class="fas fa-trash"></i>
-                    </div>
-                  </div>
-                </sleight-of-hand>
-              </div>
-            </cdk-virtual-scroll-viewport>
-          </div>
+        <div [class]="((darktheme$ | async) ? 'bg-dark' : 'bg-light') + ' position-absolute card'">
+          <viewer-state-controller #viewerStateController></viewer-state-controller>
         </div>
-
+        
         <!-- invisible icon to keep height of the otherwise unstable flex block -->
         <div class="invisible pe-none">
           <i class="fas fa-brain"></i>
diff --git a/src/ui/regionHierachy/regionHierarchy.component.ts b/src/ui/regionHierachy/regionHierarchy.component.ts
index 397d87c94..3c64434d2 100644
--- a/src/ui/regionHierachy/regionHierarchy.component.ts
+++ b/src/ui/regionHierachy/regionHierarchy.component.ts
@@ -38,9 +38,7 @@ export class RegionHierarchy implements OnInit, AfterViewInit{
   public selectedRegions: any[] = []
 
   @Input()
-  public selectedParcellation: any
-
-  @Input() isMobile: boolean;
+  public parcellationSelected: any
 
   private _showRegionTree: boolean = false
 
@@ -54,7 +52,7 @@ export class RegionHierarchy implements OnInit, AfterViewInit{
   private doubleClickRegion: EventEmitter<any> = new EventEmitter()
 
   @Output()
-  private clearAllRegions: EventEmitter<null> = new EventEmitter()
+  private clearAllRegions: EventEmitter<MouseEvent> = new EventEmitter()
 
   public searchTerm: string = ''
   private subscriptions: Subscription[] = []
@@ -62,6 +60,8 @@ export class RegionHierarchy implements OnInit, AfterViewInit{
   @ViewChild('searchTermInput', {read: ElementRef})
   private searchTermInput: ElementRef
 
+  public placeHolderText: string = `Start by selecting a template and a parcellation.`
+
   /**
    * set the height to max, bound by max-height
    */
@@ -95,17 +95,19 @@ export class RegionHierarchy implements OnInit, AfterViewInit{
   }
 
   ngOnChanges(){
-    this.aggregatedRegionTree = {
-      name: this.selectedParcellation.name,
-      children: this.selectedParcellation.regions
+    if (this.parcellationSelected) {
+      this.placeHolderText = `Search region in ${this.parcellationSelected.name}`
+      this.aggregatedRegionTree = {
+        name: this.parcellationSelected.name,
+        children: this.parcellationSelected.regions
+      }
     }
     this.displayTreeNode = getDisplayTreeNode(this.searchTerm, this.selectedRegions)
     this.filterTreeBySearch = getFilterTreeBySearch(this.filterNameBySearchPipe, this.searchTerm)
   }
 
   clearRegions(event:MouseEvent){
-    event.stopPropagation()
-    this.clearAllRegions.emit()
+    this.clearAllRegions.emit(event)
   }
 
   get showRegionTree(){
@@ -133,20 +135,6 @@ export class RegionHierarchy implements OnInit, AfterViewInit{
   }
 
   ngAfterViewInit(){
-    /**
-     * TODO
-     * bandaid fix on
-     * when region search loses focus, the searchTerm is cleared,
-     * but hierarchy filter does not reset
-     */
-    this.subscriptions.push(
-      fromEvent(this.searchTermInput.nativeElement, 'focus').pipe(
-        
-      ).subscribe(() => {
-        this.displayTreeNode = getDisplayTreeNode(this.searchTerm, this.selectedRegions)
-        this.filterTreeBySearch = getFilterTreeBySearch(this.filterNameBySearchPipe, this.searchTerm)
-      })
-    )
     this.subscriptions.push(
       fromEvent(this.searchTermInput.nativeElement, 'input').pipe(
         debounceTime(200)
@@ -156,13 +144,6 @@ export class RegionHierarchy implements OnInit, AfterViewInit{
     )
   }
 
-  getInputPlaceholder(parcellation:any) {
-    if (parcellation)
-      return `Search region in ${parcellation.name}`
-    else
-      return `Start by selecting a template and a parcellation.`
-  }
-
   escape(event:KeyboardEvent){
     this.showRegionTree = false
     this.searchTerm = '';
@@ -182,27 +163,12 @@ export class RegionHierarchy implements OnInit, AfterViewInit{
     })
   }
 
-  focusInput(event?:MouseEvent){
-    if (event) {
-      /**
-       * need to stop propagation, or @closeRegion will be triggered
-       */
-      event.stopPropagation()
-    }
-    this.searchTermInput.nativeElement.focus()
-    this.showRegionTree = true
-  }
-
   /* NB need to bind two way data binding like this. Or else, on searchInput blur, the flat tree will be rebuilt,
     resulting in first click to be ignored */
 
   changeSearchTerm(event: any) {
-    if (event.target.value === this.searchTerm)
-      return
+    if (event.target.value === this.searchTerm) return
     this.searchTerm = event.target.value
-    /**
-     * TODO maybe introduce debounce
-     */
     this.ngOnChanges()
     this.cdr.markForCheck()
   }
@@ -214,18 +180,17 @@ export class RegionHierarchy implements OnInit, AfterViewInit{
     /**
      * TODO figure out why @closeRegion gets triggered, but also, contains returns false
      */
-    if (event)
+    if (event) {
       event.stopPropagation()
+    }
     this.handleRegionTreeClickSubject.next(obj)
   }
 
   /* single click selects/deselects region(s) */
   private singleClick(obj: any) {
-    if (!obj)
-      return
+    if (!obj) return
     const { inputItem : region } = obj
-    if (!region)
-      return
+    if (!region) return
     this.singleClickRegion.emit(region)
   }
 
diff --git a/src/ui/sharedModules/angularMaterial.module.ts b/src/ui/sharedModules/angularMaterial.module.ts
index a1b13144f..cd9848a04 100644
--- a/src/ui/sharedModules/angularMaterial.module.ts
+++ b/src/ui/sharedModules/angularMaterial.module.ts
@@ -6,12 +6,55 @@ import {
   MatTabsModule,
   MatTooltipModule,
   MatBadgeModule,
-  MatDividerModule
+  MatDividerModule,
+  MatSelectModule,
+  MatChipsModule,
+  MatAutocompleteModule,
+  MatDialogModule,
+  MatInputModule,
+  MatBottomSheetModule,
+  MatListModule
 } from '@angular/material';
 import { NgModule } from '@angular/core';
 
+/**
+ * TODO should probably be in src/util
+ */
+
 @NgModule({
-  imports: [MatDividerModule, MatBadgeModule, MatButtonModule, MatCheckboxModule, MatSidenavModule, MatCardModule, MatTabsModule, MatTooltipModule],
-  exports: [MatDividerModule, MatBadgeModule, MatButtonModule, MatCheckboxModule, MatSidenavModule, MatCardModule, MatTabsModule, MatTooltipModule],
+  imports: [
+    MatDividerModule,
+    MatBadgeModule,
+    MatButtonModule,
+    MatCheckboxModule,
+    MatSidenavModule,
+    MatCardModule,
+    MatTabsModule,
+    MatTooltipModule,
+    MatSelectModule,
+    MatChipsModule,
+    MatAutocompleteModule,
+    MatDialogModule,
+    MatInputModule,
+    MatBottomSheetModule,
+    MatListModule
+  ],
+  exports: [
+    MatDividerModule,
+    MatBadgeModule,
+    MatButtonModule,
+    MatCheckboxModule,
+    MatSidenavModule,
+    MatCardModule,
+    MatTabsModule,
+    MatTooltipModule,
+    MatSelectModule,
+    MatChipsModule,
+    MatAutocompleteModule,
+    MatDialogModule,
+    MatInputModule,
+    MatBottomSheetModule,
+    MatListModule
+  ],
 })
 export class AngularMaterialModule { }
\ No newline at end of file
diff --git a/src/ui/signinBanner/signinBanner.components.ts b/src/ui/signinBanner/signinBanner.components.ts
index 07428cb0c..f559248fe 100644
--- a/src/ui/signinBanner/signinBanner.components.ts
+++ b/src/ui/signinBanner/signinBanner.components.ts
@@ -1,18 +1,9 @@
 import {Component, ChangeDetectionStrategy, OnDestroy, OnInit, Input, ViewChild, TemplateRef } from "@angular/core";
 import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service";
 import { AuthService, User } from "src/services/auth.service";
-import { Store, select } from "@ngrx/store";
+import { Store} from "@ngrx/store";
 import { ViewerConfiguration } from "src/services/state/viewerConfig.store";
-import { Subscription, Observable, merge, Subject, combineLatest } from "rxjs";
 import { safeFilter, isDefined, NEWVIEWER, SELECT_REGIONS, SELECT_PARCELLATION, CHANGE_NAVIGATION } from "src/services/stateStore.service";
-import { map, filter, distinctUntilChanged, bufferTime, delay, share, tap, withLatestFrom } from "rxjs/operators";
-import { regionFlattener } from "src/util/regionFlattener";
-import { ToastService } from "src/services/toastService.service";
-import { getSchemaIdFromName } from "src/util/pipes/templateParcellationDecoration.pipe";
-
-const compareParcellation = (o, n) => !o || !n
-  ? false
-  : o.name === n.name
 
 @Component({
   selector: 'signin-banner',
@@ -24,199 +15,18 @@ const compareParcellation = (o, n) => !o || !n
   changeDetection: ChangeDetectionStrategy.OnPush
 })
 
-export class SigninBanner implements OnInit, OnDestroy{
-
-  public compareParcellation = compareParcellation
-
-  private subscriptions: Subscription[] = []
-  
-  public loadedTemplates$: Observable<any[]>
+export class SigninBanner{
 
-  public selectedTemplate$: Observable<any>
-  public selectedParcellation$: Observable<any>
-
-  public selectedRegions$: Observable<any[]>
-  private selectedRegions: any[] = []
   @Input() darktheme: boolean
 
-  @ViewChild('publicationTemplate', {read:TemplateRef}) publicationTemplate: TemplateRef<any>
-
-  public focusedDatasets$: Observable<any[]>
-  private userFocusedDataset$: Subject<any> = new Subject()
-  public focusedDatasets: any[] = []
-  private dismissToastHandler: () => void
+  public isMobile: boolean
 
   constructor(
     private constantService: AtlasViewerConstantsServices,
     private authService: AuthService,
-    private store: Store<ViewerConfiguration>,
-    private toastService: ToastService
+    private store: Store<ViewerConfiguration>
   ){
-    this.loadedTemplates$ = this.store.pipe(
-      select('viewerState'),
-      safeFilter('fetchedTemplates'),
-      map(state => state.fetchedTemplates)
-    )
-
-    this.selectedTemplate$ = this.store.pipe(
-      select('viewerState'),
-      select('templateSelected'),
-      filter(v => !!v),
-      distinctUntilChanged((o, n) => o.name === n.name),
-    )
-
-    this.selectedParcellation$ = this.store.pipe(
-      select('viewerState'),
-      select('parcellationSelected'),
-      filter(v => !!v)
-    )
-
-    this.selectedRegions$ = this.store.pipe(
-      select('viewerState'),
-      safeFilter('regionsSelected'),
-      map(state => state.regionsSelected),
-      distinctUntilChanged((arr1, arr2) => arr1.length === arr2.length && (arr1 as any[]).every((item, index) => item.name === arr2[index].name))
-    )
-
-    this.focusedDatasets$ = this.userFocusedDataset$.pipe(
-      filter(v => !!v),
-      withLatestFrom(
-        combineLatest(this.selectedTemplate$, this.selectedParcellation$)
-      ),
-    ).pipe(
-      map(([userFocusedDataset, [selectedTemplate, selectedParcellation]]) => {
-        const { type, ...rest } = userFocusedDataset
-        if (type === 'template') return { ...selectedTemplate,  ...rest}
-        if (type === 'parcellation') return { ...selectedParcellation, ...rest }
-        return { ...rest }
-      }),
-      bufferTime(100),
-      filter(arr => arr.length > 0),
-      /**
-       * merge properties field with the root level
-       * with the prop in properties taking priority
-       */
-      map(arr => arr.map(item => {
-        const { properties } = item
-        return {
-          ...item,
-          ...properties
-        }
-      })),
-      share()
-    )
-  }
-
-  ngOnInit(){
-
-    this.subscriptions.push(
-      this.selectedRegions$.subscribe(regions => {
-        this.selectedRegions = regions
-      })
-    )
-
-    this.subscriptions.push(
-      this.focusedDatasets$.subscribe(() => {
-        if (this.dismissToastHandler) this.dismissToastHandler()
-      })
-    )
-
-    this.subscriptions.push(
-      this.focusedDatasets$.pipe(
-        /**
-         * creates the illusion that the toast complete disappears before reappearing
-         */
-        delay(100)
-      ).subscribe(arr => {
-        this.focusedDatasets = arr
-        this.dismissToastHandler = this.toastService.showToast(this.publicationTemplate, {
-          dismissable: true,
-          timeout:7000
-        })
-      })
-    )
-  }
-
-  ngOnDestroy(){
-    this.subscriptions.forEach(s => s.unsubscribe())
-  }
-
-  changeTemplate({ current, previous }){
-    if (previous && current && current.name === previous.name) return
-    this.store.dispatch({
-      type: NEWVIEWER,
-      selectTemplate: current,
-      selectParcellation: current.parcellations[0]
-    })
-  }
-
-  changeParcellation({ current, previous }){
-    const { ngId: prevNgId} = previous
-    const { ngId: currNgId} = current
-    if (prevNgId === currNgId) return
-    this.store.dispatch({
-      type: SELECT_PARCELLATION,
-      selectParcellation: current
-    })
-  }
-
-  // TODO handle mobile
-  handleRegionClick({ mode = 'single', region }){
-    if (!region) return
-    
-    /**
-     * single click on region hierarchy => toggle selection
-     */
-    if (mode === 'single') {
-      const flattenedRegion = regionFlattener(region).filter(r => isDefined(r.labelIndex))
-      const flattenedRegionNames = new Set(flattenedRegion.map(r => r.name))
-      const selectedRegionNames = new Set(this.selectedRegions.map(r => r.name))
-      const selectAll = flattenedRegion.every(r => !selectedRegionNames.has(r.name))
-      this.store.dispatch({
-        type: SELECT_REGIONS,
-        selectRegions: selectAll
-          ? this.selectedRegions.concat(flattenedRegion)
-          : this.selectedRegions.filter(r => !flattenedRegionNames.has(r.name))
-      })
-    }
-
-    /**
-     * double click on region hierarchy => navigate to region area if it exists
-     */
-    if (mode === 'double') {
-
-      /**
-       * if position is defined, go to position (in nm)
-       * if not, show error messagea s toast
-       * 
-       * nb: currently, only supports a single triplet
-       */
-      if (region.position) {
-        this.store.dispatch({
-          type: CHANGE_NAVIGATION,
-          navigation: {
-            position: region.position,
-            animation: {}
-          }
-        })
-      } else {
-        /**
-         * TODO convert to snack bar
-         */
-        this.toastService.showToast(`${region.name} does not have a position defined`, {
-          timeout: 5000,
-          dismissable: true
-        })
-      }
-    }
-  }
-
-  displayActiveParcellation(parcellation:any){
-    return `<div class="d-flex"><small>Parcellation</small> <small class = "flex-grow-1 mute-text">${parcellation ? '(' + parcellation.name + ')' : ''}</small> <span class = "fas fa-caret-down"></span></div>`
-  }
-
-  displayActiveTemplate(template: any) {
-    return `<div class="d-flex"><small>Template</small> <small class = "flex-grow-1 mute-text">${template ? '(' + template.name + ')' : ''}</small> <span class = "fas fa-caret-down"></span></div>`
+    this.isMobile = this.constantService.mobile
   }
 
   showHelp() {
@@ -227,56 +37,7 @@ export class SigninBanner implements OnInit, OnDestroy{
     this.constantService.showSigninSubject$.next(this.user)
   }
 
-  clearAllRegions(){
-    this.store.dispatch({
-      type: SELECT_REGIONS,
-      selectRegions: []
-    })
-  }
-
-  handleActiveDisplayBtnClicked(event, type: 'parcellation' | 'template'){
-    const { 
-      extraBtn,
-      event: extraBtnClickEvent
-    } = event
-
-    const { name } = extraBtn
-    const { kgSchema, kgId } = getSchemaIdFromName(name)
-    
-    this.userFocusedDataset$.next({
-      kgSchema,
-      kgId,
-      type
-    })
-  }
-
-  handleExtraBtnClicked(event, toastType: 'parcellation' | 'template'){
-    const { 
-      extraBtn,
-      inputItem,
-      event: extraBtnClickEvent
-    } = event
-
-    const { name } = extraBtn
-    const { kgSchema, kgId } = getSchemaIdFromName(name)
-
-    this.userFocusedDataset$.next({
-      ...inputItem,
-      kgSchema,
-      kgId
-    })
-
-    extraBtnClickEvent.stopPropagation()
-  }
-
-  get isMobile(){
-    return this.constantService.mobile
-  }
-
   get user() : User | null {
     return this.authService.user
   }
-
-  public flexItemIsMobileClass = 'mt-2'
-  public flexItemIsDesktopClass = 'mr-2'
 }
\ No newline at end of file
diff --git a/src/ui/signinBanner/signinBanner.template.html b/src/ui/signinBanner/signinBanner.template.html
index c3d288680..14f80d223 100644
--- a/src/ui/signinBanner/signinBanner.template.html
+++ b/src/ui/signinBanner/signinBanner.template.html
@@ -2,48 +2,6 @@
   class="d-flex"  
   [ngClass]="{ 'flex-column w-100 align-items-stretch' : isMobile}" >
 
-  <dropdown-component
-    (itemSelected)="changeTemplate($event)"
-    [checkSelected]="compareParcellation"
-    [activeDisplay]="displayActiveTemplate"
-    [selectedItem]="selectedTemplate$ | async"
-    [inputArray]="loadedTemplates$ | async | filterNull | appendTooltipTextPipe"
-    [ngClass]="isMobile ? flexItemIsMobileClass : flexItemIsDesktopClass"
-    (extraBtnClicked)="handleExtraBtnClicked($event, 'template')"
-    [activeDisplayBtns]="(selectedTemplate$ | async | templateParcellationsDecorationPipe)?.extraButtons"
-    (activeDisplayBtnClicked)="handleActiveDisplayBtnClicked($event, 'template')"
-    (listItemButtonClicked)="handleExtraBtnClicked($event, 'template')">
-  </dropdown-component>
-
-  <ng-container *ngIf="selectedTemplate$ | async as selectedTemplate">
-    <dropdown-component
-      *ngIf="selectedParcellation$ | async as selectedParcellation"
-      (itemSelected)="changeParcellation($event)"
-      [checkSelected]="compareParcellation"
-      [activeDisplay]="displayActiveParcellation"
-      [selectedItem]="selectedParcellation"
-      [inputArray]="selectedTemplate.parcellations | appendTooltipTextPipe"
-      [ngClass]="isMobile ? flexItemIsMobileClass : flexItemIsDesktopClass"
-      (extraBtnClicked)="handleExtraBtnClicked($event, 'parcellation')"
-      [activeDisplayBtns]="(selectedParcellation | templateParcellationsDecorationPipe)?.extraButtons"
-      (activeDisplayBtnClicked)="handleActiveDisplayBtnClicked($event, 'parcellation')"
-      (listItemButtonClicked)="handleExtraBtnClicked($event, 'parcellation')">
-
-    </dropdown-component>
-    <region-hierarchy
-      [selectedRegions]="selectedRegions$ | async | filterNull"
-      (singleClickRegion)="handleRegionClick({ mode: 'single', region: $event })"
-      (doubleClickRegion)="handleRegionClick({ mode: 'double', region: $event })"
-      (clearAllRegions)="clearAllRegions()"
-      [isMobile] = "isMobile"
-      *ngIf="selectedParcellation$ | async as selectedParcellation"
-      class="h-0"
-      [selectedParcellation]="selectedParcellation"
-      [ngClass]="isMobile ? flexItemIsMobileClass : flexItemIsDesktopClass">
-
-    </region-hierarchy>
-  </ng-container>
-
   <!-- help btn -->
   <div class="btnWrapper">
     <div
@@ -81,17 +39,3 @@
 
   </div>
 </div>
-
-<!-- TODO somehow, using async pipe here does not work -->
-<!-- maybe have something to do with bufferTime, and that it replays from the beginning?  -->
-<ng-template #publicationTemplate>
-  <single-dataset-view
-    *ngFor="let focusedDataset of focusedDatasets"
-    [name]="focusedDataset.name"
-    [description]="focusedDataset.description"
-    [publications]="focusedDataset.publications"
-    [kgSchema]="focusedDataset.kgSchema"
-    [kgId]="focusedDataset.kgId">
-    
-  </single-dataset-view>
-</ng-template>
\ No newline at end of file
diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts
index 694000b8e..6606c25ac 100644
--- a/src/ui/ui.module.ts
+++ b/src/ui/ui.module.ts
@@ -5,7 +5,7 @@ import { NehubaViewerUnit } from "./nehubaContainer/nehubaViewer/nehubaViewer.co
 import { NehubaContainer } from "./nehubaContainer/nehubaContainer.component";
 import { SplashScreen } from "./nehubaContainer/splashScreen/splashScreen.component";
 import { LayoutModule } from "../layouts/layout.module";
-import { FormsModule } from "@angular/forms";
+import { FormsModule, ReactiveFormsModule } from "@angular/forms";
 
 import { GroupDatasetByRegion } from "../util/pipes/groupDataEntriesByRegion.pipe";
 import { filterRegionDataEntries } from "../util/pipes/filterRegionDataEntries.pipe";
@@ -17,7 +17,7 @@ import { LandmarkUnit } from "./nehubaContainer/landmarkUnit/landmarkUnit.compon
 import { SafeStylePipe } from "../util/pipes/safeStyle.pipe";
 import { PluginBannerUI } from "./pluginBanner/pluginBanner.component";
 import { CitationsContainer } from "./citation/citations.component";
-import { LayerBrowser } from "./layerbrowser/layerbrowser.component";
+import { LayerBrowser, LockedLayerBtnClsPipe } from "./layerbrowser/layerbrowser.component";
 import { TooltipModule } from "ngx-bootstrap/tooltip";
 import { KgEntryViewer } from "./kgEntryViewer/kgentry.component";
 import { SubjectViewer } from "./kgEntryViewer/subjectViewer/subjectViewer.component";
@@ -38,10 +38,9 @@ import { PopoverModule } from 'ngx-bootstrap/popover'
 import { DatabrowserModule } from "./databrowserModule/databrowser.module";
 import { SigninBanner } from "./signinBanner/signinBanner.components";
 import { SigninModal } from "./signinModal/signinModal.component";
-import { FilterNgLayer } from "src/util/pipes/filterNgLayer.pipe";
 import { UtilModule } from "src/util/util.module";
-import { RegionHierarchy } from "./regionHierachy/regionHierarchy.component";
-import { FilterNameBySearch } from "./regionHierachy/filterNameBySearch.pipe";
+import { RegionHierarchy } from "./viewerStateController/regionHierachy/regionHierarchy.component";
+import { FilterNameBySearch } from "./viewerStateController/regionHierachy/filterNameBySearch.pipe";
 import { StatusCardComponent } from "./nehubaContainer/statusCard/statusCard.component";
 import { CookieAgreement } from "./cookieAgreement/cookieAgreement.component";
 import { KGToS } from "./kgtos/kgtos.component";
@@ -51,10 +50,17 @@ import { AppendtooltipTextPipe } from "src/util/pipes/appendTooltipText.pipe";
 import { MenuIconPluginBtnClsPipe } from "src/util/pipes/menuIconPluginBtnCls.pipe";
 import { MenuIconKgSearchBtnClsPipe } from "src/util/pipes/menuIconKgSearchBtnCls.pipe";
 import { ScrollingModule } from "@angular/cdk/scrolling"
+import { GetFilenamePipe } from "src/util/pipes/getFilename.pipe";
+import { GetFileExtension } from "src/util/pipes/getFileExt.pipe";
+import { ViewerStateController } from "./viewerStateController/viewerState.component";
+import { BinSavedRegionsSelectionPipe, SavedRegionsSelectionBtnDisabledPipe } from "./viewerStateController/viewerState.pipes";
+import { RegionTextSearchAutocomplete } from "./viewerStateController/regionSearch/regionSearch.component";
+
 
 @NgModule({
   imports : [
     FormsModule,
+    ReactiveFormsModule,
     LayoutModule,
     ComponentsModule,
     DatabrowserModule,
@@ -87,6 +93,8 @@ import { ScrollingModule } from "@angular/cdk/scrolling"
     StatusCardComponent,
     CookieAgreement,
     KGToS,
+    ViewerStateController,
+    RegionTextSearchAutocomplete,
 
     /* pipes */
     GroupDatasetByRegion,
@@ -98,12 +106,16 @@ import { ScrollingModule } from "@angular/cdk/scrolling"
     SortDataEntriesToRegion,
     SpatialLandmarksToDataBrowserItemPipe,
     FilterNullPipe,
-    FilterNgLayer,
     FilterNameBySearch,
     TemplateParcellationsDecorationPipe,
     AppendtooltipTextPipe,
     MenuIconPluginBtnClsPipe,
     MenuIconKgSearchBtnClsPipe,
+    LockedLayerBtnClsPipe,
+    GetFilenamePipe,
+    GetFileExtension,
+    BinSavedRegionsSelectionPipe,
+    SavedRegionsSelectionBtnDisabledPipe,
 
     /* directive */
     DownloadDirective,
@@ -114,7 +126,7 @@ import { ScrollingModule } from "@angular/cdk/scrolling"
     /* dynamically created components needs to be declared here */
     NehubaViewerUnit,
     LayerBrowser,
-    PluginBannerUI
+    PluginBannerUI,
   ],
   exports : [
     SubjectViewer,
diff --git a/src/ui/regionHierachy/filterNameBySearch.pipe.ts b/src/ui/viewerStateController/regionHierachy/filterNameBySearch.pipe.ts
similarity index 100%
rename from src/ui/regionHierachy/filterNameBySearch.pipe.ts
rename to src/ui/viewerStateController/regionHierachy/filterNameBySearch.pipe.ts
diff --git a/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts b/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts
new file mode 100644
index 000000000..2fd52a746
--- /dev/null
+++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts
@@ -0,0 +1,214 @@
+import { EventEmitter, Component, ElementRef, ViewChild, HostListener, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, Input, Output, AfterViewInit } from "@angular/core";
+import {  Subscription, Subject, fromEvent } from "rxjs";
+import { buffer, debounceTime, tap } from "rxjs/operators";
+import { FilterNameBySearch } from "./filterNameBySearch.pipe";
+import { generateLabelIndexId } from "src/services/stateStore.service";
+
+const insertHighlight :(name:string, searchTerm:string) => string = (name:string, searchTerm:string = '') => {
+  const regex = new RegExp(searchTerm, 'gi')
+  return searchTerm === '' ?
+    name :
+    name.replace(regex, (s) => `<span class = "highlight">${s}</span>`)
+}
+
+const getDisplayTreeNode : (searchTerm:string, selectedRegions:any[]) => (item:any) => string = (searchTerm:string = '', selectedRegions:any[] = []) => ({ ngId, name, status, labelIndex }) =>  {
+  return !!labelIndex
+    && !!ngId
+    && selectedRegions.findIndex(re =>
+      generateLabelIndexId({ labelIndex: re.labelIndex, ngId: re.ngId }) === generateLabelIndexId({ ngId, labelIndex })
+    ) >= 0
+      ? `<span class="cursor-default regionSelected">${insertHighlight(name, searchTerm)}</span>` + (status ? ` <span class="text-muted">(${insertHighlight(status, searchTerm)})</span>` : ``)
+      : `<span class="cursor-default regionNotSelected">${insertHighlight(name, searchTerm)}</span>` + (status ? ` <span class="text-muted">(${insertHighlight(status, searchTerm)})</span>` : ``)
+}
+
+const getFilterTreeBySearch = (pipe:FilterNameBySearch, searchTerm:string) => (node:any) => pipe.transform([node.name, node.status], searchTerm)
+
+@Component({
+  selector: 'region-hierarchy',
+  templateUrl: './regionHierarchy.template.html',
+  styleUrls: [
+    './regionHierarchy.style.css'
+  ],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+
+export class RegionHierarchy implements OnInit, AfterViewInit{
+
+  @Input()
+  public selectedRegions: any[] = []
+
+  @Input()
+  public parcellationSelected: any
+
+  private _showRegionTree: boolean = false
+
+  @Output()
+  private showRegionFlagChanged: EventEmitter<boolean> = new EventEmitter()
+
+  @Output()
+  private singleClickRegion: EventEmitter<any> = new EventEmitter()
+
+  @Output()
+  private doubleClickRegion: EventEmitter<any> = new EventEmitter()
+
+  @Output()
+  private clearAllRegions: EventEmitter<MouseEvent> = new EventEmitter()
+
+  public searchTerm: string = ''
+  private subscriptions: Subscription[] = []
+
+  @ViewChild('searchTermInput', {read: ElementRef})
+  private searchTermInput: ElementRef
+
+  public placeHolderText: string = `Start by selecting a template and a parcellation.`
+
+  /**
+   * set the height to max, bound by max-height
+   */
+  numTotalRenderedRegions: number = 999
+  windowHeight: number
+
+  @HostListener('document:click', ['$event'])
+  closeRegion(event: MouseEvent) {
+    const contains = this.el.nativeElement.contains(event.target)
+    this.showRegionTree = contains
+    if (!this.showRegionTree){
+      this.searchTerm = ''
+      this.numTotalRenderedRegions = 999
+    }
+  }
+
+  @HostListener('window:resize', ['$event'])
+  onResize(event) {
+    this.windowHeight = event.target.innerHeight;
+  }
+
+  get regionsLabelIndexMap() {
+    return null
+  }
+
+  constructor(
+    private cdr:ChangeDetectorRef,
+    private el:ElementRef
+  ){
+    this.windowHeight = window.innerHeight;
+  }
+
+  ngOnChanges(){
+    if (this.parcellationSelected) {
+      this.placeHolderText = `Search region in ${this.parcellationSelected.name}`
+      this.aggregatedRegionTree = {
+        name: this.parcellationSelected.name,
+        children: this.parcellationSelected.regions
+      }
+    }
+    this.displayTreeNode = getDisplayTreeNode(this.searchTerm, this.selectedRegions)
+    this.filterTreeBySearch = getFilterTreeBySearch(this.filterNameBySearchPipe, this.searchTerm)
+  }
+
+  clearRegions(event:MouseEvent){
+    this.clearAllRegions.emit(event)
+  }
+
+  get showRegionTree(){
+    return this._showRegionTree
+  }
+
+  set showRegionTree(flag: boolean){
+    this._showRegionTree = flag
+    this.showRegionFlagChanged.emit(this._showRegionTree)
+  }
+
+  ngOnInit(){
+    this.displayTreeNode = getDisplayTreeNode(this.searchTerm, this.selectedRegions)
+    this.filterTreeBySearch = getFilterTreeBySearch(this.filterNameBySearchPipe, this.searchTerm)
+
+    this.subscriptions.push(
+      this.handleRegionTreeClickSubject.pipe(
+        buffer(
+          this.handleRegionTreeClickSubject.pipe(
+            debounceTime(200)
+          )
+        )
+      ).subscribe(arr => arr.length > 1 ? this.doubleClick(arr[0]) : this.singleClick(arr[0]))
+    )
+  }
+
+  ngAfterViewInit(){
+    this.subscriptions.push(
+      fromEvent(this.searchTermInput.nativeElement, 'input').pipe(
+        debounceTime(200)
+      ).subscribe(ev => {
+        this.changeSearchTerm(ev)
+      })
+    )
+  }
+
+  escape(event:KeyboardEvent){
+    this.showRegionTree = false
+    this.searchTerm = '';
+    (event.target as HTMLInputElement).blur()
+
+  }
+
+  handleTotalRenderedListChanged(changeEvent: {previous: number, current: number}){
+    const { current } = changeEvent
+    this.numTotalRenderedRegions = current
+  }
+
+  regionHierarchyHeight(){
+    return({
+      'height' : (this.numTotalRenderedRegions * 15 + 60).toString() + 'px',
+      'max-height': (this.windowHeight - 100) + 'px'
+    })
+  }
+
+  /* NB need to bind two way data binding like this. Or else, on searchInput blur, the flat tree will be rebuilt,
+    resulting in first click to be ignored */
+
+  changeSearchTerm(event: any) {
+    if (event.target.value === this.searchTerm) return
+    this.searchTerm = event.target.value
+    this.ngOnChanges()
+    this.cdr.markForCheck()
+  }
+
+  private handleRegionTreeClickSubject: Subject<any> = new Subject()
+
+  handleClickRegion(obj: any) {
+    const {event} = obj
+    /**
+     * TODO figure out why @closeRegion gets triggered, but also, contains returns false
+     */
+    if (event) {
+      event.stopPropagation()
+    }
+    this.handleRegionTreeClickSubject.next(obj)
+  }
+
+  /* single click selects/deselects region(s) */
+  private singleClick(obj: any) {
+    if (!obj) return
+    const { inputItem : region } = obj
+    if (!region) return
+    this.singleClickRegion.emit(region)
+  }
+
+  /* double click navigate to the interested area */
+  private doubleClick(obj: any) {
+    if (!obj)
+      return
+    const { inputItem : region } = obj
+    if (!region)
+      return
+    this.doubleClickRegion.emit(region)
+  }
+
+  public displayTreeNode: (item:any) => string
+
+  private filterNameBySearchPipe = new FilterNameBySearch()
+  public filterTreeBySearch: (node:any) => boolean 
+
+  public aggregatedRegionTree: any
+
+}
\ No newline at end of file
diff --git a/src/ui/regionHierachy/regionHierarchy.style.css b/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css
similarity index 86%
rename from src/ui/regionHierachy/regionHierarchy.style.css
rename to src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css
index a4aa507f4..8fbf9e702 100644
--- a/src/ui/regionHierachy/regionHierarchy.style.css
+++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css
@@ -26,7 +26,6 @@ div[treeContainer]
 
 div[hideScrollbarcontainer]
 {
-  width: 20em;
   overflow:hidden;
   margin-top:2px;
 }
@@ -62,4 +61,20 @@ input[type="text"]
 .tree-body
 {
   flex: 1 1 auto;
+}
+
+:host
+{
+  display: flex;
+  flex-direction: column;
+}
+
+:host > mat-form-field
+{
+  flex: 0 0 auto;
+}
+
+:host > [hideScrollbarContainer]
+{
+  flex: 1 1 0;
 }
\ No newline at end of file
diff --git a/src/ui/regionHierachy/regionHierarchy.template.html b/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html
similarity index 72%
rename from src/ui/regionHierachy/regionHierarchy.template.html
rename to src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html
index 91d9f552c..48e0540fe 100644
--- a/src/ui/regionHierachy/regionHierarchy.template.html
+++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html
@@ -1,24 +1,19 @@
-<div class="input-group regionSearch">
+<mat-form-field class="w-100">
   <input 
     #searchTermInput
-    tabindex="0"
+    matInput
     (keydown.esc)="escape($event)"
-    (focus)="showRegionTree = true && !isMobile"
+    (focus)="showRegionTree = true"
     [value]="searchTerm"
     class="form-control form-control-sm"
     type="text" 
     autocomplete="off"
-    [placeholder]="getInputPlaceholder(selectedParcellation)"/>
-
-</div>
+    [placeholder]="placeHolderText"/>
+</mat-form-field>
   
-<div
-  *ngIf="showRegionTree" 
-  hideScrollbarContainer>
-
+<div hideScrollbarContainer>
   <div
-    [ngStyle]="regionHierarchyHeight()"
-    class="d-flex flex-column"
+    class="d-flex flex-column h-100"
     treeContainer
     #treeContainer>
     <div class="tree-header d-inline-flex align-items-center">
@@ -34,7 +29,7 @@
     </div>
     
     <div
-      *ngIf="selectedParcellation && selectedParcellation.regions as regions"
+      *ngIf="parcellationSelected && parcellationSelected.regions as regions"
       class="tree-body">
       <flat-tree-component
         (treeNodeClick)="handleClickRegion($event)"
diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts
new file mode 100644
index 000000000..c0e178323
--- /dev/null
+++ b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts
@@ -0,0 +1,149 @@
+import { Component, EventEmitter, Output, ViewChild, ElementRef, TemplateRef } from "@angular/core";
+import { Store, select } from "@ngrx/store";
+import { Observable } from "rxjs";
+import { map, distinctUntilChanged, startWith, withLatestFrom, filter, debounceTime, tap, share, shareReplay, take } from "rxjs/operators";
+import { getMultiNgIdsRegionsLabelIndexMap, generateLabelIndexId } from "src/services/stateStore.service";
+import { FormControl } from "@angular/forms";
+import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger, MatDialog } from "@angular/material";
+import { ADD_TO_REGIONS_SELECTION_WITH_IDS, SELECT_REGIONS } from "src/services/state/viewerState.store";
+import { VIEWERSTATE_ACTION_TYPES } from "../viewerState.component";
+
+const filterRegionBasedOnText = searchTerm => region => region.name.toLowerCase().includes(searchTerm.toLowerCase())
+
+@Component({
+  selector: 'region-text-search-autocomplete',
+  templateUrl: './regionSearch.template.html',
+  styleUrls: [
+    './regionSearch.style.css'
+  ]
+})
+
+export class RegionTextSearchAutocomplete{
+
+  @ViewChild('autoTrigger', {read: ElementRef}) autoTrigger: ElementRef 
+  @ViewChild('regionHierarchy', {read:TemplateRef}) regionHierarchyTemplate: TemplateRef<any>
+  constructor(
+    private store$: Store<any>,
+    private dialog: MatDialog,
+  ){
+
+    const viewerState$ = this.store$.pipe(
+      select('viewerState'),
+      shareReplay(1)
+    )
+
+    this.regionsWithLabelIndex$ = viewerState$.pipe(
+      select('parcellationSelected'),
+      distinctUntilChanged(),
+      map(parcellationSelected => {
+        const returnArray = []
+        const ngIdMap = getMultiNgIdsRegionsLabelIndexMap(parcellationSelected)
+        for (const [ngId, labelIndexMap] of ngIdMap) {
+          for (const [labelIndex, region] of labelIndexMap){
+            returnArray.push({
+              ...region,
+              ngId,
+              labelIndex,
+              labelIndexId: generateLabelIndexId({ ngId, labelIndex })
+            })
+          }
+        }
+        return returnArray
+      })
+    ) 
+
+    this.autocompleteList$ = this.formControl.valueChanges.pipe(
+      startWith(''),
+      debounceTime(200),
+      withLatestFrom(this.regionsWithLabelIndex$.pipe(
+        startWith([])
+      )),
+      map(([searchTerm, regionsWithLabelIndex]) => regionsWithLabelIndex.filter(filterRegionBasedOnText(searchTerm))),
+      map(arr => arr.slice(0, 5))
+    )
+
+    this.regionsSelected$ = viewerState$.pipe(
+      select('regionsSelected'),
+      distinctUntilChanged(),
+      shareReplay(1)
+    )
+
+    this.parcellationSelected$ = viewerState$.pipe(
+      select('parcellationSelected'),
+      distinctUntilChanged(),
+      shareReplay(1)
+    )
+  }
+
+  public optionSelected(ev: MatAutocompleteSelectedEvent){
+    const id = ev.option.value
+    this.store$.dispatch({
+      type: ADD_TO_REGIONS_SELECTION_WITH_IDS,
+      selectRegionIds : [id]
+    })
+
+    this.autoTrigger.nativeElement.value = ''
+    this.autoTrigger.nativeElement.focus()
+  }
+
+  private regionsWithLabelIndex$: Observable<any[]>
+  public autocompleteList$: Observable<any[]>
+  public formControl = new FormControl()
+
+  public regionsSelected$: Observable<any>
+  public parcellationSelected$: Observable<any>
+
+
+  @Output()
+  public focusedStateChanged: EventEmitter<boolean> = new EventEmitter()
+
+  private _focused: boolean = false
+  set focused(val: boolean){
+    this._focused = val
+    this.focusedStateChanged.emit(val)
+  }
+  get focused(){
+    return this._focused
+  }
+
+  public deselectAllRegions(event: MouseEvent){
+    this.store$.dispatch({
+      type: SELECT_REGIONS,
+      selectRegions: []
+    })
+  }
+
+  // TODO handle mobile
+  handleRegionClick({ mode = null, region = null } = {}){
+    const type = mode === 'single'
+      ? VIEWERSTATE_ACTION_TYPES.SINGLE_CLICK_ON_REGIONHIERARCHY
+      : mode === 'double'
+        ? VIEWERSTATE_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY
+        : ''
+    this.store$.dispatch({
+      type,
+      payload: { region }
+    })
+  }
+
+  showHierarchy(event:MouseEvent){
+    const dialog = this.dialog.open(this.regionHierarchyTemplate, {
+      height: '90vh',
+      width: '90vw'
+    })
+
+    /**
+     * keep sleight of hand shown while modal is shown
+     * 
+     */
+    this.focused = true
+    
+    /**
+     * take 1 to avoid memory leak
+     */
+    dialog.afterClosed().pipe(
+      take(1)
+    ).subscribe(() => this.focused = false)
+  }
+
+}
\ No newline at end of file
diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.style.css b/src/ui/viewerStateController/regionSearch/regionSearch.style.css
new file mode 100644
index 000000000..17cda15a4
--- /dev/null
+++ b/src/ui/viewerStateController/regionSearch/regionSearch.style.css
@@ -0,0 +1,4 @@
+region-hierarchy
+{
+  height: 100%;
+}
\ No newline at end of file
diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.template.html b/src/ui/viewerStateController/regionSearch/regionSearch.template.html
new file mode 100644
index 000000000..ef828ac2e
--- /dev/null
+++ b/src/ui/viewerStateController/regionSearch/regionSearch.template.html
@@ -0,0 +1,45 @@
+<div class="d-flex flex-row align-items-center">
+
+  <form class="flex-grow-1 flex-shrink-1">
+    <mat-form-field class="w-100">
+      <input
+        placeholder="Regions"
+        #autoTrigger
+        #trigger="matAutocompleteTrigger"
+        type="text"
+        matInput
+        [formControl]="formControl"
+        [matAutocomplete]="auto">
+    </mat-form-field>
+    <mat-autocomplete
+      (opened)="focused = true"
+      (closed)="focused = false"
+      (optionSelected)="optionSelected($event)"
+      autoActiveFirstOption
+      #auto="matAutocomplete">
+      <mat-option
+        *ngFor="let region of autocompleteList$ | async"
+        [value]="region.labelIndexId">
+        {{ region.name }}
+      </mat-option>
+    </mat-autocomplete>
+  </form>
+    
+  <button
+    class="flex-grow-0 flex-shrink-0"
+    (click)="showHierarchy($event)"
+    mat-icon-button color="primary">
+    <i class="fas fa-sitemap"></i>
+  </button>
+</div>
+
+<ng-template #regionHierarchy>
+  <region-hierarchy
+    [selectedRegions]="regionsSelected$ | async | filterNull"
+    (singleClickRegion)="handleRegionClick({ mode: 'single', region: $event })"
+    (doubleClickRegion)="handleRegionClick({ mode: 'double', region: $event })"
+    (clearAllRegions)="deselectAllRegions($event)"
+    [parcellationSelected]="parcellationSelected$ | async">
+  
+  </region-hierarchy>
+</ng-template>
diff --git a/src/ui/viewerStateController/viewerState.component.ts b/src/ui/viewerStateController/viewerState.component.ts
new file mode 100644
index 000000000..726c1e895
--- /dev/null
+++ b/src/ui/viewerStateController/viewerState.component.ts
@@ -0,0 +1,304 @@
+import { Component, ViewChild, TemplateRef, OnInit } from "@angular/core";
+import { Store, select } from "@ngrx/store";
+import { Observable, Subject, combineLatest, Subscription } from "rxjs";
+import { distinctUntilChanged, shareReplay, bufferTime, filter, map, withLatestFrom, delay, take, tap } from "rxjs/operators";
+import { SELECT_REGIONS, USER_CONFIG_ACTION_TYPES } from "src/services/stateStore.service";
+import { DESELECT_REGIONS, CHANGE_NAVIGATION } from "src/services/state/viewerState.store";
+import { ToastService } from "src/services/toastService.service";
+import { getSchemaIdFromName } from "src/util/pipes/templateParcellationDecoration.pipe";
+import { MatDialog, MatSelectChange, MatBottomSheet, MatBottomSheetRef } from "@angular/material";
+import { ExtraButton } from "src/components/radiolist/radiolist.component";
+import { DialogService } from "src/services/dialogService.service";
+import { RegionSelection } from "src/services/state/userConfigState.store";
+
+const compareWith = (o, n) => !o || !n
+  ? false
+  : o.name === n.name
+
+@Component({
+  selector: 'viewer-state-controller',
+  templateUrl: './viewerState.template.html',
+  styleUrls: [
+    './viewerState.style.css'
+  ]
+})
+
+export class ViewerStateController implements OnInit{
+
+  @ViewChild('publicationTemplate', {read:TemplateRef}) publicationTemplate: TemplateRef<any>
+  @ViewChild('savedRegionBottomSheetTemplate', {read:TemplateRef}) savedRegionBottomSheetTemplate: TemplateRef<any>
+
+  public focused: boolean = false
+
+  private subscriptions: Subscription[] = []
+
+  public availableTemplates$: Observable<any[]>
+  public availableParcellations$: Observable<any[]>
+
+  public templateSelected$: Observable<any>
+  public parcellationSelected$: Observable<any>
+  public regionsSelected$: Observable<any>
+
+  public savedRegionsSelections$: Observable<any[]>
+
+  public focusedDatasets$: Observable<any[]>
+  private userFocusedDataset$: Subject<any> = new Subject()
+  private dismissToastHandler: () => void
+
+  public compareWith = compareWith
+
+  private savedRegionBottomSheetRef: MatBottomSheetRef
+
+  constructor(
+    private store$: Store<any>,
+    private toastService: ToastService,
+    private dialogService: DialogService,
+    private bottomSheet: MatBottomSheet
+  ){
+    const viewerState$ = this.store$.pipe(
+      select('viewerState'),
+      shareReplay(1)
+    )
+
+    this.savedRegionsSelections$ = this.store$.pipe(
+      select('userConfigState'),
+      select('savedRegionsSelection'),
+      shareReplay(1)
+    )
+
+    this.templateSelected$ = viewerState$.pipe(
+      select('templateSelected'),
+      distinctUntilChanged()
+    )
+
+    this.parcellationSelected$ = viewerState$.pipe(
+      select('parcellationSelected'),
+      distinctUntilChanged(),
+      shareReplay(1)
+    )
+
+    this.regionsSelected$ = viewerState$.pipe(
+      select('regionsSelected'),
+      distinctUntilChanged(),
+      shareReplay(1)
+    )
+
+    this.availableTemplates$ = viewerState$.pipe(
+      select('fetchedTemplates'),
+      distinctUntilChanged()
+    )
+
+    this.availableParcellations$ = this.templateSelected$.pipe(
+      select('parcellations')
+    )
+    
+    this.focusedDatasets$ = this.userFocusedDataset$.pipe(
+      filter(v => !!v),
+      withLatestFrom(
+        combineLatest(this.templateSelected$, this.parcellationSelected$)
+      ),
+    ).pipe(
+      map(([userFocusedDataset, [selectedTemplate, selectedParcellation]]) => {
+        const { type, ...rest } = userFocusedDataset
+        if (type === 'template') return { ...selectedTemplate,  ...rest}
+        if (type === 'parcellation') return { ...selectedParcellation, ...rest }
+        return { ...rest }
+      }),
+      bufferTime(100),
+      filter(arr => arr.length > 0),
+      /**
+       * merge properties field with the root level
+       * with the prop in properties taking priority
+       */
+      map(arr => arr.map(item => {
+        const { properties } = item
+        return {
+          ...item,
+          ...properties
+        }
+      })),
+      shareReplay(1)
+    )
+  }
+
+  ngOnInit(){
+    this.subscriptions.push(
+      this.savedRegionsSelections$.pipe(
+        filter(srs => srs.length === 0)
+      ).subscribe(() => this.savedRegionBottomSheetRef && this.savedRegionBottomSheetRef.dismiss())
+    )
+    this.subscriptions.push(
+      this.focusedDatasets$.subscribe(() => this.dismissToastHandler && this.dismissToastHandler())
+    )
+    this.subscriptions.push(
+      this.focusedDatasets$.pipe(
+        /**
+         * creates the illusion that the toast complete disappears before reappearing
+         */
+        delay(100)
+      ).subscribe(() => this.dismissToastHandler = this.toastService.showToast(this.publicationTemplate, {
+        dismissable: true,
+        timeout:7000
+      }))
+    )
+  }
+
+  handleActiveDisplayBtnClicked(event: MouseEvent, type: 'parcellation' | 'template', extraBtn: ExtraButton, inputItem:any = {}){
+    const { name } = extraBtn
+    const { kgSchema, kgId } = getSchemaIdFromName(name)
+    this.userFocusedDataset$.next({
+      ...inputItem,
+      kgSchema,
+      kgId
+    })
+  }
+
+  handleTemplateChange(event:MatSelectChange){
+    
+    this.store$.dispatch({
+      type: ACTION_TYPES.SELECT_TEMPLATE_WITH_NAME,
+      payload: {
+        name: event.value
+      }
+    })
+  }
+
+  handleParcellationChange(event:MatSelectChange){
+    if (!event.value) return
+    this.store$.dispatch({
+      type: ACTION_TYPES.SELECT_PARCELLATION_WITH_NAME,
+      payload: {
+        name: event.value
+      }
+    })
+  }
+
+  loadSavedRegion(event:MouseEvent, savedRegionsSelection:RegionSelection){
+    this.store$.dispatch({
+      type: USER_CONFIG_ACTION_TYPES.LOAD_REGIONS_SELECTION,
+      payload: {
+        savedRegionsSelection
+      }
+    })
+  }
+
+  public editSavedRegion(event: MouseEvent, savedRegionsSelection: RegionSelection){
+    event.preventDefault()
+    event.stopPropagation()
+    this.dialogService.getUserInput({
+      defaultValue: savedRegionsSelection.name,
+      placeholder: `Enter new name`,
+      title: 'Edit name'
+    }).then(name => {
+      if (!name) throw new Error('user cancelled')
+      this.store$.dispatch({
+        type: USER_CONFIG_ACTION_TYPES.UPDATE_REGIONS_SELECTION,
+        payload: {
+          ...savedRegionsSelection,
+          name
+        }
+      })
+    }).catch(e => {
+      // TODO catch user cancel
+    })
+  }
+  public removeSavedRegion(event: MouseEvent, savedRegionsSelection: RegionSelection){
+    event.preventDefault()
+    event.stopPropagation()
+    this.store$.dispatch({
+      type: USER_CONFIG_ACTION_TYPES.DELETE_REGIONS_SELECTION,
+      payload: {
+        ...savedRegionsSelection
+      }
+    })
+  }
+
+
+  displayActiveParcellation(parcellation:any){
+    return `<div class="d-flex"><small>Parcellation</small> <small class = "flex-grow-1 mute-text">${parcellation ? '(' + parcellation.name + ')' : ''}</small> <span class = "fas fa-caret-down"></span></div>`
+  }
+
+  displayActiveTemplate(template: any) {
+    return `<div class="d-flex"><small>Template</small> <small class = "flex-grow-1 mute-text">${template ? '(' + template.name + ')' : ''}</small> <span class = "fas fa-caret-down"></span></div>`
+  }
+
+  public loadSelection(event: MouseEvent){
+    this.focused = true
+    
+    this.savedRegionBottomSheetRef = this.bottomSheet.open(this.savedRegionBottomSheetTemplate)
+    this.savedRegionBottomSheetRef.afterDismissed()
+      .subscribe(val => {
+        
+      }, error => {
+
+      }, () => {
+        this.focused = false
+        this.savedRegionBottomSheetRef = null
+      })
+  }
+
+  public saveSelection(event: MouseEvent){
+    this.focused = true
+    this.dialogService.getUserInput({
+      defaultValue: `Saved Region`,
+      placeholder: `Name the selection`,
+      title: 'Save region selection'
+    })
+      .then(name => {
+        if (!name) throw new Error('User cancelled')
+        this.store$.dispatch({
+          type: USER_CONFIG_ACTION_TYPES.SAVE_REGIONS_SELECTION,
+          payload: { name }
+        })
+      })
+      .catch(e => {
+        /**
+         * USER CANCELLED, HANDLE
+         */
+      })
+      .finally(() => this.focused = false)
+  }
+
+  public deselectAllRegions(event: MouseEvent){
+    this.store$.dispatch({
+      type: SELECT_REGIONS,
+      selectRegions: []
+    })
+  }
+
+  public deselectRegion(event: MouseEvent, region: any){
+    this.store$.dispatch({
+      type: DESELECT_REGIONS,
+      deselectRegions: [region]
+    })
+  }
+
+  public gotoRegion(event: MouseEvent, region:any){
+    if (region.position) {
+      this.store$.dispatch({
+        type: CHANGE_NAVIGATION,
+        navigation: {
+          position: region.position,
+          animation: {}
+        }
+      })
+    } else {
+      /**
+       * TODO convert to snack bar
+       */
+      this.toastService.showToast(`${region.name} does not have a position defined`, {
+        timeout: 5000,
+        dismissable: true
+      })
+    }
+  }
+}
+
+const ACTION_TYPES = {
+  SINGLE_CLICK_ON_REGIONHIERARCHY: 'SINGLE_CLICK_ON_REGIONHIERARCHY',
+  DOUBLE_CLICK_ON_REGIONHIERARCHY: 'DOUBLE_CLICK_ON_REGIONHIERARCHY',
+  SELECT_TEMPLATE_WITH_NAME: 'SELECT_TEMPLATE_WITH_NAME',
+  SELECT_PARCELLATION_WITH_NAME: 'SELECT_PARCELLATION_WITH_NAME',
+}
+
+export const VIEWERSTATE_ACTION_TYPES = ACTION_TYPES
diff --git a/src/ui/viewerStateController/viewerState.pipes.ts b/src/ui/viewerStateController/viewerState.pipes.ts
new file mode 100644
index 000000000..659d35778
--- /dev/null
+++ b/src/ui/viewerStateController/viewerState.pipes.ts
@@ -0,0 +1,38 @@
+import { Pipe, PipeTransform } from "@angular/core";
+import { RegionSelection } from "src/services/state/userConfigState.store";
+
+@Pipe({
+  name: 'binSavedRegionsSelectionPipe'
+})
+
+export class BinSavedRegionsSelectionPipe implements PipeTransform{
+  public transform(regionSelections:RegionSelection[]):{parcellationSelected:any, templateSelected:any, regionSelections: RegionSelection[]}[]{
+    const returnMap = new Map()
+    for (let regionSelection of regionSelections){
+      const key = `${regionSelection.templateSelected.name}\n${regionSelection.parcellationSelected.name}`
+      const existing = returnMap.get(key)
+      if (existing) existing.push(regionSelection)
+      else returnMap.set(key, [regionSelection])
+    }
+    return Array.from(returnMap)
+      .map(([_, regionSelections]) => {
+        const {parcellationSelected = null, templateSelected = null} = regionSelections[0] || {}
+        return {
+          regionSelections,
+          parcellationSelected,
+          templateSelected
+        }
+      })
+  }
+}
+
+@Pipe({
+  name: 'savedRegionsSelectionBtnDisabledPipe'
+})
+
+export class SavedRegionsSelectionBtnDisabledPipe implements PipeTransform{
+  public transform(regionSelection: RegionSelection, templateSelected: any, parcellationSelected: any): boolean{
+    return regionSelection.parcellationSelected.name !== parcellationSelected.name
+      || regionSelection.templateSelected.name !== templateSelected.name
+  }
+}
\ No newline at end of file
diff --git a/src/ui/viewerStateController/viewerState.style.css b/src/ui/viewerStateController/viewerState.style.css
new file mode 100644
index 000000000..0da6d0ea5
--- /dev/null
+++ b/src/ui/viewerStateController/viewerState.style.css
@@ -0,0 +1,35 @@
+.virtual-scroll-viewport-container
+{
+  height: 20em;
+  width: 20em;
+  overflow: hidden;
+}
+
+.virtual-scroll-viewport-container > cdk-virtual-scroll-viewport
+{
+  width: 100%;
+  height: 100%;
+  box-sizing: content-box;
+  padding-right: 3em;
+}
+
+.virtual-scroll-row
+{
+  width: 20em;
+}
+
+/* required to match virtual scroll itemSize property */
+.virtual-scroll-unit
+{
+  height: 26px
+}
+
+.selected-region-container
+{
+  flex: 1 1 auto;
+}
+
+.selected-region-actionbtn
+{
+  flex: 0 0 auto;
+}
diff --git a/src/ui/viewerStateController/viewerState.template.html b/src/ui/viewerStateController/viewerState.template.html
new file mode 100644
index 000000000..9e1718231
--- /dev/null
+++ b/src/ui/viewerStateController/viewerState.template.html
@@ -0,0 +1,202 @@
+<mat-card>
+
+  <!-- template selection -->
+  <mat-form-field>
+    <mat-label>
+      Template
+    </mat-label>
+    <mat-select
+      [value]="(templateSelected$ | async)?.name"
+      (selectionChange)="handleTemplateChange($event)"
+      (openedChange)="focused = $event">
+      <mat-option
+        *ngFor="let template of (availableTemplates$ | async)"
+        [value]="template.name">
+        {{ template.name }}
+      </mat-option>
+    </mat-select>
+  </mat-form-field>
+  <ng-container *ngIf="templateSelected$ | async as templateSelected">
+    <ng-container *ngIf="(templateSelected | templateParcellationsDecorationPipe)?.extraButtons as extraButtons">
+      <button
+        *ngFor="let extraBtn of extraButtons"
+        (click)="handleActiveDisplayBtnClicked($event, 'template', extraBtn, templateSelected)"
+        mat-icon-button>
+        <i [class]="extraBtn.faIcon"></i>
+      </button>
+    </ng-container>
+  </ng-container>
+
+  <!-- parcellation selection -->
+  <mat-form-field *ngIf="templateSelected$ | async as templateSelected">
+    <mat-label>
+      Parcellation
+    </mat-label>
+    <mat-select
+      (selectionChange)="handleParcellationChange($event)"
+      [value]="(parcellationSelected$ | async)?.name"
+      (openedChange)="focused = $event">
+      <mat-option
+        *ngFor="let parcellation of (templateSelected.parcellations | appendTooltipTextPipe)"
+        [value]="parcellation.name">
+        {{ parcellation.name }}
+      </mat-option>
+    </mat-select>
+  </mat-form-field>
+
+  <ng-container *ngIf="parcellationSelected$ | async as parcellationSelected">
+    <ng-container *ngIf="(parcellationSelected | templateParcellationsDecorationPipe)?.extraButtons as extraButtons">
+      <button
+        *ngFor="let extraBtn of extraButtons"
+        (click)="handleActiveDisplayBtnClicked($event, 'parcellation', extraBtn, parcellationSelected)"
+        mat-icon-button>
+        <i [class]="extraBtn.faIcon"></i>
+      </button>
+    </ng-container>
+  </ng-container>
+
+  <!-- divider -->
+  <mat-divider></mat-divider>
+
+  <!-- selected regions -->
+
+  <div class="d-flex">
+    <region-text-search-autocomplete
+      (focusedStateChanged)="focused = $event">
+    </region-text-search-autocomplete>
+  </div>
+
+  <!-- chips -->
+  <mat-card class="w-20em mh-10em overflow-auto overflow-x-hidden">
+    <mat-chip-list class="mat-chip-list-stacked" #selectedRegionsChipList>
+      <mat-chip class="w-100" *ngFor="let region of (regionsSelected$ | async)">
+        <span class="flex-grow-1 flex-shrink-1 text-truncate">
+          {{ region.name }}
+        </span>
+        <button
+          *ngIf="region.position"
+          (click)="gotoRegion($event, region)"
+          mat-icon-button>
+          <i class="fas fa-map-marked-alt"></i>
+        </button>
+        <button
+          (click)="deselectRegion($event, region)"
+          mat-icon-button>
+          <i class="fas fa-trash"></i>
+        </button>
+      </mat-chip>
+    </mat-chip-list>
+
+    <!-- place holder when no regions has been selected -->
+    <span class="muted"  *ngIf="(regionsSelected$ | async).length === 0">
+      No regions selected. Double click on any regions in the viewer, or use the search tool to select regions of interest.
+    </span>
+  </mat-card>
+
+  <!-- control btns -->
+  <div class="mt-2 mb-2 d-flex justify-content-between">
+    <div class="d-flex">
+
+      <!-- save  -->
+      <button
+        matTooltip="Save this selection of regions"
+        matTooltipPosition="below"
+        mat-button
+        (click)="saveSelection($event)"
+        color="primary">
+        <i class="fas fa-save"></i>
+        
+      </button>
+
+      <!-- load -->
+      <button
+        (click)="loadSelection($event)"
+        matTooltip="Load a selection of regions"
+        matTooltipPosition="below"
+        mat-button
+        color="primary"
+        [disabled]="(savedRegionsSelections$ | async)?.length === 0">
+        <i
+          matBadgeColor="accent"
+          [matBadgeOverlap]="false"
+          [matBadge]="(savedRegionsSelections$ | async)?.length > 0 ? (savedRegionsSelections$ | async)?.length : null"
+          class="fas fa-folder-open"></i>
+        
+      </button>
+    </div>
+
+    <!-- deselect all  -->
+    <button
+      (click)="deselectAllRegions($event)"
+      matTooltip="Deselect all selected regions"
+      matTooltipPosition="below"
+      mat-raised-button
+      color="warn"
+      [disabled]="(regionsSelected$ | async)?.length === 0">
+      <i class="fas fa-trash"></i>
+    </button>
+  </div>
+</mat-card>
+
+<ng-template #publicationTemplate>
+  <single-dataset-view
+    *ngFor="let focusedDataset of (focusedDatasets$ | async)"
+    [name]="focusedDataset.name"
+    [description]="focusedDataset.description"
+    [publications]="focusedDataset.publications"
+    [kgSchema]="focusedDataset.kgSchema"
+    [kgId]="focusedDataset.kgId">
+    
+  </single-dataset-view>
+</ng-template>
+
+<!-- bottom sheet for saved regions  -->
+<ng-template #savedRegionBottomSheetTemplate>
+  <mat-action-list>
+
+    <!-- separated (binned) by template/parcellation -->
+    <ng-container *ngFor="let binnedRS of (savedRegionsSelections$ | async | binSavedRegionsSelectionPipe); let index = index">
+
+      <!-- only render divider if it is not the leading element -->
+      <mat-divider *ngIf="index !== 0"></mat-divider>
+
+      <!-- header -->
+      <h3 mat-subheader>
+        {{ binnedRS.templateSelected.name }} / {{ binnedRS.parcellationSelected.name }}
+      </h3>
+
+      <!-- ng for all saved regions -->
+      <button
+        *ngFor="let savedRegionsSelection of binnedRS.regionSelections"
+        (click)="loadSavedRegion($event, savedRegionsSelection)"
+        mat-list-item>
+        <!-- [class]="savedRegionsSelection | savedRegionsSelectionBtnDisabledPipe : (templateSelected$ | async) : (parcellationSelected$ | async) ? 'text-muted' : ''" -->
+        <!-- [disabled]="savedRegionsSelection | savedRegionsSelectionBtnDisabledPipe : (templateSelected$ | async) : (parcellationSelected$ | async)" -->
+        <!-- main content -->
+        <span class="flex-grow-0 flex-shrink-1">
+          {{ savedRegionsSelection.name }}
+        </span>
+        <small class="ml-1 mr-1 text-muted flex-grow-1 flex-shrink-0">
+          ({{ savedRegionsSelection.regionsSelected.length }} selected regions)
+        </small>
+
+        <!-- edit btn -->
+        <button
+          (mousedown)="$event.stopPropagation()"
+          (click)="editSavedRegion($event, savedRegionsSelection)"
+          mat-icon-button>
+          <i class="fas fa-edit"></i>
+        </button>
+
+        <!-- trash btn -->
+        <button
+          (mousedown)="$event.stopPropagation()"
+          (click)="removeSavedRegion($event, savedRegionsSelection)"
+          mat-icon-button
+          color="warn">
+          <i class="fas fa-trash"></i>
+        </button>
+      </button>
+    </ng-container>
+  </mat-action-list>
+</ng-template>
\ No newline at end of file
diff --git a/src/ui/viewerStateController/viewerState.useEffect.ts b/src/ui/viewerStateController/viewerState.useEffect.ts
new file mode 100644
index 000000000..4c8232e1c
--- /dev/null
+++ b/src/ui/viewerStateController/viewerState.useEffect.ts
@@ -0,0 +1,180 @@
+import { Subscription, Observable } from "rxjs";
+import { Injectable, OnInit, OnDestroy } from "@angular/core";
+import { Actions, ofType, Effect } from "@ngrx/effects";
+import { Store, select, Action } from "@ngrx/store";
+import { ToastService } from "src/services/toastService.service";
+import { shareReplay, distinctUntilChanged, map, withLatestFrom, filter } from "rxjs/operators";
+import { VIEWERSTATE_ACTION_TYPES } from "./viewerState.component";
+import { CHANGE_NAVIGATION, SELECT_REGIONS, NEWVIEWER, GENERAL_ACTION_TYPES, SELECT_PARCELLATION, isDefined } from "src/services/stateStore.service";
+import { regionFlattener } from "src/util/regionFlattener";
+
+@Injectable({
+  providedIn: 'root'
+})
+
+export class ViewerStateControllerUseEffect implements OnInit, OnDestroy{
+
+  private subscriptions: Subscription[] = []
+
+  private selectedRegions$: Observable<any[]>
+
+  @Effect()
+  singleClickOnHierarchy$: Observable<any>
+
+  @Effect()
+  selectTemplateWithName$: Observable<any>
+  
+  @Effect()
+  selectParcellationWithName$: Observable<any>
+
+  doubleClickOnHierarchy$: Observable<any>
+
+  constructor(
+    private actions$: Actions,
+    private store$: Store<any>,
+    private toastService: ToastService
+  ){
+    const viewerState$ = this.store$.pipe(
+      select('viewerState'),
+      shareReplay(1)
+    )
+
+    this.selectedRegions$ = viewerState$.pipe(
+      select('regionsSelected'),
+      distinctUntilChanged()
+    )
+
+    this.selectParcellationWithName$ = this.actions$.pipe(
+      ofType(VIEWERSTATE_ACTION_TYPES.SELECT_PARCELLATION_WITH_NAME),
+      map(action => {
+        const { payload = {} } = action as ViewerStateAction
+        const { name } = payload
+        return name
+      }),
+      filter(name => !!name),
+      withLatestFrom(viewerState$.pipe(
+        select('parcellationSelected')
+      )),
+      filter(([name,  parcellationSelected]) => {
+        if (parcellationSelected && parcellationSelected.name === name) return false
+        return true
+      }),
+      map(([name,  _]) => name),
+      withLatestFrom(viewerState$.pipe(
+        select('templateSelected')
+      )),
+      map(([name, templateSelected]) => {
+
+        const { parcellations: availableParcellations } = templateSelected
+        const newParcellation = availableParcellations.find(t => t.name === name)
+        if (!newParcellation) {
+          return {
+            type: GENERAL_ACTION_TYPES.ERROR,
+            payload: {
+              message: 'Selected parcellation not found.'
+            }
+          }
+        }
+        return {
+          type: SELECT_PARCELLATION,
+          selectParcellation: newParcellation
+        }
+      })
+    )
+    
+    this.selectTemplateWithName$ = this.actions$.pipe(
+      ofType(VIEWERSTATE_ACTION_TYPES.SELECT_TEMPLATE_WITH_NAME),
+      map(action => {
+        const { payload = {} } = action as ViewerStateAction
+        const { name } = payload
+        return name
+      }),
+      filter(name => !!name),
+      withLatestFrom(viewerState$.pipe(
+        select('templateSelected')
+      )),
+      filter(([name,  templateSelected]) => {
+        if (templateSelected && templateSelected.name === name) return false
+        return true
+      }),
+      map(([name,  templateSelected]) => name),
+      withLatestFrom(viewerState$.pipe(
+        select('fetchedTemplates')
+      )),
+      map(([name, availableTemplates]) => {
+        const newTemplateTobeSelected = availableTemplates.find(t => t.name === name)
+        if (!newTemplateTobeSelected) {
+          return {
+            type: GENERAL_ACTION_TYPES.ERROR,
+            payload: {
+              message: 'Selected template not found.'
+            }
+          }
+        }
+        return {
+          type: NEWVIEWER,
+          selectTemplate: newTemplateTobeSelected,
+          selectParcellation: newTemplateTobeSelected.parcellations[0]
+        }
+      })
+    )
+
+    this.doubleClickOnHierarchy$ = this.actions$.pipe(
+      ofType(VIEWERSTATE_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY)
+    )
+
+    this.singleClickOnHierarchy$ = this.actions$.pipe(
+      ofType(VIEWERSTATE_ACTION_TYPES.SINGLE_CLICK_ON_REGIONHIERARCHY),
+      withLatestFrom(this.selectedRegions$),
+      map(([action, regionsSelected]) => {
+
+        const {payload = {}} = action as ViewerStateAction
+        const { region } = payload
+
+        const flattenedRegion = regionFlattener(region).filter(r => isDefined(r.labelIndex))
+        const flattenedRegionNames = new Set(flattenedRegion.map(r => r.name))
+        const selectedRegionNames = new Set(regionsSelected.map(r => r.name))
+        const selectAll = flattenedRegion.every(r => !selectedRegionNames.has(r.name))
+        return {
+          type: SELECT_REGIONS,
+          selectRegions: selectAll
+            ? regionsSelected.concat(flattenedRegion)
+            : regionsSelected.filter(r => !flattenedRegionNames.has(r.name))
+        }
+      })
+    )
+  }
+
+  ngOnInit(){
+    this.subscriptions.push(
+      this.doubleClickOnHierarchy$.subscribe(({ region } = {}) => {
+        const { position } = region
+        if (position) {
+          this.store$.dispatch({
+            type: CHANGE_NAVIGATION,
+            navigation: {
+              position,
+              animation: {}
+            }
+          })
+        } else {
+          this.toastService.showToast(`${region.name} does not have a position defined`, {
+            timeout: 5000,
+            dismissable: true
+          })
+        }
+      })
+    )
+  }
+
+  ngOnDestroy(){
+    while(this.subscriptions.length > 0) {
+      this.subscriptions.pop().unsubscribe()
+    }
+  }
+}
+
+interface ViewerStateAction extends Action{
+  payload: any
+  config: any
+}
\ No newline at end of file
diff --git a/src/util/pipes/filterNgLayer.pipe.ts b/src/util/pipes/filterNgLayer.pipe.ts
deleted file mode 100644
index 798075159..000000000
--- a/src/util/pipes/filterNgLayer.pipe.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Pipe, PipeTransform } from "@angular/core";
-import { NgLayerInterface } from "src/atlasViewer/atlasViewer.component";
-
-/**
- * TODO deprecate
- * use regular pipe to achieve the same effect
- */
-
-@Pipe({
-  name: 'filterNgLayer'
-})
-
-export class FilterNgLayer implements PipeTransform{
-  public transform(excludedLayers: string[] = [], ngLayers: NgLayerInterface[]): NgLayerInterface[] {
-    const set = new Set(excludedLayers)
-    return ngLayers.filter(l => !set.has(l.name))
-  }
-}
\ No newline at end of file
diff --git a/src/util/pipes/getFileExt.pipe.ts b/src/util/pipes/getFileExt.pipe.ts
new file mode 100644
index 000000000..aea77ceba
--- /dev/null
+++ b/src/util/pipes/getFileExt.pipe.ts
@@ -0,0 +1,36 @@
+import { PipeTransform, Pipe } from "@angular/core";
+
+const NIFTI = `NIFTI Volume`
+const VTK = `VTK Mesh`
+
+const extMap = new Map([
+  ['.nii', NIFTI],
+  ['.nii.gz', NIFTI],
+  ['.vtk', VTK]
+])
+
+@Pipe({
+  name: 'getFileExtension'
+})
+
+export class GetFileExtension implements PipeTransform{
+  private regex: RegExp = new RegExp('(\\.[\\w\\.]*?)$')
+
+  private getRegexp(ext){
+    return new RegExp(`${ext.replace(/\./g, '\\.')}$`, 'i')
+  }
+
+  private detFileExt(filename:string):string{
+    for (let [key, val] of extMap){
+      if(this.getRegexp(key).test(filename)){
+        return val
+      }
+    }
+    return filename
+  }
+
+  public transform(filename:string):string{
+    return this.detFileExt(filename)
+  }
+}
+
diff --git a/src/util/pipes/getFileNameFromPathName.pipe.ts b/src/util/pipes/getFileNameFromPathName.pipe.ts
deleted file mode 100644
index d64a96c1c..000000000
--- a/src/util/pipes/getFileNameFromPathName.pipe.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Pipe, PipeTransform } from "@angular/core";
-
-
-@Pipe({
-  name : 'getFilenameFromPathname'
-})
-
-export class GetFilenameFromPathnamePipe implements PipeTransform{
-  public transform(pathname:string):string{
-    return pathname.split('/')[pathname.split('/').length - 1]
-  }
-}
\ No newline at end of file
diff --git a/src/util/pipes/getFilename.pipe.ts b/src/util/pipes/getFilename.pipe.ts
new file mode 100644
index 000000000..76afea562
--- /dev/null
+++ b/src/util/pipes/getFilename.pipe.ts
@@ -0,0 +1,14 @@
+import { PipeTransform, Pipe } from "@angular/core";
+
+@Pipe({
+  name: 'getFilenamePipe'
+})
+
+export class GetFilenamePipe implements PipeTransform{
+  private regex: RegExp = new RegExp('[\\/\\\\]([\\w\\.]*?)$')
+  public transform(fullname: string): string{
+    return this.regex.test(fullname)
+      ? this.regex.exec(fullname)[1]
+      : fullname
+  }
+}
\ No newline at end of file
-- 
GitLab