diff --git a/angular.json b/angular.json index af94adeb5..46d2ba128 100644 --- a/angular.json +++ b/angular.json @@ -34,7 +34,13 @@ "styles": [ "src/theme.scss", "src/overwrite.scss", - "src/extra_styles.css" + "src/extra_styles.css", + + { + "input": "export-nehuba/dist/min/main.css", + "inject": false, + "bundleName": "vanillaMain" + } ], "scripts": [{ "input": "worker/worker.js", @@ -65,7 +71,9 @@ "input": "third_party/vanilla_nehuba.js", "inject": false, "bundleName": "vanilla_nehuba" - },{ + }, + + { "input": "export-nehuba/dist/min/main.bundle.js", "inject": false, "bundleName": "main.bundle" @@ -73,7 +81,22 @@ "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" diff --git a/common/constants.js b/common/constants.js index d3fe2b2b3..d3f6f2454 100644 --- a/common/constants.js +++ b/common/constants.js @@ -148,6 +148,9 @@ If you do not accept the Terms & Conditions you are not permitted to access or u REMOVE_FRONTAL_OCTANT_HELPER_TEXT: `Hide the octant facing the user, and overlaying the slice views.`, AUXMESH_DESC: `Some templates contain auxiliary meshes, which compliment the appearance of the template in the perspective view.`, + + OVERWRITE_SAPI_ENDPOINT_ATTR: `x-sapi-base-url`, + DATA_ERROR_ATTR: `data-error` } exports.QUICKTOUR_DESC ={ diff --git a/deploy/app.js b/deploy/app.js index f4b3dcd07..bc3444df3 100644 --- a/deploy/app.js +++ b/deploy/app.js @@ -1,4 +1,3 @@ -const fs = require('fs') const path = require('path') const express = require('express') const app = express.Router() @@ -6,8 +5,7 @@ const session = require('express-session') const crypto = require('crypto') const cookieParser = require('cookie-parser') const bkwdMdl = require('./bkwdCompat')() - -const LOCAL_CDN_FLAG = !!process.env.LOCAL_CDN +const { CONST } = require("../common/constants") if (process.env.NODE_ENV !== 'production') { app.use(require('cors')()) @@ -123,36 +121,6 @@ const PUBLIC_PATH = process.env.NODE_ENV === 'production' */ app.use('/.well-known', express.static(path.join(__dirname, 'well-known'))) -if (LOCAL_CDN_FLAG) { - /* - * TODO setup local cdn for supported libraries map - */ - const LOCAL_CDN = process.env.LOCAL_CDN - const CDN_ARRAY = [ - 'https://stackpath.bootstrapcdn.com', - 'https://use.fontawesome.com', - 'https://unpkg.com' - ] - - let indexFile - fs.readFile(path.join(PUBLIC_PATH, 'index.html'), 'utf-8', (err, data) => { - if (err) throw err - if (!LOCAL_CDN) { - indexFile = data - return - } - const regexString = CDN_ARRAY.join('|').replace(/\/|\./g, s => `\\${s}`) - const regex = new RegExp(regexString, 'gm') - indexFile = data.replace(regex, LOCAL_CDN) - }) - - app.get('/', bkwdMdl, (_req, res) => { - if (!indexFile) return res.status(404).end() - res.setHeader('Content-Type', 'text/html; charset=utf-8') - return res.status(200).send(indexFile) - }) -} - app.use((_req, res, next) => { res.setHeader('Referrer-Policy', 'origin-when-cross-origin') next() @@ -182,23 +150,42 @@ app.get('/', (req, res, next) => { middelware(req, res, next) } -}, bkwdMdl, cookieParser(), (req, res) => { +}, bkwdMdl, cookieParser(), async (req, res) => { + res.setHeader('Content-Type', 'text/html') + + let returnIndex = indexTemplate + + if (!!process.env.LOCAL_CDN) { + const CDN_ARRAY = [ + 'https://stackpath.bootstrapcdn.com', + 'https://use.fontawesome.com', + 'https://unpkg.com' + ] + + const regexString = CDN_ARRAY.join('|').replace(/\/|\./g, s => `\\${s}`) + const regex = new RegExp(regexString, 'gm') + returnIndex = returnIndex.replace(regex, process.env.LOCAL_CDN) + } const iavError = req.cookies && req.cookies['iav-error'] - res.setHeader('Content-Type', 'text/html') + const attributeToAppend = {} if (iavError) { res.clearCookie('iav-error', { httpOnly: true, sameSite: 'strict' }) + attributeToAppend[CONST.DATA_ERROR_ATTR] = iavError + } - const returnTemplate = indexTemplate - .replace(/\$\$NONCE\$\$/g, res.locals.nonce) - .replace('', ``) - res.status(200).send(returnTemplate) - } else { - const returnTemplate = indexTemplate - .replace(/\$\$NONCE\$\$/g, res.locals.nonce) - res.status(200).send(returnTemplate) + if (!!process.env.OVERWRITE_API_ENDPOING) { + attributeToAppend[CONST.OVERWRITE_SAPI_ENDPOINT_ATTR] = process.env.OVERWRITE_API_ENDPOING } + + const attr = Object.entries(attributeToAppend).map(([key, value]) => `${key}="${value.replace(/"/g, '"')}"`).join(" ") + + const returnTemplate = returnIndex + .replace(/\$\$NONCE\$\$/g, res.locals.nonce) + .replace('', ``) + + res.status(200).send(returnTemplate) }) app.get('/ready', async (req, res) => { diff --git a/deploy/csp/index.js b/deploy/csp/index.js index e48f7f155..92ee364fd 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.13/dist/ng-layer-tune/', // needed for ng layer control + 'https://unpkg.com/ng-layer-tune@0.0.14/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.12.0.md b/docs/releases/v2.12.0.md new file mode 100644 index 000000000..99ad15e06 --- /dev/null +++ b/docs/releases/v2.12.0.md @@ -0,0 +1,17 @@ +# v2.12.0 + +## Feature + +- added opacity slider for external volumes, even if the more detail is collapsed. +- enable rat connectivity +- added visual indicators for selected subject and dataset in connectivity browser + +## Bugfix + +- fixed fsaverage viewer "rubber banding" + +## Behind the scene + +- update spotlight mechanics from in-house to angular CDK +- updated neuroglancer/nehuba dependency. This allows volumes with non-rigid affine to be displayed properly. +- allow siibra-api endpoint to be configured at runtime diff --git a/mkdocs.yml b/mkdocs.yml index c15fb6342..a43dc5565 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.12.0: 'releases/v2.12.0.md' - v2.11.4: 'releases/v2.11.4.md' - v2.11.3: 'releases/v2.11.3.md' - v2.11.2: 'releases/v2.11.2.md' diff --git a/package-lock.json b/package-lock.json index 792fff775..c6e536b6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "siibra-explorer", - "version": "2.11.2", + "version": "2.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "siibra-explorer", - "version": "2.11.2", + "version": "2.12.0", "license": "apache-2.0", "dependencies": { "@angular/animations": "^14.2.12", @@ -23,7 +23,7 @@ "@ngrx/effects": "^14.3.2", "@ngrx/store": "^14.3.2", "acorn": "^8.4.1", - "export-nehuba": "0.0.12", + "export-nehuba": "^0.1.0", "file-loader": "^6.2.0", "jszip": "^3.6.0", "postcss": "^8.3.6", @@ -26966,9 +26966,9 @@ "dev": true }, "node_modules/export-nehuba": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.0.12.tgz", - "integrity": "sha512-pf3hAwpXaOqlfBfgmPLYQ+uLqJ+ElyvE1bDrrCrf5Qf0Otsekw+8CcyAJhP5O15Yacmhe7Py3G96tw5bbvZyIA==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.0.tgz", + "integrity": "sha512-49mp9MiR6n+1zzeoVOfYTmr1g9CWBXrCtXK6PxwnRj+VBFrmjbp5PzBjVsGr5HsODrhwBWCLInK7zXmXaDnE/Q==", "dependencies": { "pako": "^1.0.6" } diff --git a/package.json b/package.json index 5b30b7065..5362349f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siibra-explorer", - "version": "2.11.4", + "version": "2.12.0", "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", "scripts": { "lint": "eslint src --ext .ts", @@ -66,7 +66,7 @@ "@ngrx/effects": "^14.3.2", "@ngrx/store": "^14.3.2", "acorn": "^8.4.1", - "export-nehuba": "0.0.12", + "export-nehuba": "^0.1.0", "file-loader": "^6.2.0", "jszip": "^3.6.0", "postcss": "^8.3.6", diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts index 3969bc420..543271a3a 100644 --- a/src/atlasComponents/annotations/annotation.service.ts +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -1,6 +1,7 @@ import { BehaviorSubject, Observable } from "rxjs"; import { distinctUntilChanged } from "rxjs/operators"; import { getUuid } from "src/util/fn"; +import { PeriodicSvc } from "src/util/periodic.service"; export type TNgAnnotationEv = { pickedAnnotationId: string @@ -38,6 +39,7 @@ type _AnnotationSpec = Omit & { type: number } type AnnotationRef = Record interface NgAnnotationLayer { + isReady: () => boolean layer: { localAnnotations: { references: { @@ -124,16 +126,23 @@ export class AnnotationLayer { } } - addAnnotation(spec: AnnotationSpec){ + async addAnnotation(spec: AnnotationSpec){ if (!this.nglayer) { throw new Error(`layer has already been disposed`) } - const localAnnotations = this.nglayer.layer.localAnnotations - this.idset.add(spec.id) - const annSpec = this.parseNgSpecType(spec) - localAnnotations.add( - annSpec - ) + + PeriodicSvc.AddToQueue(() => { + if (this.nglayer.isReady()) { + const localAnnotations = this.nglayer.layer.localAnnotations + this.idset.add(spec.id) + const annSpec = this.parseNgSpecType(spec) + localAnnotations.add( + annSpec + ) + return true + } + return false + }) } removeAnnotation(spec: { id: string }) { if (!this.nglayer) return @@ -145,7 +154,7 @@ export class AnnotationLayer { localAnnotations.references.delete(spec.id) } } - updateAnnotation(spec: AnnotationSpec) { + async updateAnnotation(spec: AnnotationSpec) { const localAnnotations = this.nglayer?.layer?.localAnnotations if (!localAnnotations) return const ref = localAnnotations.references.get(spec.id) @@ -167,9 +176,6 @@ export class AnnotationLayer { } private parseNgSpecType(spec: AnnotationSpec): _AnnotationSpec{ - const voxelSize = this.viewer.navigationState.voxelSize.toJSON() - const sanitizePoint = (p: [number, number, number]) => p.map((v, idx) => v / voxelSize[idx]) as [number, number, number] - const needSanitizePosition = voxelSize[0] !== 1 || voxelSize[1] !== 1 || voxelSize[2] !== 1 const overwrite: Partial<_AnnotationSpec> = {} switch (spec.type) { case "point": { @@ -187,15 +193,6 @@ export class AnnotationLayer { default: throw new Error(`overwrite type lookup failed for ${(spec as any).type}`) } - /** - * The unit of annotation(s) depends on voxel size. If it is 1,1,1 then it would be in um, but often it is not. - * If not sanitized, the annotation can be miles off. - */ - if (needSanitizePosition) { - for (const key of ['point', 'pointA', 'pointB'] ) { - if (!!spec[key]) overwrite[key] = sanitizePoint(spec[key]) - } - } return { ...spec, ...overwrite, diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 132a094b7..0750c0931 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -13,6 +13,7 @@ import { import { FeatureType, PathReturn, RouteParam, SapiRoute } from "./typeV3"; import { BoundingBox, SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate, VoiFeature, Feature } from "./sxplrTypes"; import { parcBanList, speciesOrder } from "src/util/constants"; +import { CONST } from "common/constants" export const useViewer = { THREESURFER: "THREESURFER", @@ -21,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.8' +export const EXPECTED_SIIBRA_API_VERSION = '0.3.9' let BS_ENDPOINT_CACHED_VALUE: Observable = null @@ -94,7 +95,12 @@ export class SAPI{ */ static get BsEndpoint$(): Observable { if (!!BS_ENDPOINT_CACHED_VALUE) return BS_ENDPOINT_CACHED_VALUE - const endpoints = environment.SIIBRA_API_ENDPOINTS.split(',') + const rootEl = document.querySelector('atlas-viewer') + const overwriteSapiUrl = rootEl?.getAttribute(CONST.OVERWRITE_SAPI_ENDPOINT_ATTR) + + const endpoints = overwriteSapiUrl + ? [ overwriteSapiUrl ] + : environment.SIIBRA_API_ENDPOINTS.split(',') if (endpoints.length === 0) { SAPI.ErrorMessage = `No siibra-api endpoint defined!` return NEVER @@ -296,7 +302,7 @@ export class SAPI{ switchMap(atlases => forkJoin( atlases.items.map(atlas => translateV3Entities.translateAtlas(atlas)) )), - map(atlases => atlases.sort((a, b) => speciesOrder.indexOf(a.species) - speciesOrder.indexOf(b.species))), + map(atlases => atlases.sort((a, b) => (speciesOrder as string[]).indexOf(a.species) - (speciesOrder as string[]).indexOf(b.species))), tap(() => { const respVersion = SAPI.API_VERSION if (respVersion !== EXPECTED_SIIBRA_API_VERSION) { diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index c28d15f9a..b80a7c4de 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -76,11 +76,11 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @Inject(DARKTHEME) private darktheme$: Observable ) { - const error = this.el.nativeElement.getAttribute('data-error') + const error = this.el.nativeElement.getAttribute(CONST.DATA_ERROR_ATTR) if (error) { this.snackbar.open(error, 'Dismiss', { duration: 5000 }) - this.el.nativeElement.removeAttribute('data-error') + this.el.nativeElement.removeAttribute(CONST.DATA_ERROR_ATTR) } } diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 75284adf0..98a9c4814 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -4,7 +4,12 @@ export const environment = { VERSION: 'unknown version', PRODUCTION: false, BACKEND_URL: null, - SIIBRA_API_ENDPOINTS: 'https://siibra-api-latest.apps-dev.hbp.eu/v3_0', //'https://siibra-api-stable.apps.hbp.eu/v2_0,https://siibra-api-stable.apps.jsc.hbp.eu/v2_0,https://siibra-api-stable-ns.apps.hbp.eu/v2_0', + // N.B. do not update the SIIBRA_API_ENDPOITNS directly + // some libraries rely on the exact string formatting to work properly + SIIBRA_API_ENDPOINTS: + // 'http://localhost:10081/v3_0', // endpoint-local-10081 + 'https://siibra-api-latest.apps-dev.hbp.eu/v3_0', //endpoint-latest + // 'https://siibra-api-stable.apps.hbp.eu/v3_0', // endpoint-stable SPATIAL_TRANSFORM_BACKEND: 'https://hbp-spatial-backend.apps.hbp.eu', MATOMO_URL: null, MATOMO_ID: null, diff --git a/src/extra_styles.css b/src/extra_styles.css index 82f71ff95..381208408 100644 --- a/src/extra_styles.css +++ b/src/extra_styles.css @@ -821,10 +821,10 @@ mat-list.sm mat-list-item display: grid; } -.grid.grid-col-3 +.grid.grid-col-4 { grid-auto-columns: 1fr; - grid-template-columns: 1fr auto auto; + grid-template-columns: 1fr auto auto auto; gap: 0.2rem 0.2rem; } diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts index 77b69d99d..b90f25adb 100644 --- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts +++ b/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts @@ -370,6 +370,7 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { selectedView } }), + distinctUntilChanged((o, n) => o?.feature_id === n?.feature_id && o?.subject === n?.subject && o?.selectedView === n?.selectedView && o?.parcellation?.id === n?.parcellation?.id), shareReplay(1), ) @@ -450,6 +451,7 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { ) view$ = combineLatest([ + this.busy$, this.selectedDataset$, this.formValue$, this.#fetchingMatrix$, @@ -459,13 +461,16 @@ export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { ), this.region$, ]).pipe( - map(([sDs, form, fetchingMatrix, pureConnections, region]) => { + map(([busy, sDs, form, fetchingMatrix, pureConnections, region]) => { return { showSubject: sDs && form.selectedView === "subject", numSubjects: sDs?.subjects.length, - fetchingMatrix, connections: pureConnections, region, + showAverageToggle: form.selectedCohort !== null && typeof form.selectedCohort !== "undefined", + busy: busy || fetchingMatrix, + selectedSubject: (sDs?.subjects || [])[form.selectedSubjectIndex], + selectedDataset: form?.selectedDatasetIndex } }), shareReplay(1), diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html index 416763f02..1b56fa9e4 100644 --- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html +++ b/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html @@ -32,7 +32,7 @@ - + Average @@ -54,7 +54,7 @@
- Dataset + Dataset: {{ view$ | async | getProperty : 'selectedDataset' }}
- Subject + Subject: {{ view$ | async | getProperty : 'selectedSubject' }} - -
- -
- @@ -154,3 +150,13 @@ + + +
+ +
+
+ + + + diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index 610af8724..e4cfbe75e 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -10,6 +10,7 @@ import { CategoryAccDirective } from "../category-acc.directive" import { combineLatest, concat, forkJoin, merge, of, Subject, Subscription } from 'rxjs'; import { DsExhausted, IsAlreadyPulling, PulledDataSource } from 'src/util/pullable'; import { TranslatedFeature } from '../list/list.directive'; +import { SPECIES_ENUM } from 'src/util/constants'; const categoryAcc = >(categories: T[]) => { const returnVal: Record = {} @@ -148,10 +149,10 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest public showConnectivity$ = combineLatest([ this.selectedAtlas$.pipe( - map(atlas => atlas?.species === "Homo sapiens") + map(atlas => atlas?.species === SPECIES_ENUM.HOMO_SAPIENS || atlas?.species === SPECIES_ENUM.RATTUS_NORVEGICUS) ), this.TPRBbox$.pipe( - map(({ parcellation }) => parcellation?.id === IDS.PARCELLATION.JBA29) + map(({ parcellation }) => parcellation?.id === IDS.PARCELLATION.JBA29 || parcellation?.id === IDS.PARCELLATION.WAXHOLMV4) ) ]).pipe( map(flags => flags.every(f => f)) diff --git a/src/index.html b/src/index.html index 1ed2d8187..625cc6595 100644 --- a/src/index.html +++ b/src/index.html @@ -14,7 +14,7 @@ - + diff --git a/src/messagingGlue.ts b/src/messagingGlue.ts index ae415e68c..994bf30d8 100644 --- a/src/messagingGlue.ts +++ b/src/messagingGlue.ts @@ -86,7 +86,8 @@ export class MessagingGlue implements IWindowMessaging, OnDestroy { "1" ], transform: transform, - clType: 'customlayer/nglayer' as const + clType: 'customlayer/nglayer' as const, + type: 'segmentation', } this.store.dispatch( diff --git a/src/spotlight/const.ts b/src/spotlight/const.ts new file mode 100644 index 000000000..7af90c565 --- /dev/null +++ b/src/spotlight/const.ts @@ -0,0 +1,3 @@ +import { InjectionToken, TemplateRef } from "@angular/core"; + +export const TMPL_INJ_TOKEN = new InjectionToken>('TMPL_INJ_TOKEN') \ No newline at end of file diff --git a/src/spotlight/sl-service.service.ts b/src/spotlight/sl-service.service.ts index 241fe068c..f45aad652 100644 --- a/src/spotlight/sl-service.service.ts +++ b/src/spotlight/sl-service.service.ts @@ -1,44 +1,57 @@ -import { Injectable, OnDestroy, ComponentFactoryResolver, Injector, ComponentRef, ApplicationRef, EmbeddedViewRef, TemplateRef, ComponentFactory } from '@angular/core'; -import './sl-style.css' -import { SpotlightBackdropComponent } from './spotlight-backdrop/spotlight-backdrop.component'; +import { Injectable, Injector, OnDestroy, TemplateRef } from '@angular/core'; import { Subject } from 'rxjs'; +import { Overlay, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { SpotlightBackdropComponent } from './spotlight-backdrop/spotlight-backdrop.component'; +import { TMPL_INJ_TOKEN } from './const'; @Injectable({ providedIn: 'root' }) export class SlServiceService implements OnDestroy{ - private backdropRef: ComponentRef - private dom: HTMLElement - private cf: ComponentFactory onClick: Subject = new Subject() - + private overlayRef: OverlayRef + constructor( - cfr: ComponentFactoryResolver, + private overlay: Overlay, private injector: Injector, - private appRef: ApplicationRef ) { - this.cf = cfr.resolveComponentFactory(SpotlightBackdropComponent) } - /** - * TODO use angular cdk overlay - */ - public showBackdrop(tmp?: TemplateRef){ + public showBackdrop(tmp: TemplateRef){ this.hideBackdrop() - this.backdropRef = this.cf.create(this.injector) - this.backdropRef.instance.slService = this - this.backdropRef.instance.insert = tmp + const positionStrategy = this.overlay.position() + .global() + .centerHorizontally() + .centerVertically() + + this.overlayRef = this.overlay.create({ + positionStrategy, + hasBackdrop: true, + }) - this.appRef.attachView(this.backdropRef.hostView) - this.dom = (this.backdropRef.hostView as EmbeddedViewRef).rootNodes[0] as HTMLElement - document.body.appendChild(this.dom) + const injector = Injector.create({ + parent: this.injector, + providers: [{ + provide: SlServiceService, + useValue: this + }, { + provide: TMPL_INJ_TOKEN, + useValue: tmp + }] + }) + const portal = new ComponentPortal(SpotlightBackdropComponent, null, injector) + this.overlayRef.attach(portal) + } public hideBackdrop(){ - this.backdropRef && this.appRef.detachView(this.backdropRef.hostView) - this.backdropRef && this.backdropRef.destroy() + if (this.overlayRef) { + this.overlayRef.dispose() + this.overlayRef = null + } } ngOnDestroy(){ diff --git a/src/spotlight/sl-style.css b/src/spotlight/sl-style.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/spotlight/spot-light.module.ts b/src/spotlight/spot-light.module.ts index a485e0d23..d73465017 100644 --- a/src/spotlight/spot-light.module.ts +++ b/src/spotlight/spot-light.module.ts @@ -4,16 +4,20 @@ import { SlSpotlightDirective } from './sl-spotlight.directive'; import { SpotlightBackdropComponent } from './spotlight-backdrop/spotlight-backdrop.component'; import { SpotLightOverlayDirective } from './spot-light-overlay.directive'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { OverlayModule } from '@angular/cdk/overlay'; +import { PortalModule } from '@angular/cdk/portal'; @NgModule({ declarations: [ SlSpotlightDirective, SpotlightBackdropComponent, - SpotLightOverlayDirective + SpotLightOverlayDirective, ], imports: [ BrowserAnimationsModule, - CommonModule + CommonModule, + OverlayModule, + PortalModule, ], exports: [ SlSpotlightDirective diff --git a/src/spotlight/spotlight-backdrop/spotlight-backdrop.component.ts b/src/spotlight/spotlight-backdrop/spotlight-backdrop.component.ts index fbf9495a4..7e258082c 100644 --- a/src/spotlight/spotlight-backdrop/spotlight-backdrop.component.ts +++ b/src/spotlight/spotlight-backdrop/spotlight-backdrop.component.ts @@ -1,6 +1,7 @@ -import { Component, HostListener, TemplateRef, HostBinding } from '@angular/core'; +import { Component, HostListener, TemplateRef, HostBinding, Inject } from '@angular/core'; import { SlServiceService } from '../sl-service.service'; import { transition, animate, state, style, trigger } from '@angular/animations'; +import { TMPL_INJ_TOKEN } from '../const'; @Component({ selector: 'sl-spotlight-backdrop', @@ -25,9 +26,6 @@ import { transition, animate, state, style, trigger } from '@angular/animations' }) export class SpotlightBackdropComponent { - // TODO use DI for service injection ? - public slService: SlServiceService - @HostBinding('@onShownOnDismiss') animation: string = 'attach' @@ -36,5 +34,9 @@ export class SpotlightBackdropComponent { this.slService && this.slService.onClick.next(ev) } - insert: TemplateRef + constructor( + private slService: SlServiceService, + @Inject(TMPL_INJ_TOKEN) public insert: TemplateRef, + ){ + } } diff --git a/src/state/atlasAppearance/const.ts b/src/state/atlasAppearance/const.ts index 41c9ebd72..9804bc430 100644 --- a/src/state/atlasAppearance/const.ts +++ b/src/state/atlasAppearance/const.ts @@ -31,7 +31,7 @@ export type NgLayerCustomLayer = { transform?: number[][] opacity?: number segments?: (number|string)[] - // type?: string + type?: string // annotation?: string // TODO what is this used for? } & CustomLayerBase diff --git a/src/state/atlasSelection/effects.spec.ts b/src/state/atlasSelection/effects.spec.ts index fe9926671..9ba912441 100644 --- a/src/state/atlasSelection/effects.spec.ts +++ b/src/state/atlasSelection/effects.spec.ts @@ -158,13 +158,13 @@ describe("> effects.ts", () => { }, previous: { atlas: { - "@id": IDS.ATLAES.RAT + id: IDS.ATLAES.RAT } as any, parcellation: { - "@id": IDS.PARCELLATION.WAXHOLMV4 + id: IDS.PARCELLATION.WAXHOLMV4 } as any, template: { - "@id": IDS.TEMPLATES.WAXHOLM + id: IDS.TEMPLATES.WAXHOLM } as any, } }) @@ -186,24 +186,24 @@ describe("> effects.ts", () => { const obs = hook({ current: { atlas: { - "@id": IDS.ATLAES.HUMAN + id: IDS.ATLAES.HUMAN } as any, parcellation: { - "@id": IDS.PARCELLATION.JBA29 + id: IDS.PARCELLATION.JBA29 } as any, template: { - "@id": IDS.TEMPLATES.MNI152 + id: IDS.TEMPLATES.MNI152 } as any, }, previous: { atlas: { - "@id": IDS.ATLAES.RAT + id: IDS.ATLAES.RAT } as any, parcellation: { - "@id": IDS.PARCELLATION.WAXHOLMV4 + id: IDS.PARCELLATION.WAXHOLMV4 } as any, template: { - "@id": IDS.TEMPLATES.WAXHOLM + id: IDS.TEMPLATES.WAXHOLM } as any, } }) diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts index 816c23c24..5967e5102 100644 --- a/src/state/atlasSelection/effects.ts +++ b/src/state/atlasSelection/effects.ts @@ -63,19 +63,17 @@ export class Effect { }) } - /** - * if either space name is undefined, return default state for navigation - */ - if (!prevSpcName || !currSpcName) { - return of({ - navigation: atlasSelection.defaultState.navigation - }) - } return this.store.pipe( select(atlasSelection.selectors.navigation), take(1), switchMap(({ position, ...rest }) => - this.interSpaceCoordXformSvc.transform(prevSpcName, currSpcName, position as [number, number, number]).pipe( + + /** + * if either space name is undefined, return default state for navigation + */ + !prevSpcName || !currSpcName + ? of({ navigation: { position, ...rest } }) + : this.interSpaceCoordXformSvc.transform(prevSpcName, currSpcName, position as [number, number, number]).pipe( map(value => { if (value.status === "error") { return {} diff --git a/src/util/constants.ts b/src/util/constants.ts index 001b12839..763bc0695 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -126,14 +126,21 @@ export const UNSUPPORTED_PREVIEW = [{ export const UNSUPPORTED_INTERVAL = 7000 +export const SPECIES_ENUM = { + HOMO_SAPIENS: "Homo sapiens", + MACACA_FASCICULARIS: "Macaca fascicularis", + RATTUS_NORVEGICUS: "Rattus norvegicus", + MUS_MUSCULUS: "Mus musculus", +} as const + /** * atlas should follow the following order */ export const speciesOrder = [ - "Homo sapiens", - "Macaca fascicularis", - "Rattus norvegicus", - "Mus musculus" + SPECIES_ENUM.HOMO_SAPIENS, + SPECIES_ENUM.MACACA_FASCICULARIS, + SPECIES_ENUM.RATTUS_NORVEGICUS, + SPECIES_ENUM.MUS_MUSCULUS, ] export const parcBanList: string[] = [ diff --git a/src/util/fn.ts b/src/util/fn.ts index 6457ccf78..a859bad08 100644 --- a/src/util/fn.ts +++ b/src/util/fn.ts @@ -1,18 +1,6 @@ import { interval, Observable, of } from 'rxjs' import { filter, mapTo, take } from 'rxjs/operators' -export function getViewer() { - return (window as any).viewer -} - -export function setViewer(viewer) { - (window as any).viewer = viewer -} - -export function setNehubaViewer(nehubaViewer) { - (window as any).nehubaViewer = nehubaViewer -} - export function getDebug() { return (window as any).__DEBUG__ } diff --git a/src/util/periodic.service.ts b/src/util/periodic.service.ts new file mode 100644 index 000000000..f73f64076 --- /dev/null +++ b/src/util/periodic.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from "@angular/core"; +import { wait } from "./fn"; + +@Injectable({ + providedIn: 'root' +}) +export class PeriodicSvc{ + + async addToQueue(callback: () => boolean) { + return await PeriodicSvc.AddToQueue(callback) + } + + /** + * @description retry a callback until it succeeds + * @param callback + */ + static async AddToQueue(callback: () => boolean) { + // eslint-disable-next-line no-constant-condition + while (true) { + if (callback()) { + break + } + await wait(160) + } + } +} diff --git a/src/viewerModule/nehuba/config.service/util.ts b/src/viewerModule/nehuba/config.service/util.ts index ddc82c648..2ef5a1cc9 100644 --- a/src/viewerModule/nehuba/config.service/util.ts +++ b/src/viewerModule/nehuba/config.service/util.ts @@ -8,6 +8,7 @@ import { RecursivePartial, } from "./type" import { translateV3Entities } from "src/atlasComponents/sapi/translateV3" +import { PERSPECTIVE_ZOOM_FUDGE_FACTOR } from "../constants" // fsaverage uses threesurfer, which, whilst do not use ngId, uses 'left' and 'right' as keys const fsAverageKeyVal = { [IDS.PARCELLATION.JBA29]: { @@ -374,8 +375,8 @@ export function getNehubaConfig(space: SxplrTemplate): NehubaConfig { "drawSubstrates": drawSubstrates, "drawZoomLevels": drawZoomLevels, "restrictZoomLevel": { - "minZoom": 1200000 * scale, - "maxZoom": 3500000 * scale + "minZoom": 1200000 * scale * PERSPECTIVE_ZOOM_FUDGE_FACTOR, + "maxZoom": 3500000 * scale * PERSPECTIVE_ZOOM_FUDGE_FACTOR } } } diff --git a/src/viewerModule/nehuba/constants.ts b/src/viewerModule/nehuba/constants.ts index 667c5b714..d35b8393c 100644 --- a/src/viewerModule/nehuba/constants.ts +++ b/src/viewerModule/nehuba/constants.ts @@ -35,3 +35,10 @@ export type TNehubaViewerUnit = { export const SET_MESHES_TO_LOAD = new InjectionToken>('SET_MESHES_TO_LOAD') export const PMAP_LAYER_NAME = 'regional-pmap' + +/** + * since export_nehuba@0.1.0 onwards (the big update that changed a lot of neuroglancer's internals) + * there is now a multiplier bewteen old and new perspective views + * to maintain interop with previous states, translate the multiplier + */ +export const PERSPECTIVE_ZOOM_FUDGE_FACTOR = 82.842712474619 diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts index b8e5e4b26..a37e9608e 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts @@ -75,7 +75,8 @@ export class LayerCtrlEffects { highThreshold: meta.max, lowThreshold: meta.min, removeBg: true, - }) + }), + type: 'image' } }) ) diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts index 7db5392a4..f5bea95d1 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts @@ -107,6 +107,10 @@ describe('> mesh.service.ts', () => { [labelIndex2]: fits2 } }) + const mockStore = TestBed.inject(MockStore) + + mockStore.overrideSelector(atlasSelection.selectors.selectedTemplate, {} as any) + mockStore.overrideSelector(atlasSelection.selectors.selectedParcellation, {} as any) }) describe("> auxMesh defined", () => { diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.ts index 2af102701..2654a1b30 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.ts @@ -139,7 +139,7 @@ export class NehubaMeshService implements OnDestroy { * TODO monkey patching jba29 in colin to show all meshes * */ - if (selectedParcellation.id === IDS.PARCELLATION.JBA29 && selectedTemplate.id === IDS.TEMPLATES.COLIN27) { + if ((selectedParcellation.id === IDS.PARCELLATION.JBA29 || IDS.PARCELLATION.JBA30 === selectedParcellation.id) && selectedTemplate.id === IDS.TEMPLATES.COLIN27) { return of(...allSegMesh) } const hasSegSelected = selectedSegMesh.some(v => v.labelIndicies.length !== 0) diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts index 18b8805b2..89b3a42f9 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts @@ -5,6 +5,7 @@ import { LoggingModule, LoggingService } from "src/logging" import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants" import { Subject } from "rxjs" import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service" +import { rgbToHex } from 'common/util' describe('> nehubaViewer.component.ts', () => { describe('> #scanFn', () => { @@ -305,15 +306,56 @@ describe('> nehubaViewer.component.ts', () => { describe('> # setColorMap', () => { let nehubaViewerSpy: any + let ngViewerStatechildrenGetSpy = jasmine.createSpy('get') + let layersMngerToJsonSpy = jasmine.createSpy('layersMngerToJsonSpy') + let posToJsonSpy = jasmine.createSpy('posToJsonSpy') + let layerMgerRestoreStateSpy = jasmine.createSpy('layerMgerRestoreStateSpy') + let posRestoreStateSpy = jasmine.createSpy("posRestoreStateSpy") + + const ngId1 = 'foo-bar' + const ngId2 = 'hello-world' beforeEach(() => { nehubaViewerSpy = { - batchAddAndUpdateSegmentColors: jasmine.createSpy(), dispose(){ + }, + ngviewer: { + state: { + children: { + get: ngViewerStatechildrenGetSpy + } + } } } + + ngViewerStatechildrenGetSpy.and.callFake(prop => { + if (prop === "position") { + return { + toJSON: posToJsonSpy, + restoreState: posRestoreStateSpy + } + } + if (prop === "layers") { + return { + toJSON: layersMngerToJsonSpy, + restoreState: layerMgerRestoreStateSpy, + } + } + throw new Error(`prop ${prop} is not anticipated`) + }) + posToJsonSpy.and.returnValue([1.1, 2.2, 3.3]) + layersMngerToJsonSpy.and.returnValue([{ + name: ngId1 + }, { + name: ngId2 + }]) + }) + afterEach(() => { + ngViewerStatechildrenGetSpy.calls.reset() + layersMngerToJsonSpy.calls.reset() + layerMgerRestoreStateSpy.calls.reset() }) - it('> calls nehubaViewer.batchAddAndUpdateSegmentColors', () => { + it('> calls nehubaViewer.restoreState', () => { const fixture = TestBed.createComponent(NehubaViewerUnit) fixture.componentInstance.nehubaViewer = nehubaViewerSpy fixture.detectChanges() @@ -322,27 +364,31 @@ describe('> nehubaViewer.component.ts', () => { const fooBarMap = new Map() fooBarMap.set(1, {red: 100, green: 100, blue: 100}) fooBarMap.set(2, {red: 200, green: 200, blue: 200}) - mainMap.set('foo-bar', fooBarMap) + mainMap.set(ngId1, fooBarMap) const helloWorldMap = new Map() helloWorldMap.set(1, {red: 10, green: 10, blue: 10}) helloWorldMap.set(2, {red: 20, green: 20, blue: 20}) - mainMap.set('hello-world', helloWorldMap) + mainMap.set(ngId2, helloWorldMap) fixture.componentInstance['setColorMap'](mainMap) - expect( - nehubaViewerSpy.batchAddAndUpdateSegmentColors - ).toHaveBeenCalledTimes(2) - - expect(nehubaViewerSpy.batchAddAndUpdateSegmentColors).toHaveBeenCalledWith( - fooBarMap, - { name: 'foo-bar' } - ) + expect(layerMgerRestoreStateSpy).toHaveBeenCalledOnceWith([{ + name: ngId1, + segmentColors: { + 1: rgbToHex([100, 100, 100]), + 2: rgbToHex([200, 200, 200]), + } + }, { + name: ngId2, + segmentColors: { + 1: rgbToHex([10, 10, 10]), + 2: rgbToHex([20, 20, 20]), + } + }]) - expect(nehubaViewerSpy.batchAddAndUpdateSegmentColors).toHaveBeenCalledWith( - helloWorldMap, - { name: 'hello-world' } + expect(posRestoreStateSpy).toHaveBeenCalledOnceWith( + [ 1.1, 2.2, 3.3 ] ) }) }) diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index e0e25cd15..0bee1941f 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -1,17 +1,27 @@ import { Component, ElementRef, EventEmitter, OnDestroy, Output, Inject, Optional } from "@angular/core"; -import { Subscription, BehaviorSubject, Observable, Subject, of, interval } from 'rxjs' -import { debounceTime, filter, scan, switchMap, take, distinctUntilChanged, debounce } from "rxjs/operators"; +import { Subscription, BehaviorSubject, Observable, Subject, of, interval, combineLatest } from 'rxjs' +import { debounceTime, filter, scan, switchMap, take, distinctUntilChanged, debounce, map } from "rxjs/operators"; import { LoggingService } from "src/logging"; -import { bufferUntil, getExportNehuba, getViewer, setNehubaViewer, switchMapWaitFor } from "src/util/fn"; +import { bufferUntil, getExportNehuba, switchMapWaitFor } from "src/util/fn"; import { deserializeSegment, NEHUBA_INSTANCE_INJTKN } from "../util"; -import { arrayOrderedEql } from 'common/util' -import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants"; +import { arrayOrderedEql, rgbToHex } from 'common/util' +import { IMeshesToLoad, SET_MESHES_TO_LOAD, PERSPECTIVE_ZOOM_FUDGE_FACTOR } from "../constants"; import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service"; /** * import of nehuba js files moved to angular.json */ import { INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl } from "../layerCtrl.service/layerCtrl.util"; +import { NgCoordinateSpace, Unit } from "../types"; +import { PeriodicSvc } from "src/util/periodic.service"; + +function translateUnit(unit: Unit) { + if (unit === "m") { + return 1e9 + } + + throw new Error(`Cannot translate unit: ${unit}`) +} export const IMPORT_NEHUBA_INJECT_TOKEN = `IMPORT_NEHUBA_INJECT_TOKEN` @@ -49,10 +59,11 @@ export const scanFn = (acc: LayerLabelIndex[], curr: LayerLabelIndex) => { export class NehubaViewerUnit implements OnDestroy { + #translateVoxelToReal: (voxels: number[]) => number[] public ngIdSegmentsMap: Record = {} - public viewerPosInVoxel$ = new BehaviorSubject(null) + public viewerPosInVoxel$ = new BehaviorSubject(null) public viewerPosInReal$ = new BehaviorSubject<[number, number, number]>(null) public mousePosInVoxel$ = new BehaviorSubject<[number, number, number]>(null) public mousePosInReal$ = new BehaviorSubject(null) @@ -97,33 +108,18 @@ export class NehubaViewerUnit implements OnDestroy { : [1.5e9, 1.5e9, 1.5e9] } - public _s2$: any = null - public _s3$: any = null - public _s4$: any = null - public _s5$: any = null - public _s6$: any = null - public _s7$: any = null - public _s8$: any = null - - public _s$: any[] = [ - this._s2$, - this._s3$, - this._s4$, - this._s5$, - this._s6$, - this._s7$, - this._s8$, - ] + #newViewerSubs: { unsubscribe: () => void }[] = [] public ondestroySubscriptions: Subscription[] = [] public nehubaLoaded: boolean = false - public landmarksLoaded: boolean = false + #triggerMeshLoad$ = new BehaviorSubject(null) constructor( public elementRef: ElementRef, private log: LoggingService, + private periodicSvc: PeriodicSvc, @Inject(IMPORT_NEHUBA_INJECT_TOKEN) getImportNehubaPr: () => Promise, @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaViewer$: Subject, @Optional() @Inject(SET_MESHES_TO_LOAD) private injSetMeshesToLoad$: Observable, @@ -152,9 +148,10 @@ export class NehubaViewerUnit implements OnDestroy { this.loadNehuba() const viewer = this.nehubaViewer.ngviewer - this.layersChangedHandler = viewer.layerManager.layersChanged.add(() => { + + this.layersChangedHandler = viewer.layerManager.readyStateChanged.add(() => { this.layersChanged.emit(null) - const readiedLayerNames: string[] = viewer.layerManager.managedLayers.filter(l => l.layer).map(l => l.name) + const readiedLayerNames: string[] = viewer.layerManager.managedLayers.filter(l => l.isReady()).map(l => l.name) for (const layerName in this.ngIdSegmentsMap) { if (!readiedLayerNames.includes(layerName)) { return @@ -291,9 +288,13 @@ export class NehubaViewerUnit implements OnDestroy { if (this.injSetMeshesToLoad$) { this.subscriptions.push( - this.injSetMeshesToLoad$.pipe( - scan(scanFn, []), - debounceTime(16), + combineLatest([ + this.#triggerMeshLoad$, + this.injSetMeshesToLoad$.pipe( + scan(scanFn, []), + ), + ]).pipe( + map(([_, val]) => val), debounce(() => this._nehubaReady ? of(true) : interval(160).pipe( @@ -325,14 +326,6 @@ export class NehubaViewerUnit implements OnDestroy { } } - public navPosReal: [number, number, number] = [0, 0, 0] - public navPosVoxel: [number, number, number] = [0, 0, 0] - - public mousePosReal: [number, number, number] = [0, 0, 0] - public mousePosVoxel: [number, number, number] = [0, 0, 0] - - public viewerState: ViewerState - private _multiNgIdColorMap: Map> get multiNgIdColorMap() { return this._multiNgIdColorMap @@ -353,7 +346,9 @@ export class NehubaViewerUnit implements OnDestroy { this.nehubaViewer = this.exportNehuba.createNehubaViewer(this.config, (err: string) => { /* print in debug mode */ this.log.error(err) - }) + }); + + (window as any).nehubaViewer = this.nehubaViewer /** * Hide all layers except the base layer (template) @@ -362,15 +357,15 @@ export class NehubaViewerUnit implements OnDestroy { /* creation of the layout is done on next frame, hence the settimeout */ setTimeout(() => { - getViewer().display.panels.forEach(patchSliceViewPanel) + window['viewer'].display.panels.forEach(patchSliceViewPanel) }) this.newViewerInit() - this.loadNewParcellation() - - setNehubaViewer(this.nehubaViewer) + window['nehubaViewer'] = this.nehubaViewer - this.onDestroyCb.push(() => setNehubaViewer(null)) + this.onDestroyCb.push(() => { + window['nehubaViewer'] = null + }) } public ngOnDestroy() { @@ -380,10 +375,10 @@ export class NehubaViewerUnit implements OnDestroy { while (this.subscriptions.length > 0) { this.subscriptions.pop().unsubscribe() } - - this._s$.forEach(_s$ => { - if (_s$) { _s$.unsubscribe() } - }) + while (this.#newViewerSubs.length > 0) { + this.#newViewerSubs.pop().unsubscribe() + } + this.ondestroySubscriptions.forEach(s => s.unsubscribe()) while (this.onDestroyCb.length > 0) { this.onDestroyCb.pop()() @@ -482,8 +477,36 @@ export class NehubaViewerUnit implements OnDestroy { /* if the layer exists, it will not be loaded */ !viewer.layerManager.getLayerByName(key)) .map(key => { + /** + * new implementation of neuroglancer treats swc as a mesh layer of segmentation layer + * But it cannot *directly* be accessed by nehuba's setMeshesToLoad, since it filters by + * UserSegmentationLayer. + * + * The below monkey patch sets the mesh to load, allow the SWC to be shown + */ + const isSwc = layerObj[key]['source'].includes("swc://") + const hasSegment = (layerObj[key]["segments"] || []).length > 0 + if (isSwc && hasSegment) { + this.periodicSvc.addToQueue( + () => { + const layer = viewer.layerManager.getLayerByName(key) + if (!(layer?.layer)) { + return false + } + layer.layer.displayState.visibleSegments.setMeshesToLoad([1]) + return true + } + ) + } + const { transform=null, ...rest } = layerObj[key] + + const combined = { + type: 'image', + ...rest, + ...(transform ? { transform } : {}) + } viewer.layerManager.addManagedLayer( - viewer.layerSpecification.getLayer(key, layerObj[key])) + viewer.layerSpecification.getLayer(key, combined)) return layerObj[key] }) @@ -509,7 +532,6 @@ export class NehubaViewerUnit implements OnDestroy { name: ngId, }) } - this.nehubaViewer.showSegment(0, { name: ngId, }) @@ -524,7 +546,6 @@ export class NehubaViewerUnit implements OnDestroy { name: ngId, }) } - this.nehubaViewer.hideSegment(0, { name: ngId, }) @@ -604,7 +625,7 @@ export class NehubaViewerUnit implements OnDestroy { } = newViewerState || {} if ( perspectiveZoom ) { - this.nehubaViewer.ngviewer.perspectiveNavigationState.zoomFactor.restoreState(perspectiveZoom) + this.nehubaViewer.ngviewer.perspectiveNavigationState.zoomFactor.restoreState(perspectiveZoom * PERSPECTIVE_ZOOM_FUDGE_FACTOR) } if ( zoom ) { this.nehubaViewer.ngviewer.navigationState.zoomFactor.restoreState(zoom) @@ -620,18 +641,6 @@ export class NehubaViewerUnit implements OnDestroy { } } - public obliqueRotateX(amount: number) { - this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([0, 1, 0]), -amount / 4.0 * Math.PI / 180.0) - } - - public obliqueRotateY(amount: number) { - this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([1, 0, 0]), amount / 4.0 * Math.PI / 180.0) - } - - public obliqueRotateZ(amount: number) { - this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([0, 0, 1]), amount / 4.0 * Math.PI / 180.0) - } - public toggleOctantRemoval(flag?: boolean) { const ctrl = this.nehubaViewer?.ngviewer?.showPerspectiveSliceViews if (!ctrl) { @@ -642,13 +651,6 @@ export class NehubaViewerUnit implements OnDestroy { ? !ctrl.value : flag ctrl.restoreState(newVal) - - if (this.landmarksLoaded) { - /** - * showPerspectSliceView -> ! meshTransparency - */ - this.setMeshTransparency(!newVal) - } } private setLayerTransparency(layerName: string, alpha: number) { @@ -688,29 +690,29 @@ export class NehubaViewerUnit implements OnDestroy { } private newViewerInit() { + + while (this.#newViewerSubs.length > 0) { + this.#newViewerSubs.pop().unsubscribe() + } - /* isn't this layer specific? */ - /* TODO this is layer specific. need a way to distinguish between different segmentation layers */ - this._s2$ = this.nehubaViewer.mouseOver.segment - .subscribe(({ segment, layer }) => { + this.#newViewerSubs.push( + + /* isn't this layer specific? */ + /* TODO this is layer specific. need a way to distinguish between different segmentation layers */ + this.nehubaViewer.mouseOver.segment.subscribe(({ segment, layer }) => { this.mouseOverSegment = segment this.mouseOverLayer = { ...layer } - }) + }), - if (this.initNav) { - this.setNavigationState(this.initNav) - this.initNav = null - } - - this._s8$ = this.nehubaViewer.mouseOver.segment.subscribe(({segment: segmentId, layer }) => { - this.mouseoverSegmentEmitter.emit({ - layer, - segmentId, - }) - }) + this.nehubaViewer.mouseOver.segment.subscribe(({segment: segmentId, layer }) => { + this.mouseoverSegmentEmitter.emit({ + layer, + segmentId, + }) + }), - // nehubaViewer.navigationState.all emits every time a new layer is added or removed from the viewer - this._s3$ = this.nehubaViewer.navigationState.all + // nehubaViewer.navigationState.all emits every time a new layer is added or removed from the viewer + this.nehubaViewer.navigationState.all .distinctUntilChanged((a, b) => { const { orientation: o1, @@ -733,71 +735,107 @@ export class NehubaViewerUnit implements OnDestroy { [0, 1, 2].every(idx => p1[idx] === p2[idx]) && z1 === z2 }) - .filter(() => !this.initNav) + /** + * somewhat another fudge factor + * navigationState.all occassionally emits slice zoom and perspective zoom that maeks no sense + * filter those out + * + * TODO find out why, and perhaps inform pavel about this + */ + .filter(val => !this.initNav && val?.perspectiveZoom > 10) .subscribe(({ orientation, perspectiveOrientation, perspectiveZoom, position, zoom }) => { - this.viewerState = { - orientation, - perspectiveOrientation, - perspectiveZoom, - zoom, - position, - positionReal : false, - } this.viewerPositionChange.emit({ orientation : Array.from(orientation), perspectiveOrientation : Array.from(perspectiveOrientation), - perspectiveZoom, + perspectiveZoom: perspectiveZoom / PERSPECTIVE_ZOOM_FUDGE_FACTOR, zoom, position: Array.from(position), positionReal : true, }) - }) + }), + + this.nehubaViewer.navigationState.position.inVoxels + .filter(v => typeof v !== 'undefined' && v !== null) + .subscribe((v: Float32Array) => { + const coordInVoxel = Array.from(v) + this.viewerPosInVoxel$.next(coordInVoxel) + if (this.#translateVoxelToReal) { + + const coordInReal = this.#translateVoxelToReal(coordInVoxel) + this.viewerPosInReal$.next(coordInReal as [number, number, number]) + } + }), - this._s4$ = this.nehubaViewer.navigationState.position.inRealSpace - .filter(v => typeof v !== 'undefined' && v !== null) - .subscribe(v => { - this.navPosReal = Array.from(v) as [number, number, number] - this.viewerPosInReal$.next(Array.from(v) as [number, number, number]) - }) - this._s5$ = this.nehubaViewer.navigationState.position.inVoxels - .filter(v => typeof v !== 'undefined' && v !== null) - .subscribe(v => { - this.navPosVoxel = Array.from(v) as [number, number, number] - this.viewerPosInVoxel$.next(Array.from(v)) - }) - this._s6$ = this.nehubaViewer.mousePosition.inRealSpace - .filter(v => typeof v !== 'undefined' && v !== null) - .subscribe(v => { - this.mousePosReal = Array.from(v) as [number, number, number] - this.mousePosInReal$.next(Array.from(v)) - }) - this._s7$ = this.nehubaViewer.mousePosition.inVoxels - .filter(v => typeof v !== 'undefined' && v !== null) - .subscribe(v => { - this.mousePosVoxel = Array.from(v) as [number, number, number] - this.mousePosInVoxel$.next(Array.from(v) as [number, number, number] ) - }) - } + this.nehubaViewer.mousePosition.inVoxels + .filter((v: Float32Array) => typeof v !== 'undefined' && v !== null) + .subscribe((v: Float32Array) => { + const coordInVoxel = Array.from(v) as [number, number, number] + this.mousePosInVoxel$.next( coordInVoxel ) + if (this.#translateVoxelToReal) { + + const coordInReal = this.#translateVoxelToReal(coordInVoxel) + this.mousePosInReal$.next( coordInReal ) + } + }), - private loadNewParcellation() { + ) - this._s$.forEach(_s$ => { - if (_s$) { _s$.unsubscribe() } + const coordSpListener = this.nehubaViewer.ngviewer.coordinateSpace.changed.add(() => { + const coordSp = this.nehubaViewer.ngviewer.coordinateSpace.value as NgCoordinateSpace + if (coordSp.valid) { + this.#translateVoxelToReal = (coordInVoxel: number[]) => { + return coordInVoxel.map((voxel, idx) => ( + translateUnit(coordSp.units[idx]) + * coordSp.scales[idx] + * voxel + )) + } + } }) + this.nehubaViewer.ngviewer.registerDisposer(coordSpListener) + + if (this.initNav) { + this.setNavigationState(this.initNav) + this.initNav = null + } + } private setColorMap(map: Map>) { this.multiNgIdColorMap = map + const mainDict: Record> = {} for (const [ ngId, cMap ] of map.entries()) { - const nMap = new Map() + const nRecord: Record = {} for (const [ key, cm ] of cMap.entries()) { - nMap.set(Number(key), cm) + nRecord[key] = rgbToHex([cm.red, cm.green, cm.blue]) + } + mainDict[ngId] = nRecord + + /** + * n.b. + * cannot restoreState on each individual layer + * it seems to create duplicated datasources, which eats memory, and wrecks opacity + */ + } + + /** + * n.b. 2 + * updating layer colormap seems to also mess up the position () + */ + + const layersManager = this.nehubaViewer.ngviewer.state.children.get("layers") + const position = this.nehubaViewer.ngviewer.state.children.get("position") + const prevPos = position.toJSON() + const layerJson = layersManager.toJSON() + for (const layer of layerJson) { + if (layer.name in mainDict) { + layer['segmentColors'] = mainDict[layer.name] } - this.nehubaViewer.batchAddAndUpdateSegmentColors( - nMap, - { name : ngId }) } + layersManager.restoreState(layerJson) + position.restoreState(prevPos) + this.#triggerMeshLoad$.next(null) } } @@ -828,51 +866,4 @@ export interface ViewerState { zoom: number } -export const ICOSAHEDRON = `# vtk DataFile Version 2.0 -Converted using https://github.com/HumanBrainProject/neuroglancer-scripts -ASCII -DATASET POLYDATA -POINTS 12 float --525731.0 0.0 850651.0 -525731.0 0.0 850651.0 --525731.0 0.0 -850651.0 -525731.0 0.0 -850651.0 -0.0 850651.0 525731.0 -0.0 850651.0 -525731.0 -0.0 -850651.0 525731.0 -0.0 -850651.0 -525731.0 -850651.0 525731.0 0.0 --850651.0 525731.0 0.0 -850651.0 -525731.0 0.0 --850651.0 -525731.0 0.0 -POLYGONS 20 80 -3 1 4 0 -3 4 9 0 -3 4 5 9 -3 8 5 4 -3 1 8 4 -3 1 10 8 -3 10 3 8 -3 8 3 5 -3 3 2 5 -3 3 7 2 -3 3 10 7 -3 10 6 7 -3 6 11 7 -3 6 0 11 -3 6 1 0 -3 10 1 6 -3 11 0 9 -3 2 11 9 -3 5 2 9 -3 11 2 7` - -declare const TextEncoder - -export const _encoder = new TextEncoder() -export const ICOSAHEDRON_VTK_URL = URL.createObjectURL( new Blob([ _encoder.encode(ICOSAHEDRON) ], {type : 'application/octet-stream'} )) - -export const FRAGMENT_MAIN_WHITE = `void main(){emitRGB(vec3(1.0,1.0,1.0));}` -export const FRAGMENT_EMIT_WHITE = `emitRGB(vec3(1.0, 1.0, 1.0));` -export const FRAGMENT_EMIT_RED = `emitRGB(vec3(1.0, 0.1, 0.12));` export const computeDistance = (pt1: [number, number], pt2: [number, number]) => ((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) ** 0.5 diff --git a/src/viewerModule/nehuba/types.ts b/src/viewerModule/nehuba/types.ts index c7684e637..aaa701600 100644 --- a/src/viewerModule/nehuba/types.ts +++ b/src/viewerModule/nehuba/types.ts @@ -13,3 +13,25 @@ export type TNehubaContextInfo = { regions: SxplrRegion[] }[] } + +export type Unit = 'm' +type Bound = { + lowerBounds: Float64Array + upperBounds: Float64Array +} +type BBox = { + transform: Float64Array + box: Bound +} + +export type NgCoordinateSpace = { + valid: boolean + rank: number + names: string[] + timestamps: number[] + ids: number[] + units: Unit[] + scales: Float64Array + boundingBoxes:BBox[] + bounds: Bound +} diff --git a/src/viewerModule/nehuba/userLayers/module.ts b/src/viewerModule/nehuba/userLayers/module.ts index 6de5343d8..ae8068454 100644 --- a/src/viewerModule/nehuba/userLayers/module.ts +++ b/src/viewerModule/nehuba/userLayers/module.ts @@ -8,6 +8,8 @@ import { UserLayerService } from "./service" import { MatButtonModule } from "@angular/material/button" import { MatTooltipModule } from "@angular/material/tooltip" import { UserLayerInfoCmp } from "./userlayerInfo/userlayerInfo.component" +import { UtilModule } from "src/util" +import { SpinnerModule } from "src/components/spinner" @NgModule({ imports: [ @@ -17,6 +19,8 @@ import { UserLayerInfoCmp } from "./userlayerInfo/userlayerInfo.component" MatDialogModule, MatButtonModule, MatTooltipModule, + UtilModule, + SpinnerModule, ], declarations: [UserLayerDragDropDirective, UserLayerInfoCmp], exports: [UserLayerDragDropDirective], diff --git a/src/viewerModule/nehuba/userLayers/service.ts b/src/viewerModule/nehuba/userLayers/service.ts index 3fe9d0a7d..41d16e71b 100644 --- a/src/viewerModule/nehuba/userLayers/service.ts +++ b/src/viewerModule/nehuba/userLayers/service.ts @@ -77,6 +77,7 @@ export class UserLayerService implements OnDestroy { options: { segments: ["1"], transform: xform, + type: "segmentation" }, } } @@ -121,6 +122,7 @@ export class UserLayerService implements OnDestroy { lowThreshold: meta.min || 0, highThreshold: meta.max || 1, }), + type: 'image' }, } } diff --git a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.component.ts b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.component.ts index fb6603223..204da49bf 100644 --- a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.component.ts +++ b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.component.ts @@ -1,6 +1,9 @@ -import { Component, Inject } from "@angular/core"; +import { Component, Inject, ViewChild } from "@angular/core"; import { MAT_DIALOG_DATA } from "@angular/material/dialog"; import { ARIA_LABELS, CONST } from 'common/constants' +import { BehaviorSubject, Subject, combineLatest, concat, of, timer } from "rxjs"; +import { map, switchMap, take } from "rxjs/operators"; +import { MediaQueryDirective } from "src/util/directives/mediaQuery.directive"; export type UserLayerInfoData = { layerName: string @@ -21,10 +24,51 @@ export type UserLayerInfoData = { export class UserLayerInfoCmp { ARIA_LABELS = ARIA_LABELS CONST = CONST + public HIDE_NG_TUNE_CTRL = { + ONLY_SHOW_OPACITY: 'lower_threshold,higher_threshold,brightness,contrast,colormap,hide-threshold-checkbox,hide-zero-value-checkbox' + } + + #mediaQuery = new Subject() + + @ViewChild(MediaQueryDirective, { read: MediaQueryDirective }) + set mediaQuery(val: MediaQueryDirective) { + this.#mediaQuery.next(val) + } + constructor( @Inject(MAT_DIALOG_DATA) public data: UserLayerInfoData ){ } - public showMoreInfo = false + + #showMore = new BehaviorSubject(false) + + view$ = concat( + timer(1000).pipe( + take(1), + map(() => null as { showMore: boolean, compact: boolean }) + ), + combineLatest([ + this.#showMore, + concat( + of(null as MediaQueryDirective), + this.#mediaQuery, + ).pipe( + switchMap(mediaQueryD => mediaQueryD + ? mediaQueryD.mediaBreakPoint$.pipe( + map(val => val >= 2) + ) + : of(false)) + ) + ]).pipe( + map(([ showMore, compact ]) => ({ + showMore, + compact, + })) + ) + ) + + toggleShowMore(){ + this.#showMore.next(!this.#showMore.value) + } } diff --git a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.style.css b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.style.css index e69de29bb..e8ba5734c 100644 --- a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.style.css +++ b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.style.css @@ -0,0 +1,11 @@ +.spinner +{ + justify-self: center; + display: inline-block; + margin: 1rem; +} + +:has(> .spinner) +{ + display: grid; +} diff --git a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.template.html b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.template.html index 0f878b732..c999b1c1b 100644 --- a/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.template.html +++ b/src/viewerModule/nehuba/userLayers/userlayerInfo/userlayerInfo.template.html @@ -1,37 +1,62 @@ -
+ +
- - - {{ data.filename }} - - - + - +
-
- - -
    + + + {{ data.filename }} {{ data.filename }} + + + + + + + + + + + +
    + + +
    + +
    • {{ warn }}
    + +
+ + + +
+
+
+ -
\ No newline at end of file + + + + diff --git a/src/viewerModule/nehuba/util.ts b/src/viewerModule/nehuba/util.ts index c8d56c7e5..0af076efd 100644 --- a/src/viewerModule/nehuba/util.ts +++ b/src/viewerModule/nehuba/util.ts @@ -1,7 +1,6 @@ import { InjectionToken } from '@angular/core' import { Observable, pipe } from 'rxjs' import { filter, scan, take } from 'rxjs/operators' -import { getViewer } from 'src/util/fn' import { NehubaViewerUnit } from './nehubaViewer/nehubaViewer.component' import { userInterface } from 'src/state' @@ -202,7 +201,7 @@ export const takeOnePipe = () => { * * 4 ??? */ - const panels = getViewer()['display']['panels'] + const panels = window['viewer']['display']['panels'] const panelEls = Array.from(panels).map(({ element }) => element) const identifySrcElement = (element: HTMLElement) => { diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index 60ba8899d..522d54c51 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -1,8 +1,8 @@ import { Component, Output, EventEmitter, ElementRef, OnDestroy, AfterViewInit, Inject, Optional, ChangeDetectionStrategy } from "@angular/core"; import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.interface"; import { combineLatest, concat, forkJoin, from, merge, NEVER, Observable, of, Subject } from "rxjs"; -import { catchError, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, switchMap, withLatestFrom } from "rxjs/operators"; -import { ComponentStore } from "src/viewerModule/componentStore"; +import { catchError, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, startWith, switchMap, tap, withLatestFrom } from "rxjs/operators"; +import { ComponentStore, LockError } from "src/viewerModule/componentStore"; import { select, Store } from "@ngrx/store"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { MatSnackBar } from "@angular/material/snack-bar"; @@ -27,7 +27,7 @@ type TInternalState = { mode: string hemisphere: 'left' | 'right' | 'both' } -const pZoomFactor = 5e3 +const pZoomFactor = 7e3 type THandlingCustomEv = { regions: SxplrRegion[] @@ -107,11 +107,15 @@ type LateralityRecord = Record const threshold = 1e-3 function cameraNavsAreSimilar(c1: TCameraOrientation, c2: TCameraOrientation){ + + // if same reference, return true if (c1 === c2) return true - if (!!c1 && !!c2) return true - if (!c1 && !!c2) return false - if (!c2 && !!c1) return false + // if both falsy, return true + if (!c1 && !c2) return true + + if (!c1 && c2) return false + if (!c2 && c1) return false if (Math.abs(c1.perspectiveZoom - c2.perspectiveZoom) > threshold) return false if ([0, 1, 2, 3].some( @@ -141,9 +145,57 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit viewerEvent = new EventEmitter>() private domEl: HTMLElement - private mainStoreCameraNav: TCameraOrientation = null - private localCameraNav: TCameraOrientation = null + #storeNavigation = this.store$.pipe( + select(atlasSelection.selectors.navigation) + ) + + #componentStoreNavigation = this.navStateStoreRelay.select(s => s) + + #internalNavigation = this.#cameraEv$.pipe( + filter(v => !!v && !!(this.tsRef?.camera?.matrix)), + map(() => { + const { tsRef } = this + return { + _po: null, + _pz: null, + _calculate(){ + if (!tsRef) return + const THREE = (window as any).ThreeSurfer.THREE + + const q = new THREE.Quaternion() + const t = new THREE.Vector3() + const s = new THREE.Vector3() + + /** + * ThreeJS interpretes the scene differently to neuroglancer in subtle ways. + * At [0, 0, 0, 1] decomposed camera quaternion, for example, + * - ThreeJS: view from superior -> inferior, anterior as top, right hemisphere as right + * - NG: view from from inferior -> superior, posterior as top, left hemisphere as right + * + * multiplying the exchange factor [-1, 0, 0, 0] converts ThreeJS convention to NG convention + */ + const cameraM = tsRef.camera.matrix + cameraM.decompose(t, q, s) + const exchangeFactor = new THREE.Quaternion(-1, 0, 0, 0) + this._po = q.multiply(exchangeFactor).toArray() + this._pz = t.length() * pZoomFactor // use zoom as used in main store + }, + get perspectiveOrientation(){ + if (!this._po) { + this._calculate() + } + return this._po + }, + get perspectiveZoom() { + if (!this._pz) { + this._calculate() + } + return this._pz + } + } as TCameraOrientation + }) + ) private internalStateNext: (arg: TInteralStatePayload) => void @@ -336,9 +388,9 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit constructor( private effect: ThreeSurferEffects, - private el: ElementRef, + el: ElementRef, private store$: Store, - private navStateStoreRelay: ComponentStore<{ perspectiveOrientation: [number, number, number, number], perspectiveZoom: number }>, + private navStateStoreRelay: ComponentStore, private sapi: SAPI, private snackbar: MatSnackBar, @Optional() intViewerStateSvc: ViewerInternalStateSvc, @@ -379,7 +431,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit const handleClick = (ev: MouseEvent) => { // if does not click inside container, ignore - if (!(this.el.nativeElement as HTMLElement).contains(ev.target as HTMLElement)) { + if (!(el.nativeElement as HTMLElement).contains(ev.target as HTMLElement)) { return true } @@ -404,88 +456,87 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit ) } - this.domEl = this.el.nativeElement + this.domEl = el.nativeElement /** * subscribe to camera custom event */ - const cameraSub = this.#cameraEv$.pipe( - filter(v => !!v), - debounceTime(160) - ).subscribe(() => { - - const THREE = (window as any).ThreeSurfer.THREE - - const q = new THREE.Quaternion() - const t = new THREE.Vector3() - const s = new THREE.Vector3() - - /** - * ThreeJS interpretes the scene differently to neuroglancer in subtle ways. - * At [0, 0, 0, 1] decomposed camera quaternion, for example, - * - ThreeJS: view from superior -> inferior, anterior as top, right hemisphere as right - * - NG: view from from inferior -> superior, posterior as top, left hemisphere as right - * - * multiplying the exchange factor [-1, 0, 0, 0] converts ThreeJS convention to NG convention - */ - const cameraM = this.tsRef.camera.matrix - cameraM.decompose(t, q, s) - const exchangeFactor = new THREE.Quaternion(-1, 0, 0, 0) - + const setReconcilState = merge( + this.#internalNavigation.pipe( + filter(v => !!v), + tap(() => { + try { + this.releaseRelayLock = this.navStateStoreRelay.getLock() + } catch (e) { + if (!(e instanceof LockError)) { + throw e + } + } + }), + debounceTime(160), + tap(() => { + if (this.releaseRelayLock) { + this.releaseRelayLock() + this.releaseRelayLock = null + } else { + console.warn(`this.releaseRelayLock not aquired, component may not function properly`) + } + }) + ), + this.#storeNavigation, + ).pipe( + filter(v => !!v) + ).subscribe(nav => { try { this.navStateStoreRelay.setState({ - perspectiveOrientation: q.multiply(exchangeFactor).toArray(), - perspectiveZoom: t.length() + perspectiveOrientation: nav.perspectiveOrientation, + perspectiveZoom: nav.perspectiveZoom }) - } catch (_e) { - // LockError, ignore + } catch (e) { + if (!(e instanceof LockError)) { + throw e + } } }) this.onDestroyCb.push( - () => cameraSub.unsubscribe() + () => setReconcilState.unsubscribe() ) /** * subscribe to navstore relay store and negotiate setting global state */ - const navStateSub = this.navStateStoreRelay.select(s => s).subscribe(v => { - this.store$.dispatch( - atlasSelection.actions.setNavigation({ + const reconciliatorSub = combineLatest([ + this.#storeNavigation.pipe( + startWith(null as TCameraOrientation) + ), + this.#componentStoreNavigation.pipe( + startWith(null as TCameraOrientation), + ), + this.#internalNavigation.pipe( + startWith(null as TCameraOrientation), + ) + ]).pipe( + debounceTime(160), + filter(() => !this.navStateStoreRelay.isLocked) + ).subscribe(([ storeNav, reconcilNav, internalNav ]) => { + if (!cameraNavsAreSimilar(storeNav, reconcilNav) && reconcilNav) { + this.store$.dispatch(atlasSelection.actions.setNavigation({ navigation: { position: [0, 0, 0], orientation: [0, 0, 0, 1], zoom: 1e6, - perspectiveOrientation: v.perspectiveOrientation, - perspectiveZoom: v.perspectiveZoom * pZoomFactor + perspectiveOrientation: reconcilNav.perspectiveOrientation, + perspectiveZoom: reconcilNav.perspectiveZoom } - }) - ) - }) - - this.onDestroyCb.push( - () => navStateSub.unsubscribe() - ) - - /** - * subscribe to main store and negotiate with relay to set camera - */ - const navSub = this.store$.pipe( - select(atlasSelection.selectors.navigation), - filter(v => !!v), - ).subscribe(nav => { - const { perspectiveOrientation, perspectiveZoom } = nav - this.mainStoreCameraNav = { - perspectiveOrientation, - perspectiveZoom + })) } - if (!cameraNavsAreSimilar(this.mainStoreCameraNav, this.localCameraNav)) { - this.relayStoreLock = this.navStateStoreRelay.getLock() + if (!cameraNavsAreSimilar(reconcilNav, internalNav) && reconcilNav) { const THREE = (window as any).ThreeSurfer.THREE - const cameraQuat = new THREE.Quaternion(...this.mainStoreCameraNav.perspectiveOrientation) - const cameraPos = new THREE.Vector3(0, 0, this.mainStoreCameraNav.perspectiveZoom / pZoomFactor) + const cameraQuat = new THREE.Quaternion(...reconcilNav.perspectiveOrientation) + const cameraPos = new THREE.Vector3(0, 0, reconcilNav.perspectiveZoom / pZoomFactor) /** * ThreeJS interpretes the scene differently to neuroglancer in subtle ways. @@ -501,19 +552,18 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit cameraPos.applyQuaternion(cameraQuat) this.toTsRef(tsRef => { tsRef.camera.position.copy(cameraPos) - if (this.relayStoreLock) this.relayStoreLock() }) } }) this.onDestroyCb.push( - () => navSub.unsubscribe() + () => reconciliatorSub.unsubscribe() ) } private tsRef: TThreeSurfer - private relayStoreLock: () => void = null + private releaseRelayLock: () => void = null private tsRefInitCb: ((tsRef: any) => void)[] = [] private toTsRef(callback: (tsRef: any) => void) { if (this.tsRef) { diff --git a/third_party/vanilla.html b/third_party/vanilla.html index 38206ec40..5513962d4 100644 --- a/third_party/vanilla.html +++ b/third_party/vanilla.html @@ -8,6 +8,8 @@ + +
diff --git a/worker/worker.js b/worker/worker.js index ab8cc8371..314e08380 100644 --- a/worker/worker.js +++ b/worker/worker.js @@ -11,15 +11,6 @@ if (typeof self.importScripts === 'function') self.importScripts('./worker-plot if (typeof self.importScripts === 'function') self.importScripts('./worker-nifti.js') if (typeof self.importScripts === 'function') self.importScripts('./worker-typedarray.js') -/** - * TODO migrate processing functionalities to other scripts - * see worker-plotly.js - */ - -const validTypes = [ - 'GET_USERLANDMARKS_VTK', - 'PROPAGATE_PARC_REGION_ATTR' -] const VALID_METHOD = { PROCESS_PLOTLY: `PROCESS_PLOTLY`, @@ -39,177 +30,10 @@ const VALID_METHODS = [ VALID_METHOD.PROCESS_TYPED_ARRAY_RAW, ] -const validOutType = [ - 'ASSEMBLED_USERLANDMARKS_VTK', -] - -const getVertexHeader = (numVertex) => `POINTS ${numVertex} float` - -const getPolyHeader = (numPoly) => `POLYGONS ${numPoly} ${4 * numPoly}` - -const getLabelHeader = (numVertex) => `POINT_DATA ${numVertex} -SCALARS label unsigned_char 1 -LOOKUP_TABLE none` - -//pos in nm -const getIcoVertex = (pos, scale) => `-525731.0 0.0 850651.0 -525731.0 0.0 850651.0 --525731.0 0.0 -850651.0 -525731.0 0.0 -850651.0 -0.0 850651.0 525731.0 -0.0 850651.0 -525731.0 -0.0 -850651.0 525731.0 -0.0 -850651.0 -525731.0 -850651.0 525731.0 0.0 --850651.0 525731.0 0.0 -850651.0 -525731.0 0.0 --850651.0 -525731.0 0.0` - .split('\n') - .map(line => - line - .split(' ') - .map((string, idx) => (Number(string) * (scale ? scale : 1) + pos[idx]).toString() ) - .join(' ') - ) - .join('\n') - - -const getIcoPoly = (startingIdx) => `3 1 4 0 -3 4 9 0 -3 4 5 9 -3 8 5 4 -3 1 8 4 -3 1 10 8 -3 10 3 8 -3 8 3 5 -3 3 2 5 -3 3 7 2 -3 3 10 7 -3 10 6 7 -3 6 11 7 -3 6 0 11 -3 6 1 0 -3 10 1 6 -3 11 0 9 -3 2 11 9 -3 5 2 9 -3 11 2 7` - .split('\n') - .map((line) => - line - .split(' ') - .map((v,idx) => idx === 0 ? v : (Number(v) + startingIdx).toString() ) - .join(' ') - ) - .join('\n') - -const getMeshVertex = (vertices) => vertices.map(vertex => vertex.join(' ')).join('\n') -const getMeshPoly = (polyIndices, currentIdx) => polyIndices.map(triplet => - '3 '.concat(triplet.map(index => - index + currentIdx - ).join(' ')) -).join('\n') - - const encoder = new TextEncoder() -const parseLmToVtk = (landmarks, scale) => { - - const reduce = landmarks.reduce((acc,curr,idx) => { - //curr : null | [number,number,number] | [ [number,number,number], [number,number,number], [number,number,number] ][] - if(curr === null) return acc - if(!isNaN(curr[0])) - /** - * point primitive, render icosahedron - */ - return { - currentVertexIndex : acc.currentVertexIndex + 12, - vertexString : acc.vertexString.concat(getIcoVertex(curr, scale)), - polyCount : acc.polyCount + 20, - polyString : acc.polyString.concat(getIcoPoly(acc.currentVertexIndex)), - labelString : acc.labelString.concat(Array(12).fill(idx.toString()).join('\n')) - } - else{ - //curr[0] : [number,number,number][] vertices - //curr[1] : [number,number,number][] indices for the vertices that poly forms - - /** - * poly primitive - */ - const vertices = curr[0] - const polyIndices = curr[1] - - return { - currentVertexIndex : acc.currentVertexIndex + vertices.length, - vertexString : acc.vertexString.concat(getMeshVertex(vertices)), - polyCount : acc.polyCount + polyIndices.length, - polyString : acc.polyString.concat(getMeshPoly(polyIndices, acc.currentVertexIndex)), - labelString : acc.labelString.concat(Array(vertices.length).fill(idx.toString()).join('\n')) - } - } - }, { - currentVertexIndex : 0, - vertexString : [], - polyCount : 0, - polyString: [], - labelString : [], - }) - - // if no vertices are been rendered, do not replace old - if(reduce.currentVertexIndex === 0) - return false - - return vtkHeader - .concat('\n') - .concat(getVertexHeader(reduce.currentVertexIndex)) - .concat('\n') - .concat(reduce.vertexString.join('\n')) - .concat('\n') - .concat(getPolyHeader(reduce.polyCount)) - .concat('\n') - .concat(reduce.polyString.join('\n')) - .concat('\n') - .concat(getLabelHeader(reduce.currentVertexIndex)) - .concat('\n') - .concat(reduce.labelString.join('\n')) -} - let userLandmarkVtkUrl -const getuserLandmarksVtk = (action) => { - const landmarks = action.landmarks - const scale = action.scale - ? action.scale - : 2.8 - - /** - * if userlandmarks vtk is empty, that means user removed all landmarks - * thus, removing revoking URL, and send null as assembled userlandmark vtk - */ - if (landmarks.length === 0) { - - if(userLandmarkVtkUrl) URL.revokeObjectURL(userLandmarkVtkUrl) - - postMessage({ - type: 'ASSEMBLED_USERLANDMARKS_VTK' - }) - - return - } - - const vtk = parseLmToVtk(landmarks, scale) - if(!vtk) return - - if(userLandmarkVtkUrl) - URL.revokeObjectURL(userLandmarkVtkUrl) - - userLandmarkVtkUrl = URL.createObjectURL(new Blob( [encoder.encode(vtk)], {type : 'application/octet-stream'} )) - postMessage({ - type : 'ASSEMBLED_USERLANDMARKS_VTK', - url : userLandmarkVtkUrl - }) -} - let plotyVtkUrl onmessage = (message) => { @@ -369,16 +193,4 @@ onmessage = (message) => { }) return } - - if(validTypes.findIndex(type => type === message.data.type) >= 0){ - switch(message.data.type){ - case 'GET_USERLANDMARKS_VTK': - getuserLandmarksVtk(message.data) - return - default: - console.warn('unhandled worker action', message) - } - } else { - console.warn('unhandled worker action', message) - } }