diff --git a/docs/releases/v2.6.6.md b/docs/releases/v2.6.6.md new file mode 100644 index 0000000000000000000000000000000000000000..ccdd90b285f00430cb4cba39aaa2467c63631cf5 --- /dev/null +++ b/docs/releases/v2.6.6.md @@ -0,0 +1,5 @@ +# v2.6.6 + +## Feature + +- (experimental) link to 1um VOI diff --git a/mkdocs.yml b/mkdocs.yml index d56285fcb2696c7884fb90844282f9ba87e94d2b..fb8f1aded1ce21f342f7149a36483a6025183377 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ pages: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.6.6: 'releases/v2.6.6.md' - v2.6.5: 'releases/v2.6.5.md' - v2.6.4: 'releases/v2.6.4.md' - v2.6.3: 'releases/v2.6.3.md' diff --git a/package.json b/package.json index 75826c222e0691790b3faac539f396e0a590bccf..d24a488d857bce622022afa1440248afc640dc21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interactive-viewer", - "version": "2.6.5", + "version": "2.6.6", "description": "HBP interactive atlas viewer. Integrating KG query, dataset previews & more. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", "scripts": { "build-aot": "ng build && node ./third_party/matomo/processMatomo.js", diff --git a/src/main.module.ts b/src/main.module.ts index ea82fa7eb23dbd27e2037f7f52ad1f3e7bdd43e1..dccad299c9f882744a6ae9d7fc40b88844fc782b 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -1,8 +1,8 @@ import { DragDropModule } from '@angular/cdk/drag-drop' import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; +import { APP_INITIALIZER, NgModule } from "@angular/core"; import { FormsModule } from "@angular/forms"; -import { StoreModule, ActionReducer } from "@ngrx/store"; +import { StoreModule, ActionReducer, Store } from "@ngrx/store"; import { AngularMaterialModule } from 'src/sharedModules' import { AtlasViewer } from "./atlasViewer/atlasViewer.component"; import { ComponentsModule } from "./components/components.module"; @@ -54,10 +54,13 @@ import { MessagingGlue } from './messagingGlue'; import { BS_ENDPOINT } from './util/constants'; import { QuickTourModule } from './ui/quickTour'; import { of } from 'rxjs'; +import { debounceTime, filter, map } from 'rxjs/operators'; import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME, OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN, kgTos, IAV_DATASET_PREVIEW_ACTIVE } from './databrowser.fallback' import { CANCELLABLE_DIALOG } from './util/interfaces'; import { environment } from 'src/environments/environment' import { NotSupportedCmp } from './notSupportedCmp/notSupported.component'; +import { RouterService } from './routerModule/router.service'; +import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer } from './services/state/ngViewerState.store.helper'; export function debug(reducer: ActionReducer<any>): ActionReducer<any> { return function(state, action) { @@ -242,6 +245,43 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> { provide: BS_ENDPOINT, useValue: (environment.BS_REST_URL || `https://siibra-api-stable.apps.hbp.eu/v1_0`).replace(/\/$/, '') }, + { + /** + * monkey patch 1um data as x-voi:d71d369a-c401-4d7e-b97a-3fb78eed06c5 + */ + provide: APP_INITIALIZER, + useFactory: (rSvc: RouterService, store: Store) => { + rSvc.customRoute$.pipe( + map(val => val[`x-voi`] === `d71d369a-c401-4d7e-b97a-3fb78eed06c5`), + debounceTime(160) + ).subscribe(flag => { + if (flag) { + store.dispatch( + ngViewerActionAddNgLayer({ + layer: { + name: `1um`, + source: `precomputed://https://1um.brainatlas.eu/cyto_reconstructions/ebrains_release/BB_1um/VOI_1/precomputed`, + shader: `void main(){ emitGrayscale(toNormalized(getDataValue()));}`, + transform: [[1.002276062965393,0.1810370832681656,0.15283183753490448,-10704839],[-0.14879435300827026,-0.0360119566321373,1.018455982208252,-60994436],[0.18436983227729797,-1.0132216215133667,-0.008890812285244465,-2825862.75],[0,0,0,1]], + opacity: 1, + } + }) + ) + } else { + store.dispatch( + ngViewerActionRemoveNgLayer({ + layer: { + name: `1um` + } + }) + ) + } + }) + return () => Promise.resolve() + }, + multi: true, + deps: [ RouterService, Store ] + } ], bootstrap : [ AtlasViewer, diff --git a/src/services/state/ngViewerState/constants.ts b/src/services/state/ngViewerState/constants.ts index 3406494f9d6a4a578b54325b1b2869df7f3f3da9..3e93ae4d0ec03ceec73175d1cba8254d6b4b894b 100644 --- a/src/services/state/ngViewerState/constants.ts +++ b/src/services/state/ngViewerState/constants.ts @@ -1,12 +1,13 @@ export interface INgLayerInterface { name: string // displayName source: string - mixability: string // base | mixable | nonmixable + mixability?: string // base | mixable | nonmixable annotation?: string // id?: string // unique identifier visible?: boolean shader?: string transform?: any + opacity?: number } export enum PANELS { diff --git a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts index 043e0005a7d84e5a99f7e3acf52a48410df73378..bc866e2556cd784e9668a3f418362553667265ca 100644 --- a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts +++ b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, Pipe, PipeTransform } from "@angular/core"; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output, Pipe, PipeTransform } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { combineLatest, Observable, Subscription } from "rxjs"; import { debounceTime, distinctUntilChanged, map, shareReplay, startWith } from "rxjs/operators"; @@ -20,7 +20,8 @@ const SHOW_LAYER_NAMES = [ 'Blockface Image', 'PLI Transmittance', 'T2w MRI', - 'MRI Labels' + 'MRI Labels', + '1um' ] @Component({ @@ -29,6 +30,7 @@ const SHOW_LAYER_NAMES = [ styleUrls : [ './layerbrowser.style.css', ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class LayerBrowser implements OnInit, OnDestroy { @@ -137,7 +139,6 @@ export class LayerBrowser implements OnInit, OnDestroy { this.forceShowSegment$.subscribe(state => this.forceShowSegmentCurrentState = state), ) - this.viewer = getViewer() } public ngOnDestroy() { @@ -159,7 +160,9 @@ export class LayerBrowser implements OnInit, OnDestroy { } } - public viewer: any + get viewer() { + return getViewer() + } public toggleVisibility(layer: any) { const layerName = layer.name diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index d3232e3e098d260c3771b95858fb428ea7f1caf0..35732fd5214f68e020ad1da22879d0c96c6ad8b6 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, OnDestroy, Optional } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { BehaviorSubject, combineLatest, from, merge, NEVER, Observable, of, Subject, Subscription } from "rxjs"; -import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators"; +import { debounceTime, distinctUntilChanged, filter, map, mapTo, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators"; import { viewerStateCustomLandmarkSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; import { getRgb, IColorMap, INgLayerCtrl, INgLayerInterface, TNgLayerCtrl } from "./layerCtrl.util"; import { getMultiNgIdsRegionsLabelIndexMap } from "../constants"; @@ -13,6 +13,7 @@ import { getShader, PMAP_DEFAULT_CONFIG } from "src/util/constants"; import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerSelectorClearView, ngViewerSelectorLayers } from "src/services/state/ngViewerState.store.helper"; import { serialiseParcellationRegion } from 'common/util' import { _PLI_VOLUME_INJ_TOKEN, _TPLIVal } from "src/glue"; +import { RouterService } from "src/routerModule/router.service"; export const BACKUP_COLOR = { red: 255, @@ -162,6 +163,7 @@ export class NehubaLayerControlService implements OnDestroy{ : NEVER constructor( private store$: Store<any>, + private routerSvc: RouterService, @Optional() @Inject(_PLI_VOLUME_INJ_TOKEN) private _pliVol$: Observable<_TPLIVal[]>, @Optional() @Inject(REGION_OF_INTEREST) roi$: Observable<TRegionDetail> ){ @@ -328,9 +330,15 @@ export class NehubaLayerControlService implements OnDestroy{ this.pliVol$.pipe( startWith([]) ), + this.routerSvc.customRoute$.pipe( + startWith({}), + map(val => val['x-voi'] === "d71d369a-c401-4d7e-b97a-3fb78eed06c5" + ? ["1um"] + : []), + ) ]).pipe( - map(([ expectedLayerNames, layerNames ]) => { - const ngIdSet = new Set<string>([...layerNames, ...expectedLayerNames]) + map(([ expectedLayerNames, layerNames, voiLayers ]) => { + const ngIdSet = new Set<string>([...layerNames, ...expectedLayerNames, ...voiLayers]) return Array.from(ngIdSet) }) ) diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 68b8a93746f8cc72d2c29750e8b73aee581f12eb..b1b8c323547335170228b71aac27b0ca937418a8 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactory, ComponentFactoryResolver, Inject, Injector, Input, OnDestroy, Optional, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { combineLatest, merge, NEVER, Observable, of, Subscription } from "rxjs"; -import {catchError, debounceTime, distinctUntilChanged, map, shareReplay, startWith, switchMap } from "rxjs/operators"; +import {catchError, debounceTime, distinctUntilChanged, map, shareReplay, startWith, switchMap, mapTo } from "rxjs/operators"; import { viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; import { viewerStateContextedSelectedRegionsSelector, @@ -26,6 +26,7 @@ import { _PLI_VOLUME_INJ_TOKEN, _TPLIVal } from "src/glue"; import { uiActionSetPreviewingDatasetFiles } from "src/services/state/uiState.store.helper"; import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper"; import { DialogService } from "src/services/dialogService.service"; +import { RouterService } from "src/routerModule/router.service"; type TCStoreViewerCmp = { overlaySideNav: any @@ -124,6 +125,10 @@ export class ViewerCmp implements OnDestroy { public _pliDesc = "The collected datasets provide real multimodal, multiscale structural connectivity insights into the human hippocampus. One post mortem hippocampus was scanned with Anatomical and Diffusion MRI (dMRI) [1], 3D Polarized Light Imaging (3D-PLI) [2], and Two-Photon Fluorescence Microscopy (TPFM) [3] using protocols specifically developed during SGA1 and SGA2, rendering joint tissue imaging possible. MRI scanning was performed with a 11.7 T Preclinical MRI system (gradients: 760 mT/m, slew rate: 9500 T/m/s) yielding T1-w and T2-w maps at 200 µm and dMRI-based maps at 300 µm resolution. During tissue sectioning (60 µm thickness) blockface (en-face) images were acquired from the surface of the frozen brain block, serving as reference for data integration/co-alignment. 530 brain sections were scanned with 3D-PLI. HPC-based image analysis provided transmittance, retardation, and fiber orientation maps at 1.3 µm in-plane resolution. TPFM was finally applied to selected brain sections utilizing autofluorescence properties of the fibrous tissue which appears after PBS washing (MAGIC protocol). The TPFM measurements provide a resolution of 0.44 µm x 0.44 µm x 1 µm." public _pliLink = "https://doi.org/10.25493/JQ30-E08" + public _1umTitle = `Cellular level 3D reconstructed volumes at 1µm resolution within the human occipital cortex (v1.0)` + public _1umDesc = `` + public _1umLink = `https://search.kg.ebrains.eu/instances/d71d369a-c401-4d7e-b97a-3fb78eed06c5` + public CONST = CONST public ARIA_LABELS = ARIA_LABELS @@ -185,8 +190,33 @@ export class ViewerCmp implements OnDestroy { return 'notsupported' }) ) - - public pliVol$ = this._pliVol$ || NEVER + + public viewerCtx$ = this.viewerModuleSvc.context$ + + private _1umVoi$ = this.routerSvc.customRoute$.pipe( + map(obj => obj[`x-voi`] === "d71d369a-c401-4d7e-b97a-3fb78eed06c5"), + distinctUntilChanged() + ) + + public pliVol$ = merge( + this._pliVol$?.pipe( + mapTo({ + title: this._pliTitle, + description: this._pliDesc, + url: [{ doi: this._pliLink }] + }) + ) || NEVER, + this._1umVoi$.pipe( + map(flag => flag + ? ({ + title: this._1umTitle, + description: this._1umDesc, + url: [{ doi: this._1umLink }] + }) + : null + ) + ) + ) /** * if no regions are selected, nor any additional layers (being deprecated) @@ -197,13 +227,13 @@ export class ViewerCmp implements OnDestroy { public onlyShowMiniTray$: Observable<boolean> = combineLatest([ this.selectedRegions$, this.pliVol$.pipe( - startWith([]) + startWith(null as { title: string, description: string, url: { doi: string }[] }) ), this.viewerMode$.pipe( startWith(null as string) ), ]).pipe( - map(([ regions, layers, viewerMode ]) => regions.length === 0 && layers.length === 0 && !viewerMode) + map(([ regions, layers, viewerMode ]) => regions.length === 0 && !layers && !viewerMode) ) @ViewChild('viewerStatusCtxMenu', { read: TemplateRef }) @@ -224,6 +254,7 @@ export class ViewerCmp implements OnDestroy { previewingDatasetFiles: [] }) ) + this.routerSvc.setCustomRoute('x-voi', null) } constructor( private store$: Store<any>, @@ -232,6 +263,7 @@ export class ViewerCmp implements OnDestroy { cfr: ComponentFactoryResolver, private dialogSvc: DialogService, private cdr: ChangeDetectorRef, + private routerSvc: RouterService, @Optional() @Inject(_PLI_VOLUME_INJ_TOKEN) private _pliVol$: Observable<_TPLIVal[]>, @Optional() @Inject(REGION_OF_INTEREST) public regionOfInterest$: Observable<any> ){ diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index abb3c1c34715215fae7f6d98ed850d1804436bbd..fae280bfbd67128f67fc6567e1fe7b87883aeca2 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -164,7 +164,7 @@ <!-- if pli voi is visible, show pli template otherwise show region tmpl --> <ng-template - [ngTemplateOutlet]="(pliVol$ | async)?.[0] + [ngTemplateOutlet]="(pliVol$ | async) ? voiTmpl : sidenavRegionTmpl" [ngTemplateOutletContext]="{ @@ -553,7 +553,7 @@ <mat-card class="_pli-container"> <mat-card-title> - {{ _pliTitle }} + {{ pliVol$ | async | getProperty : 'title' }} </mat-card-title> <mat-card-subtitle class="d-inline-flex align-items-center flex-wrap"> @@ -564,7 +564,7 @@ <mat-divider vertical="true" class="ml-2 h-2rem"></mat-divider> - <a [href]="_pliLink" + <a *ngFor="let url of pliVol$ | async | getProperty : 'url'" [href]="url.doi" mat-icon-button matTooltip="Explore in EBRAINS Knowledge Graph" target="_blank"> @@ -574,7 +574,7 @@ </mat-card-subtitle> <small class="d-block text-muted iv-custom-comp darker-bg"> - {{ _pliDesc }} + {{ pliVol$ | async | getProperty : 'description' }} </small> <mat-expansion-panel class="sidenav-cover-header-container">