diff --git a/angular.json b/angular.json index dcbadb4911f1203a6204541cc20e00b2ec00947b..46d2ba12824d187f38aab94428da75566e9fccee 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", diff --git a/common/constants.js b/common/constants.js index d3fe2b2b308520de3e5e9142d58b681c9dbc2a5a..d3f6f24546090062459459f82f8652db30691244 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 f4b3dcd07ead1a7cad8f3c987db8619c1d16e219..bc3444df3848e83ef16d9e9a8e9e2d3391cbbb58 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('<atlas-viewer>', `<atlas-viewer data-error="${iavError.replace(/"/g, '"')}">`) - 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('<atlas-viewer>', `<atlas-viewer ${attr}>`) + + res.status(200).send(returnTemplate) }) app.get('/ready', async (req, res) => { diff --git a/docs/releases/v2.12.0.md b/docs/releases/v2.12.0.md index ba23e2fa1c0a5e1dc3cdb00fdfef918654e02bf0..99ad15e06c399f56481ee50d135ecb889ae3e1ec 100644 --- a/docs/releases/v2.12.0.md +++ b/docs/releases/v2.12.0.md @@ -13,4 +13,5 @@ ## 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. +- 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/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index b5aaf5bc4a03501f57dda3e3312240f67b800343..0750c09315d429dee2d64b41680eecdd6288b35a 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", @@ -94,7 +95,12 @@ export class SAPI{ */ static get BsEndpoint$(): Observable<string> { 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 diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index c28d15f9a80133259294b4de407a543014adcf78..b80a7c4de7fa4836e9916ccff47e5aa228385fdf 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<boolean> ) { - 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/viewerModule/nehuba/mesh.service/mesh.service.spec.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts index 7db5392a46b74554df8c9e2db2ab8d5e221ab44b..f5bea95d183a58649bb52caedbd27430cfc2b47c 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/nehubaViewer/nehubaViewer.component.spec.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts index ed281a389241e4a8fbaec5c004557601f2af1aa2..89b3a42f99d93c8222d314784bfa9b6057de5aa1 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts @@ -307,8 +307,10 @@ describe('> nehubaViewer.component.ts', () => { describe('> # setColorMap', () => { let nehubaViewerSpy: any let ngViewerStatechildrenGetSpy = jasmine.createSpy('get') - let toJsonSpy = jasmine.createSpy('toJsonSpy') - let restoreStateSpy = jasmine.createSpy('restoreStateSpy') + 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' @@ -326,11 +328,23 @@ describe('> nehubaViewer.component.ts', () => { } } - ngViewerStatechildrenGetSpy.and.returnValue({ - toJSON: toJsonSpy, - restoreState: restoreStateSpy, + 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`) }) - toJsonSpy.and.returnValue([{ + posToJsonSpy.and.returnValue([1.1, 2.2, 3.3]) + layersMngerToJsonSpy.and.returnValue([{ name: ngId1 }, { name: ngId2 @@ -338,8 +352,8 @@ describe('> nehubaViewer.component.ts', () => { }) afterEach(() => { ngViewerStatechildrenGetSpy.calls.reset() - toJsonSpy.calls.reset() - restoreStateSpy.calls.reset() + layersMngerToJsonSpy.calls.reset() + layerMgerRestoreStateSpy.calls.reset() }) it('> calls nehubaViewer.restoreState', () => { const fixture = TestBed.createComponent(NehubaViewerUnit) @@ -359,7 +373,7 @@ describe('> nehubaViewer.component.ts', () => { fixture.componentInstance['setColorMap'](mainMap) - expect(restoreStateSpy).toHaveBeenCalledOnceWith([{ + expect(layerMgerRestoreStateSpy).toHaveBeenCalledOnceWith([{ name: ngId1, segmentColors: { 1: rgbToHex([100, 100, 100]), @@ -372,6 +386,10 @@ describe('> nehubaViewer.component.ts', () => { 2: rgbToHex([20, 20, 20]), } }]) + + 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 017187edb71c29fc59bc0dd589790a6dc3bb4e02..0bee1941f311aee0df808e66678299649bcc6128 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -505,7 +505,6 @@ export class NehubaViewerUnit implements OnDestroy { ...rest, ...(transform ? { transform } : {}) } - console.log(combined) viewer.layerManager.addManagedLayer( viewer.layerSpecification.getLayer(key, combined)) @@ -820,7 +819,14 @@ export class NehubaViewerUnit implements OnDestroy { */ } + /** + * 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) { @@ -828,6 +834,7 @@ export class NehubaViewerUnit implements OnDestroy { } } layersManager.restoreState(layerJson) + position.restoreState(prevPos) this.#triggerMeshLoad$.next(null) } } diff --git a/third_party/vanilla.html b/third_party/vanilla.html index be223e1e38ab861e649e4c4666e894d0a48c6f8c..5513962d41c1a6cb07e54a164e06a6bae15784f5 100644 --- a/third_party/vanilla.html +++ b/third_party/vanilla.html @@ -9,6 +9,7 @@ <script src="main.bundle.js"></script> <link rel="stylesheet" href="vanilla_styles.css"> <link rel="stylesheet" href="main.css"> + <link rel="stylesheet" href="vanillaMain.css"> </head> <body> <div id="neuroglancer-container"></div>