From b08ba41440685135136ee14df07454f0d047d17a Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Wed, 18 Oct 2023 17:14:47 +0200
Subject: [PATCH] feat: changed appearance of related region refactor: using
 collapse for selecting ATP

---
 .../sapi/decisionCollapse.service.ts          | 127 ++++++
 src/atlasComponents/sapi/sapi.service.ts      |  16 +-
 .../sapiViews/core/region/module.ts           |   2 +
 .../region/region/region.base.directive.ts    |  48 ++-
 .../region/rich/region.rich.component.ts      |  18 +-
 .../region/region/rich/region.rich.style.css  |   6 +
 .../region/rich/region.rich.template.html     |  94 +++--
 .../ATPSelector/wrapper/wrapper.component.ts  | 113 +-----
 src/state/atlasSelection/actions.ts           |  31 +-
 src/state/atlasSelection/effects.ts           | 370 ++++++++----------
 .../viewerCmp/viewerCmp.component.ts          |  16 +-
 .../viewerCmp/viewerCmp.template.html         |   1 +
 12 files changed, 463 insertions(+), 379 deletions(-)
 create mode 100644 src/atlasComponents/sapi/decisionCollapse.service.ts

diff --git a/src/atlasComponents/sapi/decisionCollapse.service.ts b/src/atlasComponents/sapi/decisionCollapse.service.ts
new file mode 100644
index 000000000..bb3c1cb73
--- /dev/null
+++ b/src/atlasComponents/sapi/decisionCollapse.service.ts
@@ -0,0 +1,127 @@
+import { Injectable } from "@angular/core";
+import { SAPI } from "./sapi.service";
+import { SxplrAtlas, SxplrParcellation, SxplrTemplate } from "./sxplrTypes";
+import { take } from "rxjs/operators";
+
+type PossibleATP = {
+  atlases: SxplrAtlas[]
+  spaces: SxplrTemplate[]
+  parcellations: SxplrParcellation[]
+}
+
+@Injectable({
+  providedIn: "root"
+})
+export class DecisionCollapse{
+  constructor(private sapi: SAPI){}
+
+  cleanup<T>(val: T[]|T|null){
+    if (!val) {
+      return []
+    }
+    if (Array.isArray(val)){
+      return val?.filter(v => !!v) || []
+    }
+    return [val]
+  }
+
+  async collapseAtlasId(atlasId: string): Promise<PossibleATP> {
+    const atlases = await this.sapi.atlases$.pipe(
+      take(1)
+    ).toPromise()
+    const atlas = atlases.find(a => a.id === atlasId)
+    const parcellations = atlas && await this.sapi.getAllParcellations(atlas).toPromise()
+    const spaces = atlas && await this.sapi.getAllSpaces(atlas).toPromise()
+
+    return {
+      atlases: this.cleanup(atlas),
+      parcellations: this.cleanup(parcellations),
+      spaces: this.cleanup(spaces),
+    }
+  }
+  
+  async collapseTemplateId(templateId: string): Promise<PossibleATP> {
+    const atlases = await this.sapi.atlases$.pipe(
+      take(1)
+    ).toPromise()
+
+    const atlasId = this.sapi.reverseLookupAtlas(templateId)
+    const atlas = atlasId && atlases.find(a => a.id === atlasId)
+    const spaces = atlas && await this.sapi.getAllSpaces(atlas).toPromise()
+
+    const space = spaces.find(s => s.id === templateId)
+    const parcellations = atlas && space && await this.sapi.getSupportedParcellations(atlas, space).toPromise()
+
+    return {
+      atlases: this.cleanup(atlas),
+      parcellations: this.cleanup(parcellations),
+      spaces: this.cleanup(space),
+    }
+  }
+
+  async collapseParcId(parcellationId: string): Promise<PossibleATP> {
+    const atlases = await this.sapi.atlases$.pipe(
+      take(1)
+    ).toPromise()
+
+    const atlasId = this.sapi.reverseLookupAtlas(parcellationId)
+    const atlas = atlasId && atlases.find(a => a.id === atlasId)
+    const parcellations = atlas && await this.sapi.getAllParcellations(atlas).toPromise()
+    
+    const parcellation = parcellations && parcellations.find(p => p.id === parcellationId)
+    const spaces = atlas && parcellation && await this.sapi.getSupportedTemplates(atlas, parcellation).toPromise()
+
+    return {
+      atlases: this.cleanup(atlas),
+      parcellations: this.cleanup(parcellation),
+      spaces: this.cleanup(spaces),
+    }
+  }
+
+  static _Intersect(parta: PossibleATP, partb: PossibleATP): PossibleATP {
+    const partbAtlasIds = partb.atlases.map(a => a.id)
+    const partbParcIds = partb.parcellations.map(a => a.id)
+    const partbSpaceIds = partb.spaces.map(a => a.id)
+    return {
+      atlases: parta.atlases.filter(a => partbAtlasIds.includes(a.id)),
+      parcellations: parta.parcellations.filter(a => partbParcIds.includes(a.id)),
+      spaces: parta.spaces.filter(a => partbSpaceIds.includes(a.id)),
+    }
+  }
+
+  static Intersect(...allATPs: (PossibleATP|null)[]): PossibleATP {
+    let result: PossibleATP
+    for (const item of allATPs) {
+      if (!item) {
+        continue
+      }
+      if (!result) {
+        result = item
+        continue
+      }
+      result = DecisionCollapse._Intersect(result, item)
+    }
+    return result
+  }
+
+  /**
+   * @param result 
+   * @returns error of Verify as a list of string
+   */
+  static Verify(result: PossibleATP|null): string[] {
+    const errorMessage: string[] = []
+    if (!result) {
+      errorMessage.push(`Result is nullish.`)
+    }
+    if (result?.atlases.length === 0) {
+      errorMessage.push(`No overlapping atlas is found!`)
+    }
+    if (result?.parcellations.length === 0) {
+      errorMessage.push(`No overlapping parcellation is found!`)
+    }
+    if (result?.spaces.length === 0) {
+      errorMessage.push(`No overlapping space is found!`)
+    }
+    return errorMessage
+  }
+}
diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts
index 53868ab6a..a8a423ed6 100644
--- a/src/atlasComponents/sapi/sapi.service.ts
+++ b/src/atlasComponents/sapi/sapi.service.ts
@@ -279,7 +279,16 @@ export class SAPI{
     query: {}
   }).pipe(
     switchMap(atlases => forkJoin(
-      atlases.items.map(atlas => translateV3Entities.translateAtlas(atlas))
+      atlases.items.map(atlas => {
+        const { parcellations, spaces } = atlas
+        for (const parc of parcellations){
+          this.#reverseMap.set(parc["@id"], atlas["@id"])
+        }
+        for (const space of spaces){
+          this.#reverseMap.set(space["@id"], atlas["@id"])
+        }
+        return translateV3Entities.translateAtlas(atlas)
+      })
     )),
     map(atlases => atlases.sort((a, b) => (speciesOrder as string[]).indexOf(a.species) - (speciesOrder as string[]).indexOf(b.species))),
     tap(() => {
@@ -293,6 +302,11 @@ export class SAPI{
     shareReplay(1),
   )
 
+  #reverseMap = new Map<string, string>()
+  public reverseLookupAtlas(parcOrSpaceId: string): string {
+    return this.#reverseMap.get(parcOrSpaceId)
+  }
+
   public getAllSpaces(atlas: SxplrAtlas): Observable<SxplrTemplate[]> {
     return forkJoin(
       translateV3Entities.retrieveAtlas(atlas).spaces.map(
diff --git a/src/atlasComponents/sapiViews/core/region/module.ts b/src/atlasComponents/sapiViews/core/region/module.ts
index 46b01dfb0..b1e56652d 100644
--- a/src/atlasComponents/sapiViews/core/region/module.ts
+++ b/src/atlasComponents/sapiViews/core/region/module.ts
@@ -19,6 +19,7 @@ import { MatTooltipModule } from "@angular/material/tooltip";
 import { TranslateQualificationPipe } from "./translateQualification.pipe";
 import { DedupRelatedRegionPipe } from "./dedupRelatedRegion.pipe";
 import { MatExpansionModule } from "@angular/material/expansion";
+import { MatTableModule } from "@angular/material/table";
 
 @NgModule({
   imports: [
@@ -37,6 +38,7 @@ import { MatExpansionModule } from "@angular/material/expansion";
     SapiViewsCoreParcellationModule,
     MatTooltipModule,
     MatExpansionModule,
+    MatTableModule,
     ExperimentalModule,
   ],
   declarations: [
diff --git a/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts b/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts
index fc5b5c1df..d8ecb2a83 100644
--- a/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts
+++ b/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts
@@ -3,8 +3,9 @@ import { SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate } from "src/a
 import { translateV3Entities } from "src/atlasComponents/sapi/translateV3"
 import { rgbToHsl } from 'common/util'
 import { SAPI } from "src/atlasComponents/sapi/sapi.service";
-import { BehaviorSubject, combineLatest, forkJoin, of } from "rxjs";
-import { map, switchMap } from "rxjs/operators";
+import { BehaviorSubject, combineLatest, forkJoin, from, of } from "rxjs";
+import { catchError, map, switchMap } from "rxjs/operators";
+import { DecisionCollapse } from "src/atlasComponents/sapi/decisionCollapse.service";
 
 @Directive({
   selector: `[sxplr-sapiviews-core-region]`,
@@ -147,11 +148,50 @@ export class SapiViewsCoreRegionRegionBase {
           region: translateV3Entities.translateRegion(assigned_structure),
           parcellation: translateV3Entities.translateParcellation(assigned_structure_parcellation),
         }))
-      ))
+      )),
+      switchMap(relatedRegions => {
+        
+        const uniqueParc = relatedRegions.map(v => v.parcellation).reduce(
+          (acc, curr) => acc.map(v => v.id).includes(curr.id) ? acc : acc.concat(curr),
+          [] as SxplrParcellation[]
+        )
+        
+        return forkJoin(
+          uniqueParc.map(parc =>
+            from(this.collapser.collapseParcId(parc.id)).pipe(
+              switchMap(collapsed => forkJoin(
+                collapsed.spaces.map(space =>
+                  from(this.sapi.getLabelledMap(parc, space)).pipe(
+                    catchError(() => of(null))
+                  )
+                )
+              )),
+              map(labelMap => ({
+                parcellation: parc,
+                mappedRegions: labelMap
+                  .filter(v => !!v)
+                  .map(m => Object.keys(m.indices))
+                  .flatMap(v => v),
+              })),
+            )
+          )
+        ).pipe(
+          map(allMappedRegions => {
+            const regMap: Record<string, string[]> = {}
+            for (const { parcellation, mappedRegions } of allMappedRegions) {
+              regMap[parcellation.id] = mappedRegions
+            }
+            return relatedRegions.map(prev => ({
+              ...prev,
+              mapped: (regMap[prev.parcellation.id] || []).includes(prev.region.name)
+            }))
+          })
+        )
+      })
     ).toPromise()
   }
 
-  constructor(protected sapi: SAPI){
+  constructor(protected sapi: SAPI, private collapser: DecisionCollapse){
 
   }
 }
diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts
index ce52048ff..242bb2ff5 100644
--- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts
+++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts
@@ -3,11 +3,12 @@ import { Component, EventEmitter, Inject, Output } from "@angular/core";
 import { DARKTHEME } from "src/util/injectionTokens";
 import { SapiViewsCoreRegionRegionBase } from "../region.base.directive";
 import { ARIA_LABELS, CONST } from 'common/constants'
-import { Feature } from "src/atlasComponents/sapi/sxplrTypes";
+import { Feature, SxplrParcellation, SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes";
 import { SAPI } from "src/atlasComponents/sapi/sapi.service";
 import { environment } from "src/environments/environment";
 import { catchError, map, shareReplay, switchMap } from "rxjs/operators";
 import { PathReturn } from "src/atlasComponents/sapi/typeV3";
+import { DecisionCollapse } from "src/atlasComponents/sapi/decisionCollapse.service";
 
 @Component({
   selector: 'sxplr-sapiviews-core-region-region-rich',
@@ -28,11 +29,15 @@ export class SapiViewsCoreRegionRegionRich extends SapiViewsCoreRegionRegionBase
   @Output('sxplr-sapiviews-core-region-region-rich-feature-clicked')
   featureClicked = new EventEmitter<Feature>()
 
+  @Output('sxplr-sapiviews-core-region-region-rich-related-region-clicked')
+  relatedRegion = new EventEmitter<{ region: SxplrRegion, parcellation: SxplrParcellation }>()
+
   constructor(
     sapi: SAPI,
+    collapser: DecisionCollapse,
     @Inject(DARKTHEME) public darktheme$: Observable<boolean>,
   ){
-    super(sapi)
+    super(sapi, collapser)
   }
 
   handleRegionalFeatureClicked(feat: Feature) {
@@ -58,7 +63,7 @@ export class SapiViewsCoreRegionRegionRich extends SapiViewsCoreRegionRegionBase
             return this.sapi.getMap(parcellation.id, template.id, "LABELLED").pipe(
               map(v => {
                 const mapIndices = v.indices[region.name]
-                return mapIndices.map(mapIdx => v.volumes[mapIdx.volume])
+                return (mapIndices || []).map(mapIdx => v.volumes[mapIdx.volume])
               })
             )
           })
@@ -97,4 +102,11 @@ export class SapiViewsCoreRegionRegionRich extends SapiViewsCoreRegionRegionBase
     switchMap(({ region }) => this.fetchRelated(region)),
     shareReplay(1),
   )
+
+  public selectATPR(region: SxplrRegion, parcellation: SxplrParcellation){
+    this.relatedRegion.next({
+      region,
+      parcellation
+    })
+  }
 }
diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.style.css b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.style.css
index 2f9290a93..83724a58b 100644
--- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.style.css
+++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.style.css
@@ -11,6 +11,12 @@
   overflow-y: scroll;
 }
 
+.mat-column-qualification,
+.mat-column-relatedRegion
+{
+  padding: 0.5rem;
+}
+
 readmore-component
 {
   width: 100%;
diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html
index b48e7804e..c3587d8ec 100644
--- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html
+++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html
@@ -36,6 +36,7 @@
 
       <mat-action-list class="overview-container" dense>
         
+        <!-- parcellation button -->
         <button
           mat-list-item
           sxplr-dialog
@@ -49,6 +50,7 @@
           <div mat-line class="overview-content">{{ parcellation.name }}</div>
         </button>
 
+        <!-- region position (if eixsts) -->
         <ng-template [ngIf]="regionPosition">
           <button mat-list-item (click)="navigateTo(regionPosition)">
             <mat-icon mat-list-icon fontSet="fas" fontIcon="fa-map-marker"></mat-icon>
@@ -56,6 +58,7 @@
           </button>
         </ng-template>
 
+        <!-- all dois -->
         <ng-template ngFor [ngForOf]="dois$ | async" let-doi>
           <a mat-list-item [href]="doi" target="_blank" class="no-hover">
             <mat-icon mat-list-icon fontSet="ai" fontIcon="ai-doi"></mat-icon>
@@ -66,7 +69,8 @@
         <ng-template sxplrExperimentalFlag [experimental]="true"
           #relatedRegionsExport="sxplrExperimentalFlag"
           [ngIf]="relatedRegionsExport.show$ | async">
-
+        
+        <!-- related regions -->
         <ng-template [ngIf]="relatedRegions$ | async | dedupRelatedRegionPipe" let-relatedRegions>
           <!-- only show icon if related regions length > 0 -->
           <ng-template [ngIf]="relatedRegions.length > 0">
@@ -76,45 +80,59 @@
               <mat-icon mat-list-icon fontSet="fas" fontIcon="fa-link"></mat-icon>
               <div mat-line class="overview-content">Related Regions ({{ relatedRegions.length }})</div>
             </button>
+            
+            <!-- dialog when user clicks related regions -->
+            <ng-template #relatedRegionsTmpl>
+              <!-- header -->
+              <h2 mat-dialog-title>Related Regions</h2>
+
+              <!-- body -->
+              <mat-dialog-content>
+
+                <!-- iterate over all related -->
+                <table mat-table [dataSource]="relatedRegions">
+                  
+                  <ng-container matColumnDef="currentRegion">
+                    <th mat-header-cell *matHeaderCellDef> Current Region </th>
+                    <td mat-cell *matCellDef="let related"> {{ region.name }} </td>
+                  </ng-container>
+
+                  <ng-container matColumnDef="qualification">
+                    <th mat-header-cell *matHeaderCellDef> Qualification </th>
+                    <td mat-cell *matCellDef="let related"> {{ related.qualification | translateQualificationPipe }} </td>
+                  </ng-container>
+
+                  <ng-container matColumnDef="relatedRegion">
+                    <th mat-header-cell *matHeaderCellDef> Related Region </th>
+                    <td mat-cell *matCellDef="let related">
+                      <button tabindex="-1" mat-stroked-button
+                        (click)="selectATPR(related.region, related.parcellation)"
+                        class="sxplr-w-100"
+                        [disabled]="!related.mapped">
+                        {{ related.region.name }}
+                      </button>
+                    </td>
+                  </ng-container>
+
+                  <ng-container matColumnDef="relatedRegionParc">
+                    <th mat-header-cell *matHeaderCellDef> Related Region Parcellation </th>
+                    <td mat-cell *matCellDef="let related">
+                      {{ related.parcellation.name }}
+                    </td>
+                  </ng-container>
+
+                  <tr mat-header-row *matHeaderRowDef="['currentRegion', 'qualification', 'relatedRegion', 'relatedRegionParc']"></tr>
+                  <tr mat-row *matRowDef="let row; columns: ['currentRegion', 'qualification', 'relatedRegion', 'relatedRegionParc'];"></tr>
+                </table>
+              </mat-dialog-content>
+
+              <!-- footer -->
+              <mat-dialog-actions align="center">
+                <button mat-button mat-dialog-close>close</button>
+              </mat-dialog-actions>
+            </ng-template>
           </ng-template>
 
-          <!-- dialog when user clicks related regions -->
-          <ng-template #relatedRegionsTmpl>
-            <!-- header -->
-            <h2 mat-dialog-title>Current region {{ region.name }} ...</h2>
-
-            <!-- body -->
-            <mat-dialog-content>
-
-              <!-- iterate over all related -->
-              <ng-template ngFor [ngForOf]="relatedRegions" let-related let-isLast="last">
-
-                <!-- related region body -->
-                <div class="sxplr-p-2">
-                  <div>
-                    {{ related.qualification | translateQualificationPipe }}
-                  </div>
-
-                  <div>
-                    {{ related.region.name }} in
-                  </div>
-                  <div>
-                    {{ related.parcellation.name}}
-                  </div>
-                </div>
-                
-                <!-- divider -->
-                <ng-template [ngIf]="!isLast">
-                  <mat-divider></mat-divider>
-                </ng-template>
-              </ng-template>
-            </mat-dialog-content>
-
-            <!-- footer -->
-            <mat-dialog-actions>
-              <button mat-button mat-dialog-close>close</button>
-            </mat-dialog-actions>
-          </ng-template>
         </ng-template>
 
         </ng-template>
diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts
index 064e46124..9da752e04 100644
--- a/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts
+++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts
@@ -1,8 +1,8 @@
 import { Component, EventEmitter, Inject, OnDestroy, Output } from "@angular/core";
 import { MatDialog } from "@angular/material/dialog";
 import { select, Store } from "@ngrx/store";
-import { Observable, of, Subject, Subscription } from "rxjs";
-import { filter, map, switchMap, tap, withLatestFrom } from "rxjs/operators";
+import { Observable, Subject, Subscription } from "rxjs";
+import { switchMap, withLatestFrom } from "rxjs/operators";
 import { SAPI } from "src/atlasComponents/sapi/sapi.service";
 import { atlasAppearance, atlasSelection } from "src/state";
 import { fromRootStore } from "src/state/atlasSelection";
@@ -15,11 +15,6 @@ type AskUserConfig = {
   actionsAsList: boolean
 }
 
-function isATPGuard(obj: any): obj is Partial<ATP&{ requested: Partial<ATP> }> {
-  if (!obj) return false
-  return (obj.atlas || obj.template || obj.parcellation) && (!obj.requested || isATPGuard(obj.requested))
-}
-
 @Component({
   selector: 'sxplr-wrapper-atp-selector',
   templateUrl: './wrapper.template.html',
@@ -63,6 +58,8 @@ export class WrapperATPSelector implements OnDestroy{
     select(atlasSelection.selectors.selectedAtlas),
     switchMap(atlas => this.sapi.getAllParcellations(atlas))
   )
+
+  // TODO how do we check busy'ness?
   isBusy$ = new Subject<boolean>()
   
   parcellationVisibility$ = this.store$.pipe(
@@ -78,92 +75,24 @@ export class WrapperATPSelector implements OnDestroy{
   ){
     this.#subscription.push(
       this.selectLeaf$.pipe(
-        tap(() => this.isBusy$.next(true)),
         withLatestFrom(this.selectedATP$),
-        switchMap(([{ atlas, template, parcellation }, selectedATP]) => {
-          if (atlas) {
-            /**
-             * do not need to ask permission to switch atlas
-             */
-            return of({ atlas })
-          }
-          if (template) {
-            return this.sapi.getSupportedParcellations(selectedATP.atlas, template).pipe(
-              switchMap(parcs => {
-                if (parcs.find(p => p.id === selectedATP.parcellation.id)) {
-                  return of({ template })
-                }
-                return this.#askUser(
-                  null,
-                  `Current parcellation **${selectedATP.parcellation.name}** is not mapped in the selected template **${template.name}**. Please select one of the following parcellations:`,
-                  null,
-                  parcs.map(p => p.name),
-                  {
-                    actionsAsList: true
-                  }
-                ).pipe(
-                  map(parcname => {
-                    const foundParc = parcs.find(p => p.name === parcname)
-                    if (foundParc) {
-                      return ({ template, requested: { parcellation: foundParc } })
-                    }
-                    return null
-                  })
-                )
-              })
-            )
-          }
-          if (parcellation) {
-            return this.sapi.getSupportedTemplates(selectedATP.atlas, parcellation).pipe(
-              switchMap(tmpls => {
-                if (tmpls.find(t => t.id === selectedATP.template.id)) {
-                  return of({ parcellation })
-                }
-                return this.#askUser(
-                  null,
-                  `Selected parcellation **${parcellation.name}** is not mapped in the current template **${selectedATP.template.name}**. Please select one of the following templates:`,
-                  null,
-                  tmpls.map(tmpl => tmpl.name),
-                  {
-                    actionsAsList: true
-                  }
-                ).pipe(
-                  map(tmplname => {
-                    const foundTmpl = tmpls.find(tmpl => tmpl.name === tmplname)
-                    if (foundTmpl) {
-                      return ({ requested: { template: foundTmpl }, parcellation })
-                    }
-                    return null
-                  })
-                )
-              })
-            )
-          }
-          return of(null)
-        }),
-        filter(val => {
-          this.isBusy$.next(false)
-          return !!val
-        })
-      ).subscribe((obj) => {
-        if (!isATPGuard(obj)) return
-        const { atlas, parcellation, template, requested } = obj
-        if (atlas) {
-          this.store$.dispatch(
-            atlasSelection.actions.selectAtlas({ atlas })
-          )
-        }
-        if (parcellation) {
-          this.store$.dispatch(
-            atlasSelection.actions.selectParcellation({ parcellation, requested })
-          )
-        }
-        if (template) {
-          this.store$.dispatch(
-            atlasSelection.actions.selectTemplate({ template, requested })
-          )
-        }
-      })
+      ).subscribe(([{ template, parcellation, atlas }, selectedATP]) => {
+
+        this.store$.dispatch(
+          atlasSelection.actions.selectATPById({
+            templateId: template?.id,
+            parcellationId: parcellation?.id,
+            atlasId: atlas?.id,
+            config: {
+              autoSelect: !!atlas,
+              messages: {
+                parcellation: `Current parcellation **${selectedATP?.parcellation?.name}** is not mapped in the selected template **${template?.name}**. Please select one of the following parcellations:`,
+                template: `Selected parcellation **${parcellation?.name}** is not mapped in the current template **${selectedATP?.template?.name}**. Please select one of the following templates:`,
+              }
+            }
+          })
+        )
+      }),
     )
   }
 
diff --git a/src/state/atlasSelection/actions.ts b/src/state/atlasSelection/actions.ts
index a4d044ae4..fb9713d76 100644
--- a/src/state/atlasSelection/actions.ts
+++ b/src/state/atlasSelection/actions.ts
@@ -14,28 +14,6 @@ export const selectAtlas = createAction(
   }>()
 )
 
-export const selectTemplate = createAction(
-  `${nameSpace} selectTemplate`,
-  props<{
-    template: SxplrTemplate
-    requested?: {
-      template?: SxplrTemplate
-      parcellation?: SxplrParcellation
-    }
-  }>()
-)
-
-export const selectParcellation = createAction(
-  `${nameSpace} selectParcellation`,
-  props<{
-    parcellation: SxplrParcellation
-    requested?: {
-      parcellation?: SxplrParcellation
-      template?: SxplrTemplate
-    }
-  }>()
-)
-
 /**
  * setAtlasSelectionState is called as a final step to (potentially) set:
  * - selectedAtlas
@@ -133,6 +111,15 @@ export const selectATPById = createAction(
     atlasId?: string
     templateId?: string
     parcellationId?: string
+    regionId?: string
+
+    config?: {
+      autoSelect?: boolean
+      messages?: {
+        template?: string
+        parcellation?: string
+      }
+    }
   }>()
 )
 
diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts
index 3960d8624..fdebec2b6 100644
--- a/src/state/atlasSelection/effects.ts
+++ b/src/state/atlasSelection/effects.ts
@@ -1,16 +1,19 @@
 import { Injectable } from "@angular/core";
 import { Actions, createEffect, ofType } from "@ngrx/effects";
-import { forkJoin, merge, NEVER, Observable, of } from "rxjs";
-import { catchError, filter, map, mapTo, switchMap, switchMapTo, take, withLatestFrom } from "rxjs/operators";
+import { forkJoin, from, NEVER, Observable, of, throwError } from "rxjs";
+import { catchError, filter, map, mapTo, switchMap, take, withLatestFrom } from "rxjs/operators";
 import { SAPI } from "src/atlasComponents/sapi";
 import * as mainActions from "../actions"
 import { select, Store } from "@ngrx/store";
-import { selectors, actions } from '.'
+import { selectors, actions, fromRootStore } from '.'
 import { AtlasSelectionState } from "./const"
 import { atlasAppearance, atlasSelection } from "..";
 
 import { InterSpaceCoordXformSvc } from "src/atlasComponents/sapi/core/space/interSpaceCoordXform.service";
 import { SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes";
+import { DecisionCollapse } from "src/atlasComponents/sapi/decisionCollapse.service";
+import { DialogFallbackCmp } from "src/ui/dialogInfo";
+import { MatDialog } from "@angular/material/dialog";
 
 type OnTmplParcHookArg = {
   previous: {
@@ -30,11 +33,27 @@ const prefParcId = [
   "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290",
 ]
 
-const prefSpcId = []
-
 @Injectable()
 export class Effect {
 
+  #askUserATP<T extends SxplrAtlas|SxplrTemplate|SxplrParcellation>(titleMd: string, options: T[]) {
+    if (options.length === 0) {
+      return throwError(`Expecting at least one option, but got 0`)
+    }
+    if (options.length === 1) {
+      return of(options[0])
+    }
+    return this.dialog.open(DialogFallbackCmp, {
+      data: {
+        titleMd,
+        actions: options.map(v => v.name),
+        actionsAsList: true
+      }
+    }).afterClosed().pipe(
+      map(v => options.find(o => o.name === v))
+    )
+  }
+
   onTemplateParcSelectionPostHook: ((arg: OnTmplParcHookArg) => Observable<Partial<AtlasSelectionState>>)[] = [
     /**
      * This hook gets the region associated with the selected parcellation and template,
@@ -91,115 +110,10 @@ export class Effect {
     }
   ]
 
-  onTemplateParcSelection = createEffect(() => merge(
-    this.action.pipe(
-      ofType(actions.selectTemplate),
-      map(({ template, requested }) => {
-        return {
-          template,
-          parcellation: null as SxplrParcellation,
-          requested,
-        }
-      })
-    ),
-    this.action.pipe(
-      ofType(actions.selectParcellation),
-      map(({ parcellation, requested }) => {
-        return {
-          template: null as SxplrTemplate,
-          parcellation,
-          requested,
-        }
-      })
-    )
-  ).pipe(
-    withLatestFrom(this.store),
-    switchMap(([ { template, parcellation, requested }, store ]) => {
-
-      const currTmpl = selectors.selectedTemplate(store)
-      const currParc = selectors.selectedParcellation(store)
-      const currAtlas = selectors.selectedAtlas(store)
-
-      const requestedTmpl = requested?.template
-      const requestedParc = requested?.parcellation
-
-      const resolvedTmpl = template || requestedTmpl || currTmpl
-      const resolvedParc = parcellation || requestedParc || currParc
-
-      return this.sapiSvc.getSupportedTemplates(currAtlas, resolvedParc).pipe(
-        switchMap(tmpls => {
-          const flag = tmpls.some(tmpl => tmpl.id === resolvedTmpl.id)
-          if (flag) {
-            return of({
-              atlas: currAtlas,
-              template: resolvedTmpl,
-              parcellation: resolvedParc,
-            })
-          }
-
-          /**
-           * TODO code below should not be reached
-           */
-          /**
-           * if template is defined, find the first parcellation that is supported
-           */
-          if (!!template) {
-            return this.sapiSvc.getSupportedParcellations(currAtlas, template).pipe(
-              map(parcs => {
-                if (parcs.length === 0) {
-                  throw new Error(`Cannot find any supported parcellations for template ${template.name}`)
-                }
-                const sortedByPref = parcs.sort((a, b) => prefParcId.indexOf(a.id) - prefParcId.indexOf(b.id))
-                const selectParc = sortedByPref.find(p => requestedParc?.id === p.id) || sortedByPref[0]
-                return {
-                  atlas: currAtlas,
-                  template,
-                  parcellation: selectParc
-                }
-              })
-            )
-          }
-          if (!!parcellation) {
-            return this.sapiSvc.getSupportedTemplates(currAtlas, parcellation).pipe(
-              map(templates => {
-                if (templates.length === 0) {
-                  throw new Error(`Cannot find any supported templates for parcellation ${parcellation.name}`)
-                }
-                const selectTmpl = templates.find(tmp => requestedTmpl?.id === tmp.id || prefSpcId.includes(tmp.id)) || templates[0]
-                return {
-                  atlas: currAtlas,
-                  template: selectTmpl,
-                  parcellation
-                }
-              })
-            )
-          }
-          throw new Error(`neither template nor parcellation has been defined!`)
-        }),
-        switchMap(({ atlas, template, parcellation }) => 
-          forkJoin(
-            this.onTemplateParcSelectionPostHook.map(fn => fn({ previous: { atlas: currAtlas, template: currTmpl, parcellation: currParc }, current: { atlas, template, parcellation } }))
-          ).pipe(
-            map(partialStates => {
-              let returnState: Partial<AtlasSelectionState> = {
-                selectedAtlas: atlas,
-                selectedTemplate: template,
-                selectedParcellation: parcellation
-              }
-              for (const s of partialStates) {
-                returnState = {
-                  ...returnState,
-                  ...s,
-                }
-              }
-              return actions.setAtlasSelectionState(returnState)
-            })
-          )
-        )
-      )
-    })
-  ))
-
+  /**
+   * clear template/parc to trigger loading screen
+   * since getting the map/config etc are not sync
+   */
   onAtlasSelClearTmplParc = createEffect(() => this.action.pipe(
     ofType(actions.selectAtlas),
     map(() => actions.setAtlasSelectionState({
@@ -211,46 +125,12 @@ export class Effect {
   onAtlasSelectionSelectTmplParc = createEffect(() => this.action.pipe(
     ofType(actions.selectAtlas),
     filter(action => !!action.atlas),
-    switchMap(({ atlas }) => 
-      this.sapiSvc.getAllParcellations(atlas).pipe(
-        map(parcellations => {
-          const parcPrevIds = parcellations.map(p => p.prevId)
-          const latestParcs = parcellations.filter(p => !parcPrevIds.includes(p.id))
-          const prefParc = parcellations.filter(p => prefParcId.includes(p.id)).sort((a, b) => prefParcId.indexOf(a.id) - prefParcId.indexOf(b.id))
-          const selectedParc = prefParc[0] || latestParcs[0] || parcellations[0]
-          return {
-            parcellation: selectedParc,
-            atlas
-          }
-        })
-      )
-    ),
-    switchMap(({ atlas, parcellation }) => {
-      return this.sapiSvc.getSupportedTemplates(atlas, parcellation).pipe(
-        switchMap(spaces => {
-          const selectedSpace = spaces.find(s => s.name.includes("152")) || spaces[0]
-          return forkJoin(
-            this.onTemplateParcSelectionPostHook.map(fn => fn({ previous: null, current: { atlas, parcellation, template: selectedSpace } }))
-          ).pipe(
-            map(partialStates => {
-
-              let returnState: Partial<AtlasSelectionState> = {
-                selectedAtlas: atlas,
-                selectedTemplate: selectedSpace,
-                selectedParcellation: parcellation
-              }
-              for (const s of partialStates) {
-                returnState = {
-                  ...returnState,
-                  ...s,
-                }
-              }
-              return actions.setAtlasSelectionState(returnState)
-            })
-          )
-        })
-      )
-    })
+    map(({ atlas }) => actions.selectATPById({
+      atlasId: atlas.id,
+      config: {
+        autoSelect: true,
+      }
+    }))
   ))
 
   onATPSelectionClearBaseLayerColorMap = createEffect(() => this.store.pipe(
@@ -313,59 +193,139 @@ export class Effect {
    */
   onSelectATPById = createEffect(() => this.action.pipe(
     ofType(actions.selectATPById),
-    switchMap(({ atlasId, parcellationId, templateId }) =>
-      this.sapiSvc.atlases$.pipe(
-        switchMap(atlases => {
+    switchMap(({ atlasId, parcellationId, templateId, regionId, config }) => {
+      const { autoSelect, messages } = config || { autoSelect: false, messages: {} }
+      return from(
+        Promise.all([
+          atlasId && this.collapser.collapseAtlasId(atlasId),
+          templateId && this.collapser.collapseTemplateId(templateId),
+          parcellationId && this.collapser.collapseParcId(parcellationId),
+        ])
+      ).pipe(
+        withLatestFrom(this.store.pipe(
+          fromRootStore.distinctATP()
+        )),
+        switchMap(([requestedPossibleATPs, { atlas, template, parcellation }]) => {
+          let result = DecisionCollapse.Intersect(...requestedPossibleATPs)
+          
+          const errorMessages = DecisionCollapse.Verify(result)
+          if (errorMessages.length > 0) {
+            const errMessage = `Cannot process selectATP with parameter ${atlasId}, ${parcellationId}, ${templateId} and ${regionId}. ${errorMessages.join(" ")}`
+            return throwError(errMessage)
+          }
 
-          const selectedAtlas = atlasId
-            ? atlases.find(atlas => atlas.id === atlasId)
-            : atlases[0]
+          /**
+           * narrow down the valid atlas, template and parcellation according to the current state
+           * If intersection is None, then leave the possibility intact
+           */
+          const foundAtlas = atlas && result.atlases.find(a => a.id === atlas.id)
+          const foundParc = parcellation && result.parcellations.find(a => a.id === parcellation.id)
+          const foundSpace = template && result.spaces.find(a => a.id === template.id)
+          
+          result.atlases = foundAtlas && [foundAtlas] || result.atlases
+          result.parcellations = foundParc && [foundParc] || result.parcellations
+          result.spaces = foundSpace && [foundSpace] || result.spaces
 
-          if (!selectedAtlas) {
-            return of(
-              mainActions.generalActionError({
-                message: `Atlas with id ${atlasId} not found!`
-              })
-            )
+          /**
+           * 
+           * with the remaining possible options, ask user to decide on which atlas/parc/space to select
+           */
+          const prAskUser = async () => {
+            let atlas: SxplrAtlas
+            let template: SxplrTemplate
+            let parcellation: SxplrParcellation
+
+            if (result.atlases.length === 1) {
+              atlas = result.atlases[0]
+            }
+            if (result.spaces.length === 1) {
+              template = result.spaces[0]
+            }
+            if (result.parcellations.length === 1) {
+              parcellation = result.parcellations[0]
+            }
+            
+            if (autoSelect) {
+              atlas ||= atlas[0]
+              template ||= result.spaces.find(s => s.name.includes("152")) || result.spaces[0]
+
+              const parcPrevIds = result.parcellations.map(p => p.prevId)
+              const latestParcs = result.parcellations.filter(p => !parcPrevIds.includes(p.id))
+              const prefParc = result.parcellations.filter(p => prefParcId.includes(p.id)).sort((a, b) => prefParcId.indexOf(a.id) - prefParcId.indexOf(b.id))
+
+              parcellation ||= prefParc[0] || latestParcs[0] || result.parcellations[0]
+            }
+
+            atlas ||= await this.#askUserATP("Please select an atlas", result.atlases).toPromise()
+            if (!atlas) return // user cancelled
+            template ||= await this.#askUserATP(messages.template || "Please select a space", result.spaces).toPromise()
+            if (!template) return // user cancelled
+            parcellation ||= await this.#askUserATP(messages.parcellation || "Please select a parcellation", result.parcellations).toPromise()
+            if (!parcellation) return // user cancelled
+
+            return {
+              atlas,
+              template,
+              parcellation,
+            }
           }
-          return this.sapiSvc.getAllParcellations(selectedAtlas).pipe(
-            switchMap(parcs => {
-              const selectedParcellation = parcellationId
-                ? parcs.find(parc => parc.id === parcellationId)
-                : parcs[0]
-              if (!selectedParcellation) {
+          return from(prAskUser()).pipe(
+            switchMap(val => {
+              /** user cancelled */
+              if (!val) {
+                return of(null)
+              }
+              const { atlas, parcellation, template } = val
+              return of({
+                atlas, parcellation, template
+              })
+            }),
+            switchMap(current => {
+              if (!current) {
                 return of(
-                  mainActions.generalActionError({
-                    message: `Parcellation with id ${parcellationId} not found!`
-                  })
+                  mainActions.noop()
                 )
               }
-              return this.sapiSvc.getSupportedTemplates(selectedAtlas, selectedParcellation).pipe(
-                switchMap(templates => {
-                  const selectedTemplate = templateId
-                    ? templates.find(tmpl => tmpl.id === templateId)
-                    : templates[0]
-                  if (!selectedTemplate) {
-                    return of(
-                      mainActions.generalActionError({
-                        message: `Template with id ${templateId} not found`
-                      })
-                    )
+              return forkJoin(
+                this.onTemplateParcSelectionPostHook.map(fn =>
+                  fn({previous: { atlas, template, parcellation }, current})
+                )
+              ).pipe(
+                map(partialState => {
+                  let state: Partial<AtlasSelectionState> = {
+                    selectedAtlas: current.atlas,
+                    selectedParcellation: current.parcellation,
+                    selectedTemplate: current.template
                   }
-                  return of(
-                    actions.setAtlasSelectionState({
-                      selectedAtlas,
-                      selectedParcellation,
-                      selectedTemplate
-                    })
-                  )
+                  for (const partial of partialState){
+                    state = {
+                      ...state,
+                      ...partial,
+                    }
+                  }
+
+                  if (!!regionId) {
+                    const selectedRegions = (state.selectedParcellationAllRegions || []).filter(r => r.name === regionId)
+                    state.selectedRegions = selectedRegions
+                  }
+
+                  
+                  return actions.setAtlasSelectionState(state)
                 })
               )
+            }),
+          )
+        }),
+        catchError((err) => {
+          console.log("error!", err)  
+          return of(
+            mainActions.generalActionError({
+              message: err.toString()
             })
           )
         })
       )
-    )
+    })
   ))
   
   onClearViewerMode = createEffect(() => this.action.pipe(
@@ -425,26 +385,6 @@ export class Effect {
     })
   ))
 
-  onSelAtlasTmplParcClearRegion = createEffect(() => merge(
-    this.action.pipe(
-      ofType(actions.selectAtlas)
-    ),
-    this.action.pipe(
-      ofType(actions.selectTemplate)
-    ),
-    this.action.pipe(
-      ofType(actions.selectParcellation)
-    )
-  ).pipe(
-    switchMapTo(
-      of(
-        actions.setSelectedRegions({
-          regions: []
-        })
-      )
-    )
-  ))
-
   onRegionSelectionClearPointSelection = createEffect(() => this.action.pipe(
     ofType(actions.selectRegion),
     map(() => actions.clearSelectedPoint())
@@ -460,6 +400,8 @@ export class Effect {
     private sapiSvc: SAPI,
     private store: Store,
     private interSpaceCoordXformSvc: InterSpaceCoordXformSvc,
+    private collapser: DecisionCollapse,
+    private dialog: MatDialog,
   ){
   }
 }
\ No newline at end of file
diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts
index 3fb85a4d6..e6dd65887 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.component.ts
+++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts
@@ -404,11 +404,8 @@ export class ViewerCmp implements OnDestroy {
     }
     if (pointOfInterest) {
       this.store$.dispatch(
-        atlasSelection.actions.selectTemplate({
-          template,
-          requested: {
-            parcellation: this.#parcellationSelected
-          }
+        atlasSelection.actions.selectATPById({
+          templateId: template.id
         })
       )
       this.store$.dispatch(
@@ -513,4 +510,13 @@ export class ViewerCmp implements OnDestroy {
       this.voiFeatureEntryCmp.pullAll()
     }
   }
+
+  selectATPR(region: SxplrRegion, parcellation: SxplrParcellation){
+    this.store$.dispatch(
+      atlasSelection.actions.selectATPById({
+        parcellationId: parcellation.id,
+        regionId: region.name
+      })
+    )
+  }
 }
diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html
index ecdd84d7e..486717925 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.template.html
+++ b/src/viewerModule/viewerCmp/viewerCmp.template.html
@@ -729,6 +729,7 @@
         [sxplr-sapiviews-core-region-parcellation]="view.selectedParcellation"
         [sxplr-sapiviews-core-region-region]="view.selectedRegions[0]"
         (sxplr-sapiviews-core-region-region-rich-feature-clicked)="showDataset($event)"
+        (sxplr-sapiviews-core-region-region-rich-related-region-clicked)="selectATPR($event.region, $event.parcellation)"
         (sxplr-sapiviews-core-region-navigate-to)="navigateTo($event)"
         #regionDirective="sapiViewsCoreRegionRich"
       >
-- 
GitLab