diff --git a/deploy/csp/index.js b/deploy/csp/index.js index 1d67da419baa8f3c60bb7e93893ce5939f9d5ee2..e976dd482d2845490dd24bebef212ea1ab0aeb17 100644 --- a/deploy/csp/index.js +++ b/deploy/csp/index.js @@ -115,7 +115,7 @@ module.exports = { 'https://unpkg.com/d3@6.2.0/', // required for preview component 'https://unpkg.com/mathjax@3.1.2/', // math jax 'https://unpkg.com/three-surfer@0.0.13/dist/bundle.js', // for threeSurfer (freesurfer support in browser) - 'https://unpkg.com/ng-layer-tune@0.0.6/dist/ng-layer-tune/', // needed for ng layer control + 'https://unpkg.com/ng-layer-tune@0.0.12/dist/ng-layer-tune/', // needed for ng layer control 'https://unpkg.com/hbp-connectivity-component@0.6.6/', // needed for connectivity component (req, res) => res.locals.nonce ? `'nonce-${res.locals.nonce}'` : null, ...SCRIPT_SRC, diff --git a/docs/releases/v2.11.1.md b/docs/releases/v2.11.1.md new file mode 100644 index 0000000000000000000000000000000000000000..3c4b52dceda310dbf1790cef6ff3b245e5875ffa --- /dev/null +++ b/docs/releases/v2.11.1.md @@ -0,0 +1,20 @@ +# v2.11.1 + +## Feature + +- Allow point assignment result to be sorted +- Allow point assignment result to be downloaded as csv +- Informs user when atlas download should be occurring, and check popup blocker +- Restores Julich Brain 2.9 in Colin 27 space full mesh view + +## Bugfixes + +- Fixed point assignment full table not showing +- On template/parcellation/atlas change, clear currently selected feature + +## Behind the scenes + +- Bump siibra-api version dependency. Remove guard for feature type query restrictions +- Removed unused components +- Tweaked context menu, showing on hover effects +- now also attempts to fetch `transform.json` when external layer is populated diff --git a/mkdocs.yml b/mkdocs.yml index 0f21368640d98f4d765b121fe151f77a45ad86c5..df2d3dcc9969d727dc219a4a2450f0da70533330 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.11.1: 'releases/v2.11.1.md' - v2.11.0: 'releases/v2.11.0.md' - v2.10.3: 'releases/v2.10.3.md' - v2.10.2: 'releases/v2.10.2.md' diff --git a/package.json b/package.json index 593c06d72221ece38464f61b65e29828639ff594..9719e8ac3d423f69b3e79bc3c266675522175327 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siibra-explorer", - "version": "2.11.0", + "version": "2.11.1", "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/atlas-download/atlas-download.directive.spec.ts b/src/atlas-download/atlas-download.directive.spec.ts index e81fe2966b376e21eded2b6ec96a79a85faaf616..61c17884a4151cbf5d22cd056e8ca42fc9e242d6 100644 --- a/src/atlas-download/atlas-download.directive.spec.ts +++ b/src/atlas-download/atlas-download.directive.spec.ts @@ -3,7 +3,7 @@ import { AtlasDownloadDirective } from './atlas-download.directive'; describe('AtlasDownloadDirective', () => { it('should create an instance', () => { - const directive = new AtlasDownloadDirective(NEVER as any); + const directive = new AtlasDownloadDirective(NEVER as any, null); expect(directive).toBeTruthy(); }); }); diff --git a/src/atlas-download/atlas-download.directive.ts b/src/atlas-download/atlas-download.directive.ts index ce873301181fa39c1975848bd4a76de9b09adbf4..9b2ba98268b8f52cebec05803ca637a0d75e2353 100644 --- a/src/atlas-download/atlas-download.directive.ts +++ b/src/atlas-download/atlas-download.directive.ts @@ -1,4 +1,5 @@ import { Directive, HostListener } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; import { Store, select } from '@ngrx/store'; import { Subject, concat, of } from 'rxjs'; import { distinctUntilChanged, shareReplay, take } from 'rxjs/operators'; @@ -70,6 +71,7 @@ export class AtlasDownloadDirective { */ window.open(`${endpoint}/atlas_download/${task_id}/download`, "_blank") this.#busy$.next(false) + this.snackbar.open(`Download starting. If it has not, please check your browser's popup blocker.`, 'Dismiss') } catch (e) { this.#busy$.next(false) this.#error$.next(e.toString()) @@ -89,6 +91,6 @@ export class AtlasDownloadDirective { #error$ = new Subject<string>() error$ = this.#error$.pipe() - constructor(private store: Store<MainState>) { } + constructor(private store: Store<MainState>, private snackbar: MatSnackBar) { } } diff --git a/src/atlas-download/atlas-download.module.ts b/src/atlas-download/atlas-download.module.ts index b4181a9cc0526254f2cabd6fbe1aa7b9894acfc8..910154cb4bce732b351da2a27738d85cab6a275a 100644 --- a/src/atlas-download/atlas-download.module.ts +++ b/src/atlas-download/atlas-download.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AtlasDownloadDirective } from './atlas-download.directive'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; @NgModule({ @@ -9,6 +10,7 @@ import { AtlasDownloadDirective } from './atlas-download.directive'; ], imports: [ CommonModule, + MatSnackBarModule, ], exports: [ AtlasDownloadDirective diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index faf55163bb5607a3b9214cee6e4b6089e75cfc40..7959d63fbd7715d7be672d626e02ffaa77d68100 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -22,7 +22,7 @@ export const useViewer = { } as const export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version' -export const EXPECTED_SIIBRA_API_VERSION = '0.3.3' +export const EXPECTED_SIIBRA_API_VERSION = '0.3.4' let BS_ENDPOINT_CACHED_VALUE: Observable<string> = null @@ -515,11 +515,11 @@ export class SAPI{ const map = await this.getLabelledMap(parcellation, template) for (const regionname in map.indices) { - if (parcellation.id === IDS.PARCELLATION.CORTICAL_LAYERS) { - if (regionname.includes("left") || regionname.includes("right")) { - continue - } - } + // if (parcellation.id === IDS.PARCELLATION.CORTICAL_LAYERS) { + // if (regionname.includes("left") || regionname.includes("right")) { + // continue + // } + // } for (const { volume: volumeIdx, fragment, label } of map.indices[regionname]) { const { providedVolumes } = map.volumes[volumeIdx] if (!("neuroglancer/precomputed" in providedVolumes)) { diff --git a/src/atlasComponents/sapi/schemaV3.ts b/src/atlasComponents/sapi/schemaV3.ts index de27935f67b5a381ef1d04069f41fad1733a6b6e..e73a25736e182902ac13528a75848ee3a6f44e3c 100644 --- a/src/atlasComponents/sapi/schemaV3.ts +++ b/src/atlasComponents/sapi/schemaV3.ts @@ -81,6 +81,10 @@ export interface paths { /** Get Task Id */ get: operations["get_task_id_atlas_download__task_id__get"] } + "/atlas_download/{task_id}/download": { + /** Get Task Id */ + get: operations["get_task_id_atlas_download__task_id__download_get"] + } "/feature/_types": { /** Get All Feature Types */ get: operations["get_all_feature_types_feature__types_get"] @@ -441,12 +445,6 @@ export interface components { */ versionIdentifier: string } - /** - * ConnectivityTypes - * @description An enumeration. - * @enum {unknown} - */ - ConnectivityTypes: "FunctionalConnectivity" | "StreamlineCounts" | "StreamlineLengths" /** CoordinatePointModel */ CoordinatePointModel: { /** @Type */ @@ -494,20 +492,14 @@ export interface components { */ year: string } - /** - * CorticalProfileTypes - * @description An enumeration. - * @enum {unknown} - */ - CorticalProfileTypes: "ReceptorDensityProfile" | "CellDensityProfile" | "BigBrainIntensityProfile" /** DataFrameModel */ DataFrameModel: { /** @Type */ "@type": string /** Index */ - index: unknown[] + index: any[] /** Columns */ - columns: unknown[] + columns: any[] /** Ndim */ ndim: number /** Data */ @@ -1749,6 +1741,28 @@ export interface operations { } } } + get_task_id_atlas_download__task_id__download_get: { + /** Get Task Id */ + parameters: { + path: { + task_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record<string, never> + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } get_all_feature_types_feature__types_get: { /** Get All Feature Types */ parameters?: { @@ -1777,7 +1791,7 @@ export interface operations { parameters: { query: { parcellation_id: string - type?: components["schemas"]["ConnectivityTypes"] + type?: string page?: number size?: number } @@ -1808,7 +1822,7 @@ export interface operations { query: { parcellation_id: string subject?: string - type?: components["schemas"]["ConnectivityTypes"] + type?: string } path: { feature_id: string @@ -1835,7 +1849,7 @@ export interface operations { query: { parcellation_id: string region_id: string - type?: components["schemas"]["CorticalProfileTypes"] + type?: string page?: number size?: number } @@ -1861,7 +1875,7 @@ export interface operations { query: { parcellation_id: string region_id: string - type?: components["schemas"]["CorticalProfileTypes"] + type?: string } path: { feature_id: string diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index 79775ba84f4ca64aa8a03169373ed1049a487c92..e92c14d9fe61a46e14b7bb4be99a1541c8e39ec8 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -287,17 +287,7 @@ class TranslateV3 { segLayerSpec.layer.labelIndicies.push(label) segLayerSpec.region.push(region) } - const { ['@id']: mapId } = map for (const regionname in map.indices) { - /** - * temporary fix - * see https://github.com/FZJ-INM1-BDA/siibra-python/issues/317 - */ - if (mapId === "siibra-map-v0.0.1_bigbrain-cortical-labelled") { - if (regionname.includes("left") || regionname.includes("right")) { - continue - } - } for (const index of map.indices[regionname]) { const { volume:volumeIdx=0, fragment, label } = index if (!label) { diff --git a/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts b/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts index 72d0e2e66fc7ff0b8da0d520a6ec4438432a88df..ed1aa7731dfeda9b771a64cdadfc82236bf9eec1 100644 --- a/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts +++ b/src/atlasComponents/sapiViews/core/space/boundingBox.directive.ts @@ -1,4 +1,4 @@ -import { Directive, Input, OnChanges } from "@angular/core"; +import { Directive, Input, OnChanges, Output } from "@angular/core"; import { BehaviorSubject, Observable } from "rxjs"; import { distinctUntilChanged } from "rxjs/operators"; import { BoundingBox, SxplrTemplate, SxplrAtlas } from "src/atlasComponents/sapi/sxplrTypes" @@ -60,6 +60,7 @@ export class SapiViewsCoreSpaceBoundingBox implements OnChanges{ bbox: null }) + @Output('sxplr-sapiviews-core-space-boundingbox-changed') public bbox$: Observable<{ atlas: SxplrAtlas space: SxplrTemplate diff --git a/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.html b/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.html index 8fc0aa7b4411e39a46e1d30f200f45907eb8d1ee..57a9cf3d5031587bc37f7ac36e115319b396f500 100644 --- a/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.html +++ b/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.html @@ -1,7 +1,7 @@ <div class="sxplr-m-2" *ngIf="busy$ | async"> <spinner-cmp class="sxplr-d-inline-block"></spinner-cmp> <span> - Loading assignment ... + Performing probabilistic assignment ... </span> </div> @@ -13,9 +13,14 @@ </button> <!-- simple table --> - <table mat-table [dataSource]="df | dfToDs" class="sxplr-w-100"> + <table mat-table [dataSource]="df | dfToDs : simpleTblSort" + matSort + class="sxplr-w-100" + #simpleTblSort="matSort" + matSortActive="map value" + matSortDirection="desc"> <ng-container matColumnDef="region"> - <th mat-header-cell *matHeaderCellDef> + <th mat-header-cell *matHeaderCellDef mat-sort-header> region </th> <td mat-cell *matCellDef="let element"> @@ -25,12 +30,12 @@ </button> </td> </ng-container> - <ng-container matColumnDef="intersection over union"> - <th mat-header-cell *matHeaderCellDef> - intersection over union + <ng-container matColumnDef="map value"> + <th mat-header-cell *matHeaderCellDef mat-sort-header> + map value </th> <td mat-cell *matCellDef="let element"> - {{ element['intersection over union'] | prettyPresent }} + {{ element['map value'] | prettyPresent }} </td> </ng-container> @@ -42,10 +47,14 @@ <ng-template #datatableTmpl> <h2 mat-dialog-title>Assignment</h2> <mat-dialog-content> - <table mat-table [dataSource]="df$ | async | dfToDs"> + <table mat-table [dataSource]="df$ | async | dfToDs : comphTableSort" + matSort + #comphTableSort="matSort" + matSortActive="map value" + matSortDirection="desc"> <ng-container *ngFor="let column of columns$ | async" [matColumnDef]="column"> - <th mat-header-cell *matHeaderCellDef> + <th mat-header-cell *matHeaderCellDef mat-sort-header> {{ column }} </th> <td mat-cell *matCellDef="let element"> @@ -58,6 +67,14 @@ </table> </mat-dialog-content> <mat-dialog-actions align="center"> + <button mat-raised-button color="primary" + [zip-files-output]="zipfileConfig$ | async" + zip-files-output-zip-filename="pointassignment.zip"> + <i class="fas fa-download"></i> + <span> + Download CSV + </span> + </button> <button mat-button mat-dialog-close>Close</button> - </mat-dialog-actions> + </mat-dialog-actions> </ng-template> diff --git a/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.ts b/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.ts index 2bf281e7816015f0607b4f77122373d7097705c5..90cf9b9ed7aaca875eec919e4a718e6a6ef66898 100644 --- a/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.ts +++ b/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.ts @@ -1,12 +1,14 @@ -import { Component, Input, OnDestroy, Output, TemplateRef, EventEmitter } from '@angular/core'; +import { Component, Input, OnDestroy, Output, TemplateRef, EventEmitter, ViewChild, AfterViewInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { BehaviorSubject, EMPTY, Subscription, combineLatest, concat, of } from 'rxjs'; +import { BehaviorSubject, EMPTY, Observable, Subscription, combineLatest, concat, of } from 'rxjs'; import { map, shareReplay, switchMap, tap } from 'rxjs/operators'; -import { SAPI } from 'src/atlasComponents/sapi/sapi.service'; +import { SAPI, EXPECTED_SIIBRA_API_VERSION } from 'src/atlasComponents/sapi/sapi.service'; import { SxplrParcellation, SxplrRegion, SxplrTemplate } from 'src/atlasComponents/sapi/sxplrTypes'; import { translateV3Entities } from 'src/atlasComponents/sapi/translateV3'; import { PathReturn } from 'src/atlasComponents/sapi/typeV3'; import { TSandsPoint } from 'src/util/types'; +import { TZipFileConfig } from "src/zipFilesOutput/type" +import { environment } from "src/environments/environment" @Component({ selector: 'sxplr-point-assignment', @@ -17,7 +19,7 @@ export class PointAssignmentComponent implements OnDestroy { SIMPLE_COLUMNS = [ "region", - "intersection over union", + "map value", ] #busy$ = new BehaviorSubject(false) @@ -44,7 +46,7 @@ export class PointAssignmentComponent implements OnDestroy { @Output() clickOnRegion = new EventEmitter<{ target: SxplrRegion, event: MouseEvent }>() - df$ = combineLatest([ + df$: Observable<PathReturn<"/map/assign">> = combineLatest([ this.#point, this.#parcellation, this.#template, @@ -66,15 +68,14 @@ export class PointAssignmentComponent implements OnDestroy { parcellation_id: parcellation.id, point: point.coordinates.map(v => `${v.value/1e6}mm`).join(','), space_id: template.id, - sigma_mm: 3.0 + sigma_mm: 0 } }).pipe( tap(() => this.#busy$.next(false)), ) - ).pipe( - shareReplay(1), ) - }) + }), + shareReplay(1), ) columns$ = this.df$.pipe( @@ -95,4 +96,76 @@ export class PointAssignmentComponent implements OnDestroy { const sxplrReg = await translateV3Entities.translateRegion(region) this.clickOnRegion.emit({ target: sxplrReg, event }) } + + zipfileConfig$: Observable<TZipFileConfig[]> = combineLatest([ + this.#point, + this.#parcellation, + this.#template, + this.df$ + ]).pipe( + map(([ pt, parc, tmpl, df ]) => { + return [{ + filename: 'README.md', + filecontent: generateReadMe(pt, parc, tmpl) + }, { + filename: 'pointassignment.csv', + filecontent: generateCsv(df) + }] as TZipFileConfig[] + }) + ) +} + +function generateReadMe(pt: TSandsPoint, parc: SxplrParcellation, tmpl: SxplrTemplate){ + return `# Point assignment exporter + +Exported by siibra-explorer verison \`${environment.VERSION}\` hash: \`${environment.GIT_HASH}\`. + +On: ${new Date().toString()} + +Data retrieved through siibra-api version \`${EXPECTED_SIIBRA_API_VERSION}\` + +Retrieval parameters: + +Point +- coord: ${pt.coordinates.map(v => v.value/1e6).join(',')} mm + +Parcellation +- name: ${parc.name || parc.shortName} +- id: ${parc.id} + +Space +- name: ${tmpl.name || tmpl.shortName} +- id: ${tmpl.id} +` +} + +function escapeFactory(chars: string[] = []){ + const search = new RegExp(`[${chars.join('').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}]`, 'g') + return function escape(s: string) { + return s.replace(search, s => `\\${s}`) + } +} + +const escapeDoubleQuotes = escapeFactory(['"']) + +function processRow(v: unknown[]): string{ + const returnValue: string[] = [] + for (const item of v) { + + // region + if (typeof item === "object" && item?.['@type'] === "siibra-0.4/region") { + returnValue.push(item['name']) + continue + } + + returnValue.push(JSON.stringify(item)) + } + return returnValue.map(escapeDoubleQuotes).map(v => `"${v}"`).join(",") +} + +function generateCsv(df: PathReturn<"/map/assign">) { + return [ + df.columns.map(escapeDoubleQuotes).map(v => `"${v}"`).join(","), + ...df.data.map(processRow) + ].join("\n") } diff --git a/src/atlasComponents/sapiViews/volumes/volumes.module.ts b/src/atlasComponents/sapiViews/volumes/volumes.module.ts index eee2503b61afec1b5533c4d265ce178669ee492c..1a5a32d0f5cf07a536f3e559c29ad835ca1abb6f 100644 --- a/src/atlasComponents/sapiViews/volumes/volumes.module.ts +++ b/src/atlasComponents/sapiViews/volumes/volumes.module.ts @@ -6,6 +6,8 @@ import { UtilModule } from 'src/util'; import { SpinnerModule } from 'src/components/spinner'; import { MatDialogModule } from '@angular/material/dialog'; import { MatButtonModule } from '@angular/material/button'; +import { MatSortModule } from '@angular/material/sort'; +import { ZipFilesOutputModule } from 'src/zipFilesOutput/module'; @@ -20,6 +22,8 @@ import { MatButtonModule } from '@angular/material/button'; SpinnerModule, MatDialogModule, MatButtonModule, + MatSortModule, + ZipFilesOutputModule, ], exports: [ PointAssignmentComponent diff --git a/src/features/category-acc.directive.ts b/src/features/category-acc.directive.ts index cddba9e69912494aa8a156eb802fe2b25f76ad1e..73156e7af17e6cf4e39e97fca101655eb02b07bb 100644 --- a/src/features/category-acc.directive.ts +++ b/src/features/category-acc.directive.ts @@ -118,7 +118,8 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy { return this.datasource }), ) - }) + }), + shareReplay(1), ) constructor(){ diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts index dbc546a7ead3d44685043b5654767b8499debe28..f4a2c8e6231db8386047195264206516136d8577 100644 --- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts +++ b/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts @@ -156,13 +156,15 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { * on * - accordion update * - colormap change - * - fetching matrix + * - fetching matrix flag is true * remove custom layer */ merge( this.#accordionExpanded$, this.colormap$, - this.#fetchingMatrix$, + this.#fetchingMatrix$.pipe( + filter(flag => !!flag) + ), ).subscribe(() => { this.removeCustomLayer() }), @@ -207,6 +209,7 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { * on pure connection update, update logchecked box */ this.#pureConnections$.subscribe(v => { + if (!v) return for (const val of Object.values(v)) { if (val > 1) { this.displayForm.get("logChecked").enable() @@ -256,30 +259,32 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { } const typedName = getType(selectedType.name) - if (!guardType(typedName)) { - throw new Error(`type ${typedName} is not in ${validTypes.join(',')}`) - } const query = { parcellation_id: parc.id, type: typedName } this.busy$.next(true) - return this.sapi.v3Get( - "/feature/RegionalConnectivity", - { query } - ).pipe( - switchMap(resp => - this.sapi.iteratePages( - resp, - page => this.sapi.v3Get( - "/feature/RegionalConnectivity", - { query: { ...query, page } } - ) - ) + return concat( + of( + [] as PathReturn<"/feature/RegionalConnectivity/{feature_id}">[], ), - finalize(() => { - this.busy$.next(false) - }) + this.sapi.v3Get( + "/feature/RegionalConnectivity", + { query } + ).pipe( + switchMap(resp => + this.sapi.iteratePages( + resp, + page => this.sapi.v3Get( + "/feature/RegionalConnectivity", + { query: { ...query, page } } + ) + ) + ), + finalize(() => { + this.busy$.next(false) + }) + ) ) }) )), @@ -328,7 +333,7 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { displaySubject$ = this.selectedDataset$.pipe( distinctUntilChanged((o, n) => o?.id === n?.id), map(ds => { - return (idx: number) => ds.subjects[idx] + return (idx: number) => ds?.subjects?.[idx] }) ) @@ -336,7 +341,7 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { map(ds => ds ? ds.datasets : []) ) - #fetchingMatrix$ = new BehaviorSubject<boolean>(true) + #fetchingMatrix$ = new BehaviorSubject<boolean>(false) #matrixInput$ = combineLatest([ this.parcellation$, @@ -346,7 +351,8 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { map(([ parcellation, form, dss ]) => { const { selectedDatasetIndex: dsIdx, - selectedSubjectIndex: subIdx + selectedSubjectIndex: subIdx, + selectedView } = form const ds = dss[dsIdx] if (!ds) { @@ -360,7 +366,8 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { return { parcellation, feature_id: ds.id, - subject + subject, + selectedView } }), shareReplay(1), @@ -379,26 +386,33 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { { query: { parcellation_id: parcellation.id, - subject, + ...(input.selectedView === "average" + ? {} + : { subject }) }, path: { feature_id } } + ).pipe( + finalize(() => { + this.#fetchingMatrix$.next(false) + }) ) }), - tap(() => this.#fetchingMatrix$.next(false)), shareReplay(1), ) #pureConnections$ = this.#matrixInput$.pipe( - filter(v => !!v), - switchMap(({ subject }) => + switchMap(matrixInput => this.#selectedMatrix$.pipe( - filter(v => !!v.matrices[subject]), withLatestFrom(this.region$), map(([ v, region ]) => { - const b = v.matrices[subject] + const matrixKey = matrixInput?.selectedView === "average" ? "_average" : matrixInput?.subject + if (!v || !matrixInput || !v.matrices?.[matrixKey]) { + return null + } + const b = v.matrices[matrixKey] const foundIdx = b.columns.findIndex(v => v['name'] === region.name) if (typeof foundIdx !== 'number') { return null @@ -429,6 +443,7 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { distinctUntilChanged() ) ]).pipe( + filter(conn => !!conn), map(([ conn, flag ]) => processProfile(conn, flag)) ) )) @@ -545,12 +560,6 @@ function getType(name: string) { return name.split(".").slice(-1)[0] } -const validTypes = ["FunctionalConnectivity", "StreamlineCounts", "StreamlineLengths"] - -function guardType(name: unknown): name is "FunctionalConnectivity" | "StreamlineCounts" | "StreamlineLengths" { - return typeof name === "string" && validTypes.includes(name) -} - type ConnectedArea = { color: {r: number, g: number, b: number} name: string diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html index a7061fe7a83fedfb63c776e39deb613d1c5008dd..416763f02dab74f3dd141ab7d954d7155739a5b7 100644 --- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html +++ b/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html @@ -16,29 +16,37 @@ </mat-select> </mat-form-field> - <mat-form-field *ngIf="!(busy$ | async) && (formValue$ | async | getProperty : 'selectedType')" class="flex-grow-1 flex-shrink-1 w-100"> - <mat-label> - Cohort - </mat-label> - <mat-select formControlName="selectedCohort"> - <mat-option *ngFor="let cohort of cohorts$ | async" - [value]="cohort"> - {{ cohort }} - </mat-option> - </mat-select> - </mat-form-field> - </div> - </ng-template> + <ng-template [ngIf]="cohorts$ | async" let-cohorts> + <ng-template [ngIf]="cohorts.length > 0"> - <ng-template [ngIf]="formValue$ | async | getProperty : 'selectedCohort'"> - <mat-radio-group formControlName="selectedView"> - <mat-radio-button value="average" class="m-2" color="primary"> - Average - </mat-radio-button> - <mat-radio-button value="subject" class="m-2" color="primary"> - Subject - </mat-radio-button> - </mat-radio-group> + <mat-form-field class="flex-grow-1 flex-shrink-1 w-100"> + <mat-label> + Cohort + </mat-label> + <mat-select formControlName="selectedCohort"> + <mat-option *ngFor="let cohort of cohorts$ | async" + [value]="cohort"> + {{ cohort }} + </mat-option> + </mat-select> + </mat-form-field> + + + <ng-template [ngIf]="formValue$ | async | getProperty : 'selectedCohort'"> + <mat-radio-group formControlName="selectedView"> + <mat-radio-button value="average" class="m-2" color="primary"> + Average + </mat-radio-button> + <mat-radio-button value="subject" class="m-2" color="primary"> + Subject + </mat-radio-button> + </mat-radio-group> + </ng-template> + + </ng-template> + </ng-template> + + </div> </ng-template> <ng-template [ngIf]="cohortDatasets$ | async" let-cohortDatasets> @@ -84,7 +92,7 @@ <!-- loading spinner --> <ng-template [ngIf]="view$ | async | getProperty : 'fetchingMatrix'" [ngIfElse]="profileTmpl"> - <div class="d-flex justify-content-center" id = 'blabla'> + <div class="d-flex justify-content-center"> <mat-spinner></mat-spinner> </div> </ng-template> @@ -92,7 +100,13 @@ <!-- profile --> <!-- <pre>{{ view$ | async | json }}</pre> --> <ng-template #profileTmpl> - <ng-template [ngIf]="view$ | async | getProperty : 'connections'" let-conn> + + <ng-template #noConnTmpl> + No connectivity Found + </ng-template> + + <ng-template [ngIf]="view$ | async | getProperty : 'connections'" let-conn + [ngIfElse]="noConnTmpl"> <div class="d-flex align-items-center"> <form [formGroup]="displayForm"> <mat-checkbox diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index 979fea404ef798b8f0bfc510477bf423782938b0..722fd8e99735fb88a04ab0435c6a0bccaa760306 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -1,13 +1,13 @@ import { AfterViewInit, Component, OnDestroy, QueryList, ViewChildren } from '@angular/core'; import { select, Store } from '@ngrx/store'; -import { distinctUntilChanged, map, scan, shareReplay, switchMap, tap } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, map, scan, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators'; import { IDS, 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, concat, merge, of, Subscription } from 'rxjs'; +import { combineLatest, concat, forkJoin, merge, of, Subject, Subscription } from 'rxjs'; import { DsExhausted, IsAlreadyPulling, PulledDataSource } from 'src/util/pullable'; import { TranslatedFeature } from '../list/list.directive'; @@ -33,56 +33,63 @@ 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> - public busyTallying$ = new BehaviorSubject<boolean>(false) - public totals$ = new BehaviorSubject<number>(null) - constructor(private sapi: SAPI, private store: Store) { super() } #subscriptions: Subscription[] = [] + #catAccDirs = new Subject<CategoryAccDirective[]>() + features$ = this.#catAccDirs.pipe( + switchMap(dirs => concat( + of([] as TranslatedFeature[]), + merge(...dirs.map((dir, idx) => + dir.datasource$.pipe( + switchMap(ds => ds.data$), + map(val => ({ val, idx })) + )) + ).pipe( + map(({ idx, val }) => ({ [idx.toString()]: val })), + scan((acc, curr) => ({ ...acc, ...curr })), + map(record => Object.values(record).flatMap(v => v)) + ) + )), + shareReplay(1), + ) - private _busy$ = new BehaviorSubject<boolean>(false) - busy$ = this._busy$.pipe( + busy$ = this.#catAccDirs.pipe( + switchMap(dirs => combineLatest( + dirs.map(dir => dir.isBusy$) + )), + map(flags => flags.some(flag => flag)), + distinctUntilChanged(), shareReplay(1) ) - 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(dirs => combineLatest( - dirs.map(dir => dir.isBusy$) - )), - map(flags => flags.some(flag => flag)), - distinctUntilChanged(), - ).subscribe(value => { - this._busy$.next(value) - }), - catAccDirs$.pipe( - tap(() => this.busyTallying$.next(true)), - switchMap(catArrDirs => merge( - ...catArrDirs.map((dir, idx) => dir.total$.pipe( - map(val => ({ idx, val })) - )) - )), - + public busyTallying$ = this.#catAccDirs.pipe( + switchMap(arr => concat( + of(true), + forkJoin( + arr.map(dir => dir.total$) + ).pipe( + map(() => false) + ) + )), + shareReplay(1) + ) + + public totals$ = this.#catAccDirs.pipe( + switchMap(arr => concat( + of(0), + merge( + ...arr.map((dir, idx) => + dir.total$.pipe( + map(val => ({ val, idx })) + ) + ) + ).pipe( map(({ idx, val }) => ({ [idx.toString()]: val })), scan((acc, curr) => ({ ...acc, ...curr })), map(record => { @@ -92,11 +99,47 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest } return tally }), - tap(num => { - this.busyTallying$.next(false) - this.totals$.next(num) - }), - ).subscribe(), + ) + )) + ) + + ngOnDestroy(): void { + while (this.#subscriptions.length > 0) this.#subscriptions.pop().unsubscribe() + } + ngAfterViewInit(): void { + this.#subscriptions.push( + merge( + of(null), + this.catAccDirs.changes + ).pipe( + map(() => Array.from(this.catAccDirs)) + ).subscribe(dirs => this.#catAccDirs.next(dirs)), + + this.#pullAll.pipe( + debounceTime(320), + withLatestFrom(this.#catAccDirs), + switchMap(([_, dirs]) => combineLatest(dirs.map(dir => dir.datasource$))), + ).subscribe(async dss => { + await Promise.all( + dss.map(async ds => { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await ds.pull() + } catch (e) { + if (e instanceof DsExhausted) { + console.log('exhausted') + break + } + if (e instanceof IsAlreadyPulling ) { + continue + } + throw e + } + } + }) + ) + }) ) } @@ -167,28 +210,8 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest } } - async pullAll(){ - const dss = Array.from(this.catAccDirs).map(catAcc => catAcc.datasource) - - this._features$.next([]) - await Promise.all( - dss.map(async ds => { - // eslint-disable-next-line no-constant-condition - while (true) { - try { - await ds.pull() - } catch (e) { - if (e instanceof DsExhausted) { - break - } - if (e instanceof IsAlreadyPulling ) { - continue - } - throw e - } - } - }) - ) - this._features$.next(dss.flatMap(ds => ds.finalValue)) + #pullAll = new Subject() + pullAll(){ + this.#pullAll.next(null) } } diff --git a/src/features/voi-bbox.directive.ts b/src/features/voi-bbox.directive.ts index d6ed2e0aac4ca11816b5f214699ed9c07cc8a47d..f150225b9cf66f5ce2f68f26c7a936f1c30f6576 100644 --- a/src/features/voi-bbox.directive.ts +++ b/src/features/voi-bbox.directive.ts @@ -47,7 +47,7 @@ export class VoiBboxDirective implements OnDestroy { @Input() set features(feats: Feature[]){ - this.#voiFeatures = feats.filter(isVoiData) + this.#voiFeatures = (feats || []).filter(isVoiData) this.#features$.next(this.#voiFeatures) } get features(): VoiFeature[]{ diff --git a/src/index.html b/src/index.html index 0f009f4f2d5ab33219754015cfc136a5c8758899..c1cef937495c9ed9218e6ec90326307b07a8816e 100644 --- a/src/index.html +++ b/src/index.html @@ -14,7 +14,7 @@ <script src="extra_js.js"></script> <script src="https://unpkg.com/kg-dataset-previewer@1.2.0/dist/kg-dataset-previewer/kg-dataset-previewer.js" defer></script> <script src="https://unpkg.com/three-surfer@0.0.13/dist/bundle.js" defer></script> - <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.6/dist/ng-layer-tune/ng-layer-tune.esm.js"></script> + <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.12/dist/ng-layer-tune/ng-layer-tune.esm.js"></script> <script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.6/dist/connectivity-component/connectivity-component.js" ></script> <script defer src="https://unpkg.com/mathjax@3.1.2/es5/tex-svg.js"></script> <script defer src="https://unpkg.com/d3@6.2.0/dist/d3.min.js"></script> diff --git a/src/overwrite.scss b/src/overwrite.scss index 9fd0bb0b88e2fc3039a03040dfa659a2ec341cf8..5b17076e66aca6d569e24b3e6f5d0b6ab4c65f32 100644 --- a/src/overwrite.scss +++ b/src/overwrite.scss @@ -289,3 +289,38 @@ a[mat-raised-button] { pointer-events: none!important; } + +// list like button +$llb: "#{$nsp}-list-like-button"; +button.#{$llb} +{ + width: 100%; + + > .mat-button-wrapper + { + display: flex; + + > .#{$llb}-icon + { + width: 2rem; + flex: 0 0 auto; + margin: auto; + font-size: 150%; + } + + > .#{$llb}-body + { + flex: 1 1 0px; + display: flex; + flex-direction: column; + margin:1rem; + + .#{$llb}-body-line + { + flex: 0 0 0px; + text-align: start; + line-height: 1.5rem; + } + } + } +} diff --git a/src/state/atlasSelection/util.ts b/src/state/atlasSelection/util.ts index a76fd1f9f2e8fc45335a5b3695c65bb68322dfef..525df7f1fdc00f7528bdfbbc210a547db3b738ac 100644 --- a/src/state/atlasSelection/util.ts +++ b/src/state/atlasSelection/util.ts @@ -1,8 +1,6 @@ import { createSelector, select } from "@ngrx/store"; -import { forkJoin, of, pipe } from "rxjs"; -import { distinctUntilChanged, map, switchMap } from "rxjs/operators"; -import { SAPI } from "src/atlasComponents/sapi"; -import { translateV3Entities } from "src/atlasComponents/sapi/translateV3" +import { pipe } from "rxjs"; +import { distinctUntilChanged, map } from "rxjs/operators"; import { SxplrAtlas, SxplrParcellation, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; import { jsonEqual } from "src/util/json"; import * as selectors from "./selectors" diff --git a/src/state/index.ts b/src/state/index.ts index 40e84c234c4bca8d7eb7fece2c2c999ab249629f..12adc4b974fb893cef495e9310a5b990966478d4 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -71,6 +71,7 @@ export function getStoreEffects() { plugins.Effects, atlasSelection.Effect, userInterface.Effects, + userInteraction.Effect, ] } diff --git a/src/state/userInteraction/effects.ts b/src/state/userInteraction/effects.ts index 5d4986ad5965117705e6d1aa869407fabdbd8a65..7fd81d8ae1252522deba3c4f589eda32d61513e7 100644 --- a/src/state/userInteraction/effects.ts +++ b/src/state/userInteraction/effects.ts @@ -1,17 +1,33 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; -import * as atlasSelectionActions from "../atlasSelection/actions" import * as userInterface from "../userInterface" -import { mapTo } from "rxjs/operators"; +import * as atlasSelection from "../atlasSelection" +import * as actions from "./actions" +import { filter, map, mapTo, skip } from "rxjs/operators"; +import { Store } from "@ngrx/store"; @Injectable() export class Effect { onStandAloneVolumesExistCloseMatDrawer = createEffect(() => this.action.pipe( - ofType(atlasSelectionActions.clearStandAloneVolumes), + ofType(atlasSelection.actions.clearStandAloneVolumes), mapTo(userInterface.actions.closeSidePanel()) )) - constructor(private action: Actions){ + #distinctATP$ = this.store.pipe( + atlasSelection.fromRootStore.distinctATP(), + ) + + onATPUpdateUnselectFeature = createEffect(() => this.#distinctATP$.pipe( + filter(v => !!v.atlas && !!v.parcellation && !!v.template), + /** + * First non empty emit would be from selecting atlas. + * So ignore it. + */ + skip(1), + map(() => actions.clearShownFeature()) + )) + + constructor(private action: Actions, private store: Store){ } -} \ No newline at end of file +} diff --git a/src/ui/topMenu/topMenuCmp/topMenu.components.ts b/src/ui/topMenu/topMenuCmp/topMenu.components.ts index cf6922a353a041bd18b136ac7c1876dbc6e74c05..688da21160db6fef1fd93c1e95e17423cb627cbf 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.components.ts +++ b/src/ui/topMenu/topMenuCmp/topMenu.components.ts @@ -3,7 +3,6 @@ import { Component, Input, TemplateRef, - ViewChild, } from "@angular/core"; import { Observable } from "rxjs"; import { map } from "rxjs/operators"; diff --git a/src/ui/topMenu/topMenuCmp/topMenu.template.html b/src/ui/topMenu/topMenuCmp/topMenu.template.html index a33377a72f41a7126bd272754e3862651fd8b86a..c64c0dcee48ba00781195e882d4ffe17cd84e72d 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.template.html +++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html @@ -22,7 +22,7 @@ <!-- pinned dataset --> <div iav-fab-speed-dial-child> - <ng-container *ngTemplateOutlet="pinnedDatasetBtnTmpl"> + <ng-container *ngTemplateOutlet="downloadAllTmpl"> </ng-container> </div> @@ -53,7 +53,7 @@ </ng-container> <!-- pinned dataset --> - <ng-container *ngTemplateOutlet="pinnedDatasetBtnTmpl"> + <ng-container *ngTemplateOutlet="downloadAllTmpl"> </ng-container> <!-- help one pager --> @@ -100,7 +100,7 @@ </ng-template> <!-- pinned dataset btn --> -<ng-template #pinnedDatasetBtnTmpl> +<ng-template #downloadAllTmpl> <div class="btnWrapper" [matTooltip]="(atlasDlDct.busy$| async) ? busyTxt : idleTxt" quick-tour diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index a657e32cc68916a0b63a485f226dbdd7a05e948e..1d9c79d562f70331b06637468450d964b6d733b4 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -10,7 +10,6 @@ import { DownloadDirective } from "../util/directives/download.directive"; import { MobileOverlay } from "./nehubaContainer/mobileOverlay/mobileOverlay.component"; import { HumanReadableFileSizePipe } from "src/util/pipes/humanReadableFileSize.pipe"; import { ReorderPanelIndexPipe } from "./nehubaContainer/reorderPanelIndex.pipe"; -import { FixedMouseContextualContainerDirective } from "src/util/directives/FixedMouseContextualContainerDirective.directive"; import { ShareModule } from "src/share"; import { AuthModule } from "src/auth"; import { ActionDialog } from "./actionDialog/actionDialog.component"; @@ -41,7 +40,6 @@ import { HANDLE_SCREENSHOT_PROMISE, TypeHandleScrnShotPromise } from "../screens ReorderPanelIndexPipe, /* directive */ DownloadDirective, - FixedMouseContextualContainerDirective, ], providers: [ { @@ -104,7 +102,6 @@ import { HANDLE_SCREENSHOT_PROMISE, TypeHandleScrnShotPromise } from "../screens // NehubaContainer, MobileOverlay, // StatusCardComponent, - FixedMouseContextualContainerDirective, ] }) diff --git a/src/util/df-to-ds.pipe.ts b/src/util/df-to-ds.pipe.ts index 612b8b48220cb6463458d21bd63f5f5c2eb60e7b..f50be028d82a3522df9277be89ef75b59accd33d 100644 --- a/src/util/df-to-ds.pipe.ts +++ b/src/util/df-to-ds.pipe.ts @@ -1,5 +1,7 @@ import { CdkTableDataSourceInput } from '@angular/cdk/table'; import { Pipe, PipeTransform } from '@angular/core'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; import { components } from "src/atlasComponents/sapi/schemaV3" type DF = components["schemas"]["DataFrameModel"] @@ -19,11 +21,11 @@ function isDf(val: object): val is DF { }) export class DfToDsPipe implements PipeTransform { - transform(df: object): CdkTableDataSourceInput<unknown> { + transform(df: object, sort: MatSort): CdkTableDataSourceInput<unknown> { if (!isDf(df)) { return null } - return df.data.map((arr, idx) => { + const v = df.data.map((arr, idx) => { const val = df.index[idx] as any const returnVal: Record<string, string|number|number[]> = { index: val, @@ -37,6 +39,9 @@ export class DfToDsPipe implements PipeTransform { }) return returnVal }) + const ds = new MatTableDataSource(v) + ds.sort = sort + return ds } } diff --git a/src/util/directives/FixedMouseContextualContainerDirective.directive.spec.ts b/src/util/directives/FixedMouseContextualContainerDirective.directive.spec.ts deleted file mode 100644 index 0cf11aeb387d08a01a89f616e7095cc716fb63c5..0000000000000000000000000000000000000000 --- a/src/util/directives/FixedMouseContextualContainerDirective.directive.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Component, ViewChild } from "@angular/core"; -import { TestBed } from "@angular/core/testing"; -import { FixedMouseContextualContainerDirective } from "./FixedMouseContextualContainerDirective.directive"; -import { By } from "@angular/platform-browser"; - -@Component({ - template: '' -}) - -class TestCmp{ - @ViewChild(FixedMouseContextualContainerDirective) directive: FixedMouseContextualContainerDirective -} - -describe('FixedMouseContextualContainerDirective', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - - ], - declarations: [ - TestCmp, - FixedMouseContextualContainerDirective - ] - }) - }) - - it('> can instantiate directive properly', () => { - TestBed.overrideComponent(TestCmp, { - set: { - template: ` - <div fixedMouseContextualContainerDirective> - </div> - ` - } - }).compileComponents() - - const fixture = TestBed.createComponent(TestCmp) - fixture.detectChanges() - const directive = fixture.debugElement.query( By.directive(FixedMouseContextualContainerDirective) ) - expect(directive).toBeTruthy() - - expect(fixture.componentInstance.directive).toBeTruthy() - }) - - describe('> hides if no content', () => { - it('> on #show, if content exists, isShown will be true', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: ` - <div fixedMouseContextualContainerDirective> - <span>Hello World</span> - </div> - ` - } - }).compileComponents() - - const fixture = TestBed.createComponent(TestCmp) - fixture.detectChanges() - - const cmp = fixture.componentInstance - cmp.directive.show() - fixture.detectChanges() - expect(cmp.directive.isShown).toBeTrue() - }) - - it('> on #show, if only comment exists, isShown will be false', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: ` - <div fixedMouseContextualContainerDirective> - <!-- hello world --> - </div> - ` - } - }).compileComponents() - - const fixture = TestBed.createComponent(TestCmp) - fixture.detectChanges() - - const cmp = fixture.componentInstance - cmp.directive.show() - fixture.detectChanges() - expect(cmp.directive.isShown).toBeFalse() - }) - - it('> on #show, if only text exists, isShown will be false', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: ` - <div fixedMouseContextualContainerDirective> - hello world - </div> - ` - } - }).compileComponents() - - const fixture = TestBed.createComponent(TestCmp) - fixture.detectChanges() - - const cmp = fixture.componentInstance - cmp.directive.show() - fixture.detectChanges() - expect(cmp.directive.isShown).toBeFalse() - }) - - it('> on #show, if nothing exists, isShown will be false', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: ` - <div fixedMouseContextualContainerDirective> - </div> - ` - } - }).compileComponents() - - const fixture = TestBed.createComponent(TestCmp) - fixture.detectChanges() - - const cmp = fixture.componentInstance - cmp.directive.show() - fixture.detectChanges() - expect(cmp.directive.isShown).toBeFalse() - }) - }) - - // TODO complete tests for FixedMouseContextualContainerDirective -}) \ No newline at end of file diff --git a/src/util/directives/FixedMouseContextualContainerDirective.directive.ts b/src/util/directives/FixedMouseContextualContainerDirective.directive.ts deleted file mode 100644 index d7fbf0905efa08166e8f864f45cf375a97950102..0000000000000000000000000000000000000000 --- a/src/util/directives/FixedMouseContextualContainerDirective.directive.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Directive, ElementRef, EventEmitter, HostBinding, Input, Output, AfterContentChecked, ChangeDetectorRef, AfterViewInit } from "@angular/core"; - -@Directive({ - selector: '[fixedMouseContextualContainerDirective]', - exportAs: 'iavFixedMouseCtxContainer' -}) - -export class FixedMouseContextualContainerDirective implements AfterContentChecked { - - private defaultPos: [number, number] = [-1e3, -1e3] - public isShown: boolean = false - - @Input() - public mousePos: [number, number] = this.defaultPos - - @Output() - public onShow: EventEmitter<null> = new EventEmitter() - - @Output() - public onHide: EventEmitter<null> = new EventEmitter() - - constructor( - public el: ElementRef, - private cdr: ChangeDetectorRef, - ) { - } - - public recalculatePosition(){ - const clientWidth = this.el.nativeElement.clientWidth - const clientHeight = this.el.nativeElement.clientHeight - - const windowInnerWidth = window.innerWidth - const windowInnerHeight = window.innerHeight - if (windowInnerHeight - this.mousePos[1] < clientHeight) { - this.mousePos[1] = windowInnerHeight - clientHeight - } - - if ((windowInnerWidth - this.mousePos[0]) < clientWidth) { - this.mousePos[0] = windowInnerWidth - clientWidth - } - - this.transform = `translate(${this.mousePos.map(v => v.toString() + 'px').join(', ')})` - } - - ngAfterContentChecked(){ - if (this.el.nativeElement.childElementCount === 0) { - this.hide() - } - this.recalculatePosition() - this.cdr.markForCheck() - } - - public show() { - this.styleDisplay = 'inline-block' - this.isShown = true - this.onShow.emit() - } - - public hide() { - this.transform = `translate(${this.defaultPos.map(v => v.toString() + 'px').join(', ')})` - this.styleDisplay = 'none' - this.isShown = false - this.onHide.emit() - } - - @HostBinding('style.display') - public styleDisplay = `none` - - @HostBinding('style.transform') - public transform = `translate(${this.mousePos.map(v => v.toString() + 'px').join(', ')})` - -} diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index f68170be4a59e2610000d997b852084512529dc4..be87e8eeec2a8fad91596feadac14a1dde29da3f 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -46,13 +46,16 @@ export class NehubaLayerControlService implements OnDestroy{ combineLatest([ this.completeNgIdLabelRegionMap$, this.customLayers$, + this.selectedRegion$, ]).pipe( - map(([record, layers]) => { + map(([record, layers, selectedRegions]) => { const returnVal: IColorMap = {} const cmCustomLayers = layers.filter(l => l.clType === "customlayer/colormap") as atlasAppearance.const.ColorMapCustomLayer[] const cmBaseLayers = layers.filter(l => l.clType === "baselayer/colormap") as atlasAppearance.const.ColorMapCustomLayer[] + const usingCustomCM = cmCustomLayers.length > 0 + const useCm = (() => { /** * if custom layer exist, use the last custom layer @@ -72,11 +75,20 @@ export class NehubaLayerControlService implements OnDestroy{ get: (r: SxplrRegion) => r.color } })() + + const selectedRegionNameSet = new Set(selectedRegions.map(v => v.name)) for (const [ngId, labelRecord] of Object.entries(record)) { for (const [label, region] of Object.entries(labelRecord)) { if (!region.color) continue - const [ red, green, blue ] = useCm.get(region) || [200, 200, 200] + /** + * if custom color map is used, do *not* selectively paint selected region + * custom color map can choose to subscribe to selected regions, and update the color map accordingly, + * if they wish to respect the selected regions + */ + const [ red, green, blue ] = usingCustomCM || selectedRegionNameSet.size === 0 || selectedRegionNameSet.has(region.name) + ? useCm.get(region) || [200, 200, 200] + : [255, 255, 255] if (!returnVal[ngId]) { returnVal[ngId] = {} } diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts index 051d366a882a44f75f782e10fe58ccd25314869e..7db5392a46b74554df8c9e2db2ab8d5e221ab44b 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts @@ -196,6 +196,8 @@ describe('> mesh.service.ts', () => { beforeEach(() => { const mockStore = TestBed.inject(MockStore) mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, []) + mockStore.overrideSelector(atlasSelection.selectors.selectedTemplate, {} as any) + mockStore.overrideSelector(atlasSelection.selectors.selectedParcellation, {} as any) }) it("> load all meshes", () => { @@ -226,6 +228,8 @@ describe('> mesh.service.ts', () => { beforeEach(() => { const mockStore = TestBed.inject(MockStore) mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, [fits1]) + mockStore.overrideSelector(atlasSelection.selectors.selectedTemplate, {} as any) + mockStore.overrideSelector(atlasSelection.selectors.selectedParcellation, {} as any) }) it("> load only selected mesh", () => { diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.ts index 29a40e94c6882b6022033b2d55103627d3d9b8cf..d46e128f4a84d082272101b67a215bf75d5165c3 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.ts @@ -7,6 +7,7 @@ import { selectorAuxMeshes } from "../store"; import { LayerCtrlEffects } from "../layerCtrl.service/layerCtrl.effects"; import { atlasSelection } from "src/state"; import { BaseService } from "../base.service/base.service"; +import { IDS } from "src/atlasComponents/sapi" /** * control mesh loading etc @@ -126,8 +127,21 @@ export class NehubaMeshService implements OnDestroy { this.#allSegmentMeshes$, this.#selectedSegmentMeshes$, this.#auxMesh$, + this.store$.pipe( + select(atlasSelection.selectors.selectedTemplate) + ), + this.store$.pipe( + select(atlasSelection.selectors.selectedParcellation) + ) ]).pipe( - switchMap(([ allSegMesh, selectedSegMesh, auxmesh ]) => { + switchMap(([ allSegMesh, selectedSegMesh, auxmesh, selectedTemplate, selectedParcellation ]) => { + /** + * TODO monkey patching jba29 in colin to show all meshes + * + */ + if (selectedParcellation.id === IDS.PARCELLATION.JBA29 && selectedTemplate.id === IDS.TEMPLATES.COLIN27) { + return of(...allSegMesh) + } const hasSegSelected = selectedSegMesh.some(v => v.labelIndicies.length !== 0) const hasAuxMesh = auxmesh.length > 0 const meshesToLoad: IMeshesToLoad[] = [] diff --git a/src/viewerModule/nehuba/userLayers/service.ts b/src/viewerModule/nehuba/userLayers/service.ts index 042cc54851be1e31cf457926d81768dfcbc2a5a8..3fe9d0a7db656354ed6217ebd13a0fd0354deae4 100644 --- a/src/viewerModule/nehuba/userLayers/service.ts +++ b/src/viewerModule/nehuba/userLayers/service.ts @@ -1,8 +1,8 @@ import { Injectable, OnDestroy } from "@angular/core" import { MatDialog } from "@angular/material/dialog" import { select, Store } from "@ngrx/store" -import { concat, of, Subscription } from "rxjs" -import { pairwise } from "rxjs/operators" +import { concat, from, of, Subscription } from "rxjs" +import { catchError, map, pairwise, switchMap } from "rxjs/operators" import { linearTransform, TVALID_LINEAR_XFORM_DST, @@ -198,29 +198,48 @@ export class UserLayerService implements OnDestroy { ) { this.#subscription.push( concat( - of(null), - this.routerSvc.customRoute$.pipe(select((v) => v[OVERLAY_LAYER_KEY])) - ) - .pipe(pairwise()) - .subscribe(([prev, curr]) => { - if (prev) { - this.removeUserLayer(prev) + of(null as string), + this.routerSvc.customRoute$.pipe( + select(v => v[OVERLAY_LAYER_KEY]) + ) + ).pipe( + pairwise(), + switchMap(([prev, curr]) => { + /** + * for precomputed sources, check if transform.json exists. + * if so, try to fetch it, and set it as transform + */ + if (!curr) { + return of([prev, curr, null]) } - if (curr) { - this.addUserLayer( - curr, - { - filename: curr, - message: `Overlay layer populated in URL`, - }, - { - shader: getShader({ - colormap: EnumColorMapName.MAGMA, - }), - } - ) + if (!curr.startsWith("precomputed://")) { + return of([prev, curr, null]) } + return from(fetch(`${curr.replace('precomputed://', '')}/transform.json`).then(res => res.json())).pipe( + catchError(() => of([prev, curr, null])), + map(transform => [prev, curr, transform]) + ) }) + ).subscribe(([prev, curr, transform]) => { + if (prev) { + this.removeUserLayer(prev) + } + if (curr) { + this.addUserLayer( + curr, + { + filename: curr, + message: `Overlay layer populated in URL`, + }, + { + shader: getShader({ + colormap: EnumColorMapName.MAGMA, + }), + transform + } + ) + } + }) ) } diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 136f0baf82166534a9e0d7480159e0dce58edc32..2dbd534dbd0a309ec23fcf1f6545a88bfe1b5a18 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -14,6 +14,7 @@ import { atlasAppearance, atlasSelection, userInteraction } from "src/state"; import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; import { EntryComponent } from "src/features/entry/entry.component"; import { TFace, TSandsPoint, getCoord } from "src/util/types"; +import { wait } from "src/util/fn"; @Component({ selector: 'iav-cmp-viewer-container', @@ -438,4 +439,14 @@ export class ViewerCmp implements OnDestroy { }) ) } + + @ViewChild('voiFeatureEntryCmp') + voiFeatureEntryCmp: EntryComponent + + async pullAllVoi(){ + if (this.voiFeatureEntryCmp){ + await wait(320) + this.voiFeatureEntryCmp.pullAll() + } + } } diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 0c80076b1593496a3f94cfb7892f312d192cad22..459fb19e841d22944af9cf5a8bca36512bcb6d0c 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -905,62 +905,69 @@ <ng-template #viewerStatusCtxMenu let-data> <ng-template [ngIf]="data.context" let-context> - <mat-list> + <!-- ref space & position --> + <ng-container [ngSwitch]="context.viewerType"> - <!-- ref space & position --> - <ng-container [ngSwitch]="context.viewerType"> - - <!-- volumetric i.e. nehuba --> - <ng-container *ngSwitchCase="'nehuba'"> - <mat-list-item mat-ripple - (click)="selectPoint({ point: context.payload.mouse.real }, data.metadata.template)"> - <div mat-list-icon> - <i class="fas fa-map"></i> - </div> - - <div mat-line> + <!-- volumetric i.e. nehuba --> + <ng-container *ngSwitchCase="'nehuba'"> + <button mat-button class="sxplr-list-like-button" + (click)="selectPoint({ point: context.payload.mouse.real }, data.metadata.template)"> + + <div class="sxplr-list-like-button-icon"> + <i class="fas fa-map"></i> + </div> + + <div class="sxplr-list-like-button-body"> + + <span class="sxplr-list-like-button-body-line"> {{ context.payload.mouse.real | nmToMm | numbers | addUnitAndJoin : '' }} (mm) - </div> - <div mat-line class="text-muted"> + </span> + <span class="sxplr-list-like-button-body-line text-muted"> Point - </div> - <div mat-line class="text-muted"> + </span> + <span class="sxplr-list-like-button-body-line text-muted"> {{ data.metadata.template.name }} - </div> - </mat-list-item> - </ng-container> - - <ng-container *ngSwitchCase="'threeSurfer'"> - - <ng-template [ngIf]="context.payload?.faceIndex" let-faceIndex> - <ng-template [ngIf]="context.payload?.vertexIndices" let-vertexIndices> - <mat-list-item mat-ripple - (click)="selectPoint({ face: faceIndex, vertices: vertexIndices }, data.metadata.template)"> - - <div mat-list-icon> - <i class="fas fa-map"></i> - </div> - - <div mat-line> + </span> + </div> + + </button> + </ng-container> + + <ng-container *ngSwitchCase="'threeSurfer'"> + + <ng-template [ngIf]="context.payload?.faceIndex" let-faceIndex> + <ng-template [ngIf]="context.payload?.vertexIndices" let-vertexIndices> + <button mat-button class="sxplr-list-like-button" + (click)="selectPoint({ face: faceIndex, vertices: vertexIndices }, data.metadata.template)"> + + <div class="sxplr-list-like-button-icon"> + <i class="fas fa-map"></i> + </div> + + <div class="sxplr-list-like-button-body"> + + <span class="sxplr-list-like-button-body-line"> Face Index: {{ faceIndex }}, Vertices Index: {{ vertexIndices | addUnitAndJoin : '' }} - </div> - <div mat-line class="text-muted"> + </span> + <span class="sxplr-list-like-button-body-line text-muted"> Mesh Face - </div> - <div mat-line class="text-muted"> + </span> + <span class="sxplr-list-like-button-body-line text-muted"> {{ data.metadata.template.name }} - </div> - </mat-list-item> - </ng-template> + </span> + </div> + + </button> + </ng-template> - - </ng-container> - - <ng-container *ngSwitchDefault> - DEFAULT - </ng-container> + </ng-template> + + </ng-container> + + <ng-container *ngSwitchDefault> + DEFAULT </ng-container> - </mat-list> + </ng-container> </ng-template> </ng-template> @@ -968,41 +975,32 @@ <!-- viewer state hover ctx menu --> <ng-template #viewerStatusRegionCtxMenu let-data> <!-- hovered ROIs --> - <mat-list> - <ng-template ngFor [ngForOf]="data.metadata.hoveredRegions" - let-region - let-first="first"> + <ng-template ngFor [ngForOf]="data.metadata.hoveredRegions" + let-region + let-first="first"> - <mat-divider class="top-0" *ngIf="!first"></mat-divider> + <mat-divider class="top-0" *ngIf="!first"></mat-divider> - <mat-list-item mat-ripple - class="cursor-default" - (click)="$event.ctrlKey ? toggleRoi(region) : selectRoi(region)"> + <button mat-button + (click)="$event.ctrlKey ? toggleRoi(region) : selectRoi(region)" + class="sxplr-list-like-button"> + + <div class="sxplr-list-like-button-icon"> + <i class="fas fa-brain"></i> + </div> - <div mat-list-icon> - <i class="fas fa-brain"></i> - </div> + <div class="sxplr-list-like-button-body"> - <span mat-line> + <span class="sxplr-list-like-button-body-line"> {{ region.name }} </span> - <span mat-line class="text-muted"> - <span> - Brain region - </span> + <span class="sxplr-list-like-button-body-line text-muted"> + Brain region </span> - - <!-- lookup region --> - <!-- <button mat-icon-button - (click)="selectRoi(region)" - ctx-menu-dismiss> - <i class="fas fa-search"></i> - </button> --> - </mat-list-item> - + </div> + </button> - </ng-template> - </mat-list> + </ng-template> </ng-template> <!-- feature tmpls --> @@ -1126,6 +1124,7 @@ class="sxplr-pe-all mat-elevation-z8" [template]="view.selectedTemplate" [bbox]="bbox.bbox$ | async | getProperty : 'bbox'" + [attr.data-feature-length]="((voiFeatureEntryCmp.features$ | async) || []).length" #voiFeatureEntryCmp="featureEntryCmp"> </sxplr-feature-entry> @@ -1143,7 +1142,7 @@ iav-switch [iav-switch-state]="false" #voiSwitch="iavSwitch" - (iav-switch-event)="$event && voiFeatureEntryCmp.pullAll()" + (iav-switch-event)="$event && pullAllVoi()" (click)="voiSwitch.toggle()"> <ng-template [ngIf]="voiSwitch.switchState$ | async" [ngIfElse]="chevronCollapseTmpl"> @@ -1175,6 +1174,7 @@ <div sxplr-sapiviews-core-space-boundingbox + (sxplr-sapiviews-core-space-boundingbox-changed)="pullAllVoi()" [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"