diff --git a/angular.json b/angular.json index 46d2ba12824d187f38aab94428da75566e9fccee..e755712e210013183c9dd1f5eca16231c0bc9c4b 100644 --- a/angular.json +++ b/angular.json @@ -42,65 +42,80 @@ "bundleName": "vanillaMain" } ], - "scripts": [{ - "input": "worker/worker.js", - "inject": false, - "bundleName": "worker" - },{ - "input": "worker/worker-plotly.js", - "inject": false, - "bundleName": "worker-plotly" - },{ - "input": "worker/worker-nifti.js", - "inject": false, - "bundleName": "worker-nifti" - },{ - "input": "worker/worker-typedarray.js", - "inject": false, - "bundleName": "worker-typedarray" - },{ - "input": "third_party/catchSyntaxError.js", - "inject": false, - "bundleName": "catchSyntaxError" - },{ - "input": "third_party/extra_js.js", - "inject": false, - "bundleName": "extra_js" - }, - { - "input": "third_party/vanilla_nehuba.js", - "inject": false, - "bundleName": "vanilla_nehuba" - }, - - { - "input": "export-nehuba/dist/min/main.bundle.js", - "inject": false, - "bundleName": "main.bundle" - },{ - "input": "export-nehuba/dist/min/chunk_worker.bundle.js", - "inject": false, - "bundleName": "chunk_worker.bundle" - }, - { - "input": "export-nehuba/dist/min/draco.bundle.js", - "inject": false, - "bundleName": "draco.bundle" - },{ - "input": "export-nehuba/dist/min/async_computation.bundle.js", - "inject": false, - "bundleName": "async_computation.bundle" - },{ - "input": "export-nehuba/dist/min/blosc.bundle.js", - "inject": false, - "bundleName": "blosc.bundle" - }, - - { - "inject": false, - "input": "third_party/leap-0.6.4.js", - "bundleName": "leap-0.6.4" - }] + "scripts": [ + { + "input": "worker/worker.js", + "inject": false, + "bundleName": "worker" + }, + { + "input": "worker/worker-plotly.js", + "inject": false, + "bundleName": "worker-plotly" + }, + { + "input": "worker/worker-nifti.js", + "inject": false, + "bundleName": "worker-nifti" + }, + { + "input": "worker/worker-typedarray.js", + "inject": false, + "bundleName": "worker-typedarray" + }, + { + "input": "worker/worker-regionFilter.js", + "inject": false, + "bundleName": "worker-regionFilter" + }, + { + "input": "third_party/catchSyntaxError.js", + "inject": false, + "bundleName": "catchSyntaxError" + }, + { + "input": "third_party/extra_js.js", + "inject": false, + "bundleName": "extra_js" + }, + { + "input": "third_party/vanilla_nehuba.js", + "inject": false, + "bundleName": "vanilla_nehuba" + }, + + { + "input": "export-nehuba/dist/min/main.bundle.js", + "inject": false, + "bundleName": "main.bundle" + }, + { + "input": "export-nehuba/dist/min/chunk_worker.bundle.js", + "inject": false, + "bundleName": "chunk_worker.bundle" + }, + { + "input": "export-nehuba/dist/min/draco.bundle.js", + "inject": false, + "bundleName": "draco.bundle" + }, + { + "input": "export-nehuba/dist/min/async_computation.bundle.js", + "inject": false, + "bundleName": "async_computation.bundle" + }, + { + "input": "export-nehuba/dist/min/blosc.bundle.js", + "inject": false, + "bundleName": "blosc.bundle" + }, + + { + "inject": false, + "input": "third_party/leap-0.6.4.js", + "bundleName": "leap-0.6.4" + } + ] }, "configurations": { "production": { diff --git a/docs/releases/v2.14.0.md b/docs/releases/v2.14.0.md index fd75241e279355715372abcb3858f84288d6d804..e683b6817dc225c3d38e0bb1038c777a79d797c6 100644 --- a/docs/releases/v2.14.0.md +++ b/docs/releases/v2.14.0.md @@ -8,6 +8,8 @@ - experimental support for other versions of regions - experimental support for drag/drop pointcloud `.json` files - experimental support for `deepzoom://` source format +- quick search now show branches in addition to leaves +- added context in region hierarchy view ## Bugfix @@ -18,3 +20,5 @@ - minor refactoring - update to Angular 15 - migrated to Angular material 15 +- filter region now runs on worker thread. This should speed up region filter responsiveness +- quick search now show all results, rather than the first four diff --git a/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.style.css b/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.style.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0f153b70e5bfa05bdb65c2b3ca86d77df8efb94c 100644 --- a/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.style.css +++ b/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.style.css @@ -0,0 +1,11 @@ +.container +{ + display: inline-flex; + width: 100%; + align-items: center; +} + +.container .mat-body +{ + text-wrap: nowrap; +} diff --git a/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.template.html b/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.template.html index 4c833e9218409ff1ed338d4cc781dc852742fd52..5c6ff521806ea8d0ea9501b52054f878ff16968d 100644 --- a/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.template.html +++ b/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.template.html @@ -1,7 +1,7 @@ -<div *ngIf="region" matRipple [matRippleDisabled]="!ripple" class="sxplr-d-inline-flex"> +<div *ngIf="region" matRipple [matRippleDisabled]="!ripple" class="container"> <ng-content select="[prefix]"></ng-content> <span class="mat-body"> {{ region.name }} </span> <ng-content select="[suffix]"></ng-content> -</div> \ No newline at end of file +</div> diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.components.ts b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.components.ts index 2a169d93a49517e37c1bd59c6d069ed3aafce31e..da005aece81a89384a6e3b09d56d90aeb28f96c1 100644 --- a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.components.ts +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.components.ts @@ -1,9 +1,10 @@ -import { AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, OnDestroy, Output, QueryList, ViewChildren } from "@angular/core"; -import { BehaviorSubject, Subscription, combineLatest, concat, merge, of } from "rxjs"; -import { map, switchMap } from "rxjs/operators"; +import { AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, Output, QueryList, ViewChildren, inject } from "@angular/core"; +import { BehaviorSubject, combineLatest, concat, merge, of } from "rxjs"; +import { debounceTime, map, switchMap, takeUntil } from "rxjs/operators"; import { SxplrAtlas, SxplrParcellation, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; import { FilterGroupedParcellationPipe, GroupedParcellation } from "src/atlasComponents/sapiViews/core/parcellation"; import { SmartChip } from "src/components/smartChip"; +import { DestroyDirective } from "src/util/directives/destroy.directive"; export const darkThemePalette = [ "#141414", @@ -41,15 +42,21 @@ const pipe = new FilterGroupedParcellationPipe() `./pureATPSelector.style.scss` ], changeDetection: ChangeDetectionStrategy.OnPush, + hostDirectives: [ + DestroyDirective + ] }) -export class PureATPSelector implements AfterViewInit, OnDestroy{ +export class PureATPSelector implements AfterViewInit{ - #subscriptions: Subscription[] = [] + #onDestroy$ = inject(DestroyDirective).destroyed$ @Input('sxplr-pure-atp-selector-color-palette') colorPalette: string[] = darkThemePalette + @Input("sxplr-pure-atp-selector-minimized") + minimized = true + #selectedATP$ = new BehaviorSubject<ATP>(null) @Input(`sxplr-pure-atp-selector-selected-atp`) set selectedATP(val: ATP){ @@ -112,8 +119,8 @@ export class PureATPSelector implements AfterViewInit, OnDestroy{ ] const selectedIds = [atlas?.id, parcellation?.id, template?.id].filter(v => !!v) - const hideParcChip = parcAndGroup.length <= 1 - const hideTmplChip = availableTemplates?.length <= 1 + const hideParcChip = this.minimized && parcAndGroup.length <= 1 + const hideTmplChip = this.minimized && availableTemplates?.length <= 1 return { atlas, @@ -130,53 +137,48 @@ export class PureATPSelector implements AfterViewInit, OnDestroy{ ) ngAfterViewInit(): void { - this.#subscriptions.push( - concat( - of(null), - this.smartChips.changes, - ).pipe( - switchMap(() => - combineLatest( - Array.from(this.smartChips).map(chip => - concat( - of(false), - merge( - chip.menuOpened.pipe( - map(() => true) - ), - chip.menuClosed.pipe( - map(() => false) - ) + concat( + of(null), + this.smartChips.changes, + ).pipe( + switchMap(() => + combineLatest( + Array.from(this.smartChips).map(chip => + concat( + of(false), + merge( + chip.menuOpened.pipe( + map(() => true) + ), + chip.menuClosed.pipe( + map(() => false) ) ) ) ) - ), - ).subscribe(arr => { - const newVal = { - some: arr.some(val => val), - all: arr.every(val => val), - none: arr.every(val => !val), - } - this.#menuOpen$.next(newVal) - - this.menuOpen = null - if (newVal.none) { - this.menuOpen = 'none' - } - if (newVal.all) { - this.menuOpen = 'all' - } - if (newVal.some) { - this.menuOpen = 'some' - } - }) - ) - } + ) + ), + debounceTime(0), + takeUntil(this.#onDestroy$) + ).subscribe(arr => { + const newVal = { + some: arr.some(val => val), + all: arr.every(val => val), + none: arr.every(val => !val), + } + this.#menuOpen$.next(newVal) - ngOnDestroy(): void { - while (this.#subscriptions.length > 0) { - this.#subscriptions.pop().unsubscribe() - } + this.menuOpen = null + if (newVal.none) { + this.menuOpen = 'none' + } + if (newVal.all) { + this.menuOpen = 'all' + } + if (newVal.some) { + this.menuOpen = 'some' + } + }) } + } diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html index 936214c582184df9395980f04bf938963c588a78..7ad281dc612baf43381c3308bc4729426196bf8c 100644 --- a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html @@ -1,31 +1,25 @@ <ng-template [ngIf]="view$ | async" let-view> <!-- fallback for parcellation info icon --> - <ng-template [ngIf]="view.hideParcChip"> - <button mat-icon-button - sxplr-dialog - [sxplr-dialog-size]="null" - [sxplr-dialog-data]="{ - title: view.parcellation.name, - descMd: view.parcellation.desc, - actions: view.parcellation | parcTmplDoiPipe - }"> - <i class="fas fa-info"></i> - </button> - </ng-template> - - <!-- fallback for space info icon --> - <ng-template [ngIf]="view.hideTmplChip"> - <button mat-icon-button - sxplr-dialog - [sxplr-dialog-size]="null" - [sxplr-dialog-data]="{ - title: view.template.name, - descMd: view.template.desc, - actions: view.template | parcTmplDoiPipe - }"> - <i class="fas fa-info"></i> - </button> + <ng-template [ngIf]="view.hideParcChip"> + <sxplr-smart-chip + [items]="[]" + [noMenu]="true" + [color]="colorPalette[1]"> + <ng-template sxplrSmartChipContent></ng-template> + <ng-template sxplrSmartChipAction> + <button mat-icon-button + sxplr-dialog + [sxplr-dialog-size]="null" + [sxplr-dialog-data]="{ + title: view.parcellation.name, + descMd: view.parcellation.desc, + actions: view.parcellation | parcTmplDoiPipe + }"> + <i class="fas fa-info"></i> + </button> + </ng-template> + </sxplr-smart-chip> </ng-template> <!-- parcellation smart chip --> @@ -43,7 +37,7 @@ </span> <span class="sxplr-ml-1 text-muted"> - ({{ view.parcellations.length }}) + ({{ view.parcellations?.length }}) </span> </ng-template> @@ -87,6 +81,29 @@ </ng-template> </sxplr-smart-chip> + + <!-- fallback for space info icon --> + <ng-template [ngIf]="view.hideTmplChip"> + <sxplr-smart-chip + [items]="[]" + [noMenu]="true" + [color]="colorPalette[1]"> + <ng-template sxplrSmartChipContent></ng-template> + <ng-template sxplrSmartChipAction> + <button mat-icon-button + sxplr-dialog + [sxplr-dialog-size]="null" + [sxplr-dialog-data]="{ + title: view.template.name, + descMd: view.template.desc, + actions: view.template | parcTmplDoiPipe + }"> + <i class="fas fa-info"></i> + </button> + </ng-template> + </sxplr-smart-chip> + </ng-template> + <!-- space smart chip --> <sxplr-smart-chip *ngIf="!view.hideTmplChip" [items]="view.availableTemplates || []" diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts index f7655e5aff437e1b6eebcb702469e8dbc9769548..c946baa2bdc922dfa6a11ebb339edf69d12f552f 100644 --- a/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Inject, OnDestroy, Output } from "@angular/core"; +import { Component, EventEmitter, Inject, Input, OnDestroy, Output } from "@angular/core"; import { MatDialog } from "src/sharedModules/angularMaterial.exports"; import { select, Store } from "@ngrx/store"; import { Observable, Subject, Subscription } from "rxjs"; @@ -6,15 +6,10 @@ import { switchMap, withLatestFrom } from "rxjs/operators"; import { SAPI } from "src/atlasComponents/sapi/sapi.service"; import { atlasAppearance, atlasSelection } from "src/state"; import { fromRootStore } from "src/state/atlasSelection"; -import { DialogFallbackCmp } from "src/ui/dialogInfo"; import { DARKTHEME } from "src/util/injectionTokens"; import { ParcellationVisibilityService } from "../../../parcellation/parcellationVis.service"; import { darkThemePalette, lightThemePalette, ATP } from "../pureDumb/pureATPSelector.components" -type AskUserConfig = { - actionsAsList: boolean -} - @Component({ selector: 'sxplr-wrapper-atp-selector', templateUrl: './wrapper.template.html', @@ -25,6 +20,9 @@ type AskUserConfig = { export class WrapperATPSelector implements OnDestroy{ + @Input('sxplr-wrapper-atp-selector-minimized') + minimized = true + @Output('sxplr-wrapper-atp-selector-menu-open') menuOpen = new EventEmitter<{some: boolean, all: boolean, none: boolean}>() @@ -33,18 +31,6 @@ export class WrapperATPSelector implements OnDestroy{ #subscription: Subscription[] = [] - #askUser(title: string, titleMd: string, descMd: string, actions: string[], config?: Partial<AskUserConfig>): Observable<string> { - return this.dialog.open(DialogFallbackCmp, { - data: { - title, - titleMd, - descMd, - actions: actions, - actionsAsList: config?.actionsAsList - } - }).afterClosed() - } - selectedATP$ = this.store$.pipe( fromRootStore.distinctATP(), ) diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.template.html b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.template.html index 72f3b51cc9e54f3f5e0dfaffcc40bb58b955b395..f18f0077fc2b2a0f5bfd9a42f0396fd913717bd6 100644 --- a/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.template.html +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/wrapper/wrapper.template.html @@ -1,4 +1,5 @@ <sxplr-pure-atp-selector + [sxplr-pure-atp-selector-minimized]="minimized" [sxplr-pure-atp-selector-color-palette]="(darktheme$ | async) ? darkThemePalette : lightThemePalette" [sxplr-pure-atp-selector-selected-atp]="selectedATP$ | async" [sxplr-pure-atp-selector-atlases]="allAtlases$ | async" diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/filterByRegex.pipe.ts b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/filterByRegex.pipe.ts deleted file mode 100644 index 5b1aaa4d71d1c4791cb09d83dbc5b00100523502..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/filterByRegex.pipe.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; - -@Pipe({ - name : 'filterByRegex', - pure: true, -}) - -export class FilterByRegexPipe implements PipeTransform { - public transform(searchFields: string[], searchTerm: string) { - try { - return searchFields.some(searchField => new RegExp(searchTerm, 'i').test(searchField)) - } catch (e) { - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - // CC0 or MIT - return searchFields.some(searchField => new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).test(searchField)) - } - } -} diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/highlight.pipe.ts b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/highlight.pipe.ts index 73f3e3ddced7b1ab08f3c53e31e25b11f30d93d1..eca834c77d680113430d8eb35fb9928c187cc2c1 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/highlight.pipe.ts +++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/highlight.pipe.ts @@ -10,19 +10,26 @@ export class HighlightPipe implements PipeTransform { constructor(private sanitizer: DomSanitizer){} - transform(input: string, highlight: string = ''): SafeHtml { - let regex: RegExp - if (highlight === '') return input - try { - regex = new RegExp(highlight, 'i') - } catch (e) { - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - // CC0 or MIT - regex = new RegExp(highlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + transform(input: string, highlight: string = '', regexFlag=false): SafeHtml { + if (!highlight) return input + if (regexFlag) { + let regex: RegExp + try { + regex = new RegExp(highlight, 'i') + } catch (e) { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + // CC0 or MIT + regex = new RegExp(highlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + } + return this.sanitizer.sanitize( + SecurityContext.HTML, + input.replace(regex, s => `<mark>${s}</mark>`) + ) } + const regex = new RegExp(highlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i') return this.sanitizer.sanitize( SecurityContext.HTML, input.replace(regex, s => `<mark>${s}</mark>`) ) } -} \ No newline at end of file +} diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionTreeFilter.pipe.ts b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionTreeFilter.pipe.ts deleted file mode 100644 index b125458a82355e482fd9340ca3631d9aa198fded..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionTreeFilter.pipe.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; - -@Pipe({ - name : 'regionTreeFilter', - pure: true -}) - -export class RegionTreeFilterPipe implements PipeTransform { - public transform<T>(array: T[], filterFn: (item: T) => boolean, getChildren: (item: T) => T[]): T[] { - const transformSingle = (item: T): boolean => - filterFn(item) || (getChildren(item) || []).some(transformSingle) - - return array - ? array.filter(transformSingle) - : [] - } -} diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts index 0c85164a964950a7dec142e1fb047ac1e1aa9106..690706285adf8c629569dc750b818cc16fe53e48 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts +++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.component.ts @@ -1,14 +1,10 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; -import { UntypedFormControl } from "@angular/forms"; -import { Subscription } from "rxjs"; -import { debounceTime, distinctUntilChanged, filter, startWith } from "rxjs/operators"; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { BehaviorSubject, combineLatest, concat, from, of, timer } from "rxjs"; +import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from "rxjs/operators"; import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes"; import { SxplrFlatHierarchyTreeView } from "src/components/flatHierarchy/treeView/treeView.component"; -import { FilterByRegexPipe } from "./filterByRegex.pipe"; -import { RegionTreeFilterPipe } from "./regionTreeFilter.pipe"; - -const regionTreeFilterPipe = new RegionTreeFilterPipe() -const filterByRegexPipe = new FilterByRegexPipe() +import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; @Component({ selector: `sxplr-sapiviews-core-rich-regionshierarchy`, @@ -20,23 +16,13 @@ const filterByRegexPipe = new FilterByRegexPipe() }) export class SapiViewsCoreRichRegionsHierarchy { + TXT_CANNOT_BE_SELECTED = "Not mapped in this template space." static IsParent(region: SxplrRegion, parentRegion: SxplrRegion): boolean { return region.parentIds.some(id => parentRegion.id === id) } - static FilterRegions(regions: SxplrRegion[], searchTerm: string): SxplrRegion[]{ - if (searchTerm === '' || !searchTerm) { - return regions - } - return regionTreeFilterPipe.transform( - regions, - region => filterByRegexPipe.transform([ region.name ], searchTerm), - region => regions.filter(child => SapiViewsCoreRichRegionsHierarchy.IsParent(child, region)) - ) - } - @Input('sxplr-sapiviews-core-rich-regionshierarchy-label-mapped-region-names') labelMappedRegionNames: string[] = [] @@ -46,19 +32,16 @@ export class SapiViewsCoreRichRegionsHierarchy { @Input('sxplr-sapiviews-core-rich-regionshierarchy-placeholder') placeholderText: string = 'Search all regions' - passedRegions: SxplrRegion[] = [] - - private _regions: SxplrRegion[] = [] - get regions(){ - return this._regions + @Input('sxplr-sapiviews-core-rich-regionshierarchy-searchstring') + set initialSearchTerm(val: string) { + this.#initialSearchTerm.next(val) } + #initialSearchTerm = new BehaviorSubject<string>(null) + + #allAvailableRegions$ = new BehaviorSubject<SxplrRegion[]>([]) @Input('sxplr-sapiviews-core-rich-regionshierarchy-regions') set regions(val: SxplrRegion[]){ - this._regions = val - this.passedRegions = SapiViewsCoreRichRegionsHierarchy.FilterRegions( - this._regions, - this.searchTerm - ) + this.#allAvailableRegions$.next(val) } @Output('sxplr-sapiviews-core-rich-regionshierarchy-region-select') @@ -72,36 +55,53 @@ export class SapiViewsCoreRichRegionsHierarchy { isParent = SapiViewsCoreRichRegionsHierarchy.IsParent - searchFormControl = new UntypedFormControl() + searchFormControl = new FormControl<string>('') + + searchTerm$ = concat( + of(null as string), + this.#initialSearchTerm.pipe( + filter(v => !!v), + take(1), + tap(val => { + if (val) { + this.searchFormControl.setValue(val) + } + }), + takeUntil(timer(160)), + ), + this.searchFormControl.valueChanges, + ).pipe( + shareReplay(1), + ) + + #filteredRegions$ = combineLatest([ + this.searchTerm$, + this.#allAvailableRegions$, + ]).pipe( + debounceTime(320), + switchMap(([ searchTerm, regions ]) => { + return from( + this.worker.sendMessage({ + method: "FILTER_REGIONS", + param: { regions, searchTerm } + }) + ).pipe( + map(({ result }) => { + const { filteredRegions } = result + const { regions, dups } = filteredRegions as Record<string, SxplrRegion[]> + return { regions, dups } + }) + ) + }), + shareReplay(1) + ) - searchTerm: string - - constructor( - private cdr: ChangeDetectorRef - ){ - this.subs.push( - this.searchFormControl.valueChanges.pipe( - startWith(''), - distinctUntilChanged(), - debounceTime(320), - /** - * empty string should trigger search - * showing all regions - */ - filter(val => val === '' || !!val) - ).subscribe(val => { - this.searchTerm = val - this.passedRegions = SapiViewsCoreRichRegionsHierarchy.FilterRegions( - this._regions, - this.searchTerm - ) - this.cdr.markForCheck() - }) - ) - } + passedRegions$ = this.#filteredRegions$.pipe( + map(({ regions }) => regions) + ) + + constructor(private worker: AtlasWorkerService){} - private subs: Subscription[] = [] - onNodeClick({node: roi, event }: {node: SxplrRegion, event: MouseEvent}){ /** * Only allow the regions that are labelled mapped to be selected. diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html index fe58b7efcf598036e2fbae302f54720a9d0369d0..6591f3a198f35fd55ca738ef329936e5f3df8c43 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html +++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html @@ -21,12 +21,12 @@ 'sxplr-custom-cmp accent': accentedRegions | includes : region, 'muted-7': !labelMappedRegionNames.includes(region.name) }" - [innerHTML]="region.name | hightlightPipe : searchTerm"> + [innerHTML]="region.name | hightlightPipe : (searchTerm$ | async)"> </div> </ng-template> <sxplr-flat-hierarchy-tree-view - [sxplr-flat-hierarchy-nodes]="passedRegions" + [sxplr-flat-hierarchy-nodes]="passedRegions$ | async" [sxplr-flat-hierarchy-is-parent]="isParent" [sxplr-flat-hierarchy-render-node-tmpl]="tmplRef" [sxplr-flat-hierarchy-tree-view-expand-on-init]="true" diff --git a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts index 0fa6f0384a42726f0f24725ba362037a965308ce..23dd87062e1f5e9f127488f0377b5875c662de0c 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts +++ b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.component.ts @@ -1,30 +1,53 @@ -import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, HostListener, Input, Output, TemplateRef } from "@angular/core"; +import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, HostListener, Input, Output, TemplateRef, ViewChild, inject } from "@angular/core"; import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes"; import { ARIA_LABELS } from "common/constants" -import { UntypedFormControl } from "@angular/forms"; -import { debounceTime, distinctUntilChanged, map, startWith } from "rxjs/operators"; +import { FormControl } from "@angular/forms"; +import { debounceTime, distinctUntilChanged, filter, map, shareReplay, takeUntil } from "rxjs/operators"; import { MatAutocompleteSelectedEvent } from 'src/sharedModules/angularMaterial.exports' import { SapiViewsCoreRichRegionListTemplateDirective } from "./regionListSearchTmpl.directive"; -import { BehaviorSubject, combineLatest } from "rxjs"; +import { BehaviorSubject, combineLatest, concat, of } from "rxjs"; +import { DestroyDirective } from "src/util/directives/destroy.directive"; +import { MatAutocompleteTrigger } from "@angular/material/autocomplete"; const filterRegionViaSearch = (searchTerm: string) => (region:SxplrRegion) => { return region.name.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()) } +type RegionExtra = { + extra: { + showMore?: true + noneFound?: true + } +} + +function isExtra(input: unknown): input is RegionExtra{ + return !!(input['extra']) +} + +function filterGetIsNotExtra(input: SxplrRegion|string|RegionExtra): input is SxplrRegion|string{ + return !isExtra(input) +} + @Component({ selector: `sxplr-sapiviews-core-rich-regionlistsearch`, templateUrl: './regionListSearch.template.html', styleUrls: [ `./regionListSearch.style.css` ], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + exportAs: "sapiRegionListSearch", + hostDirectives: [ + DestroyDirective, + ] }) export class SapiViewsCoreRichRegionListSearch { + readonly #ondestroy$ = inject(DestroyDirective).destroyed$ + ARIA_LABELS = ARIA_LABELS - showNOptions = 4 + showNOptions = Number.POSITIVE_INFINITY #regions = new BehaviorSubject<SxplrRegion[]>([]) @Input('sxplr-sapiviews-core-rich-regionlistsearch-regions') @@ -32,11 +55,8 @@ export class SapiViewsCoreRichRegionListSearch { this.#regions.next(reg) } - #mappedRegionNames = new BehaviorSubject<string[]>([]) - @Input('sxplr-sapiviews-core-rich-regionlistsearch-mapped-region-names') - set mappedRegions(regNames: string[]) { - this.#mappedRegionNames.next(regNames) - } + @ViewChild(MatAutocompleteTrigger) + autoComplete: MatAutocompleteTrigger @ContentChild(SapiViewsCoreRichRegionListTemplateDirective) regionTmplDirective: SapiViewsCoreRichRegionListTemplateDirective @@ -53,27 +73,63 @@ export class SapiViewsCoreRichRegionListSearch { @Output('sxplr-sapiviews-core-rich-regionlistsearch-region-toggle') onRegionToggle = new EventEmitter<SxplrRegion>() - public searchFormControl = new UntypedFormControl() + @Output('sxplr-sapiviews-core-rich-regionlistsearch-region-select-extra') + onRegionShowCustomSearch = new EventEmitter<string>() + + #searchTerm: string = "" + + constructor(){ + this.searchTerm$.pipe( + takeUntil(this.#ondestroy$) + ).subscribe(searchTerm => { + if (typeof searchTerm === "string") { + this.#searchTerm = searchTerm + return + } + }) + } + + public searchFormControl = new FormControl<string|SxplrRegion|RegionExtra>(null) + + searchTerm$ = this.searchFormControl.valueChanges.pipe( + filter(filterGetIsNotExtra), + distinctUntilChanged(), + debounceTime(160), + ) + + searchTermString$ = this.searchTerm$.pipe( + map(val => { + if (typeof val === "string") { + return val + } + if (isExtra(val)) { + return null + } + return val.name + }), + filter(val => val !== null), + shareReplay(1), + ) public searchedList$ = combineLatest([ - this.searchFormControl.valueChanges.pipe( - startWith(''), - distinctUntilChanged(), - debounceTime(160), + concat( + of(''), + this.searchTerm$, ), this.#regions, - this.#mappedRegionNames ]).pipe( - map(([searchTerm, regions, mappedRegionNames]) => { + map(([searchTerm, regions]) => { + let searchString = "" if (typeof searchTerm === "string") { - return regions.filter(r => mappedRegionNames.includes(r.name)).filter(filterRegionViaSearch(searchTerm)) + searchString = searchTerm + } else { + searchString = searchTerm.name } - return [] + return regions.filter(filterRegionViaSearch(searchString)) }) ) public autocompleteList$ = this.searchedList$.pipe( - map(list => list.slice(0, this.showNOptions)) ) displayFn(region: SxplrRegion){ @@ -81,7 +137,20 @@ export class SapiViewsCoreRichRegionListSearch { } optionSelected(opt: MatAutocompleteSelectedEvent) { - const selectedRegion = opt.option.value as SxplrRegion + const selectedRegion = opt.option.value as (SxplrRegion | RegionExtra) + if (isExtra(selectedRegion)) { + if (selectedRegion.extra.noneFound) { + this.onRegionShowCustomSearch.emit("") + return + } + if (selectedRegion.extra.showMore) { + this.onRegionShowCustomSearch.emit(this.#searchTerm || "") + return + } + + return + } + if (this.ctrlFlag) { this.onRegionToggle.emit(selectedRegion) } else { @@ -100,4 +169,8 @@ export class SapiViewsCoreRichRegionListSearch { keyup(event: KeyboardEvent) { this.ctrlFlag = event.ctrlKey } + + dismissAutoComplete(){ + this.autoComplete?.closePanel() + } } diff --git a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html index fd9edae6072eb4e5ae89cfeb590cf19e3865a111..92e413e062af501ab83ee305c4c9817ea7f134e6 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html +++ b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html @@ -24,8 +24,8 @@ </mat-form-field> <mat-autocomplete - panelWidth="auto" (optionSelected)="optionSelected($event)" + [hideSingleSelectionIndicator]="true" autoActiveFirstOption #auto="matAutocomplete" [displayWith]="displayFn"> @@ -47,7 +47,9 @@ <ng-template [ngIf]="searchedList$ | async" let-searchedList> <mat-option *ngIf="searchedList.length > showNOptions" - [disabled]="true"> + [value]="{ + extra: { showMore: true } + }"> <ng-template [ngIf]="regionTmplDirective"> @@ -61,5 +63,23 @@ </ng-template> </ng-template> </mat-option> + + <mat-option *ngIf="searchedList.length === 0" + [value]="{ + extra: { noneFound: true } + }"> + + <ng-template [ngIf]="regionTmplDirective"> + + <ng-template + [ngTemplateOutlet]="regionTmplDirective.tmplRef" + [ngTemplateOutletContext]="{ + $implicit: { + name: 'None found.' + } + }"> + </ng-template> + </ng-template> + </mat-option> </ng-template> </mat-autocomplete> \ No newline at end of file diff --git a/src/components/flatHierarchy/treeView/treeView.component.ts b/src/components/flatHierarchy/treeView/treeView.component.ts index e38281e448d3efb63c66db03dea0e109cf3ce951..994a8d6188cc5388c0a0590df5523a37cc1fd043 100644 --- a/src/components/flatHierarchy/treeView/treeView.component.ts +++ b/src/components/flatHierarchy/treeView/treeView.component.ts @@ -44,7 +44,7 @@ export class SxplrFlatHierarchyTreeView<T extends Record<string, unknown>> exten ngOnChanges(changes: SimpleChanges): void { if (changes.sxplrNodes || changes.sxplrIsParent) { - this.nodes = this.sxplrNodes + this.nodes = this.sxplrNodes || [] this.isParent = this.sxplrIsParent this.dataSource.data = this.rootNodes if (this.expandOnInit) { diff --git a/src/index.html b/src/index.html index 4be80ba95d35bd074ee4421c2436e089fd04d5d8..38aa50647e838391744f198662c64f1addece15a 100644 --- a/src/index.html +++ b/src/index.html @@ -9,7 +9,6 @@ <link rel="stylesheet" href="assets/fontawesome/css/all.min.css"> <link rel="stylesheet" href="assets/academicons/css/academicons.min.css"> <link rel="stylesheet" href="icons/iav-icons.css"> - <link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="version.css"> <link rel="icon" type="image/png" href="assets/favicons/favicon-128-light.png"/> <script src="extra_js.js"></script> diff --git a/src/screenshot/screenshotCmp/screenshot.template.html b/src/screenshot/screenshotCmp/screenshot.template.html index ee570082170d036ee4429f425ce4f0d112255d79..760053de49ffc1af6835d9e2d21d02730d33389c 100644 --- a/src/screenshot/screenshotCmp/screenshot.template.html +++ b/src/screenshot/screenshotCmp/screenshot.template.html @@ -58,16 +58,12 @@ <span class="ml-1">Save</span> </a> - <button mat-stroked-button - color="default" - mat-dialog-close="try again"> + <button mat-stroked-button mat-dialog-close="try again"> <i class="fas fa-camera"></i> <span class="ml-1">Try again</span> </button> - <button mat-button - color="default" - mat-dialog-close="cancel"> + <button mat-button mat-dialog-close="cancel"> Cancel </button> </mat-dialog-actions> diff --git a/src/ui/dialogInfo/dialog.directive.ts b/src/ui/dialogInfo/dialog.directive.ts index 48d8daf656d8958bc2e9a494a4724cdd7a583b90..9eb8a06d26a277151675a7652d11c14f300e591f 100644 --- a/src/ui/dialogInfo/dialog.directive.ts +++ b/src/ui/dialogInfo/dialog.directive.ts @@ -38,18 +38,19 @@ export class DialogDirective{ size: DialogSize = 'm' @Input('sxplr-dialog-data') - data: unknown + data: any = {} constructor(private matDialog: MatDialog){} @HostListener('click') - onClick(){ + onClick(data: any={}){ const tmpl = this.templateRef instanceof TemplateRef ? this.templateRef : DialogFallbackCmp this.matDialog.open(tmpl, { - data: this.data, + autoFocus: null, + data: {...this.data, ...data}, ...(sizeDict[this.size] || {}) }) } diff --git a/src/util/includes.pipe.ts b/src/util/includes.pipe.ts index 2625cd6d3c2d0794ef5ca5d0e54b53a056617357..8b67ad8737288742ae5d3ced9159248c1e80d433 100644 --- a/src/util/includes.pipe.ts +++ b/src/util/includes.pipe.ts @@ -1,13 +1,13 @@ import { Pipe, PipeTransform } from "@angular/core"; -const defaultCompareFn = (item: any, comparator: any): boolean => item === comparator +const defaultCompareFn = <T>(item: T, comparator: T): boolean => item === comparator @Pipe({ name: 'includes', }) -export class IncludesPipe implements PipeTransform { - public transform(array: any[], item: any, compareFn = defaultCompareFn): boolean { +export class IncludesPipe<T> implements PipeTransform { + public transform(array: T[], item: T, compareFn: (a: T, b:T) => boolean = defaultCompareFn): boolean { if (!array) { return false } if (!(array instanceof Array)) { return false } return array.some(it => compareFn(it, item)) diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 6ddd87d449d29782eca4b0efd75b4faf9c0242e2..eb91a6b8b31ce641966d34374d6dbc28282cce31 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -16,6 +16,10 @@ import { EntryComponent } from "src/features/entry/entry.component"; import { TFace, TSandsPoint, getCoord } from "src/util/types"; import { wait } from "src/util/fn"; +interface HasName { + name: string +} + @Component({ selector: 'iav-cmp-viewer-container', templateUrl: './viewerCmp.template.html', @@ -162,9 +166,10 @@ export class ViewerCmp implements OnDestroy { #view1$ = combineLatest([ this.#currentMap$, + this.allAvailableRegions$, ]).pipe( - map(( [ currentMap ] ) => ({ - currentMap + map(( [ currentMap, allAvailableRegions ] ) => ({ + currentMap, allAvailableRegions })) ) @@ -173,7 +178,7 @@ export class ViewerCmp implements OnDestroy { this.#view1$, ]).pipe( map(([v0, v1]) => ({ ...v0, ...v1 })), - map(({ selectedRegions, viewerMode, selectedFeature, selectedPoint, selectedTemplate, selectedParcellation, currentMap }) => { + map(({ selectedRegions, viewerMode, selectedFeature, selectedPoint, selectedTemplate, selectedParcellation, currentMap, allAvailableRegions }) => { let spatialObjectTitle: string let spatialObjectSubtitle: string if (selectedPoint) { @@ -189,6 +194,8 @@ export class ViewerCmp implements OnDestroy { spatialObjectSubtitle = selectedTemplate.name } + const parentIds = new Set(allAvailableRegions.flatMap(v => v.parentIds)) + const labelMappedRegionNames = currentMap && Object.keys(currentMap.indices) || [] return { viewerMode, @@ -198,6 +205,9 @@ export class ViewerCmp implements OnDestroy { selectedTemplate, selectedParcellation, labelMappedRegionNames, + allAvailableRegions, + leafRegions: allAvailableRegions.filter(r => !parentIds.has(r.id)), + branchRegions: allAvailableRegions.filter(r => parentIds.has(r.id)), /** * Selected Spatial Object @@ -358,7 +368,7 @@ export class ViewerCmp implements OnDestroy { ) } - public selectRoi(roi: SxplrRegion): void { + public selectRoi(roi: SxplrRegion) { this.store$.dispatch( atlasSelection.actions.selectRegion({ region: roi @@ -519,4 +529,8 @@ export class ViewerCmp implements OnDestroy { }) ) } + + nameEql(a: HasName, b: HasName){ + return a.name === b.name + } } diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css index f1ba346a219f1b1b460a0c44c23004d0dae57bf9..13f9f9906cce4a7d44d78ea843912dd5083fb4ab 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.style.css +++ b/src/viewerModule/viewerCmp/viewerCmp.style.css @@ -175,3 +175,9 @@ sxplr-sapiviews-core-region-region-list-item margin-top: 0.5rem; margin-bottom: 0.5rem; } + +.region-list-search-row +{ + display: flex; + align-items: center; +} diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index e31ad27551e54d3d5d4d7ff1bfdc599d9edc869a..118e5c2892d7498ddb47bb3d5d740666aee18160 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -23,7 +23,6 @@ </mat-list> </div> - </div> </div> @@ -509,28 +508,6 @@ </div> </ng-template> -<!-- region-hierarchy-tmpl --> - -<ng-template #regionHierarchyTmpl> - <ng-template [ngIf]="view$ | async" let-view> - - <div class="sxplr-d-flex sxplr-flex-column sxplr-h-100"> - <sxplr-sapiviews-core-rich-regionshierarchy - class="sxplr-w-100 sxplr-flex-var" - [sxplr-sapiviews-core-rich-regionshierarchy-regions]="allAvailableRegions$ | async" - [sxplr-sapiviews-core-rich-regionshierarchy-label-mapped-region-names]="view.labelMappedRegionNames" - [sxplr-sapiviews-core-rich-regionshierarchy-accent-regions]="view.selectedRegions" - (sxplr-sapiviews-core-rich-regionshierarchy-region-select)="selectRoi($event)" - (sxplr-sapiviews-core-rich-regionshierarchy-region-toggle)="toggleRoi($event)" - > - </sxplr-sapiviews-core-rich-regionshierarchy> - - <mat-dialog-actions align="center" class="sxplr-flex-static"> - <button mat-button mat-dialog-close>Close</button> - </mat-dialog-actions> - </div> - </ng-template> -</ng-template> <!-- auto complete search box --> <ng-template #autocompleteTmpl let-showTour="showTour"> @@ -538,28 +515,35 @@ class="pe-all auto-complete-container"> <sxplr-sapiviews-core-rich-regionlistsearch class="mat-elevation-z4" - [sxplr-sapiviews-core-rich-regionlistsearch-regions]="allAvailableRegions$ | async" - [sxplr-sapiviews-core-rich-regionlistsearch-mapped-region-names]="view.labelMappedRegionNames" + [sxplr-sapiviews-core-rich-regionlistsearch-regions]="view.allAvailableRegions" [sxplr-sapiviews-core-rich-regionlistsearch-current-search]="view.selectedRegions.length === 1 ? view.selectedRegions[0].name : null" - (sxplr-sapiviews-core-rich-regionlistsearch-region-select)="selectRoi($event)" - (sxplr-sapiviews-core-rich-regionlistsearch-region-toggle)="toggleRoi($event)"> + (sxplr-sapiviews-core-rich-regionlistsearch-region-select)="view.leafRegions.includes($event) ? selectRoi($event) : showHierarchyBtn.onClick({ 'searchTerm': $event.name })" + (sxplr-sapiviews-core-rich-regionlistsearch-region-toggle)="view.leafRegions.includes($event) ? toggleRoi($event) : showHierarchyBtn.onClick({ 'searchTerm': $event.name })" + (sxplr-sapiviews-core-rich-regionlistsearch-region-select-extra)="showHierarchyBtn.onClick({ 'searchTerm': $event })" + #regionListSearch="sapiRegionListSearch"> <ng-template regionTemplate let-region> - <div class="sxplr-d-flex"> - <button - mat-icon-button - class="sxplr-mt-a sxplr-mb-a"> - <i [ngClass]="(view.selectedRegions | includes : region) ? 'fa-circle' : 'fa-none'" class="fas"></i> - </button> + <div class="region-list-search-row"> + <span> + <i [ngClass]="(view.selectedRegions | includes : region: nameEql) ? 'fa-circle' : 'fa-none'" class="fas"></i> + </span> <sxplr-sapiviews-core-region-region-list-item - [sxplr-sapiviews-core-region-region]="region"> + [sxplr-sapiviews-core-region-region]="region" + [ngClass]="{ + 'text-muted': !view.labelMappedRegionNames.includes(region.name) + }"> + <span prefix class="sxplr-m-2"> + <i *ngIf="view.leafRegions.includes(region)" class="fas fa-brain"></i> + <i *ngIf="view.branchRegions.includes(region)" class="fas fa-code-branch"></i> + </span> + </sxplr-sapiviews-core-region-region-list-item> </div> </ng-template> <button mat-icon-button search-input-suffix *ngIf="view.selectedRegions.length > 0" - (click)="clearRoi()"> + (click)="clearRoi(); regionListSearch.dismissAutoComplete()"> <i class="fas fa-times"></i> </button> <button mat-icon-button @@ -567,9 +551,38 @@ search-input-prefix iav-stop="click" [sxplr-dialog]="regionHierarchyTmpl" - sxplr-dialog-size="xl"> + sxplr-dialog-size="xl" + #showHierarchyBtn="sxplrDialog"> <i class="fas fa-sitemap"></i> </button> + + <!-- region-hierarchy-tmpl --> + + <ng-template #regionHierarchyTmpl let-data> + <ng-template [ngIf]="view$ | async" let-view> + + <div class="sxplr-d-flex sxplr-flex-column sxplr-h-100"> + <div class="sxplr-m-2"> + <sxplr-wrapper-atp-selector [sxplr-wrapper-atp-selector-minimized]="false" > + </sxplr-wrapper-atp-selector> + </div> + <sxplr-sapiviews-core-rich-regionshierarchy + class="sxplr-w-100 sxplr-flex-var" + [sxplr-sapiviews-core-rich-regionshierarchy-searchstring]="data?.searchTerm || (regionListSearch.searchTermString$ | async)" + [sxplr-sapiviews-core-rich-regionshierarchy-regions]="view.allAvailableRegions" + [sxplr-sapiviews-core-rich-regionshierarchy-label-mapped-region-names]="view.labelMappedRegionNames" + [sxplr-sapiviews-core-rich-regionshierarchy-accent-regions]="view.selectedRegions" + (sxplr-sapiviews-core-rich-regionshierarchy-region-select)="selectRoi($event)" + (sxplr-sapiviews-core-rich-regionshierarchy-region-toggle)="toggleRoi($event)" + > + </sxplr-sapiviews-core-rich-regionshierarchy> + + <mat-dialog-actions align="center" class="sxplr-flex-static"> + <button mat-button mat-dialog-close>Close</button> + </mat-dialog-actions> + </div> + </ng-template> + </ng-template> </sxplr-sapiviews-core-rich-regionlistsearch> diff --git a/third_party/vanilla.html b/third_party/vanilla.html index 5513962d41c1a6cb07e54a164e06a6bae15784f5..c86a671744ed470edbc621f572821e39d248eb78 100644 --- a/third_party/vanilla.html +++ b/third_party/vanilla.html @@ -8,7 +8,6 @@ <script src="main.bundle.js"></script> <link rel="stylesheet" href="vanilla_styles.css"> - <link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="vanillaMain.css"> </head> <body> diff --git a/worker/worker-regionFilter.js b/worker/worker-regionFilter.js new file mode 100644 index 0000000000000000000000000000000000000000..1f35a0853b71814440f376a3b3f9857f3df72d47 --- /dev/null +++ b/worker/worker-regionFilter.js @@ -0,0 +1,111 @@ +/** + * @typedef SxplrRegionPartial + * @type {object} + * @property {Array.<string>} parentIds + * @property {string} name + * @property {string} id + */ + +(function(exports){ + const FUSE = 100 + /** + * + * @param {Array.<SxplrRegionPartial>} regions + * @returns {Array.<SxplrRegionPartial>} + */ + function findDup(regions){ + return regions.filter(region => regions.filter(r => region.name === r.name).length > 1) + } + + exports.filterRegion = { + /** + * + * @param {Array.<SxplrRegionPartial>} regions + * @param {string} searchTerm + * @returns + */ + filterRegion(regions, searchTerm){ + const dups = findDup(regions) + if (!searchTerm) return {regions, dups} + + const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i') + + /** + * + * @param {SxplrRegionPartial} region + * @param {string} searchTerm + * @returns {boolean} + */ + function filterRegionName(region){ + return regex.test(region.name) + } + + /** + * + * @param {SxplrRegionPartial} parent + * @returns {Array.<SxplrRegionPartial>} + */ + function getChildren(parent){ + return regions.filter(r => r.parentIds.includes(parent.id)) + } + /** + * + * @param {SxplrRegionPartial} parent + * @returns {Array.<SxplrRegionPartial>} + */ + function getParent(child){ + return regions.filter(r => child.parentIds.includes(r.id)) + } + /** + * + * @param {SxplrRegionPartial} item + * @returns {boolean} + */ + const transformSingle = (item) => { + const allParents = [] + const allChildren = [] + let currItemParents = [item] + let currItemChildren = [item] + let breakParent = false + let breakChildren = false + let iter = 0 + // eslint-disable-next-line no-constant-condition + while (true) { + iter ++ + if (iter > FUSE || (breakParent && breakChildren)) { + break + } + if (!breakParent) { + const parents = currItemParents.map(getParent).flatMap(v => v) + if (parents.length === 0) { + breakParent = true + } + currItemParents = parents + allParents.push(...currItemParents) + } + if (!breakChildren) { + const children = currItemChildren.map(getChildren).flatMap(v => v) + if (children.length === 0) { + breakChildren = true + } + currItemChildren = children + allChildren.push(...currItemChildren) + } + } + return ( + // if self is filtered true + filterRegionName(item) + // if any children is filtered true + || allChildren.some(filterRegionName) + // if any parent is filtered true + || allParents.some(filterRegionName) + ) + } + + const filteredRegions = regions + ? regions.filter(transformSingle) + : [] + return { regions: filteredRegions, dups } + } + } +})(typeof exports === 'undefined' ? self : exports) diff --git a/worker/worker.js b/worker/worker.js index 314e0838032142545197a572ce968eb0320d7ed1..1e825881efdd18e5e3b789c34d376f9903953e4e 100644 --- a/worker/worker.js +++ b/worker/worker.js @@ -10,6 +10,7 @@ globalThis.constants = { if (typeof self.importScripts === 'function') self.importScripts('./worker-plotly.js') if (typeof self.importScripts === 'function') self.importScripts('./worker-nifti.js') if (typeof self.importScripts === 'function') self.importScripts('./worker-typedarray.js') +if (typeof self.importScripts === 'function') self.importScripts('./worker-regionFilter.js') const VALID_METHOD = { @@ -19,6 +20,7 @@ const VALID_METHOD = { PROCESS_TYPED_ARRAY_F2RGBA: `PROCESS_TYPED_ARRAY_F2RGBA`, PROCESS_TYPED_ARRAY_CM2RGBA: "PROCESS_TYPED_ARRAY_CM2RGBA", PROCESS_TYPED_ARRAY_RAW: "PROCESS_TYPED_ARRAY_RAW", + FILTER_REGIONS: "FILTER_REGIONS" } const VALID_METHODS = [ @@ -28,6 +30,7 @@ const VALID_METHODS = [ VALID_METHOD.PROCESS_TYPED_ARRAY_F2RGBA, VALID_METHOD.PROCESS_TYPED_ARRAY_CM2RGBA, VALID_METHOD.PROCESS_TYPED_ARRAY_RAW, + VALID_METHOD.FILTER_REGIONS, ] const encoder = new TextEncoder() @@ -41,8 +44,8 @@ onmessage = (message) => { if (message.data.type === 'webpackOk') return if (message.data.method && VALID_METHODS.indexOf(message.data.method) >= 0) { - const { id } = message.data - if (message.data.method === VALID_METHOD.PROCESS_PLOTLY) { + const { id, method, param } = message.data || {} + if (method === VALID_METHOD.PROCESS_PLOTLY) { try { if (plotyVtkUrl) URL.revokeObjectURL(plotyVtkUrl) const { data: plotlyData } = message.data.param @@ -71,9 +74,9 @@ onmessage = (message) => { } } - if (message.data.method === VALID_METHOD.PROCESS_NIFTI) { + if (method === VALID_METHOD.PROCESS_NIFTI) { try { - const { nifti } = message.data.param + const { nifti } = param const { meta, buffer @@ -96,9 +99,9 @@ onmessage = (message) => { }) } } - if (message.data.method === VALID_METHOD.PROCESS_TYPED_ARRAY) { + if (method === VALID_METHOD.PROCESS_TYPED_ARRAY) { try { - const { inputArray, dtype, width, height, channel } = message.data.param + const { inputArray, dtype, width, height, channel } = param const array = self.typedArray.packNpArray(inputArray, dtype, width, height, channel) postMessage({ @@ -117,9 +120,9 @@ onmessage = (message) => { }) } } - if (message.data.method === VALID_METHOD.PROCESS_TYPED_ARRAY_F2RGBA) { + if (method === VALID_METHOD.PROCESS_TYPED_ARRAY_F2RGBA) { try { - const { inputArray, width, height, channel } = message.data.param + const { inputArray, width, height, channel } = param const buffer = self.typedArray.fortranToRGBA(inputArray, width, height, channel) postMessage({ @@ -138,9 +141,9 @@ onmessage = (message) => { }) } } - if (message.data.method === VALID_METHOD.PROCESS_TYPED_ARRAY_CM2RGBA) { + if (method === VALID_METHOD.PROCESS_TYPED_ARRAY_CM2RGBA) { try { - const { inputArray, width, height, channel, dtype, processParams } = message.data.param + const { inputArray, width, height, channel, dtype, processParams } = param const { buffer, min, max } = self.typedArray.cm2rgba(inputArray, width, height, channel, dtype, processParams) postMessage({ @@ -161,9 +164,9 @@ onmessage = (message) => { }) } } - if (message.data.method === VALID_METHOD.PROCESS_TYPED_ARRAY_RAW) { + if (method === VALID_METHOD.PROCESS_TYPED_ARRAY_RAW) { try { - const { inputArray, width, height, channel, dtype, processParams } = message.data.param + const { inputArray, width, height, channel, dtype, processParams } = param const { outputArray, min, max } = self.typedArray.rawArray(inputArray, width, height, channel, dtype, processParams) postMessage({ @@ -184,6 +187,27 @@ onmessage = (message) => { }) } } + if (method === VALID_METHOD.FILTER_REGIONS) { + const { regions, searchTerm } = param + try { + const filteredRegions = self.filterRegion.filterRegion(regions, searchTerm) + + postMessage({ + id, + result: { filteredRegions } + }) + } catch (e) { + postMessage({ + id, + error: { + code: 401, + message: `filter region error: ${e.toString()}` + } + }) + } + + return + } postMessage({ id, error: {