diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts index fd67601ee087cf32ddd230d7e42b3ca9fdaea421..3969bc420cdea13c69fef90bbc765de4b047e0de 100644 --- a/src/atlasComponents/annotations/annotation.service.ts +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -112,12 +112,12 @@ export class AnnotationLayer { this.nglayer && this.nglayer.setVisible(flag) } dispose() { - this.nglayer = null AnnotationLayer.Map.delete(this.name) this._onHover.complete() while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() try { this.viewer.layerManager.removeManagedLayer(this.nglayer) + this.nglayer = null // eslint-disable-next-line no-empty } catch (e) { diff --git a/src/atlasComponents/sapi/core/base.ts b/src/atlasComponents/sapi/core/base.ts index cb7544f04111082bcf5cb845c8bfccb7b1bd1c9d..b5fbcaefc04fc33512ae9ebb4d50cd42fdef6784 100644 --- a/src/atlasComponents/sapi/core/base.ts +++ b/src/atlasComponents/sapi/core/base.ts @@ -4,7 +4,7 @@ import { RouteParam } from "../typeV3" const AllFeatures = { CorticalProfile: "CorticalProfile", - // EbrainsDataFeature: "EbrainsDataFeature", + EbrainsDataFeature: "EbrainsDataFeature", RegionalConnectivity: "RegionalConnectivity", Tabular: "Tabular", // GeneExpressions: "GeneExpressions", diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts index 942859c6fb33390c8c36585d4b7869181f300cbc..0c5a2d0baf73fa0ba4d6e499abdbe4e3930eeb13 100644 --- a/src/atlasComponents/sapi/core/sapiRegion.ts +++ b/src/atlasComponents/sapi/core/sapiRegion.ts @@ -12,7 +12,7 @@ import { SxplrRegion } from "../sxplrTypes"; const RegionFeatures = { // ReceptorDensityFingerprint: "ReceptorDensityFingerprint", // GeneExpressions: "GeneExpressions", - // EbrainsDataFeature: "EbrainsDataFeature", + EbrainsDataFeature: "EbrainsDataFeature", // ReceptorDensityProfile: "ReceptorDensityProfile", // BigBrainIntensityProfile: "BigBrainIntensityProfile", diff --git a/src/atlasComponents/sapi/schemaV3.ts b/src/atlasComponents/sapi/schemaV3.ts index 14b6ccf20fa72d37c1d0d9d237e932b25bdf996b..78aa3e006d082b8ba95dd50716a438dd98026c0c 100644 --- a/src/atlasComponents/sapi/schemaV3.ts +++ b/src/atlasComponents/sapi/schemaV3.ts @@ -111,8 +111,16 @@ export interface paths { get: operations["get_all_gene_feature_GeneExpressions_get"] } "/feature/GeneExpressions/{feature_id}": { - /** Get All Gene */ - get: operations["get_all_gene_feature_GeneExpressions__feature_id__get"] + /** Get Single Gene */ + get: operations["get_single_gene_feature_GeneExpressions__feature_id__get"] + } + "/feature/EbrainsDataFeature": { + /** Get All Ebrains Df */ + get: operations["get_all_ebrains_df_feature_EbrainsDataFeature_get"] + } + "/feature/EbrainsDataFeature/{feature_id}": { + /** Get Single Ebrains Df */ + get: operations["get_single_ebrains_df_feature_EbrainsDataFeature__feature_id__get"] } "/feature/{feature_id}": { /** @@ -753,6 +761,17 @@ export interface components { /** Size */ size: number } + /** Page[SiibraEbrainsDataFeatureModel] */ + Page_SiibraEbrainsDataFeatureModel_: { + /** Items */ + items: (components["schemas"]["SiibraEbrainsDataFeatureModel"])[] + /** Total */ + total: number + /** Page */ + page: number + /** Size */ + size: number + } /** Page[SiibraParcellationModel] */ Page_SiibraParcellationModel_: { /** Items */ @@ -1011,6 +1030,24 @@ export interface components { /** Boundaries Mapped */ boundaries_mapped: boolean } + /** SiibraEbrainsDataFeatureModel */ + SiibraEbrainsDataFeatureModel: { + /** @Type */ + "@type": string + /** Id */ + id: string + /** Modality */ + modality: string + /** Category */ + category: string + /** Description */ + description: string + /** Name */ + name: string + /** Datasets */ + datasets: (components["schemas"]["EbrainsDatasetModel"])[] + anchor?: components["schemas"]["SiibraAnchorModel"] + } /** SiibraParcellationModel */ SiibraParcellationModel: { /** @Type */ @@ -1820,8 +1857,8 @@ export interface operations { } } } - get_all_gene_feature_GeneExpressions__feature_id__get: { - /** Get All Gene */ + get_single_gene_feature_GeneExpressions__feature_id__get: { + /** Get Single Gene */ parameters: { query: { parcellation_id: string @@ -1847,6 +1884,57 @@ export interface operations { } } } + get_all_ebrains_df_feature_EbrainsDataFeature_get: { + /** Get All Ebrains Df */ + parameters: { + query: { + parcellation_id: string + region_id: string + page?: number + size?: number + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Page_SiibraEbrainsDataFeatureModel_"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + get_single_ebrains_df_feature_EbrainsDataFeature__feature_id__get: { + /** Get Single Ebrains Df */ + parameters: { + query: { + parcellation_id: string + region_id: string + } + path: { + feature_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SiibraEbrainsDataFeatureModel"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } get_single_feature_feature__feature_id__get: { /** * Get Single Feature @@ -1864,7 +1952,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["SiibraVoiModel"] | components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraRegionalConnectivityModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"] + "application/json": components["schemas"]["SiibraVoiModel"] | components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraRegionalConnectivityModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"] | components["schemas"]["SiibraEbrainsDataFeatureModel"] } } /** @description Validation Error */ diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index 00135556626cd73ec440a2f48c559bb51d535d65..4abda4e69f64cc3476ea7f77a562bc19d6c3f709 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -120,8 +120,8 @@ class TranslateV3 { } const url = input[key] const [ transform, info ] = await Promise.all([ - fetch(`${url}/transform.json`).then(res => res.json()) as Promise<number[][]>, - fetch(`${url}/info`).then(res => res.json()) as Promise<Record<string, any>>, + this.cFetch(`${url}/transform.json`).then(res => res.json()) as Promise<number[][]>, + this.cFetch(`${url}/info`).then(res => res.json()) as Promise<Record<string, any>>, ]) returnObj[key] = { url: input[key], diff --git a/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts b/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts index fc5ba9f3f62e5a2c8923a4992b49950fd082af87..72d0e2e66fc7ff0b8da0d520a6ec4438432a88df 100644 --- a/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts +++ b/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts @@ -1,9 +1,12 @@ -import { Directive, Input, OnChanges, SimpleChanges } from "@angular/core"; +import { Directive, Input, OnChanges } from "@angular/core"; import { BehaviorSubject, Observable } from "rxjs"; import { distinctUntilChanged } from "rxjs/operators"; import { BoundingBox, SxplrTemplate, SxplrAtlas } from "src/atlasComponents/sapi/sxplrTypes" -function validateBbox(input: any): boolean { +type Point = [number, number, number] +type BBox = [Point, Point] + +function validateBbox(input: any): input is BoundingBox { if (!Array.isArray(input)) return false if (input.length !== 2) return false return input.every(el => Array.isArray(el) && el.length === 3 && el.every(val => typeof val === "number")) @@ -21,9 +24,9 @@ export class SapiViewsCoreSpaceBoundingBox implements OnChanges{ @Input('sxplr-sapiviews-core-space-boundingbox-space') space: SxplrTemplate - private _bbox: BoundingBox + private _bbox: BBox @Input('sxplr-sapiviews-core-space-boundingbox-spec') - set bbox(val: string | BoundingBox ) { + set bbox(val: string | BBox ) { if (typeof val === "string") { try { @@ -31,12 +34,7 @@ export class SapiViewsCoreSpaceBoundingBox implements OnChanges{ [number, number, number], [number, number, number], ] = JSON.parse(val) - this._bbox = { - minpoint: min, - maxpoint: max, - center: min.map((v, idx) => (v + max[idx]) / 2) as [number, number, number], - space: this.space - } + this._bbox = [min, max] } catch (e) { console.warn(`Parse bbox input error`) } @@ -48,14 +46,14 @@ export class SapiViewsCoreSpaceBoundingBox implements OnChanges{ } this._bbox = val } - get bbox(): BoundingBox { + get bbox(): BBox { return this._bbox } private _bbox$: BehaviorSubject<{ atlas: SxplrAtlas space: SxplrTemplate - bbox: BoundingBox + bbox: BBox }> = new BehaviorSubject({ atlas: null, space: null, @@ -65,11 +63,11 @@ export class SapiViewsCoreSpaceBoundingBox implements OnChanges{ public bbox$: Observable<{ atlas: SxplrAtlas space: SxplrTemplate - bbox: BoundingBox + bbox: BBox }> = this._bbox$.asObservable().pipe( distinctUntilChanged( - (prev, curr) => prev.atlas?.["@id"] === curr.atlas?.['@id'] - && prev.space?.["@id"] === curr.space?.["@id"] + (prev, curr) => prev.atlas?.id === curr.atlas?.id + && prev.space?.id === curr.space?.id && JSON.stringify(prev.bbox) === JSON.stringify(curr.bbox) ) ) diff --git a/src/features/base.ts b/src/features/base.ts index cda6bb56ed795abbfaecf68dcdf23e9644c2297d..c5085aabd44abacf2c9e198703b60e0d88df088f 100644 --- a/src/features/base.ts +++ b/src/features/base.ts @@ -1,7 +1,10 @@ -import { Input, OnChanges, Directive } from "@angular/core"; -import { BehaviorSubject } from "rxjs"; +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"; +type BBox = [[number, number, number], [number, number, number]] + @Directive() export class FeatureBase implements OnChanges{ @@ -14,14 +17,36 @@ export class FeatureBase implements OnChanges{ @Input() region: SxplrRegion + @Input() + bbox: BBox + @Input() queryParams: Record<string, string> = {} - protected TPR$ = new BehaviorSubject<{ template?: SxplrTemplate, parcellation?: SxplrParcellation, region?: SxplrRegion }>({ template: null, parcellation: null, region: null }) - - ngOnChanges(): void { - const { template, parcellation, region } = this - this.TPR$.next({ template, parcellation, region }) + #TPR$ = new BehaviorSubject<{ template?: SxplrTemplate, parcellation?: SxplrParcellation, region?: SxplrRegion }>({ template: null, parcellation: null, region: null }) + #bbox$ = new BehaviorSubject<{ bbox?: BBox }>({ bbox: null }) + protected TPRBbox$ = combineLatest([ + this.#TPR$, + this.#bbox$.pipe( + debounceTime(500) + ) + ]).pipe( + map(([ v1, v2 ]) => ({ ...v1, ...v2 })) + ) + + ngOnChanges(sc: SimpleChanges): void { + const { template, parcellation, region, bbox } = sc + if (bbox) { + this.#bbox$.next({ bbox: bbox.currentValue }) + } + if (template || parcellation || region) { + const { template: t, parcellation: p, region: r } = this + this.#TPR$.next({ + template: template?.currentValue || t, + parcellation: parcellation?.currentValue || p, + region: region?.currentValue || r + }) + } } } @@ -29,7 +54,7 @@ export class FeatureBase implements OnChanges{ export const AllFeatures = { CorticalProfile: "CorticalProfile", - // EbrainsDataFeature: "EbrainsDataFeature", + EbrainsDataFeature: "EbrainsDataFeature", RegionalConnectivity: "RegionalConnectivity", Tabular: "Tabular", // GeneExpressions: "GeneExpressions", diff --git a/src/features/category-acc.directive.ts b/src/features/category-acc.directive.ts index d578d36dabcfa260822a9208e4e276637f32f117..661896191ba70576e8354970754efee6562aab6a 100644 --- a/src/features/category-acc.directive.ts +++ b/src/features/category-acc.directive.ts @@ -2,6 +2,7 @@ import { AfterContentInit, ContentChildren, Directive, OnDestroy, QueryList } fr import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { ListComponent } from './list/list.component'; +import { Feature } from "src/atlasComponents/sapi/sxplrTypes" @Directive({ selector: '[sxplrCategoryAcc]', @@ -11,6 +12,7 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy { public isBusy$ = new BehaviorSubject<boolean>(false) public total$ = new BehaviorSubject<number>(0) + public features$ = new BehaviorSubject<Feature[]>([]) @ContentChildren(ListComponent, { read: ListComponent, descendants: true }) listCmps: QueryList<ListComponent> @@ -35,15 +37,19 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy { const listCmp = Array.from(this.listCmps) - this.#subscriptions.push( + this.#subscriptions.push( combineLatest( - listCmp.map(listCmp => listCmp.features$) + listCmp.map(listC => listC.features$) + ).subscribe(features => this.features$.next(features.flatMap(f => f))), + + combineLatest( + listCmp.map(listC => listC.features$) ).pipe( map(features => features.reduce((acc, curr) => acc + curr.length, 0)) ).subscribe(total => this.total$.next(total)), combineLatest( - listCmp.map(listCmp => listCmp.state$) + listCmp.map(listC => listC.state$) ).pipe( map(states => states.some(state => state === "busy")) ).subscribe(flag => this.isBusy$.next(flag)) diff --git a/src/features/entry/entry.component.html b/src/features/entry/entry.component.html index 899e7a4d061a29ca3296985f07d950cda66f5e86..ad474847c41c6ccd4a3c7d3c390b2855f9f01152 100644 --- a/src/features/entry/entry.component.html +++ b/src/features/entry/entry.component.html @@ -47,6 +47,7 @@ [template]="template" [parcellation]="parcellation" [region]="region" + [bbox]="bbox" [queryParams]="queryParams | mergeObj : { type: (feature.name | featureNamePipe) }" [featureRoute]="feature.path" (onClickFeature)="onClickFeature($event)" diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index ad314cb5befeaefca8b22d729589e338e2d12a65..29a8f2907d2ee3a56743888dbe8d3a9b64b60018 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -1,11 +1,13 @@ -import { Component } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy, QueryList, ViewChildren } from '@angular/core'; import { Store } from '@ngrx/store'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { map, scan, switchMap } 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 { BehaviorSubject, combineLatest, merge, of, Subscription } from 'rxjs'; const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => { const returnVal: Record<string, T[]> = {} @@ -23,15 +25,61 @@ const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => { @Component({ selector: 'sxplr-feature-entry', - templateUrl: './entry.component.html', - styleUrls: ['./entry.component.scss'] + templateUrl: './entry.nestedExpPanel.component.html', + styleUrls: ['./entry.nestedExpPanel.component.scss'], + exportAs: 'featureEntryCmp' }) -export class EntryComponent extends FeatureBase { +export class EntryComponent extends FeatureBase implements AfterViewInit, OnDestroy { + + @ViewChildren(CategoryAccDirective) + catAccDirs: QueryList<CategoryAccDirective> + + public totals$ = new BehaviorSubject<number>(null) + public features$ = new BehaviorSubject<Feature[]>([]) constructor(private sapi: SAPI, private store: Store) { super() } + #subscriptions: Subscription[] = [] + + ngOnDestroy(): void { + while (this.#subscriptions.length > 0) this.#subscriptions.pop().unsubscribe() + } + ngAfterViewInit(): void { + const catAccDirs$ = merge( + of(null), + this.catAccDirs.changes + ).pipe( + map(() => Array.from(this.catAccDirs)) + ) + this.#subscriptions.push( + catAccDirs$.pipe( + switchMap(catArrDirs => merge( + ...catArrDirs.map((dir, idx) => dir.total$.pipe( + map(val => ({ idx, val })) + )) + )), + + map(({ idx, val }) => ({ [idx.toString()]: val })), + scan((acc, curr) => ({ ...acc, ...curr })), + map(record => { + let tally = 0 + for (const idx in record) { + tally += record[idx] + } + return tally + }) + ).subscribe(num => this.totals$.next(num)), + catAccDirs$.pipe( + switchMap(catArrDirs => combineLatest( + catArrDirs.map(dir => dir.features$) + )), + map(features => features.flatMap(f => f)) + ).subscribe(features => this.features$.next(features)) + ) + } + public atlas = this.store.select(atlasSelection.selectors.selectedAtlas) private featureTypes$ = this.sapi.v3Get("/feature/_types", {}).pipe( @@ -46,7 +94,7 @@ export class EntryComponent extends FeatureBase { ), ) - public cateogryCollections$ = this.TPR$.pipe( + public cateogryCollections$ = this.TPRBbox$.pipe( switchMap(({ template, parcellation, region }) => this.featureTypes$.pipe( map(features => { const filteredFeatures = features.filter(v => { diff --git a/src/features/entry/entry.nestedExpPanel.component.html b/src/features/entry/entry.nestedExpPanel.component.html new file mode 100644 index 0000000000000000000000000000000000000000..3522c1f48fb0f7d523237bcc9ffa838685f48fb5 --- /dev/null +++ b/src/features/entry/entry.nestedExpPanel.component.html @@ -0,0 +1,84 @@ +<mat-accordion> + <mat-expansion-panel *ngFor="let keyvalue of (cateogryCollections$ | async | keyvalue | isConnectivity : false)" + sxplrCategoryAcc + #categoryAcc="categoryAcc" + [ngClass]="{ + 'sxplr-d-none': !(categoryAcc.isBusy$ | async) && (categoryAcc.total$ | async) === 0 + }"> + + <mat-expansion-panel-header> + + <mat-panel-title> + {{ keyvalue.key }} + </mat-panel-title> + + <mat-panel-description> + <spinner-cmp *ngIf="categoryAcc.isBusy$ | async"></spinner-cmp> + <ng-template [ngIf]="categoryAcc.total$ | async" let-total> + <span> + {{ total }} + </span> + </ng-template> + </mat-panel-description> + </mat-expansion-panel-header> + + <mat-accordion> + <mat-expansion-panel class="mat-elevation-z4" + *ngFor="let feature of keyvalue.value" + [ngClass]="{ + 'sxplr-d-none': (list.state$ | async) === 'noresult' + }"> + + <mat-expansion-panel-header> + <mat-panel-title> + <span class="sxplr-white-space-nowrap"> + {{ feature.name | featureNamePipe }} + </span> + </mat-panel-title> + </mat-expansion-panel-header> + + <spinner-cmp *ngIf="(list.state$ | async) === 'busy'"></spinner-cmp> + <sxplr-feature-list + [template]="template" + [parcellation]="parcellation" + [region]="region" + [bbox]="bbox" + [queryParams]="queryParams | mergeObj : { type: (feature.name | featureNamePipe) }" + [featureRoute]="feature.path" + (onClickFeature)="onClickFeature($event)" + #list="featureList" + > + </sxplr-feature-list> + + </mat-expansion-panel> + </mat-accordion> + + </mat-expansion-panel> + + + <ng-template [ngIf]="cateogryCollections$ | async | keyvalue | isConnectivity : true" let-connectivity> + <ng-template ngFor [ngForOf]="connectivity" let-conn> + <mat-expansion-panel sxplr-sapiviews-features-connectivity-check + #connectivityAccordion + *ngIf="conn"> + <mat-expansion-panel-header> + <mat-panel-title> + {{ conn.key }} + </mat-panel-title> + </mat-expansion-panel-header> + + <sxplr-features-connectivity-browser class="pe-all flex-shrink-1" + [region]="region" + [sxplr-features-connectivity-browser-atlas]="atlas | async" + [sxplr-features-connectivity-browser-template]="template" + [sxplr-features-connectivity-browser-parcellation]="parcellation" + [accordionExpanded]="connectivityAccordion.expanded" + [types]="conn.value"> + </sxplr-features-connectivity-browser> + + </mat-expansion-panel> + </ng-template> + </ng-template> + + +</mat-accordion> diff --git a/src/features/entry/entry.nestedExpPanel.component.scss b/src/features/entry/entry.nestedExpPanel.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..68a9f6d331b11a5b9723e3996f827c8240c1bae5 --- /dev/null +++ b/src/features/entry/entry.nestedExpPanel.component.scss @@ -0,0 +1,10 @@ +mat-list-item +{ + text-overflow: ellipsis; + white-space: nowrap; +} + +sxplr-feature-list +{ + height: 10rem; +} diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index bf6c7916ad45f2f0a821127bb41a4c0cc3f0fa83..61371830b8d5e81761b9de75b311523e8016d0b9 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -4,15 +4,7 @@ import { filter, map } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi/sapi.service'; import { Feature, TabularFeature, VoiFeature } from 'src/atlasComponents/sapi/sxplrTypes'; import { DARKTHEME } from 'src/util/injectionTokens'; - -function isTabularData(feature: unknown): feature is TabularFeature<number|string|number[]> { - return !!feature['index'] && !!feature['columns'] -} - -function isVoiData(feature: unknown): feature is VoiFeature { - return !!feature['bbox'] -} - +import { isTabularData, isVoiData } from "../guards" type PolarPlotData = { receptor: { diff --git a/src/features/guards.ts b/src/features/guards.ts new file mode 100644 index 0000000000000000000000000000000000000000..b258c66e7fa67ccc5f008eef071e1c215fc161d4 --- /dev/null +++ b/src/features/guards.ts @@ -0,0 +1,9 @@ +import { TabularFeature, VoiFeature } from "src/atlasComponents/sapi/sxplrTypes" + +export function isTabularData(feature: unknown): feature is TabularFeature<number|string|number[]> { + return !!feature['index'] && !!feature['columns'] +} + +export function isVoiData(feature: unknown): feature is VoiFeature { + return !!feature['bbox'] +} diff --git a/src/features/list/list.component.ts b/src/features/list/list.component.ts index cc06e706efab87233eaa7f6691deb832bec1c579..52dbcc1ff85cd51e16f241687bccfdd3b449a73e 100644 --- a/src/features/list/list.component.ts +++ b/src/features/list/list.component.ts @@ -1,5 +1,5 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { BehaviorSubject, combineLatest, NEVER, Observable, of, throwError } from 'rxjs'; +import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core'; +import { BehaviorSubject, combineLatest, Observable, of, throwError } from 'rxjs'; import { catchError, switchMap, tap } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi'; import { Feature } from 'src/atlasComponents/sapi/sxplrTypes'; @@ -27,18 +27,21 @@ export class ListComponent extends FeatureBase { super() } - ngOnChanges(): void { - super.ngOnChanges() - const featureType = (this.featureRoute || '').split("/").slice(-1)[0] - this.guardedRoute$.next(AllFeatures[featureType]) + ngOnChanges(sc: SimpleChanges): void { + super.ngOnChanges(sc) + const { featureRoute } = sc + if (featureRoute) { + const featureType = (featureRoute.currentValue || '').split("/").slice(-1)[0] + this.guardedRoute$.next(AllFeatures[featureType]) + } } public features$: Observable<Feature[]> = combineLatest([ this.guardedRoute$, - this.TPR$, + this.TPRBbox$, ]).pipe( tap(() => this.state$.next('busy')), - switchMap(([route, { template, parcellation, region }]) => { + switchMap(([route, { template, parcellation, region, bbox }]) => { if (!route) { return throwError("noresult") } @@ -46,6 +49,7 @@ export class ListComponent extends FeatureBase { if (template) query['space_id'] = template.id if (parcellation) query['parcellation_id'] = parcellation.id if (region) query['region_id'] = region.name + if (bbox) query['bbox'] = JSON.stringify(bbox) return this.sapi.getV3Features(route, { query: { ...this.queryParams, diff --git a/src/features/module.ts b/src/features/module.ts index dc9843c177063c1c7f7cc0c67a7ab94404acdc2d..37712be5079fa5b1351cc0d1029efb9f0adb0bae 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -22,6 +22,7 @@ import { MatTableModule } from "@angular/material/table"; import { FeatureViewComponent } from "./feature-view/feature-view.component"; import { TransformPdToDsPipe } from "./transform-pd-to-ds.pipe"; import { NgLayerCtlModule } from "src/viewerModule/nehuba/ngLayerCtlModule/module"; +import { VoiBboxDirective } from "./voi-bbox.directive"; @NgModule({ imports: [ @@ -49,6 +50,7 @@ import { NgLayerCtlModule } from "src/viewerModule/nehuba/ngLayerCtlModule/modul FetchDirective, CategoryAccDirective, + VoiBboxDirective, FeatureNamePipe, TransformPdToDsPipe, @@ -56,6 +58,7 @@ import { NgLayerCtlModule } from "src/viewerModule/nehuba/ngLayerCtlModule/modul exports: [ EntryComponent, FeatureViewComponent, + VoiBboxDirective, ], schemas: [ CUSTOM_ELEMENTS_SCHEMA, diff --git a/src/features/voi-bbox.directive.ts b/src/features/voi-bbox.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..2f9b615613090ffc95adf0e4ea01fe17ddc0c79b --- /dev/null +++ b/src/features/voi-bbox.directive.ts @@ -0,0 +1,155 @@ +import { Directive, Inject, Input, OnDestroy, Optional } from "@angular/core"; +import { Store } from "@ngrx/store"; +import { concat, interval, of, Subject, Subscription } from "rxjs"; +import { debounce, distinctUntilChanged, filter, pairwise, take } from "rxjs/operators"; +import { AnnotationLayer, TNgAnnotationAABBox, TNgAnnotationPoint } from "src/atlasComponents/annotations"; +import { Feature, VoiFeature } from "src/atlasComponents/sapi/sxplrTypes"; +import { userInteraction } from "src/state"; +import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; +import { arrayEqual } from "src/util/array"; +import { isVoiData } from "./guards" + +@Directive({ + selector: '[voiBbox]', +}) +export class VoiBboxDirective implements OnDestroy { + + #onDestroyCb: (() => void)[] = [] + + static VOI_LAYER_NAME = 'voi-annotation-layer' + static VOI_ANNOTATION_COLOR = "#ffff00" + + #voiSubs: Subscription[] = [] + private _voiBBoxSvc: AnnotationLayer + get voiBBoxSvc(): AnnotationLayer { + if (this._voiBBoxSvc) return this._voiBBoxSvc + try { + const layer = AnnotationLayer.Get( + VoiBboxDirective.VOI_LAYER_NAME, + VoiBboxDirective.VOI_ANNOTATION_COLOR + ) + this._voiBBoxSvc = layer + this.#voiSubs.push( + layer.onHover.subscribe(val => this.handleOnHoverFeature(val || {})) + ) + this.#onDestroyCb.push(() => { + this._voiBBoxSvc.dispose() + this._voiBBoxSvc = null + }) + return layer + } catch (e) { + return null + } + } + #annotationIdToFeature = new Map<string, VoiFeature>() + #features$ = new Subject<VoiFeature[]>() + #voiFeatures: VoiFeature[] = [] + + @Input() + set features(feats: Feature[]){ + this.#voiFeatures = feats.filter(isVoiData) + this.#features$.next(this.#voiFeatures) + } + get features(): VoiFeature[]{ + return this.#voiFeatures + } + + ngOnDestroy(): void { + while (this.#onDestroyCb.length > 0) this.#onDestroyCb.pop()() + + } + + constructor( + private store: Store, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, + ){ + if (clickInterceptor) { + const { register, deregister } = clickInterceptor + const handleClick = this.handleClick.bind(this) + register(handleClick) + this.#onDestroyCb.push(() => deregister(handleClick)) + } + + const sub = concat( + of([] as VoiFeature[]), + this.#features$ + ).pipe( + distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)), + pairwise(), + debounce(() => + interval(16).pipe( + filter(() => !!this.voiBBoxSvc), + take(1), + ) + ), + ).subscribe(([ prev, curr ]) => { + for (const v of prev) { + const box = this.#pointsToAABB(v.bbox.maxpoint, v.bbox.minpoint) + const point = this.#pointToPoint(v.bbox.center) + this.#annotationIdToFeature.delete(box.id) + this.#annotationIdToFeature.delete(point.id) + if (!this.voiBBoxSvc) continue + for (const ann of [box, point]) { + this.voiBBoxSvc.removeAnnotation({ + id: ann.id + }) + } + } + for (const v of curr) { + const box = this.#pointsToAABB(v.bbox.maxpoint, v.bbox.minpoint) + const point = this.#pointToPoint(v.bbox.center) + this.#annotationIdToFeature.set(box.id, v) + this.#annotationIdToFeature.set(point.id, v) + if (!this.voiBBoxSvc) { + throw new Error(`annotation is expected to be added, but annotation layer cannot be instantiated.`) + } + for (const ann of [box, point]) { + this.voiBBoxSvc.updateAnnotation(ann) + } + } + if (this.voiBBoxSvc) this.voiBBoxSvc.setVisible(true) + }) + + this.#onDestroyCb.push(() => sub.unsubscribe()) + this.#onDestroyCb.push(() => this.store.dispatch( + userInteraction.actions.setMouseoverVoi({ feature: null }) + )) + } + + handleClick(){ + if (this.#hoveredFeat) { + this.store.dispatch( + userInteraction.actions.showFeature({ + feature: this.#hoveredFeat + }) + ) + return true + } + } + + #hoveredFeat: VoiFeature + handleOnHoverFeature(ann: { id?: string }){ + const { id } = ann || {} + const feature = this.#annotationIdToFeature.get(id) + this.#hoveredFeat = feature + this.store.dispatch( + userInteraction.actions.setMouseoverVoi({ feature }) + ) + } + + #pointsToAABB(pointA: [number, number, number], pointB: [number, number, number]): TNgAnnotationAABBox{ + return { + id: `${VoiBboxDirective.VOI_LAYER_NAME}:${JSON.stringify(pointA)}:${JSON.stringify(pointB)}`, + type: "aabbox", + pointA: pointA.map(v => v*1e6) as [number, number, number], + pointB: pointB.map(v => v*1e6) as [number, number, number], + } + } + #pointToPoint(point: [number, number, number]): TNgAnnotationPoint{ + return { + id: `${VoiBboxDirective.VOI_LAYER_NAME}:${JSON.stringify(point)}`, + point: point.map(v => v*1e6) as [number, number, number], + type: "point" + } + } +} diff --git a/src/mouseoverModule/mouseOverCvt.pipe.ts b/src/mouseoverModule/mouseOverCvt.pipe.ts index 05be3ae63bda1d1c48d6a55ae9acf1ba927fe888..35a5ca53cce95853ba60d04281f89a728e465572 100644 --- a/src/mouseoverModule/mouseOverCvt.pipe.ts +++ b/src/mouseoverModule/mouseOverCvt.pipe.ts @@ -15,24 +15,15 @@ function render<T extends keyof TOnHoverObj>(key: T, value: TOnHoverObj[T]){ } }) } - case 'landmark': { + case 'voi': + const { name } = value as TOnHoverObj['voi'] return [{ icon: { fontSet: 'fas', - fontIcon: 'fa-map-marker-alt', + fontIcon: 'fa-database' }, - text: (value as TOnHoverObj['landmark']).landmarkName + text: name }] - } - case 'userLandmark': { - return [{ - icon: { - fontSet: 'fas', - fontIcon: 'fa-map-marker-alt', - }, - text: value as TOnHoverObj['userLandmark'] - }] - } case 'annotation': { const { annotationType, name } = (value as TOnHoverObj['annotation']) let fontIcon: string diff --git a/src/mouseoverModule/mouseover.directive.ts b/src/mouseoverModule/mouseover.directive.ts index 9e005530107e7653a559fe108c1b4891f9bc7847..fc4beeb88c4eb39e9dbc2a810045d5a21c0abde1 100644 --- a/src/mouseoverModule/mouseover.directive.ts +++ b/src/mouseoverModule/mouseover.directive.ts @@ -1,11 +1,11 @@ import { Directive } from "@angular/core" import { select, Store } from "@ngrx/store" -import { merge, NEVER, Observable, of } from "rxjs" -import { distinctUntilChanged, map, scan, shareReplay } from "rxjs/operators" -import { LoggingService } from "src/logging" +import { merge, Observable } from "rxjs" +import { distinctUntilChanged, map, scan } from "rxjs/operators" import { TOnHoverObj, temporalPositveScanFn } from "./util" import { ModularUserAnnotationToolService } from "src/atlasComponents/userAnnotations/tools/service"; import { userInteraction } from "src/state" +import { arrayEqual } from "src/util/array" @Directive({ selector: '[iav-mouse-hover]', @@ -14,111 +14,50 @@ import { userInteraction } from "src/state" export class MouseHoverDirective { - public currentOnHoverObs$: Observable<TOnHoverObj> - - constructor( - private store$: Store<any>, - private log: LoggingService, - private annotSvc: ModularUserAnnotationToolService, - ) { - - // TODO consider moving these into a single obs serviced by a DI service - // can potentially net better performance - - const onHoverUserLandmark$ = NEVER - // this.store$.pipe( - // select(uiStateMouseoverUserLandmark) - // ) - - const onHoverLandmark$ = NEVER - // this.store$.pipe( - // select(uiStateMouseOverLandmarkSelector) - // ).pipe( - // map(landmark => { - // if (landmark === null) { return null } - // const idx = Number(landmark.replace('label=', '')) - // if (isNaN(idx)) { - // this.log.warn(`Landmark index could not be parsed as a number: ${landmark}`) - // return { - // landmarkName: idx, - // } - // } - // }), - // ) - - const onHoverSegments$ = this.store$.pipe( + public currentOnHoverObs$: Observable<TOnHoverObj> = merge( + this.store$.pipe( select(userInteraction.selectors.mousingOverRegions), - - // TODO fix aux mesh filtering - - // withLatestFrom( - // this.store$.pipe( - // select(viewerStateSelectedParcellationSelector), - // startWith(null as any), - // ), - // ), - // map(([ arr, parcellationSelected ]) => parcellationSelected && parcellationSelected.auxillaryMeshIndices - // ? arr.filter(({ segment }) => { - // // if segment is not a string (i.e., not labelIndexId) return true - // if (typeof segment !== 'string') { return true } - // const { label: labelIndex } = deserializeSegment(segment) - // return parcellationSelected.auxillaryMeshIndices.indexOf(labelIndex) < 0 - // }) - // : arr), - ) - - const onHoverAnnotation$ = this.annotSvc.hoveringAnnotations$ - - const mergeObs = merge( - onHoverSegments$.pipe( - distinctUntilChanged(), - map(regions => { - return { regions } - }), - ), - onHoverAnnotation$.pipe( - distinctUntilChanged(), - map(annotation => { - return { annotation } - }), - ), - onHoverLandmark$.pipe( - distinctUntilChanged(), - map(landmark => { - return { landmark } - }), - ), - onHoverUserLandmark$.pipe( - distinctUntilChanged(), - map(userLandmark => { - return { userLandmark } - }), - ), ).pipe( - shareReplay(1), + distinctUntilChanged(arrayEqual((o, n) => o.id === n.name)), + map(regions => { + return { regions } + }), + ), + this.annotSvc.hoveringAnnotations$.pipe( + distinctUntilChanged(), + map(annotation => { + return { annotation } + }), + ), + this.store$.pipe( + select(userInteraction.selectors.mousingOverVoiFeature), + distinctUntilChanged((o, n) => o?.id === n?.id), + map(voi => ({ voi })) ) - - this.currentOnHoverObs$ = mergeObs.pipe( - scan(temporalPositveScanFn, []), - map(arr => { - - let returnObj = { - regions: null, - annotation: null, - landmark: null, - userLandmark: null, + ).pipe( + scan(temporalPositveScanFn, []), + map(arr => { + + let returnObj: TOnHoverObj = { + regions: null, + annotation: null, + voi: null + } + + for (const val of arr) { + returnObj = { + ...returnObj, + ...val } + } - for (const val of arr) { - returnObj = { - ...returnObj, - ...val - } - } + return returnObj + }), + ) - return returnObj - }), - shareReplay(1), - ) + constructor( + private store$: Store<any>, + private annotSvc: ModularUserAnnotationToolService, + ) { } } diff --git a/src/mouseoverModule/util.ts b/src/mouseoverModule/util.ts index e22552b7c770804a33c4f477056a058e164963fd..208c01ceebea6038619ee5cb781799632c14d2d7 100644 --- a/src/mouseoverModule/util.ts +++ b/src/mouseoverModule/util.ts @@ -1,13 +1,10 @@ -import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes" +import { SxplrRegion, VoiFeature } from "src/atlasComponents/sapi/sxplrTypes" import { IAnnotationGeometry } from "src/atlasComponents/userAnnotations/tools/type" export type TOnHoverObj = { regions: SxplrRegion[] annotation: IAnnotationGeometry - landmark: { - landmarkName: number - } - userLandmark: any + voi: VoiFeature } /** diff --git a/src/plugin/pluginBanner/pluginBanner.component.ts b/src/plugin/pluginBanner/pluginBanner.component.ts index df682e4cfd68a3cefac4a8b9952670cdcfb49bb0..ee763c40921a60ad08b665ecbad7303ae9c6be33 100644 --- a/src/plugin/pluginBanner/pluginBanner.component.ts +++ b/src/plugin/pluginBanner/pluginBanner.component.ts @@ -1,10 +1,11 @@ import { Component, TemplateRef } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; -import { environment } from 'src/environments/environment'; import { PluginService } from "../service"; import { PluginManifest } from "../types"; import { combineLatest, Observable, Subject } from "rxjs"; import { map, scan, startWith } from "rxjs/operators"; +import { select, Store } from "@ngrx/store"; +import { userPreference } from "src/state"; @Component({ selector : 'plugin-banner', @@ -16,9 +17,13 @@ import { map, scan, startWith } from "rxjs/operators"; export class PluginBannerUI { - EXPERIMENTAL_FEATURE_FLAG = environment.EXPERIMENTAL_FEATURE_FLAG + experimentalFlag$ = this.store.pipe( + select(userPreference.selectors.showExperimental) + ) + constructor( + private store: Store, private svc: PluginService, private matDialog: MatDialog, ) { diff --git a/src/plugin/pluginBanner/pluginBanner.template.html b/src/plugin/pluginBanner/pluginBanner.template.html index 4f9ef3ae2332b37d845eddc838c5519ce787dd2d..a45098a882a6868b92dab9a212fa4a0f83b7ce3e 100644 --- a/src/plugin/pluginBanner/pluginBanner.template.html +++ b/src/plugin/pluginBanner/pluginBanner.template.html @@ -7,7 +7,7 @@ </span> </button> - <button mat-menu-item *ngIf="EXPERIMENTAL_FEATURE_FLAG" + <button mat-menu-item *ngIf="experimentalFlag$ | async" (click)="showTmpl(thirdPartyPluginTmpl)"> <span> Add third party plugin diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts index 37d6caec7354d1343031f1760f48f99124ec4409..dc8edb2c6158b6b3b24a13f0386e39547a298512 100644 --- a/src/routerModule/router.service.ts +++ b/src/routerModule/router.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@angular/core"; +import { Injectable, NgZone } from "@angular/core"; import { APP_BASE_HREF } from "@angular/common"; import { Inject } from "@angular/core"; import { NavigationEnd, Router } from '@angular/router' @@ -40,6 +40,7 @@ export class RouterService { routeToStateTransformSvc: RouteStateTransformSvc, sapi: SAPI, store$: Store<any>, + private zone: NgZone, @Inject(APP_BASE_HREF) baseHref: string ){ @@ -245,7 +246,9 @@ export class RouterService { const newUrlUrlTree = router.parseUrl(joinedRoutes) if (currUrlUrlTree.toString() !== newUrlUrlTree.toString()) { - router.navigateByUrl(joinedRoutes) + this.zone.run(() => { + router.navigateByUrl(joinedRoutes) + }) } } }) diff --git a/src/state/userInteraction/actions.ts b/src/state/userInteraction/actions.ts index e6db64e75dcad04c3d2027d0a73ef17d45ab15d5..dfdaa14298ccad2e8804460d434a4b3caf960591 100644 --- a/src/state/userInteraction/actions.ts +++ b/src/state/userInteraction/actions.ts @@ -1,6 +1,6 @@ import { createAction, props } from "@ngrx/store" import { nameSpace } from "./const" -import { SxplrRegion, Feature, Point } from "src/atlasComponents/sapi/sxplrTypes" +import { SxplrRegion, Feature, Point, VoiFeature } from "src/atlasComponents/sapi/sxplrTypes" export const mouseOverAnnotations = createAction( `${nameSpace} mouseOverAnnotations`, @@ -35,3 +35,10 @@ export const showFeature = createAction( export const clearShownFeature = createAction( `${nameSpace} clearShownFeature`, ) + +export const setMouseoverVoi = createAction( + `${nameSpace} setMouseoverVoi`, + props<{ + feature: VoiFeature + }>() +) diff --git a/src/state/userInteraction/selectors.ts b/src/state/userInteraction/selectors.ts index 0f43a1a4c56fd0188d90c4fa4fb37f491daa35f3..8596e5d25827c8aceb5636a0d55a12ba54121c5b 100644 --- a/src/state/userInteraction/selectors.ts +++ b/src/state/userInteraction/selectors.ts @@ -18,3 +18,8 @@ export const mousingOverPosition = createSelector( selectStore, state => state.mouseoverPosition ) + +export const mousingOverVoiFeature = createSelector( + selectStore, + state => state.mousedOverVoiFeature +) diff --git a/src/state/userInteraction/store.ts b/src/state/userInteraction/store.ts index f6fba659973bf863161f4fd8c79e9b0eee190db6..7c562249392e2cab90c2ceca65005169c1249a9b 100644 --- a/src/state/userInteraction/store.ts +++ b/src/state/userInteraction/store.ts @@ -1,17 +1,19 @@ import { createReducer, on } from "@ngrx/store"; -import { SxplrRegion, Feature, Point } from "src/atlasComponents/sapi/sxplrTypes"; +import { SxplrRegion, Feature, Point, VoiFeature } from "src/atlasComponents/sapi/sxplrTypes"; import * as actions from "./actions" export type UserInteraction = { mouseoverRegions: SxplrRegion[] selectedFeature: Feature mouseoverPosition: Point + mousedOverVoiFeature: VoiFeature } export const defaultState: UserInteraction = { selectedFeature: null, mouseoverRegions: [], - mouseoverPosition: null + mouseoverPosition: null, + mousedOverVoiFeature: null, } export const reducer = createReducer( @@ -51,5 +53,12 @@ export const reducer = createReducer( mouseoverPosition: position } } + ), + on( + actions.setMouseoverVoi, + (state, { feature }) => ({ + ...state, + mousedOverVoiFeature: feature + }) ) ) diff --git a/src/state/userPreference/actions.ts b/src/state/userPreference/actions.ts index ee6f8f3062bde7c4c1e3648db385d55859134f2c..05d96e5caabe5e4067ce8903358cf78c2ac5a836 100644 --- a/src/state/userPreference/actions.ts +++ b/src/state/userPreference/actions.ts @@ -36,4 +36,11 @@ export const updateCsp = createAction( name: string csp: CSP }>() -) \ No newline at end of file +) + +export const setShowExperimental = createAction( + `${nameSpace} setShowExp`, + props<{ + flag: boolean + }>() +) diff --git a/src/state/userPreference/selectors.ts b/src/state/userPreference/selectors.ts index 608264fc6ce5d007e81b53fd1fadd25ae4a16dfb..00856d0160f0af6fd391bf1c1e3b68288a981dbb 100644 --- a/src/state/userPreference/selectors.ts +++ b/src/state/userPreference/selectors.ts @@ -33,3 +33,8 @@ export const userCsp = createSelector( storeSelector, store => store.pluginCSP ) + +export const showExperimental = createSelector( + storeSelector, + store => store.showExperimental +) diff --git a/src/state/userPreference/store.ts b/src/state/userPreference/store.ts index dbf0be95561efd8992d77a54e6c33db4f6f2c7a8..2a4fa314a1999dbacca0513bea5eaf3efe877c8e 100644 --- a/src/state/userPreference/store.ts +++ b/src/state/userPreference/store.ts @@ -1,4 +1,5 @@ import { createReducer, on } from "@ngrx/store" +import { environment } from "src/environments/environment" import { COOKIE_VERSION, KG_TOS_VERSION, LOCAL_STORAGE_CONST } from "src/util/constants" import * as actions from "./actions" import { maxGpuLimit, CSP } from "./const" @@ -13,6 +14,8 @@ export type UserPreference = { agreeCookie: boolean agreeKgTos: boolean + + showExperimental: boolean } export const defaultState: UserPreference = { @@ -23,6 +26,7 @@ export const defaultState: UserPreference = { agreeCookie: localStorage.getItem(LOCAL_STORAGE_CONST.AGREE_COOKIE) === COOKIE_VERSION, agreeKgTos: localStorage.getItem(LOCAL_STORAGE_CONST.AGREE_KG_TOS) === KG_TOS_VERSION, + showExperimental: environment.EXPERIMENTAL_FEATURE_FLAG } export const reducer = createReducer( @@ -89,5 +93,12 @@ export const reducer = createReducer( } } } + ), + on( + actions.setShowExperimental, + (state, { flag }) => ({ + ...state, + showExperimental: flag + }) ) ) diff --git a/src/ui/config/configCmp/config.component.ts b/src/ui/config/configCmp/config.component.ts index 3d1dfdce600504a6fb92b684560e61adb749d26e..b3d556c87fa882ef0029e362bcab45cd552f564c 100644 --- a/src/ui/config/configCmp/config.component.ts +++ b/src/ui/config/configCmp/config.component.ts @@ -28,7 +28,14 @@ export class ConfigComponent implements OnInit, OnDestroy { public ANIMATION_TOOLTIP = ANIMATION_TOOLTIP public MOBILE_UI_TOOLTIP = MOBILE_UI_TOOLTIP - public experimentalFlag = environment.EXPERIMENTAL_FEATURE_FLAG + /** + * n.b. do not use store to set experimental flag here, since this also shows the control to toggle exp control on and off + */ + public environment = environment + + public experimentalFlag$ = this.store.pipe( + select(userPreference.selectors.showExperimental) + ) public panelModes: Record<string, userInterface.PanelMode> = { FOUR_PANEL: "FOUR_PANEL", @@ -181,5 +188,13 @@ export class ConfigComponent implements OnInit, OnDestroy { target.classList.remove('onDragOver') } + public updateExperimentalFlag(event: MatSlideToggleChange) { + this.store.dispatch( + userPreference.actions.setShowExperimental({ + flag: event.checked + }) + ) + } + public stepSize: number = 10 } diff --git a/src/ui/config/configCmp/config.template.html b/src/ui/config/configCmp/config.template.html index 85bbb90d53f68e253e52e99407245283417d4945..4982158f5bb83596ed47eeaf5425f17981666cc8 100644 --- a/src/ui/config/configCmp/config.template.html +++ b/src/ui/config/configCmp/config.template.html @@ -52,9 +52,16 @@ </mat-tab> <!-- viewer preference --> - <mat-tab *ngIf="experimentalFlag" label="Viewer Preference"> + <mat-tab *ngIf="environment.EXPERIMENTAL_FEATURE_FLAG" label="Viewer Preference"> <div class="sxplr-custom-cmp text sxplr-m-2"> + + <mat-slide-toggle + [checked]="experimentalFlag$ | async" + (change)="updateExperimentalFlag($event)"> + Experimental Flag + </mat-slide-toggle> + <div class="mat-h2"> Rearrange Viewports </div> @@ -180,7 +187,7 @@ <!-- temporarily disabling 1-3 layout --> <!-- horizontal 1 3 card --> - <!-- <button + <button class="sxplr-m-2 sxplr-p-2" mat-flat-button (click)="usePanelMode(panelModes.H_ONE_THREE)" @@ -191,10 +198,10 @@ <div class="border chunky" cell-iii></div> <div class="border chunky" cell-iv></div> </layout-horizontal-one-three> - </button> --> + </button> <!-- vertical 1 3 card --> - <!-- <button + <button class="sxplr-m-2 sxplr-p-2" mat-flat-button (click)="usePanelMode(panelModes.V_ONE_THREE)" @@ -205,7 +212,7 @@ <div class="border chunky" cell-iii></div> <div class="border chunky" cell-iv></div> </layout-vertical-one-three> - </button> --> + </button> <!-- single --> <ng-template #singlePanelTmpl> diff --git a/src/ui/topMenu/topMenuCmp/topMenu.components.ts b/src/ui/topMenu/topMenuCmp/topMenu.components.ts index c2ed29672c1caffcaf41257b8c1fa5448f6149bc..5933480bb7cf1c047d79d99557cc1853576ded52 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.components.ts +++ b/src/ui/topMenu/topMenuCmp/topMenu.components.ts @@ -12,8 +12,9 @@ import { MatDialog, MatDialogConfig, MatDialogRef } from "@angular/material/dial import { MatBottomSheet } from "@angular/material/bottom-sheet"; import { CONST, QUICKTOUR_DESC, ARIA_LABELS } from 'common/constants' import { IQuickTourData } from "src/ui/quickTour/constrants"; -import { environment } from 'src/environments/environment' import { TypeMatBtnColor, TypeMatBtnStyle } from "src/components/dynamicMaterialBtn/dynamicMaterialBtn.component"; +import { select, Store } from "@ngrx/store"; +import { userPreference } from "src/state"; @Component({ selector: 'top-menu-cmp', @@ -26,7 +27,9 @@ import { TypeMatBtnColor, TypeMatBtnStyle } from "src/components/dynamicMaterial export class TopMenuCmp { - public EXPERIMENTAL_FEATURE_FLAG = environment.EXPERIMENTAL_FEATURE_FLAG + public experimentalFlag$ = this.store.pipe( + select(userPreference.selectors.showExperimental) + ) public ARIA_LABELS = ARIA_LABELS public PINNED_DATASETS_BADGE_DESC = CONST.PINNED_DATASETS_BADGE_DESC @@ -71,6 +74,7 @@ export class TopMenuCmp { } constructor( + private store: Store, private authService: AuthService, private dialog: MatDialog, public bottomSheet: MatBottomSheet, diff --git a/src/ui/topMenu/topMenuCmp/topMenu.template.html b/src/ui/topMenu/topMenuCmp/topMenu.template.html index f652afdd663843230c4b712d3f28a35739751dfd..319528dc8c3dae585c36cef03ab3a908c2b1a720 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.template.html +++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html @@ -168,7 +168,7 @@ </button> <button mat-menu-item - *ngIf="EXPERIMENTAL_FEATURE_FLAG" + *ngIf="experimentalFlag$ | async" [disabled]="!viewerLoaded" key-frame-play-now [matTooltip]="keyFrameText"> diff --git a/src/util/injectionTokens.ts b/src/util/injectionTokens.ts index feb547176fb2e37b7fe5222e716d1cf8c257a5ce..250a8cce50b1279b6c4d63fbb3e8bcd517b96a33 100644 --- a/src/util/injectionTokens.ts +++ b/src/util/injectionTokens.ts @@ -1,12 +1,19 @@ import { InjectionToken } from "@angular/core"; import { Observable } from "rxjs"; +/** + * Inject click interceptor + */ export const CLICK_INTERCEPTOR_INJECTOR = new InjectionToken<ClickInterceptor>('CLICK_INTERCEPTOR_INJECTOR') export type TClickInterceptorConfig = { last?: boolean } +/** + * Register callbacks + * interceptorFunction may return a truthy value. If so, no futher click interceptors will be called. + */ export interface ClickInterceptor{ register: (interceptorFunction: (ev: any) => boolean, config?: TClickInterceptorConfig) => void deregister: (interceptorFunction: (ev: any) => any) => void diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 8961b4f440ae0298602daed8efd50ad01afdeb6d..fa2aa8e61b6fc05102c21fbbfbcca19ed4b974f2 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -10,11 +10,7 @@ import { ContextMenuService, TContextMenuReg } from "src/contextMenuModule"; import { DialogService } from "src/services/dialogService.service"; import { SAPI } from "src/atlasComponents/sapi"; import { Feature, SxplrAtlas, SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes" -import { atlasAppearance, atlasSelection, userInteraction } from "src/state"; - -import { environment } from "src/environments/environment" -// import { SapiViewsFeaturesVoiQuery } from "src/atlasComponents/sapiViews/features"; -import { SapiViewsCoreSpaceBoundingBox } from "src/atlasComponents/sapiViews/core"; +import { atlasAppearance, atlasSelection, userInteraction, userPreference } from "src/state"; import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; @Component({ @@ -64,18 +60,14 @@ import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; export class ViewerCmp implements OnDestroy { + public experimentalFlag$ = this.store$.pipe( + select(userPreference.selectors.showExperimental) + ) public CONST = CONST public ARIA_LABELS = ARIA_LABELS - @ViewChild('genericInfoVCR', { read: ViewContainerRef }) - genericInfoVCR: ViewContainerRef - - // @ViewChild('voiFeatures', { read: SapiViewsFeaturesVoiQuery }) - // voiQueryDirective: SapiViewsFeaturesVoiQuery - - @ViewChild('bbox', { read: SapiViewsCoreSpaceBoundingBox }) - boundingBoxDirective: SapiViewsCoreSpaceBoundingBox - + public showVoiFlag = true + public quickTourRegionSearch: IQuickTourData = { order: 7, description: QUICKTOUR_DESC.REGION_SEARCH, diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 72c4cfdac0921c2ea38b7079e3e015b48ef2baa7..63d30d9436668e34ab5ddd51b10ed538377e792c 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -29,17 +29,6 @@ </mat-list-item> - <!-- <ng-template [ngIf]="voiQueryDirective && (voiQueryDirective.onhover | async)" let-feat> - <mat-list-item> - <mat-icon - fontSet="fas" - fontIcon="fa-database" - mat-list-icon> - </mat-icon> - <div matLine>{{ feat?.metadata?.fullName || 'Feature' }}</div> - </mat-list-item> - </ng-template> --> - </mat-list> <!-- TODO Potentially implementing plugin contextual info --> </div> @@ -266,17 +255,6 @@ </div> - <!-- tab toggling hide/show of min search tray --> - <!-- <div class="tab-toggle-container d-inline-block v-align-top"> - <ng-container *ngTemplateOutlet="tabTmpl; context: { - isOpen: minTrayVisSwitch.switchState$ | async, - regionSelected: selectedRegions$ | async, - click: minTrayVisSwitch.toggle.bind(minTrayVisSwitch), - badge: voiQueryDirective && (voiQueryDirective.features$ | async).length || null - }"> - </ng-container> - </div> --> - </ng-template> @@ -942,14 +920,6 @@ </sxplr-feature-view> - <!-- TODO FIXME --> - <!-- <sxplr-sapiviews-features-entry - [sxplr-sapiviews-features-entry-atlas]="selectedAtlas$ | async" - [sxplr-sapiviews-features-entry-space]="templateSelected$ | async" - [sxplr-sapiviews-features-entry-parcellation]="parcellationSelected$ | async" - [sxplr-sapiviews-features-entry-region]="(selectedRegions$ | async)[0]" - [sxplr-sapiviews-features-entry-feature]="feature"> - </sxplr-sapiviews-features-entry> --> </ng-template> <!-- general feature tmpl --> @@ -975,61 +945,74 @@ </ng-template> +<!-- spatial search tmpls --> <ng-template #spatialFeatureListTmpl> - <sxplr-feature-entry - class="sxplr-pe-all sxplr-d-block mat-elevation-z8" - [template]="templateSelected$ | async"> - </sxplr-feature-entry> -</ng-template> - -<!-- <ng-template #spatialFeatureListViewTmpl> - <div *ngIf="voiQueryDirective && (voiQueryDirective.busy$ | async); else notBusyTmpl" class="fs-200"> - <spinner-cmp></spinner-cmp> - </div> - - <ng-template #notBusyTmpl> - <mat-card *ngIf="voiQueryDirective && (voiQueryDirective.features$ | async).length > 0" class="pe-all mat-elevation-z4"> + <mat-card class="sxplr-pe-all" + [ngClass]="{ + 'sxplr-d-none': !showVoiFlag || (voiFeatureEntryCmp.totals$ | async) === 0 + }"> + <mat-card-header> <mat-card-title> - Volumes of interest + Image Features </mat-card-title> - <mat-card-subtitle class="overflow-hidden"> - <ng-template let-bbox [ngIf]="boundingBoxDirective && (boundingBoxDirective.bbox$ | async | getProperty : 'bbox')" [ngIfElse]="bboxFallbackTmpl"> - Bounding box: {{ bbox[0] | numbers | json }} - {{ bbox[1] | numbers | json }} mm - </ng-template> - <ng-template #bboxFallbackTmpl> - Found nearby + <mat-card-subtitle> + <div> + {{ templateSelected$ | async | getProperty : '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> </ng-template> - </mat-card-subtitle> + </mat-card-header> + </mat-card> - <mat-divider></mat-divider> + <sxplr-feature-entry + [ngClass]="showVoiFlag ? 'sxplr-d-block' : 'sxplr-d-none'" + class="sxplr-pe-all mat-elevation-z8" + [template]="templateSelected$ | async" + [bbox]="bbox.bbox$ | async | getProperty : 'bbox'" + #voiFeatureEntryCmp="featureEntryCmp"> + </sxplr-feature-entry> - <ng-template [ngIf]="voiQueryDirective"> + <button mat-raised-button + [ngClass]="{ + 'sxplr-d-none': (voiFeatureEntryCmp.totals$ | async) === 0 + }" + class="sxplr-pe-all sxplr-w-100" + (click)="showVoiFlag = !showVoiFlag"> - <div *ngFor="let feature of voiQueryDirective.features$ | async" - mat-ripple - (click)="showDataset(feature)" - class="sxplr-custom-cmp hoverable w-100 overflow-hidden text-overflow-ellipses"> - {{ feature.metadata.fullName }} - </div> - </ng-template> - </mat-card> - </ng-template> -</ng-template> --> + <ng-template [ngIf]="showVoiFlag"> + <i class="fas fa-chevron-up"></i> + <span> + Collapse + </span> + </ng-template> -<!-- TODO FIXME --> -<!-- <div class="d-none" - *ngIf="VOI_QUERY_FLAG" + <ng-template [ngIf]="!showVoiFlag"> + <i class="fas fa-chevron-down"></i> + <span> + Explore {{ voiFeatureEntryCmp.totals$ | async }} spatial features + </span> + </ng-template> + </button> + <div + *ngIf="experimentalFlag$ | async" + voiBbox + [features]="voiFeatureEntryCmp.features$ | async"> + + </div> +</ng-template> + +<div sxplr-sapiviews-core-space-boundingbox [sxplr-sapiviews-core-space-boundingbox-atlas]="selectedAtlas$ | async" [sxplr-sapiviews-core-space-boundingbox-space]="templateSelected$ | async" [sxplr-sapiviews-core-space-boundingbox-spec]="viewerCtx$ | async | nehubaVCtxToBbox" - #bbox="sxplrSapiViewsCoreSpaceBoundingBox" - sxplr-sapiviews-features-voi-query - [sxplr-sapiviews-features-voi-query-atlas]="selectedAtlas$ | async" - [sxplr-sapiviews-features-voi-query-space]="templateSelected$ | async" - [sxplr-sapiviews-features-voi-query-bbox]="bbox.bbox$ | async | getProperty : 'bbox'" - (sxplr-sapiviews-features-voi-query-onclick)="showDataset($event)" - #voiFeatures="sxplrSapiViewsFeaturesVoiQuery"> - -</div> --> + #bbox="sxplrSapiViewsCoreSpaceBoundingBox"> +</div> +