From ac4eb8867520010ec6595ed8c9ba76937f77872b Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Fri, 5 May 2023 16:24:18 +0200 Subject: [PATCH] feat: reenable wireframe --- docs/releases/v2.10.3.md | 5 + mkdocs.yml | 1 + package.json | 2 +- src/features/category-acc.directive.ts | 39 ++++- src/features/entry/entry.component.html | 91 ----------- src/features/entry/entry.component.scss | 56 ------- src/features/entry/entry.component.ts | 36 ++++- .../entry/entry.nestedExpPanel.component.html | 144 ------------------ .../entry/entry.nestedExpPanel.component.scss | 10 -- src/features/list/list.component.html | 33 ---- src/features/list/list.component.scss | 27 ---- src/features/list/list.component.spec.ts | 27 ---- src/features/list/list.component.ts | 38 ----- src/features/module.ts | 2 - src/util/fn.ts | 43 ++++++ src/util/pullable.ts | 83 ++++++---- src/viewerModule/module.ts | 2 + .../viewerCmp/viewerCmp.component.ts | 31 +++- .../viewerCmp/viewerCmp.template.html | 13 +- 19 files changed, 213 insertions(+), 470 deletions(-) create mode 100644 docs/releases/v2.10.3.md delete mode 100644 src/features/entry/entry.component.html delete mode 100644 src/features/entry/entry.component.scss delete mode 100644 src/features/entry/entry.nestedExpPanel.component.html delete mode 100644 src/features/entry/entry.nestedExpPanel.component.scss delete mode 100644 src/features/list/list.component.html delete mode 100644 src/features/list/list.component.scss delete mode 100644 src/features/list/list.component.spec.ts delete mode 100644 src/features/list/list.component.ts diff --git a/docs/releases/v2.10.3.md b/docs/releases/v2.10.3.md new file mode 100644 index 000000000..29a5b6995 --- /dev/null +++ b/docs/releases/v2.10.3.md @@ -0,0 +1,5 @@ +# v2.10.3 + +## Bugfix + +- restore the wireframe for VOI (enable via toggle) diff --git a/mkdocs.yml b/mkdocs.yml index 004934703..1d4cdfe2e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.10.3: 'releases/v2.10.3.md' - v2.10.2: 'releases/v2.10.2.md' - v2.10.1: 'releases/v2.10.1.md' - v2.10.0: 'releases/v2.10.0.md' diff --git a/package.json b/package.json index 34add91c2..e38a7b798 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siibra-explorer", - "version": "2.10.2", + "version": "2.10.3", "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", "scripts": { "lint": "eslint src --ext .ts", diff --git a/src/features/category-acc.directive.ts b/src/features/category-acc.directive.ts index ca2b5e818..ea8afb4f1 100644 --- a/src/features/category-acc.directive.ts +++ b/src/features/category-acc.directive.ts @@ -93,6 +93,7 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy { this.#unchecked$.next(val) } + datasource: ParentDatasource<TranslatedFeature> public datasource$ = combineLatest([ this.unchecked$, this.#listCmps$, @@ -111,11 +112,46 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy { return combineLatest( filteredListCmps.map(cmp => cmp.datasource$) ).pipe( - map(dss => new ParentDatasource({ children: dss })), + map(dss => { + this.datasource = new ParentDatasource({ children: dss }) + return this.datasource + }), ) }) ) + constructor(){ + + /** + * On init, if current count is less than 50, and less than total, pull. + */ + this.#subscriptions.push( + combineLatest([ + this.total$, + this.datasource$ + ]).pipe( + switchMap(([ total, ds ]) => + ds.data$.pipe( + map(items => ({ + total, + ds, + current: items.length + })) + ) + ) + ).subscribe(async ({ total, current, ds }) => { + if (total > current && current < 50) { + try { + await ds.pull() + } catch (e) { + // if already pulling, ignore + } + } + }) + ) + } + + #subscriptions: Subscription[] = [] #changeSub: Subscription ngAfterContentInit(): void { this.#changeSub = this.listCmps.changes.subscribe(() => { @@ -130,5 +166,6 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy { ngOnDestroy(): void { if (this.#changeSub) this.#changeSub.unsubscribe() + if (this.#subscriptions.length > 0) this.#subscriptions.pop().unsubscribe() } } diff --git a/src/features/entry/entry.component.html b/src/features/entry/entry.component.html deleted file mode 100644 index ad474847c..000000000 --- a/src/features/entry/entry.component.html +++ /dev/null @@ -1,91 +0,0 @@ -<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> - - <div class="c3-outer"> - <div class="c3-inner"> - - <mat-card class="c3 mat-elevation-z4" - *ngFor="let feature of keyvalue.value" - [ngClass]="{ - 'sxplr-d-none': (list.state$ | async) === 'noresult' - }"> - <mat-card-header> - - <mat-card-title> - <span class="category-title sxplr-white-space-nowrap"> - {{ feature.name | featureNamePipe }} - </span> - </mat-card-title> - - </mat-card-header> - - - <mat-card-content> - <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-card-content> - </mat-card> - </div> - </div> - </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.component.scss b/src/features/entry/entry.component.scss deleted file mode 100644 index 666bfbb6d..000000000 --- a/src/features/entry/entry.component.scss +++ /dev/null @@ -1,56 +0,0 @@ -mat-list-item -{ - text-overflow: ellipsis; - white-space: nowrap; -} - -// card in card container -.c3-outer -{ - display: inline-block; - overflow-x: auto; - height: 15rem; - width: 100%; -} - -.c3-inner -{ - - height: 100%; - display: inline-flex; - gap: 1.5rem; - flex-wrap: nowrap; - - margin: 0 2rem; - align-items: stretch; -} - -.c3-inner > mat-card -{ - width: 16rem; - overflow:hidden; - height: 95%; -} - -.category-title:hover -{ - cursor: default; -} - -mat-card.c3 -{ - display: flex; - flex-direction: column; -} - -mat-card.c3 > mat-card-header -{ - flex: 0 0 auto; -} - -mat-card.c3 > mat-card-content -{ - flex: 1 1 auto; - height: 75%; - overflow: auto; -} \ No newline at end of file diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index abe592030..465e258ef 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -1,6 +1,6 @@ import { AfterViewInit, Component, OnDestroy, QueryList, ViewChildren } from '@angular/core'; import { select, Store } from '@ngrx/store'; -import { map, scan, switchMap, tap } from 'rxjs/operators'; +import { map, scan, shareReplay, switchMap, tap } from 'rxjs/operators'; import { IDS, SAPI } from 'src/atlasComponents/sapi'; import { Feature } from 'src/atlasComponents/sapi/sxplrTypes'; import { FeatureBase } from '../base'; @@ -8,7 +8,8 @@ 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'; -import { IsAlreadyPulling, PulledDataSource } from 'src/util/pullable'; +import { DsExhausted, IsAlreadyPulling, PulledDataSource } from 'src/util/pullable'; +import { TranslatedFeature } from '../list/list.directive'; const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => { const returnVal: Record<string, T[]> = {} @@ -32,6 +33,11 @@ const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => { }) export class EntryComponent extends FeatureBase implements AfterViewInit, OnDestroy { + private _features$ = new BehaviorSubject<TranslatedFeature[]>([]) + features$ = this._features$.pipe( + shareReplay(1) + ) + @ViewChildren(CategoryAccDirective) catAccDirs: QueryList<CategoryAccDirective> @@ -139,11 +145,35 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest try { await datasource.pull() } catch (e) { - if (e instanceof IsAlreadyPulling) { + if (e instanceof IsAlreadyPulling || e instanceof DsExhausted) { return } throw e } } } + + async pullAll(){ + const dss = Array.from(this.catAccDirs).map(catAcc => catAcc.datasource) + + this._features$.next([]) + await Promise.all( + dss.map(async ds => { + while (true) { + try { + await ds.pull() + } catch (e) { + if (e instanceof DsExhausted) { + break + } + if (e instanceof IsAlreadyPulling ) { + continue + } + throw e + } + } + }) + ) + this._features$.next(dss.flatMap(ds => ds.finalValue)) + } } diff --git a/src/features/entry/entry.nestedExpPanel.component.html b/src/features/entry/entry.nestedExpPanel.component.html deleted file mode 100644 index 9c82b50fe..000000000 --- a/src/features/entry/entry.nestedExpPanel.component.html +++ /dev/null @@ -1,144 +0,0 @@ -<mat-accordion> - <mat-expansion-panel *ngFor="let keyvalue of (cateogryCollections$ | async | keyvalue | filterCategory : ['connectivity', 'dataset', 'other'] : 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> - - - <!-- only show connectivity in human atlas for now --> - <ng-template [ngIf]="(selectedAtlas$ | async)?.species === 'Homo sapiens'"> - <ng-template [ngIf]="cateogryCollections$ | async | keyvalue | filterCategory : ['connectivity']" 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]="selectedAtlas$ | 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> - </ng-template> - - - <mat-expansion-panel *ngFor="let keyvalue of (cateogryCollections$ | async | keyvalue | filterCategory : ['dataset', 'other'])" - 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> - - -</mat-accordion> diff --git a/src/features/entry/entry.nestedExpPanel.component.scss b/src/features/entry/entry.nestedExpPanel.component.scss deleted file mode 100644 index 68a9f6d33..000000000 --- a/src/features/entry/entry.nestedExpPanel.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -mat-list-item -{ - text-overflow: ellipsis; - white-space: nowrap; -} - -sxplr-feature-list -{ - height: 10rem; -} diff --git a/src/features/list/list.component.html b/src/features/list/list.component.html deleted file mode 100644 index f8316d76c..000000000 --- a/src/features/list/list.component.html +++ /dev/null @@ -1,33 +0,0 @@ -<ng-template [ngIf]="datasource$ | async" let-datasource> - - <cdk-virtual-scroll-viewport itemSize="36" - (scrolledIndexChange)="onScroll(datasource, $event)" - class="virtual-scroll-viewport"> - - <ng-template cdkVirtualFor - [cdkVirtualForOf]="datasource" - let-feature - let-last="last" - let-index="index"> - - <button - mat-button - class="virtual-scroll-item sxplr-w-100" - [matTooltip]="feature.name" - matTooltipPosition="right" - (click)="onClickItem(feature)"> - {{ feature.name }} - </button> - - <div *ngIf="last && datasource.isPulling$ | async"> - <ng-template [ngTemplateOutlet]="loadingSpinnerTmpl"> - </ng-template> - </div> - - </ng-template> - </cdk-virtual-scroll-viewport> -</ng-template> - -<ng-template #loadingSpinnerTmpl> - <spinner-cmp class="sxplr-pl-2 sxplr-d-block"></spinner-cmp> -</ng-template> diff --git a/src/features/list/list.component.scss b/src/features/list/list.component.scss deleted file mode 100644 index ad15ab830..000000000 --- a/src/features/list/list.component.scss +++ /dev/null @@ -1,27 +0,0 @@ -:host -{ - display: block; - width: 100%; - height:100%; -} - -.feature-name -{ - white-space: nowrap; -} - -.feature-name:hover -{ - cursor: default; -} - -.virtual-scroll-viewport -{ - height: 100%; -} - -.virtual-scroll-item -{ - height:36px; - display: block; -} diff --git a/src/features/list/list.component.spec.ts b/src/features/list/list.component.spec.ts deleted file mode 100644 index dce8f1b84..000000000 --- a/src/features/list/list.component.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { SAPIModule } from 'src/atlasComponents/sapi'; - -import { ListComponent } from './list.component'; - -describe('ListComponent', () => { - let component: ListComponent; - let fixture: ComponentFixture<ListComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - SAPIModule - ], - declarations: [ ListComponent ], - }) - .compileComponents(); - - fixture = TestBed.createComponent(ListComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/features/list/list.component.ts b/src/features/list/list.component.ts deleted file mode 100644 index f19f852f5..000000000 --- a/src/features/list/list.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Component, EventEmitter, Output } from '@angular/core'; -import { SAPI } from 'src/atlasComponents/sapi'; -import { Feature } from 'src/atlasComponents/sapi/sxplrTypes'; -import { ListDirective } from './list.directive'; -import { IsAlreadyPulling, PulledDataSource } from 'src/util/pullable'; - -@Component({ - selector: 'sxplr-feature-list', - templateUrl: './list.component.html', - styleUrls: ['./list.component.scss'], - exportAs: "featureList" -}) -export class ListComponent extends ListDirective { - - @Output() - onClickFeature = new EventEmitter<Feature>() - - constructor(sapi: SAPI) { - super(sapi) - } - - onClickItem(feature: Feature){ - this.onClickFeature.emit(feature) - } - - async onScroll(datasource: PulledDataSource<unknown>, scrollIndex: number){ - if ((datasource.currentValue.length - scrollIndex) < 30) { - try { - await datasource.pull() - } catch (e) { - if (e instanceof IsAlreadyPulling) { - return - } - throw e - } - } - } -} diff --git a/src/features/module.ts b/src/features/module.ts index 1e472a8df..5cda100c6 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -9,7 +9,6 @@ import { SpinnerModule } from "src/components/spinner"; import { UtilModule } from "src/util"; import { EntryComponent } from './entry/entry.component' import { FeatureNamePipe } from "./featureName.pipe"; -import { ListComponent } from './list/list.component'; import { CategoryAccDirective } from './category-acc.directive'; import { SapiViewsFeatureConnectivityModule } from "./connectivity"; import { ScrollingModule } from "@angular/cdk/scrolling"; @@ -50,7 +49,6 @@ import { GroupFeaturesToName } from "./grpFeatToName.pipe"; ], declarations: [ EntryComponent, - ListComponent, FeatureViewComponent, FilterCategoriesPipe, ListDirective, diff --git a/src/util/fn.ts b/src/util/fn.ts index 9a4ec27a9..fd9316ab4 100644 --- a/src/util/fn.ts +++ b/src/util/fn.ts @@ -66,6 +66,49 @@ type TCacheFunctionArg = { serialization?: (...arg: any[]) => string } +/** + * member function decorator + * can only be used to decorate arguementless async function + * + */ + +export const cachedPromise = <T>() => { + const key = Symbol('cachedpromise') + return (_target: any, _propertyKey: string, descriptor: TypedPropertyDescriptor<() => Promise<T>>) => { + const originalMethod = descriptor.value + descriptor.value = function() { + if (key in this) { + /** + * if cached promise exist, return cached promise + */ + return this[key] + } + const cleanup = () => { + /** + * on cleanup, delete the stored instance + */ + delete this[key] + } + const pr = new Promise<T>((rs, rj) => { + originalMethod.apply(this, []) + .then((val: T) => { + cleanup() + rs(val) + }) + .catch((e: Error) => { + cleanup() + rj(e) + }) + }) + /** + * store the promise as a property of the instance + */ + this[key] = pr + return pr + } + } +} + /** * Member function decorator * Multiple function calls with strictly equal arguments will return cached result diff --git a/src/util/pullable.ts b/src/util/pullable.ts index 2de10d4fe..09ce6cec9 100644 --- a/src/util/pullable.ts +++ b/src/util/pullable.ts @@ -1,6 +1,7 @@ import { DataSource } from "@angular/cdk/collections" -import { BehaviorSubject, Observable, ReplaySubject, Subscription, combineLatest, concat, of, pipe } from "rxjs" +import { BehaviorSubject, Observable, ReplaySubject, Subscription, combineLatest, concat, of, timer } from "rxjs" import { finalize, map, scan, shareReplay, startWith, tap } from "rxjs/operators" +import { cachedPromise } from "./fn" export interface IPuller<T> { next: (cb: (val: T) => void) => void @@ -14,6 +15,7 @@ interface PaginatedArg<T> { } export class IsAlreadyPulling extends Error {} +export class DsExhausted extends Error {} /** @@ -25,15 +27,20 @@ export class PulledDataSource<T> extends DataSource<T> { protected annotations: Record<string, string> - protected onPulled() { - return pipe( - ) - } - #pull: () => Promise<T[]> completed = false private _data = new ReplaySubject<T[]>() + data$ = this._data.pipe( + startWith([] as T[]), + scan((acc, curr) => [...acc, ...curr]), + tap((v: T[]) => { + this.currentValue = v + }), + shareReplay(1) + ) + + currentValue: T[] = [] finalValue: T[] = [] @@ -50,10 +57,10 @@ export class PulledDataSource<T> extends DataSource<T> { return this._isPulling } - + @cachedPromise() async pull(): Promise<T[]> { if (this.completed) { - return [] + throw new DsExhausted() } if (this.isPulling) { throw new IsAlreadyPulling(`PulledDataSource is already pulling`) @@ -67,7 +74,6 @@ export class PulledDataSource<T> extends DataSource<T> { this.isPulling = false if (newResults.length === 0) { this.complete() - return [] } this._data.next(newResults) return newResults @@ -85,13 +91,7 @@ export class PulledDataSource<T> extends DataSource<T> { } connect(): Observable<readonly T[]> { - return this._data.pipe( - startWith([] as T[]), - scan((acc, curr) => [...acc, ...curr]), - tap((v: T[]) => { - this.currentValue = v - }), - ) + return this.data$ } complete() { this.completed = true @@ -106,6 +106,11 @@ export class PulledDataSource<T> extends DataSource<T> { export class ParentDatasource<T> extends PulledDataSource<T> { + private _data$ = new BehaviorSubject<T[]>([]) + data$ = this._data$.pipe( + shareReplay(1), + ) + #subscriptions: Subscription[] = [] _children: PulledDataSource<T>[] = [] constructor(arg: PaginatedArg<T>){ @@ -114,13 +119,14 @@ export class ParentDatasource<T> extends PulledDataSource<T> { this._children = children } + @cachedPromise() async pull() { for (const ds of this._children) { if (!ds.completed) { return await ds.pull() } } - return [] + throw new DsExhausted() } connect(): Observable<readonly T[]> { @@ -131,25 +137,38 @@ export class ParentDatasource<T> extends PulledDataSource<T> { this.#subscriptions.push( combineLatest(this._children.map(c => c.isPulling$)).subscribe(flags => { this.isPulling = flags.some(flag => flag) - }) - ) - - return concat( - ...this._children.map(ds => ds.connect()) - ).pipe( - map(arr => { - const alreadyEmitted = this._children.filter(c => c.completed) - const prevValues = alreadyEmitted.flatMap(v => v.finalValue) - return [...prevValues, ...arr] }), - - tap((v: T[]) => { + concat( + ...this._children.map(ds => ds.connect()), + /** + * final emitted value + * in some circumstances, all children would have been completed. + * the first synchronous empty array flushes the current value + * the second timed empty array completes the observable + * + * Observable must not be completed synchronously, as this leads to the final value not emitted. + */ + of([] as T[]).pipe( + tap(() => this.finalValue = this.currentValue) + ), + timer(160).pipe( + map(() => [] as T[]) + ) + ).pipe( + map(arr => { + const alreadyCompleted = this._children.filter(c => c.completed) + const prevValues = alreadyCompleted.flatMap(v => v.finalValue) + return [...prevValues, ...arr] + }), + finalize(() => { + this._data$.complete() + }) + ).subscribe(v => { this.currentValue = v - }), - finalize(() => { - this.finalValue = this.currentValue || [] + this._data$.next(v) }) ) + return this.data$ } disconnect(): void { diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 22a5950b8..da2c56f12 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -33,6 +33,7 @@ import { ATPSelectorModule } from "src/atlasComponents/sapiViews/core/rich/ATPSe import { FeatureModule } from "src/features"; import { NgLayerCtlModule } from "./nehuba/ngLayerCtlModule/module"; import { SmartChipModule } from "src/components/smartChip"; +import { ReactiveFormsModule } from "@angular/forms"; @NgModule({ imports: [ @@ -58,6 +59,7 @@ import { SmartChipModule } from "src/components/smartChip"; FeatureModule, NgLayerCtlModule, SmartChipModule, + ReactiveFormsModule, ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : []) ], declarations: [ diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index b19b8224e..fc1a4b87b 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { combineLatest, Observable, Subscription } from "rxjs"; -import { debounceTime, map, shareReplay, startWith } from "rxjs/operators"; +import { BehaviorSubject, combineLatest, Observable, Subscription } from "rxjs"; +import { debounceTime, map, shareReplay, startWith, switchMap } from "rxjs/operators"; import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' import { animate, state, style, transition, trigger } from "@angular/animations"; import { IQuickTourData } from "src/ui/quickTour"; @@ -12,6 +12,8 @@ import { SAPI } from "src/atlasComponents/sapi"; import { Feature, SxplrAtlas, SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes" import { atlasAppearance, atlasSelection, userInteraction } from "src/state"; import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; +import { FormControl } from "@angular/forms"; +import { EntryComponent } from "src/features/entry/entry.component"; @Component({ selector: 'iav-cmp-viewer-container', @@ -60,6 +62,9 @@ import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; export class ViewerCmp implements OnDestroy { + @ViewChild('voiFeatureEntryCmp', { read: EntryComponent }) + voiCmp: EntryComponent + public CONST = CONST public ARIA_LABELS = ARIA_LABELS @@ -250,6 +255,24 @@ export class ViewerCmp implements OnDestroy { this.onDestroyCb.push( () => this.ctxMenuSvc.deregister(cb) ) + + + this.subscriptions.push( + this.showVOIWireframeSlideToggle.valueChanges.pipe( + switchMap(showWireFrame => this.voiCmp.totals$.pipe( + map(totals => ({ + totals, + showWireFrame + })) + )) + ).subscribe(async ({ showWireFrame }) => { + if (showWireFrame) { + this._loadingVoiWireFrame$.next(true) + await this.voiCmp.pullAll() + } + this._loadingVoiWireFrame$.next(false) + }) + ) } ngOnDestroy(): void { @@ -340,4 +363,8 @@ export class ViewerCmp implements OnDestroy { }) ) } + + showVOIWireframeSlideToggle = new FormControl<boolean>(false) + private _loadingVoiWireFrame$ = new BehaviorSubject<boolean>(false) + loadingVoiWireFrame$ = this._loadingVoiWireFrame$.asObservable() } diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 19914ede1..ac8aad7a4 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -975,6 +975,13 @@ </ng-template> </mat-card-subtitle> </mat-card-header> + + <mat-slide-toggle [formControl]="showVOIWireframeSlideToggle"> + <span> + Show VOI Wireframe + </span> + <spinner-cmp class="sxplr-d-inline-block" *ngIf="loadingVoiWireFrame$ | async"></spinner-cmp> + </mat-slide-toggle> </mat-card> <sxplr-feature-entry @@ -1014,12 +1021,12 @@ this has been temporarily disabled, since datasource is paginated and how bounding boxes are drawn needs to be reconsidered --> - <!-- <div - *ngIf="voiSwitch.switchState$ | async" + <div + *ngIf="showVOIWireframeSlideToggle.valueChanges | async" voiBbox [features]="voiFeatureEntryCmp.features$ | async"> - </div> --> + </div> </ng-template> <div -- GitLab