diff --git a/.helm/adhoc/certificate-sxplr-ebrains.yml b/.helm/adhoc/certificate-sxplr-ebrains.yml new file mode 100644 index 0000000000000000000000000000000000000000..19a96659ee504b18efd47a738c7a915cd823e3a9 --- /dev/null +++ b/.helm/adhoc/certificate-sxplr-ebrains.yml @@ -0,0 +1,21 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: siibra-explorer-certificate +spec: + secretName: sxplr-ebrains-secret + renewBefore: 120h + commonName: siibra-explorer.apps.ebrains.eu + isCA: false + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + usages: + - server auth + dnsNames: + # (CHANGE ME! same as `commonName`) + - siibra-explorer.apps.ebrains.eu + issuerRef: + name: letsencrypt-production-issuer-1 + kind: ClusterIssuer diff --git a/backend/app/auth.py b/backend/app/auth.py index fc3fed6a85f10dc08672a160c4010bb63dfdfeaf..9f2eaef4799e058a5e55e50035aedfe02ff5b34c 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -7,7 +7,7 @@ from uuid import uuid4 import json from .const import EBRAINS_IAM_DISCOVERY_URL, SCOPES, PROFILE_KEY -from .config import HBP_CLIENTID_V2, HBP_CLIENTSECRET_V2, HOST_PATHNAME, HOSTNAME +from .config import HBP_CLIENTID_V2, HBP_CLIENTSECRET_V2, HOST_PATHNAME from ._store import RedisEphStore _store = RedisEphStore.Ephemeral() @@ -38,13 +38,12 @@ def process_ebrains_user(resp): router = APIRouter() -redirect_uri = HOSTNAME.rstrip("/") + HOST_PATHNAME + "/hbp-oidc-v2/cb" - @router.get("/hbp-oidc-v2/auth") async def login_via_ebrains(request: Request, state: str = None): kwargs = {} if state: kwargs["state"] = state + redirect_uri = str(request.base_url).rstrip("/") + HOST_PATHNAME + "/hbp-oidc-v2/cb" return await oauth.ebrains.authorize_redirect(request, redirect_uri=redirect_uri, **kwargs) @router.get("/hbp-oidc-v2/cb") diff --git a/backend/app/config.py b/backend/app/config.py index 27c5b36409149cef4990530deda05fde970ffd74..d51631de39d3d3f0b2373a7dd7010e7db6d41f96 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -4,8 +4,6 @@ SESSION_SECRET = os.getenv("SESSION_SECRET", "hey joline, remind me to set a mor HOST_PATHNAME = os.getenv("HOST_PATHNAME", "") -HOSTNAME = os.getenv("HOSTNAME", "http://localhost:3000") - OVERWRITE_API_ENDPOINT = os.getenv("OVERWRITE_API_ENDPOINT") OVERWRITE_SPATIAL_ENDPOINT = os.getenv("OVERWRITE_SPATIAL_ENDPOINT") diff --git a/deploy_env.md b/deploy_env.md index 00df9cfd338132989d45dd9521b3a153c4e3cc7f..1ead2b4741186fc8dd0975c25dc95e59ef8a0b37 100644 --- a/deploy_env.md +++ b/deploy_env.md @@ -17,7 +17,6 @@ | name | description | default | example | | --- | --- | --- | --- | -| `HOSTNAME` | | `HOST_PATHNAME` | pathname to listen on, restrictions: leading slash, no trailing slash | `''` | `/viewer` | | `HBP_CLIENTID_V2` | | `HBP_CLIENTSECRET_V2` | diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index 634e1029640bbf2888e736988f7db82f3e7ba995..ec6e17c76690b0806fdbe9c47585193465ec4223 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -8,6 +8,7 @@ - Reworded point assignment UI - Allow multi selected region names to be copied - Added legend to region hierarchy +- Allow latest queried concept in feature view - Allow experimental flag to be set to be on at runtime (this also shows the button, allow toggling of experimental features) - (experimental) allow addition of custom linear coordinate space - (experimental) show BigBrain slice number diff --git a/package-lock.json b/package-lock.json index 59b66bed829999506b86fe796052b557b71f9775..2fd6dcfa81fc6fec1dc6d0320fea6d49657149d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7646,9 +7646,9 @@ "dev": true }, "node_modules/export-nehuba": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.5.tgz", - "integrity": "sha512-5Gsgvd0BLO4evuxp4bwrBS64Em1X92vyW2mwy9BvmaptHp9DbDf05zxi6Phu7apDY+Hzc3XEaxKOVGoMACeDJg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.7.tgz", + "integrity": "sha512-LakXeWGkEtHwWrV69snlM2GGmeVP+jGnTaevOpWQJePkdkPq6DvCkCSH0mLBriR8yOPyO0e+VHuE3V4AnV4fPA==", "dependencies": { "pako": "^1.0.6" } diff --git a/src/api/broadcast/README.md b/src/api/broadcast/README.md index 358d06ae22258da4990794f1f18325ed0595369d..fd771994651f049db8e53b02f8aedf22d94d4c49 100644 --- a/src/api/broadcast/README.md +++ b/src/api/broadcast/README.md @@ -6,7 +6,7 @@ Broadcasting messages are sent under two circumstances: - immediately after the plugin client acknowledged `handshake.init` to the specific client. This is so that the client can get the current state of the viewer. -Broadcasting messages never expects a response (and thus will never contain and `id` attribute) +Broadcasting messages never expects a response (and thus will never contain an `id` attribute) ## API diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 1fb19a6a6d8330c1d5ae5ef2408fd5a2c1195827..4ac9801290d62b834128240da2d0d27b9c845f8f 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -10,7 +10,8 @@ export const environment = { // 'http://localhost:10081/v3_0', // endpoint-local-10081 // 'https://siibra-api-latest.apps-dev.hbp.eu/v3_0', //endpoint-latest // 'https://siibra-api-rc.apps.hbp.eu/v3_0', // endpoint-rc - 'https://siibra-api-stable.apps.hbp.eu/v3_0', // endpoint-stable + // 'https://siibra-api-stable.apps.hbp.eu/v3_0', // endpoint-stable + 'https://siibra-api-rc.apps.tc.humanbrainproject.eu/v3_0', // endpoint-rc-tc SPATIAL_TRANSFORM_BACKEND: 'https://hbp-spatial-backend.apps.hbp.eu', MATOMO_URL: null, MATOMO_ID: null, diff --git a/src/features/TPBRView/TPBRView.component.ts b/src/features/TPBRView/TPBRView.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..866739f60c96a10bcdd05374c1f86f03ab09e1de --- /dev/null +++ b/src/features/TPBRView/TPBRView.component.ts @@ -0,0 +1,40 @@ +import { Component, Input } from "@angular/core"; +import { TPRB } from "../util"; +import { CommonModule } from "@angular/common"; +import { AngularMaterialModule } from "src/sharedModules"; +import { BehaviorSubject } from "rxjs"; +import { map, throttleTime } from "rxjs/operators"; + +@Component({ + selector: 'tpbr-viewer', + templateUrl: './TPBRView.template.html', + styleUrls: [ + './TPBRView.style.scss' + ], + standalone: true, + imports: [ + CommonModule, + AngularMaterialModule, + ] +}) +export class TPBRViewCmp { + @Input('tpbr-concept') + set _tpbr(value: TPRB){ + this.#tpbr.next(value) + } + #tpbr = new BehaviorSubject<TPRB>(null) + + view$ = this.#tpbr.pipe( + throttleTime(16), + map(v => { + if (!v) return null + return { + ...v, + bboxString: v.bbox && { + from: v.bbox[0].map(v => v.toFixed(2)).join(", "), + to: v.bbox[1].map(v => v.toFixed(2)).join(", "), + } + } + }) + ) +} diff --git a/src/features/TPBRView/TPBRView.style.scss b/src/features/TPBRView/TPBRView.style.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/features/TPBRView/TPBRView.template.html b/src/features/TPBRView/TPBRView.template.html new file mode 100644 index 0000000000000000000000000000000000000000..3f5845846bff2cc8af605c703eb442be65746718 --- /dev/null +++ b/src/features/TPBRView/TPBRView.template.html @@ -0,0 +1,22 @@ +<ng-template [ngIf]="view$ | async" let-tpbr> + <div *ngIf="tpbr.template"> + {{ tpbr.template.name }} + </div> + + <ng-template [ngIf]="tpbr.bboxString" let-bboxString> + <div> + from {{ bboxString.from }} + </div> + <div> + to {{ bboxString.to }} + </div> + </ng-template> + + <div *ngIf="tpbr.parcellation"> + {{ tpbr.parcellation.name }} + </div> + <div *ngIf="tpbr.region"> + {{ tpbr.region.name }} + </div> + +</ng-template> diff --git a/src/features/base.ts b/src/features/base.ts index c5085aabd44abacf2c9e198703b60e0d88df088f..353118ae88602242b642e5ecb10cc40d9a797ccf 100644 --- a/src/features/base.ts +++ b/src/features/base.ts @@ -2,8 +2,8 @@ import { Input, OnChanges, Directive, SimpleChanges } from "@angular/core"; import { BehaviorSubject, combineLatest } from "rxjs"; import { debounceTime, map } from "rxjs/operators"; import { SxplrParcellation, SxplrRegion, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; +import { BBox } from "./util"; -type BBox = [[number, number, number], [number, number, number]] @Directive() export class FeatureBase implements OnChanges{ @@ -31,7 +31,7 @@ export class FeatureBase implements OnChanges{ debounceTime(500) ) ]).pipe( - map(([ v1, v2 ]) => ({ ...v1, ...v2 })) + map(([ v1, v2 ]) => ({ ...v1, ...v2 })), ) ngOnChanges(sc: SimpleChanges): void { diff --git a/src/features/compoundFtContainer/compoundFtContainer.component.ts b/src/features/compoundFtContainer/compoundFtContainer.component.ts deleted file mode 100644 index f1529e6f0f95b0ba6e83370322dc8790d69b862b..0000000000000000000000000000000000000000 --- a/src/features/compoundFtContainer/compoundFtContainer.component.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { MatSnackBar } from "@angular/material/snack-bar"; -import { Store } from "@ngrx/store"; -import { BehaviorSubject } from "rxjs"; -import { SAPI } from "src/atlasComponents/sapi"; -import { SimpleCompoundFeature } from "src/atlasComponents/sapi/sxplrTypes"; -import { userInteraction } from "src/state"; - -@Component({ - templateUrl: './compoundFtContainer.template.html', - styleUrls: [ - './compoundFtContainer.style.css' - ], - selector: 'compound-feature-container', -}) - -export class CompoundFtContainer { - @Input() - compoundFeature: SimpleCompoundFeature - - @Output() - dismiss = new EventEmitter() - - busy$ = new BehaviorSubject(false) - - constructor(private sapi: SAPI, private store: Store, private snackbar: MatSnackBar){ - } - async showSubfeature(id: string){ - try { - this.busy$.next(true) - const feature = await this.sapi.getV3FeatureDetailWithId(id).toPromise() - this.store.dispatch( - userInteraction.actions.showFeature({ feature }) - ) - this.dismiss.emit() - } catch (e) { - console.log('error', e) - this.snackbar.open(`Error: ${e.toString()}`, "Dismiss") - } finally { - this.busy$.next(false) - } - } -} - -/** - * TODO - * - * check http://localhost:10081/v3_0/feature/lq0::BigBrainIntensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-300::r:Area%20hOc1%20(V1,%2017,%20CalcS)%20left::4c05163cac01b560cddf9d0ae2b63c94 - * - * see https://github.com/FZJ-INM1-BDA/siibra-python/issues/509 - */ \ No newline at end of file diff --git a/src/features/compoundFtContainer/compoundFtContainer.style.css b/src/features/compoundFtContainer/compoundFtContainer.style.css deleted file mode 100644 index 889029fd9edf9c4d6720a687da6ead5ac2d2f3bf..0000000000000000000000000000000000000000 --- a/src/features/compoundFtContainer/compoundFtContainer.style.css +++ /dev/null @@ -1,16 +0,0 @@ -cdk-virtual-scroll-viewport -{ - display: block; - min-height:30vh; -} - -cdk-virtual-scroll-viewport button -{ - width: 100%; - justify-content: left; -} - -mat-divider -{ - margin: 1rem 0; -} diff --git a/src/features/compoundFtContainer/compoundFtContainer.template.html b/src/features/compoundFtContainer/compoundFtContainer.template.html deleted file mode 100644 index b7ace3dd7225d282671d6764af37bf9fe8b6cd22..0000000000000000000000000000000000000000 --- a/src/features/compoundFtContainer/compoundFtContainer.template.html +++ /dev/null @@ -1,19 +0,0 @@ -<div> - Please select an element -</div> - - -<cdk-virtual-scroll-viewport itemSize="35"> - <button mat-button - *cdkVirtualFor="let entry of compoundFeature.indices" - [disabled]="busy$ | async" - (click)="showSubfeature(entry.id)"> - {{ entry.index | indexToStr }} - </button> -</cdk-virtual-scroll-viewport> - -<mat-progress-bar - *ngIf="busy$ | async" - mode="indeterminate"> -</mat-progress-bar> - diff --git a/src/features/compoundFtContainer/index.ts b/src/features/compoundFtContainer/index.ts deleted file mode 100644 index e8929213946abc11d03797ad3cc028c963589749..0000000000000000000000000000000000000000 --- a/src/features/compoundFtContainer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { CompoundFeatureModule } from "./module" -export { CompoundFtContainer } from "./compoundFtContainer.component" diff --git a/src/features/compoundFtContainer/indexToText.pipe.ts b/src/features/compoundFtContainer/indexToText.pipe.ts deleted file mode 100644 index 79fd3815567f817cf5bd16f905e3d9222b1b78f8..0000000000000000000000000000000000000000 --- a/src/features/compoundFtContainer/indexToText.pipe.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { SimpleCompoundFeature } from "src/atlasComponents/sapi/sxplrTypes"; - -@Pipe({ - name: 'indexToStr', - pure: true -}) -export class IndexToStrPipe implements PipeTransform{ - public transform(value: SimpleCompoundFeature['indices'][number]['index']): string { - if (typeof value === "string") { - return value - } - return `Point(${value.loc.join(", ")})` - } -} diff --git a/src/features/compoundFtContainer/module.ts b/src/features/compoundFtContainer/module.ts deleted file mode 100644 index f09d1d00dd8c099de95d86f433503c7df1b7c239..0000000000000000000000000000000000000000 --- a/src/features/compoundFtContainer/module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule } from "@angular/core"; -import { CompoundFtContainer } from "./compoundFtContainer.component"; -import { AngularMaterialModule } from "src/sharedModules"; -import { CommonModule } from "@angular/common"; -import { IndexToStrPipe } from "./indexToText.pipe"; - -@NgModule({ - imports: [ - AngularMaterialModule, - CommonModule, - ], - declarations: [ - CompoundFtContainer, - IndexToStrPipe, - ], - exports: [ - CompoundFtContainer - ] -}) -export class CompoundFeatureModule{} diff --git a/src/features/entry/entry.component.spec.ts b/src/features/entry/entry.component.spec.ts index dd5fc359a7980bc4488860d956f2391a882356c6..94c60cf1a262ac66573b98db7ff1656b8812832f 100644 --- a/src/features/entry/entry.component.spec.ts +++ b/src/features/entry/entry.component.spec.ts @@ -5,6 +5,7 @@ import { SAPIModule } from 'src/atlasComponents/sapi'; import { EntryComponent } from './entry.component'; import { provideMockStore } from '@ngrx/store/testing'; import { FeatureModule } from '../module'; +import { FEATURE_CONCEPT_TOKEN, TPRB } from '../util'; describe('EntryComponent', () => { let component: EntryComponent; @@ -18,7 +19,13 @@ describe('EntryComponent', () => { ], declarations: [ ], providers: [ - provideMockStore() + provideMockStore(), + { + provide: FEATURE_CONCEPT_TOKEN, + useValue: { + register(id: string, tprb: TPRB){} + } + } ] }) .compileComponents(); diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index 6201f9761999ebcf3b1f4bf61d8d7bd47701f1ef..f076c648250179a9c437f4f36f68cc5e63965339 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -1,16 +1,17 @@ -import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, QueryList, TemplateRef, ViewChildren } from '@angular/core'; -import { select, Store } from '@ngrx/store'; -import { debounceTime, distinctUntilChanged, map, scan, shareReplay, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { AfterViewInit, ChangeDetectorRef, Component, Inject, QueryList, TemplateRef, ViewChildren, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { debounceTime, distinctUntilChanged, map, scan, shareReplay, switchMap, take, takeUntil, withLatestFrom } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi'; import { Feature } from 'src/atlasComponents/sapi/sxplrTypes'; import { FeatureBase } from '../base'; import * as userInteraction from "src/state/userInteraction" -import { atlasSelection } from 'src/state'; import { CategoryAccDirective } from "../category-acc.directive" -import { combineLatest, concat, forkJoin, merge, of, Subject, Subscription } from 'rxjs'; +import { combineLatest, concat, forkJoin, merge, of, Subject } from 'rxjs'; import { DsExhausted, IsAlreadyPulling, PulledDataSource } from 'src/util/pullable'; import { TranslatedFeature } from '../list/list.directive'; import { MatDialog } from 'src/sharedModules/angularMaterial.exports'; +import { DestroyDirective } from 'src/util/directives/destroy.directive'; +import { FEATURE_CONCEPT_TOKEN, FeatureConcept, TPRB } from '../util'; const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => { const returnVal: Record<string, T[]> = {} @@ -30,18 +31,35 @@ const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => { selector: 'sxplr-feature-entry', templateUrl: './entry.flattened.component.html', styleUrls: ['./entry.flattened.component.scss'], - exportAs: 'featureEntryCmp' + exportAs: 'featureEntryCmp', + hostDirectives: [ + DestroyDirective + ] }) -export class EntryComponent extends FeatureBase implements AfterViewInit, OnDestroy { +export class EntryComponent extends FeatureBase implements AfterViewInit { + + ondestroy$ = inject(DestroyDirective).destroyed$ @ViewChildren(CategoryAccDirective) catAccDirs: QueryList<CategoryAccDirective> - constructor(private sapi: SAPI, private store: Store, private dialog: MatDialog, private cdr: ChangeDetectorRef) { + constructor( + private sapi: SAPI, + private store: Store, + private dialog: MatDialog, + private cdr: ChangeDetectorRef, + @Inject(FEATURE_CONCEPT_TOKEN) private featConcept: FeatureConcept, + ) { super() + + this.TPRBbox$.pipe( + takeUntil(this.ondestroy$) + ).subscribe(tprb => { + this.#tprb = tprb + }) } + #tprb: TPRB - #subscriptions: Subscription[] = [] #catAccDirs = new Subject<CategoryAccDirective[]>() features$ = this.#catAccDirs.pipe( switchMap(dirs => concat( @@ -106,50 +124,42 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest )) ) - ngOnDestroy(): void { - while (this.#subscriptions.length > 0) this.#subscriptions.pop().unsubscribe() - } ngAfterViewInit(): void { - this.#subscriptions.push( - merge( - of(null), - this.catAccDirs.changes - ).pipe( - map(() => Array.from(this.catAccDirs)) - ).subscribe(dirs => this.#catAccDirs.next(dirs)), - - this.#pullAll.pipe( - debounceTime(320), - withLatestFrom(this.#catAccDirs), - switchMap(([_, dirs]) => combineLatest(dirs.map(dir => dir.datasource$))), - ).subscribe(async dss => { - await Promise.all( - dss.map(async ds => { - // eslint-disable-next-line no-constant-condition - while (true) { - try { - await ds.pull() - } catch (e) { - if (e instanceof DsExhausted) { - break - } - if (e instanceof IsAlreadyPulling ) { - continue - } - throw e + merge( + of(null), + this.catAccDirs.changes + ).pipe( + map(() => Array.from(this.catAccDirs)), + takeUntil(this.ondestroy$), + ).subscribe(dirs => this.#catAccDirs.next(dirs)) + + this.#pullAll.pipe( + debounceTime(320), + withLatestFrom(this.#catAccDirs), + switchMap(([_, dirs]) => combineLatest(dirs.map(dir => dir.datasource$))), + takeUntil(this.ondestroy$), + ).subscribe(async dss => { + await Promise.all( + dss.map(async ds => { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await ds.pull() + } catch (e) { + if (e instanceof DsExhausted) { + break } + if (e instanceof IsAlreadyPulling ) { + continue + } + throw e } - }) - ) - }) - ) + } + }) + ) + }) } - public selectedAtlas$ = this.store.pipe( - select(atlasSelection.selectors.selectedAtlas) - ) - - private featureTypes$ = this.sapi.v3Get("/feature/_types", {}).pipe( switchMap(resp => this.sapi.iteratePages( @@ -191,6 +201,13 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest ) onClickFeature(feature: Feature) { + + /** + * register of TPRB (template, parcellation, region, bbox) *has* to + * happen at the moment when feature is selected + */ + this.featConcept.register(feature.id, this.#tprb) + this.store.dispatch( userInteraction.actions.showFeature({ feature diff --git a/src/features/feature-view/feature-view.component.html b/src/features/feature-view/feature-view.component.html index 86e5237805cce4bdf7de1b46b448c2f1d0344a2f..05bffb881df03a0da88cf1148a1555977512fd52 100644 --- a/src/features/feature-view/feature-view.component.html +++ b/src/features/feature-view/feature-view.component.html @@ -1,9 +1,30 @@ -<ng-template #headerTmpl> - <ng-content select="[header]"></ng-content> -</ng-template> - <ng-template [ngIf]="view$ | async" let-view> - + + <ng-template #headerTmpl> + <ng-template [ngIf]="view.prevCmpFeat"> + <button mat-button class="sxplr-mb-2" + (click)="showSubfeature(view.prevCmpFeat)"> + <i class="fas fa-chevron-left"></i> + <span class="ml-1"> + Back + </span> + </button> + </ng-template> + + <ng-template [ngIf]="!view.prevCmpFeat"> + + <button mat-button + (click)="clearSelectedFeature()" + class="sxplr-mb-2"> + <span class="ml-1"> + Dismiss + </span> + <i class="fas fa-times"></i> + </button> + </ng-template> + + </ng-template> + <mat-card class="mat-elevation-z4 sxplr-z-4 header-card"> <mat-card-header> @@ -42,6 +63,35 @@ </button> </ng-template> + <!-- anchor --> + <ng-template [ngIf]="view.concept"> + <button mat-list-item + [sxplr-dialog]="queriedConceptsTmpl" + [sxplr-dialog-size]="null"> + <mat-icon matListItemIcon fontSet="fas" fontIcon="fa-anchor"></mat-icon> + <div matListItemTitle>Queried Concepts</div> + </button> + + <ng-template #queriedConceptsTmpl> + <mat-card> + <mat-card-header class="sxplr-custom-cmp text"> + <mat-card-title> + Queried Concepts + </mat-card-title> + <mat-card-subtitle> + Concepts queried to get this feature. Please note this property is session dependent. + </mat-card-subtitle> + </mat-card-header> + <mat-card-content class="sxplr-custom-cmp text"> + <tpbr-viewer [tpbr-concept]="view.concept"></tpbr-viewer> + </mat-card-content> + <mat-card-actions> + <button mat-button matDialogClose>close</button> + </mat-card-actions> + </mat-card> + </ng-template> + </ng-template> + <!-- doi --> <ng-template ngFor [ngForOf]="view.links" let-url> <a [href]="url.href" mat-list-item target="_blank" class="no-hover"> @@ -74,7 +124,7 @@ <ng-template matTabContent> <compound-feature-indices [indices]="cmpFeatElmts" [selected-template]="view.selectedTemplate" - (on-click-index)="showSubfeature($event.id)"> + (on-click-index)="showSubfeature($event)"> </compound-feature-indices> </ng-template> </mat-tab> diff --git a/src/features/feature-view/feature-view.component.spec.ts b/src/features/feature-view/feature-view.component.spec.ts index 817347a3a5856d093f75f3c9d86f1e95a5025d95..567a8c882dbe1c2a09c56fee11c008fb01acfd56 100644 --- a/src/features/feature-view/feature-view.component.spec.ts +++ b/src/features/feature-view/feature-view.component.spec.ts @@ -7,6 +7,7 @@ import { DARKTHEME } from 'src/util/injectionTokens'; import { FeatureViewComponent } from './feature-view.component'; import { provideMockStore } from '@ngrx/store/testing'; import { AngularMaterialModule } from 'src/sharedModules'; +import { FEATURE_CONCEPT_TOKEN } from '../util'; describe('FeatureViewComponent', () => { let component: FeatureViewComponent; @@ -36,6 +37,12 @@ describe('FeatureViewComponent', () => { }, sapiEndpoint$: EMPTY } + }, + { + provide: FEATURE_CONCEPT_TOKEN, + useValue: { + concept$: EMPTY + } } ] }) diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index c96cd7ee63cfefa07329b05a403a770229396a46..f95b096cd388754725a8842d9893e2f29c7da1d8 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -1,6 +1,6 @@ -import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, Input, inject } from '@angular/core'; import { BehaviorSubject, EMPTY, Observable, combineLatest, concat, of } from 'rxjs'; -import { catchError, debounceTime, distinctUntilChanged, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators'; +import { catchError, debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi/sapi.service'; import { Feature, SimpleCompoundFeature, VoiFeature } from 'src/atlasComponents/sapi/sxplrTypes'; import { DARKTHEME } from 'src/util/injectionTokens'; @@ -8,22 +8,37 @@ import { isVoiData, notQuiteRight } from "../guards" import { Action, Store, select } from '@ngrx/store'; import { atlasSelection, userInteraction } from 'src/state'; import { PathReturn } from 'src/atlasComponents/sapi/typeV3'; -import { MatSnackBar } from '@angular/material/snack-bar'; +import { CFIndex } from '../compoundFeatureIndices'; +import { ComponentStore } from '@ngrx/component-store'; +import { DestroyDirective } from 'src/util/directives/destroy.directive'; +import { FEATURE_CONCEPT_TOKEN, FeatureConcept } from '../util'; + +type FeatureCmpStore = { + selectedCmpFeature: SimpleCompoundFeature|null +} type PlotlyResponse = PathReturn<"/feature/{feature_id}/plotly"> function isSimpleCompoundFeature(feat: unknown): feat is SimpleCompoundFeature{ - return !!feat['indices'] + return !!(feat?.['indices']) } @Component({ selector: 'sxplr-feature-view', templateUrl: './feature-view.component.html', styleUrls: ['./feature-view.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + ComponentStore + ], + hostDirectives: [ + DestroyDirective + ] }) export class FeatureViewComponent { + destroyed$ = inject(DestroyDirective).destroyed$ + busy$ = new BehaviorSubject<boolean>(false) #feature$ = new BehaviorSubject<Feature|SimpleCompoundFeature>(null) @@ -122,14 +137,17 @@ export class FeatureViewComponent { if (!id) { return of(null) } - return this.sapi.getFeaturePlot( - id, - { - template: darktheme ? 'plotly_dark' : 'plotly_white', - ...additionalParams - } - ).pipe( - catchError(() => of(null)) + return concat( + of(null), + this.sapi.getFeaturePlot( + id, + { + template: darktheme ? 'plotly_dark' : 'plotly_white', + ...additionalParams + } + ).pipe( + catchError(() => of(null)) + ) ) }), shareReplay(1), @@ -194,9 +212,18 @@ export class FeatureViewComponent { constructor( private sapi: SAPI, private store: Store, - private snackbar: MatSnackBar, - @Inject(DARKTHEME) public darktheme$: Observable<boolean>, + private readonly cmpStore: ComponentStore<FeatureCmpStore>, + @Inject(DARKTHEME) public darktheme$: Observable<boolean>, + @Inject(FEATURE_CONCEPT_TOKEN) private featConcept: FeatureConcept, ) { + this.cmpStore.setState({ selectedCmpFeature: null }) + + this.#feature$.pipe( + takeUntil(this.destroyed$), + filter(isSimpleCompoundFeature), + ).subscribe(selectedCmpFeature => { + this.cmpStore.patchState({ selectedCmpFeature }) + }) } navigateToRegionByName(regionName: string){ @@ -212,6 +239,21 @@ export class FeatureViewComponent { onAction(action: Action){ this.store.dispatch(action) } + + #etheralView$ = combineLatest([ + this.cmpStore.state$, + this.#feature$, + this.featConcept.concept$ + ]).pipe( + map(([ { selectedCmpFeature }, feature, selectedConcept ]) => { + const { id: selectedConceptFeatId, concept } = selectedConcept + const prevCmpFeat: SimpleCompoundFeature = selectedCmpFeature?.indices.some(idx => idx.id === feature?.id) && selectedCmpFeature || null + return { + prevCmpFeat, + concept: selectedConceptFeatId === feature.id && concept || null + } + }) + ) #specialView$ = combineLatest([ concat( @@ -225,11 +267,11 @@ export class FeatureViewComponent { this.#compoundFeatEmts$, this.store.pipe( select(atlasSelection.selectors.selectedTemplate) - ) + ), ]).pipe( map(([ voi, plotly, cmpFeatElmts, selectedTemplate ]) => { return { - voi, plotly, cmpFeatElmts, selectedTemplate + voi, plotly, cmpFeatElmts, selectedTemplate, } }) ) @@ -266,28 +308,30 @@ export class FeatureViewComponent { view$ = combineLatest([ this.#baseView$, - this.#specialView$ + this.#specialView$, + this.#etheralView$ ]).pipe( - map(([obj1, obj2]) => { + map(([obj1, obj2, obj3]) => { return { ...obj1, ...obj2, + ...obj3, } }) ) - async showSubfeature(id: string){ - try { - this.busy$.next(true) - const feature = await this.sapi.getV3FeatureDetailWithId(id).toPromise() - this.store.dispatch( - userInteraction.actions.showFeature({ feature }) - ) - } catch (e) { - console.log('error', e) - this.snackbar.open(`Error: ${e.toString()}`, "Dismiss") - } finally { - this.busy$.next(false) - } + showSubfeature(item: CFIndex|Feature){ + this.store.dispatch( + userInteraction.actions.showFeature({ + feature: item + }) + ) + } + + clearSelectedFeature(): void{ + this.store.dispatch( + userInteraction.actions.clearShownFeature() + ) } + } diff --git a/src/features/module.ts b/src/features/module.ts index c168d8929afb413549c78b596eb155c773233da1..e23945532c2d0fb0588518a3ef850e3ea0271dbb 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -5,7 +5,6 @@ import { UtilModule } from "src/util"; import { EntryComponent } from './entry/entry.component' import { FeatureNamePipe } from "./featureName.pipe"; import { CategoryAccDirective } from './category-acc.directive'; -import { CompoundFeatureModule } from "./compoundFtContainer"; import { ScrollingModule } from "@angular/cdk/scrolling"; import { MarkdownModule } from "src/components/markdown"; import { FeatureViewComponent } from "./feature-view/feature-view.component"; @@ -21,25 +20,30 @@ import { PlotlyComponent } from "./plotly"; import { AngularMaterialModule } from "src/sharedModules"; import { AtlasColorMapIntents } from "./atlas-colormap-intents"; import { CompoundFeatureIndicesModule } from "./compoundFeatureIndices" +import { FEATURE_CONCEPT_TOKEN, FeatureConcept, TPRB } from "./util"; +import { BehaviorSubject } from "rxjs"; +import { TPBRViewCmp } from "./TPBRView/TPBRView.component"; +import { DialogModule } from "src/ui/dialogInfo"; @NgModule({ imports: [ CommonModule, SpinnerModule, UtilModule, - CompoundFeatureModule, ScrollingModule, MarkdownModule, NgLayerCtlModule, ReadmoreModule, AngularMaterialModule, CompoundFeatureIndicesModule, + DialogModule, /** * standalone components */ PlotlyComponent, AtlasColorMapIntents, + TPBRViewCmp, ], declarations: [ EntryComponent, @@ -61,6 +65,19 @@ import { CompoundFeatureIndicesModule } from "./compoundFeatureIndices" VoiBboxDirective, ListDirective, ], + providers: [ + { + provide: FEATURE_CONCEPT_TOKEN, + useFactory: () => { + const obs = new BehaviorSubject<{ id: string, concept: TPRB}>({id: null, concept: {}}) + const returnObj: FeatureConcept = { + register: (id, concept) => obs.next({ id, concept }), + concept$: obs.asObservable() + } + return returnObj + } + } + ], schemas: [ CUSTOM_ELEMENTS_SCHEMA, ] diff --git a/src/features/util.ts b/src/features/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..928661e75b8942de7534a904cf4fecfff5429caf --- /dev/null +++ b/src/features/util.ts @@ -0,0 +1,19 @@ +import { InjectionToken } from "@angular/core"; +import { Observable } from "rxjs"; +import { SxplrParcellation, SxplrRegion, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; + +export type BBox = [[number, number, number], [number, number, number]] + +export type TPRB = { + template?: SxplrTemplate + parcellation?: SxplrParcellation + region?: SxplrRegion + bbox?: BBox +} + +export type FeatureConcept = { + register: (id: string, concept: TPRB) => void + concept$: Observable<{ id: string, concept: TPRB }> +} + +export const FEATURE_CONCEPT_TOKEN = new InjectionToken("FEATURE_CONCEPT_TOKEN") diff --git a/src/util/constants.ts b/src/util/constants.ts index b1411dc432430330bffc9d552cc486dc2a3c95bb..e3974a544e7bad44245e6b87de1d8e70e597e933 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -21,7 +21,7 @@ export const MIN_REQ_EXPLAINER = ` export const APPEND_SCRIPT_TOKEN: InjectionToken<(url: string) => Promise<HTMLScriptElement>> = new InjectionToken(`APPEND_SCRIPT_TOKEN`) export const appendScriptFactory = (document: Document, defer: boolean = false) => { - return src => new Promise((rs, rj) => { + return (src: string) => new Promise((rs, rj) => { const scriptEl = document.createElement('script') if (defer) { scriptEl.defer = true diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 0d2d8192545d269615b02e654782c9415abbc5b0..cdfcf7f4378f5e0693c38ee4a05605c1d6d92d48 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -43,6 +43,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di import { HOVER_INTERCEPTOR_INJECTOR } from "src/util/injectionTokens"; import { ViewerWrapper } from "./viewerWrapper/viewerWrapper.component"; import { MediaQueryDirective } from "src/util/directives/mediaQuery.directive"; +import { TPBRViewCmp } from "src/features/TPBRView/TPBRView.component"; @NgModule({ imports: [ @@ -75,6 +76,7 @@ import { MediaQueryDirective } from "src/util/directives/mediaQuery.directive"; MediaQueryDirective, FloatingMouseContextualContainerDirective, ExperimentalFlagDirective, + TPBRViewCmp, ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : []) ], diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 55094ebf67b51781234625188b8b95805de995c2..ac84f5c6e75e32956314bcb8537a7e3391edcbf1 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -493,12 +493,6 @@ export class ViewerCmp { ) } - clearSelectedFeature(): void{ - this.store$.dispatch( - userInteraction.actions.clearShownFeature() - ) - } - navigateTo(position: number[]): void { this.store$.dispatch( atlasSelection.actions.navigateTo({ diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index dea9c8548f2613e3661c87bda264b8e297598281..b837c843a4ef310b2af85cc27c52f8ccbd5763b1 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -829,23 +829,7 @@ <ng-template let-feature="feature" #selectedFeatureTmpl> <!-- TODO differentiate between features (spatial, regional etc) --> - <sxplr-feature-view class="sxplr-z-2 mat-elevation-z2" - [feature]="feature"> - - <div header> - <!-- back btn --> - <button mat-button - (click)="clearSelectedFeature()" - [attr.aria-label]="ARIA_LABELS.CLOSE" - class="sxplr-mb-2" - > - <span class="ml-1"> - Dismiss - </span> - <i class="fas fa-times"></i> - </button> - </div> - + <sxplr-feature-view class="sxplr-z-2 mat-elevation-z2" [feature]="feature"> </sxplr-feature-view> </ng-template> @@ -892,16 +876,14 @@ Anchored to current view </mat-card-title> <mat-card-subtitle> - <div *ngIf="view.selectedTemplate"> - {{ view.selectedTemplate.name }} - </div> + <ng-template [ngIf]="bbox.bbox$ | async | getProperty : 'bbox'" let-bbox> - <div> - from {{ bbox[0] | numbers | addUnitAndJoin : '' }} - </div> - <div> - to {{ bbox[1] | numbers | addUnitAndJoin : '' }} - </div> + + <tpbr-viewer [tpbr-concept]="{ + template: view.selectedTemplate, + bbox: bbox + }"> + </tpbr-viewer> </ng-template> </mat-card-subtitle> </mat-card-header>