diff --git a/common/constants.js b/common/constants.js index c5f7c056767d940539781b8a96efeee7d321fb6f..580136b22ef41d52cd3abebd38c0de8853ac7a6f 100644 --- a/common/constants.js +++ b/common/constants.js @@ -14,6 +14,9 @@ SHOW_ORIGIN_DATASET: `Show probabilistic map`, SHOW_CONNECTIVITY_DATA: `Show connectivity data`, SHOW_IN_OTHER_REF_SPACE: `Show in other reference space`, - AVAILABILITY_IN_OTHER_REF_SPACE: 'Availability in other reference spaces' + AVAILABILITY_IN_OTHER_REF_SPACE: 'Availability in other reference spaces', + + // additional volumes + TOGGLE_SHOW_LAYER_CONTROL: `Show layer control`, } })(typeof exports === 'undefined' ? module.exports : exports) diff --git a/docs/releases/v2.2.1.md b/docs/releases/v2.2.1.md new file mode 100644 index 0000000000000000000000000000000000000000..1934dc90e4a65c5cc990c948d392076602d4e490 --- /dev/null +++ b/docs/releases/v2.2.1.md @@ -0,0 +1,7 @@ +# v2.2.1 + +26 May 2020 + +## Bugfixes + +- fixed connectivity menu showing non existent connectivity data diff --git a/e2e/src/advanced/browsingForDatasets.prod.e2e-spec.js b/e2e/src/advanced/browsingForDatasets.prod.e2e-spec.js index b5cb270a7850655d8b580fa9f3f5ab7ff31935fb..42969809b7a962b46ac912fdcfeec99af8bec2ae 100644 --- a/e2e/src/advanced/browsingForDatasets.prod.e2e-spec.js +++ b/e2e/src/advanced/browsingForDatasets.prod.e2e-spec.js @@ -67,7 +67,7 @@ const area = 'Area hOc1 (V1, 17, CalcS)' const receptorName = `Density measurements of different receptors for Area hOc1 (V1, 17, CalcS) [human, v1.0]` -describe('> dataset previews', () => { +describe('> receptor dataset previews', () => { let iavPage beforeEach(async () => { iavPage = new AtlasPage() @@ -121,4 +121,41 @@ describe('> dataset previews', () => { const modalHasImage = await iavPage.modalHasChild('div[data-img-src]') expect(modalHasImage).toEqual(true) }) -}) \ No newline at end of file +}) + +describe('> pmap dataset preview', () => { + let iavPage + + beforeAll(async () => { + // loads pmap and centers on hot spot + const url = `/?templateSelected=MNI+152+ICBM+2009c+Nonlinear+Asymmetric&parcellationSelected=JuBrain+Cytoarchitectonic+Atlas&cNavigation=0.0.0.-W000..2_ZG29.-ASCS.2-8jM2._aAY3..BSR0..dABI~.525x0~.7iMV..1EPC&niftiLayers=https%3A%2F%2Fneuroglancer.humanbrainproject.eu%2Fprecomputed%2FJuBrain%2F17%2Ficbm152casym%2Fpmaps%2FVisual_hOc1_l_N10_nlin2MNI152ASYM2009C_2.4_publicP_d3045ee3c0c4de9820eb1516d2cc72bb.nii.gz&previewingDatasetFiles=%5B%7B"datasetId"%3A"minds%2Fcore%2Fdataset%2Fv1.0.0%2F5c669b77-c981-424a-858d-fe9f527dbc07"%2C"filename"%3A"Area+hOc1+%28V1%2C+17%2C+CalcS%29+%5Bv2.4%2C+ICBM+2009c+Asymmetric%2C+left+hemisphere%5D"%7D%5D` + iavPage = new AtlasPage() + await iavPage.init() + await iavPage.goto(url) + await iavPage.waitUntilAllChunksLoaded() + }) + + it('> can display pmap', async () => { + const { red, green, blue } = await iavPage.getRgbAt({position: [200, 597]}) + expect(red).toBeGreaterThan(green) + expect(red).toBeGreaterThan(blue) + }) + + it('> on update of layer control, pmap retains', async () => { + // by default, additional layer control is collapsed + await iavPage.toggleLayerControl() + await iavPage.wait(500) + await iavPage.toggleNthLayerControl(0) + await iavPage.wait(5500) + + // interact with control + await iavPage.click(`[aria-label="Remove background"]`) + await iavPage.wait(500) + + // color map should be unchanged + const { red, green, blue } = await iavPage.getRgbAt({position: [200, 597]}) + expect(red).toBeGreaterThan(green) + expect(red).toBeGreaterThan(blue) + + }) +}) diff --git a/e2e/src/advanced/urlParsing.prod.e2e-spec.js b/e2e/src/advanced/urlParsing.prod.e2e-spec.js index 944dce4faf6e16b829a2496118803bac1150180a..21c9c6b33183574cd811e128e22fbd80b6ba2175 100644 --- a/e2e/src/advanced/urlParsing.prod.e2e-spec.js +++ b/e2e/src/advanced/urlParsing.prod.e2e-spec.js @@ -67,35 +67,7 @@ describe('> url parsing', () => { await iavPage.goto(url) await iavPage.clearAlerts() - // TODO use screenshot API when merg v2.3.0 - const screenshotData = await iavPage._driver.takeScreenshot() - const [ red, green, blue ] = await iavPage._driver.executeAsyncScript(() => { - - const dataUri = arguments[0] - const pos = arguments[1] - const dim = arguments[2] - const cb = arguments[arguments.length - 1] - - const img = new Image() - img.onload = () => { - const canvas = document.createElement('canvas') - canvas.width = dim[0] - canvas.height = dim[1] - - const ctx = canvas.getContext('2d') - ctx.drawImage(img, 0, 0) - const imgData = ctx.getImageData(0, 0, dim[0], dim[1]) - - const idx = (dim[0] * pos[1] + pos[0]) * 4 - const red = imgData.data[idx] - const green = imgData.data[idx + 1] - const blue = imgData.data[idx + 2] - cb([red, green, blue]) - } - img.src = dataUri - - }, `data:image/png;base64,${screenshotData}`, [600, 490], [800, 796]) - + const { red, green, blue } = await iavPage.getRgbAt({ position: [600, 490] }) expect(red).toBeGreaterThan(0) expect(red).toEqual(green) expect(red).toEqual(blue) @@ -106,34 +78,7 @@ describe('> url parsing', () => { await iavPage.goto(url) await iavPage.clearAlerts() - // TODO use screenshot API when merg v2.3.0 - const screenshotData = await iavPage._driver.takeScreenshot() - const [ red, green, blue ] = await iavPage._driver.executeAsyncScript(() => { - - const dataUri = arguments[0] - const pos = arguments[1] - const dim = arguments[2] - const cb = arguments[arguments.length - 1] - - const img = new Image() - img.onload = () => { - const canvas = document.createElement('canvas') - canvas.width = dim[0] - canvas.height = dim[1] - - const ctx = canvas.getContext('2d') - ctx.drawImage(img, 0, 0) - const imgData = ctx.getImageData(0, 0, dim[0], dim[1]) - - const idx = (dim[0] * pos[1] + pos[0]) * 4 - const red = imgData.data[idx] - const green = imgData.data[idx + 1] - const blue = imgData.data[idx + 2] - cb([red, green, blue]) - } - img.src = dataUri - - }, `data:image/png;base64,${screenshotData}`, [600, 490], [800, 796]) + const { red, green, blue } = await iavPage.getRgbAt({ position: [600, 490] }) expect(red).toBeGreaterThan(0) expect(red).toEqual(green) diff --git a/e2e/src/layout/scrollDatasetContainer.prod.e2e-spec.js b/e2e/src/layout/scrollDatasetContainer.prod.e2e-spec.js index 7d80e04fcb7edce754ba20e99afcd28c562c2b19..198be8f230c3c3acddcad6e70dd7123cd57221cf 100644 --- a/e2e/src/layout/scrollDatasetContainer.prod.e2e-spec.js +++ b/e2e/src/layout/scrollDatasetContainer.prod.e2e-spec.js @@ -37,6 +37,8 @@ describe('> scroll dataset container', () => { countdown -- }while(countdown > 0) + await iavPage.wait(500) + const val = await iavPage.getScrollStatus(scrollContainerCssSelector) expect(val).toBeGreaterThanOrEqual(10000) }) diff --git a/e2e/src/material-util.js b/e2e/src/material-util.js new file mode 100644 index 0000000000000000000000000000000000000000..04f9c994fac5310b6d1c50ab915ec3f1ddcc215f --- /dev/null +++ b/e2e/src/material-util.js @@ -0,0 +1,19 @@ +const { By } = require('selenium-webdriver') + +async function polyFillClick(cssSelector){ + if (!cssSelector) throw new Error(`click method needs to define a css selector`) + const webEl = this._browser.findElement( By.css(cssSelector) ) + try { + await webEl.click() + } catch (e) { + const id = await webEl.getAttribute('id') + const newId = id.replace(/-input$/, '') + await this._browser.findElement( + By.id(newId) + ).click() + } +} + +module.exports = { + polyFillClick +} diff --git a/e2e/src/util.js b/e2e/src/util.js index 134ca978f603c656f3b11210ea8984e2615d1219..2830aad1c7c81f861237514157ed24cb3db95917 100644 --- a/e2e/src/util.js +++ b/e2e/src/util.js @@ -6,6 +6,9 @@ if (ATLAS_URL.length === 0) throw new Error(`ATLAS_URL must either be left unset if (ATLAS_URL[ATLAS_URL.length - 1] === '/') throw new Error(`ATLAS_URL should not trail with a slash: ${ATLAS_URL}`) const { By, WebDriver, Key } = require('selenium-webdriver') const CITRUS_LIGHT_URL = `https://unpkg.com/citruslight@0.1.0/citruslight.js` +const { polyFillClick } = require('./material-util') + +const { ARIA_LABELS } = require('../../common/constants') function getActualUrl(url) { return /^http\:\/\//.test(url) ? url : `${ATLAS_URL}/${url.replace(/^\//, '')}` @@ -92,6 +95,40 @@ class WdBase{ return result } + async getRgbAt({ position } = {}, cssSelector = null){ + if (!position) throw new Error(`position is required for getRgbAt`) + const { x, y } = verifyPosition(position) + const screenshotData = await this.takeScreenshot(cssSelector) + const [ red, green, blue ] = await this._driver.executeAsyncScript(() => { + + const dataUri = arguments[0] + const pos = arguments[1] + const dim = arguments[2] + const cb = arguments[arguments.length - 1] + + const img = new Image() + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = dim[0] + canvas.height = dim[1] + + const ctx = canvas.getContext('2d') + ctx.drawImage(img, 0, 0) + const imgData = ctx.getImageData(0, 0, dim[0], dim[1]) + + const idx = (dim[0] * pos[1] + pos[0]) * 4 + const red = imgData.data[idx] + const green = imgData.data[idx + 1] + const blue = imgData.data[idx + 2] + cb([red, green, blue]) + } + img.src = dataUri + + }, `data:image/png;base64,${screenshotData}`, [x, y], [800, 796]) + + return { red, green, blue } + } + async switchIsChecked(cssSelector){ if (!cssSelector) throw new Error(`switchChecked method requies css selector`) const checked = await this._browser @@ -101,8 +138,7 @@ class WdBase{ } async click(cssSelector){ - if (!cssSelector) throw new Error(`click method needs to define a css selector`) - await this._browser.findElement( By.css(cssSelector) ).click() + return await polyFillClick.bind(this)(cssSelector) } async getText(cssSelector){ @@ -561,6 +597,13 @@ class WdLayoutPage extends WdBase{ .click() } + async toggleNthLayerControl(idx) { + const els = await this._getAdditionalLayerControl() + .findElements( By.css(`[aria-label="${ARIA_LABELS.TOGGLE_SHOW_LAYER_CONTROL}"]`)) + if (!els[idx]) throw new Error(`toggleNthLayerControl index out of bound: accessor ${idx} with length ${els.length}`) + await els[idx].click() + } + // Signin banner _getToolsIcon(){ return this._driver diff --git a/mkdocs.yml b/mkdocs.yml index e2436413086040713a7cc5eef21856c5a5d2ae8b..25324af62f39c497367e7fa3f87218518c948af8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,7 @@ pages: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.2.1: 'releases/v2.2.1.md' - v2.2.0: 'releases/v2.2.0.md' - v2.1.3: 'releases/v2.1.3.md' - v2.1.2: 'releases/v2.1.2.md' diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts index 30cde5aded6877e7df1624f37333c1e17135d61b..ce7645f1ee5fa466f61470866c3a92b2fd1b1463 100644 --- a/src/atlasViewer/atlasViewer.constantService.service.ts +++ b/src/atlasViewer/atlasViewer.constantService.service.ts @@ -9,8 +9,6 @@ import { IavRootStoreInterface } from "../services/stateStore.service"; import { AtlasWorkerService } from "./atlasViewer.workerService.service"; export const CM_THRESHOLD = `0.05` -export const CM_MATLAB_JET = `float r;if( x < 0.7 ){r = 4.0 * x - 1.5;} else {r = -4.0 * x + 4.5;}float g;if (x < 0.5) {g = 4.0 * x - 0.5;} else {g = -4.0 * x + 3.5;}float b;if (x < 0.3) {b = 4.0 * x + 0.5;} else {b = -4.0 * x + 2.5;}float a = 1.0;` -export const GLSL_COLORMAP_JET = `void main(){float x = toNormalized(getDataValue());${CM_MATLAB_JET}if(x>${CM_THRESHOLD}){emitRGB(vec3(r,g,b));}else{emitTransparent();}}` const getUniqueId = () => Math.round(Math.random() * 1e16).toString(16) diff --git a/src/atlasViewer/atlasViewer.urlUtil.ts b/src/atlasViewer/atlasViewer.urlUtil.ts index 025980709189be5ae41b6189e782011ecd9976ea..6447f1f7fb7b7d8860121eb3ef573cbe1a64f524 100644 --- a/src/atlasViewer/atlasViewer.urlUtil.ts +++ b/src/atlasViewer/atlasViewer.urlUtil.ts @@ -2,8 +2,9 @@ import { getGetRegionFromLabelIndexId } from "src/services/effect/effect"; import { mixNgLayers } from "src/services/state/ngViewerState.store"; import { PLUGINSTORE_CONSTANTS } from 'src/services/state/pluginState.store' import { generateLabelIndexId, getNgIdLabelIndexFromRegion, IavRootStoreInterface } from "../services/stateStore.service"; -import { decodeToNumber, encodeNumber, GLSL_COLORMAP_JET, separator } from "./atlasViewer.constantService.service"; +import { decodeToNumber, encodeNumber, separator } from "./atlasViewer.constantService.service"; import { GetKgSchemaIdFromFullIdPipe } from "src/ui/databrowserModule/util/getKgSchemaIdFromFullId.pipe"; +import { getShader, PMAP_DEFAULT_CONFIG } from "src/util/constants"; const getKgSchemaIdFromFullIdPipe = new GetKgSchemaIdFromFullIdPipe() @@ -304,7 +305,7 @@ export const cvtSearchParamToState = (searchparams: URLSearchParams, state: IavR name : layer, source : `nifti://${layer}`, mixability : 'nonmixable', - shader : GLSL_COLORMAP_JET, + shader : getShader(PMAP_DEFAULT_CONFIG), } as any }) const { ngViewerState } = returnState diff --git a/src/ui/cookieAgreement/data/readmore.md b/src/ui/cookieAgreement/data/readmore.md index 243cfe802f6e3ea3bef24c0238b5e6c12308e54a..6692141e5199aa2c7270c70e1ba7dce9f302ea2b 100644 --- a/src/ui/cookieAgreement/data/readmore.md +++ b/src/ui/cookieAgreement/data/readmore.md @@ -1,6 +1,6 @@ **Data controller(s)**: HBP Atlas Viewer is the data controller for your login information. **List of partners responsible**: HBP Atlas Viewer is the data controller for your login information. -**HBP Data Protection Officer (HBP DPO)**: FZJ-INM1 <inm1-bda@fz-juelich.de> +**HBP Data Protection Officer (HBP DPO)**: <data.protection@humanbrainproject.eu> **Legal basis for data processing**: You can find out about HBP Data Protection policies here: <https://www.humanbrainproject.eu/en/social-ethical-reflective/ethics-support/data-protection/> and you can contact HBP Data Protection Officer at <data.protection@humanbrainproject.eu> **General categories of personal data collected**: consent **Data shared within the HBP Consortium**: Name, ORC ID, JWT ID Token diff --git a/src/ui/databrowserModule/databrowser.useEffect.ts b/src/ui/databrowserModule/databrowser.useEffect.ts index 7a55fe649643910505bf5c09e541e1740f6137ab..74eb19f0218e1df91ddd40d81f50b28a4b868d12 100644 --- a/src/ui/databrowserModule/databrowser.useEffect.ts +++ b/src/ui/databrowserModule/databrowser.useEffect.ts @@ -6,10 +6,9 @@ import { filter, map, scan, switchMap, withLatestFrom, mapTo, shareReplay, start import { LoggingService } from "src/logging"; import { DATASETS_ACTIONS_TYPES, IDataEntry, ViewerPreviewFile, DatasetPreview } from "src/services/state/dataStore.store"; import { IavRootStoreInterface, ADD_NG_LAYER, CHANGE_NAVIGATION } from "src/services/stateStore.service"; -import { LOCAL_STORAGE_CONST, DS_PREVIEW_URL } from "src/util/constants"; +import { LOCAL_STORAGE_CONST, DS_PREVIEW_URL, getShader, PMAP_DEFAULT_CONFIG } from "src/util/constants"; import { KgSingleDatasetService } from "./kgSingleDatasetService.service"; import { determinePreviewFileType, PREVIEW_FILE_TYPES, PREVIEW_FILE_TYPES_NO_UI } from "./preview/previewFileIcon.pipe"; -import { GLSL_COLORMAP_JET } from "src/atlasViewer/atlasViewer.constantService.service"; import { SHOW_BOTTOM_SHEET } from "src/services/state/uiState.store"; import { MatSnackBar } from "@angular/material/snack-bar"; import { MatDialog } from "@angular/material/dialog"; @@ -293,7 +292,7 @@ export class DataBrowserUseEffect implements OnDestroy { name: filename, source : `nifti://${url}`, mixability : 'nonmixable', - shader : GLSL_COLORMAP_JET, + shader : getShader(PMAP_DEFAULT_CONFIG), annotation: `${DATASET_PREVIEW_ANNOTATION} ${filename}` } return { diff --git a/src/ui/layerbrowser/layerDetail/layerDetail.component.spec.ts b/src/ui/layerbrowser/layerDetail/layerDetail.component.spec.ts index 9d88ec67995546615a0a3e9d6c261b11495460cb..c628d46e0476638c993a44d86a6975d2b17041aa 100644 --- a/src/ui/layerbrowser/layerDetail/layerDetail.component.spec.ts +++ b/src/ui/layerbrowser/layerDetail/layerDetail.component.spec.ts @@ -1,8 +1,9 @@ -import { LayerDetailComponent } from './layerDetail.component' +import { LayerDetailComponent, VIEWER_INJECTION_TOKEN } from './layerDetail.component' import { async, TestBed } from '@angular/core/testing' import { NgLayersService } from '../ngLayerService.service' import { UIModule } from 'src/ui/ui.module' import { By } from '@angular/platform-browser' +import * as CONSTANT from 'src/util/constants' const getSpies = (service: NgLayersService) => { const lowThMapGetSpy = spyOn(service.lowThresholdMap, 'get').and.callThrough() @@ -65,8 +66,19 @@ const getSliderChangeTest = ctrlName => describe(`testing: ${ctrlName}`, () => { }) }) -describe('layerDetail.component.ts', () => { - describe('LayerDetailComponent', () => { +const fragmentMainSpy = { + value: `test value`, + restoreState: () => {} +} + +const defaultViewer = { + layerManager: { + getLayerByName: jasmine.createSpy('getLayerByName').and.returnValue({layer: {fragmentMain: fragmentMainSpy}}) + } +} + +describe('> layerDetail.component.ts', () => { + describe('> LayerDetailComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ @@ -74,21 +86,28 @@ describe('layerDetail.component.ts', () => { UIModule ], providers: [ - NgLayersService + NgLayersService, + { + provide: VIEWER_INJECTION_TOKEN, + useValue: defaultViewer + } ] }).compileComponents() })) - describe('basic', () => { + describe('> basic funcitonalities', () => { - it('should be created', () => { + it('> it should be created', () => { const fixture = TestBed.createComponent(LayerDetailComponent) const element = fixture.debugElement.componentInstance expect(element).toBeTruthy() }) - it('on bind input, if input is truthy, calls get on layerService maps', () => { + it('> on bind input, if input is truthy, calls get on layerService maps', () => { const service = TestBed.inject(NgLayersService) + TestBed.overrideProvider(VIEWER_INJECTION_TOKEN, { + useValue: {} + }) const { brightnessMapGetSpy, contractMapGetSpy, @@ -109,7 +128,7 @@ describe('layerDetail.component.ts', () => { expect(removeBgMapGetSpy).toHaveBeenCalledWith(layerName) }) - it('on bind input, if input is falsy, does not call layerService map get', () => { + it('> on bind input, if input is falsy, does not call layerService map get', () => { const service = TestBed.inject(NgLayersService) const { brightnessMapGetSpy, @@ -144,8 +163,6 @@ describe('layerDetail.component.ts', () => { getSliderChangeTest(sliderCtrl) } - // TODO test remove bg toggle - describe('testing: removeBG toggle', () => { it('on change, calls window', () => { @@ -177,6 +194,9 @@ describe('layerDetail.component.ts', () => { describe('triggerChange', () => { it('should throw if viewer is not defined', () => { + TestBed.overrideProvider(VIEWER_INJECTION_TOKEN, { + useValue: null + }) const fixutre = TestBed.createComponent(LayerDetailComponent) expect(function(){ fixutre.componentInstance.triggerChange() @@ -184,16 +204,21 @@ describe('layerDetail.component.ts', () => { }) it('should throw if layer is not found', () => { - const fixutre = TestBed.createComponent(LayerDetailComponent) - const layerName = `test-kitty` const fakeGetLayerByName = jasmine.createSpy().and.returnValue(undefined) const fakeNgInstance = { layerManager: { getLayerByName: fakeGetLayerByName } } + + TestBed.overrideProvider(VIEWER_INJECTION_TOKEN, { + useValue: fakeNgInstance + }) + + const fixutre = TestBed.createComponent(LayerDetailComponent) + const layerName = `test-kitty` + fixutre.componentInstance.layerName = layerName - fixutre.componentInstance.ngViewerInstance = fakeNgInstance expect(function(){ fixutre.componentInstance.triggerChange() @@ -201,8 +226,6 @@ describe('layerDetail.component.ts', () => { }) it('should throw if layer.layer.fragmentMain is undefined', () => { - - const fixutre = TestBed.createComponent(LayerDetailComponent) const layerName = `test-kitty` const fakeLayer = { @@ -214,8 +237,14 @@ describe('layerDetail.component.ts', () => { getLayerByName: fakeGetLayerByName } } + + TestBed.overrideProvider(VIEWER_INJECTION_TOKEN, { + useValue: fakeNgInstance + }) + + const fixutre = TestBed.createComponent(LayerDetailComponent) + fixutre.componentInstance.layerName = layerName - fixutre.componentInstance.ngViewerInstance = fakeNgInstance expect(function(){ fixutre.componentInstance.triggerChange() @@ -225,13 +254,12 @@ describe('layerDetail.component.ts', () => { it('should call getShader and restoreState if all goes right', () => { const replacementShader = `blabla ahder` - - const service = TestBed.inject(NgLayersService) - const getShaderSpy = spyOn(service, 'getShader').and.returnValue(replacementShader) - const fixutre = TestBed.createComponent(LayerDetailComponent) + const getShaderSpy = jasmine.createSpy('getShader').and.returnValue(replacementShader) + spyOnProperty(CONSTANT, 'getShader').and.returnValue(getShaderSpy) + const layerName = `test-kitty` - const fakeRestoreState = jasmine.createSpy() + const fakeRestoreState = jasmine.createSpy('fakeGetLayerByName') const fakeLayer = { layer: { fragmentMain: { @@ -239,14 +267,19 @@ describe('layerDetail.component.ts', () => { } } } - const fakeGetLayerByName = jasmine.createSpy().and.returnValue(fakeLayer) + const fakeGetLayerByName = jasmine.createSpy('fakeGetLayerByName').and.returnValue(fakeLayer) const fakeNgInstance = { layerManager: { getLayerByName: fakeGetLayerByName } } + TestBed.overrideProvider(VIEWER_INJECTION_TOKEN, { + useValue: fakeNgInstance + }) + + const fixutre = TestBed.createComponent(LayerDetailComponent) fixutre.componentInstance.layerName = layerName - fixutre.componentInstance.ngViewerInstance = fakeNgInstance + fixutre.detectChanges() fixutre.componentInstance.triggerChange() diff --git a/src/ui/layerbrowser/layerDetail/layerDetail.component.ts b/src/ui/layerbrowser/layerDetail/layerDetail.component.ts index ea6efeafc863f1d4a9db30254b31c9d3de5249d4..ce525450d66af85854ae715723c14ff6ad6ffd35 100644 --- a/src/ui/layerbrowser/layerDetail/layerDetail.component.ts +++ b/src/ui/layerbrowser/layerDetail/layerDetail.component.ts @@ -1,7 +1,10 @@ -import { Component, Input, OnChanges, ChangeDetectionStrategy } from "@angular/core"; +import { Component, Input, OnChanges, ChangeDetectionStrategy, Optional, Inject } from "@angular/core"; import { NgLayersService } from "../ngLayerService.service"; import { MatSliderChange } from "@angular/material/slider"; import { MatSlideToggleChange } from "@angular/material/slide-toggle"; +import { COLORMAP_IS_JET, getShader, PMAP_DEFAULT_CONFIG } from "src/util/constants"; + +export const VIEWER_INJECTION_TOKEN = `VIEWER_INJECTION_TOKEN` @Component({ selector: 'layer-detail-cmp', @@ -13,15 +16,26 @@ export class LayerDetailComponent implements OnChanges{ @Input() layerName: string - @Input() - ngViewerInstance: any + private colormap = null - constructor(private layersService: NgLayersService){ + constructor( + private layersService: NgLayersService, + @Optional() @Inject(VIEWER_INJECTION_TOKEN) private injectedViewer + ){ } ngOnChanges(){ if (!this.layerName) return + + const isPmap = (this.fragmentMain.value as string).includes(COLORMAP_IS_JET) + const { colormap, lowThreshold, removeBg } = PMAP_DEFAULT_CONFIG + if (isPmap) { + this.colormap = colormap + this.lowThreshold = lowThreshold + this.removeBg = removeBg + } + this.lowThreshold = this.layersService.lowThresholdMap.get(this.layerName) || this.lowThreshold this.highThreshold = this.layersService.highThresholdMap.get(this.layerName) || this.highThreshold this.brightness = this.layersService.brightnessMap.get(this.layerName) || this.brightness @@ -65,21 +79,29 @@ export class LayerDetailComponent implements OnChanges{ } triggerChange(){ + const { lowThreshold, highThreshold, brightness, contrast, removeBg, colormap } = this + const shader = getShader({ + lowThreshold, + highThreshold, + colormap, + brightness, + contrast, + removeBg + }) + this.fragmentMain.restoreState(shader) + } + + private get viewer(){ + return this.injectedViewer || (window as any).viewer + } + + private get fragmentMain(){ + if (!this.viewer) throw new Error(`viewer is not defined`) const layer = this.viewer.layerManager.getLayerByName(this.layerName) if (!layer) throw new Error(`layer with name: ${this.layerName}, not found.`) if (! (layer.layer?.fragmentMain?.restoreState) ) throw new Error(`layer.fragmentMain is not defined... is this an image layer?`) - const shader = this.layersService.getShader( - this.lowThreshold, - this.highThreshold, - this.brightness, - this.contrast, - this.removeBg - ) - layer.layer.fragmentMain.restoreState(shader) - } - get viewer(){ - return this.ngViewerInstance || (window as any).viewer + return layer.layer.fragmentMain } } diff --git a/src/ui/layerbrowser/layerDetail/layerDetail.template.html b/src/ui/layerbrowser/layerDetail/layerDetail.template.html index ef47a9b904dcd3a1c72d9380b21073d4df62965b..376b875c3ffd924a5a69166d2555104cae0c0390 100644 --- a/src/ui/layerbrowser/layerDetail/layerDetail.template.html +++ b/src/ui/layerbrowser/layerDetail/layerDetail.template.html @@ -72,7 +72,7 @@ <mat-slide-toggle aria-label="Remove background" (change)="handleToggleBg($event)" - [value]="removeBg"> + [(ngModel)]="removeBg"> </mat-slide-toggle> </div> diff --git a/src/ui/layerbrowser/layerbrowser.component.ts b/src/ui/layerbrowser/layerbrowser.component.ts index 91121d8f27731c516768eadbc6dc62de728a2c8b..aa6e069a4e53d763030bd53989ab89ff6cc762dc 100644 --- a/src/ui/layerbrowser/layerbrowser.component.ts +++ b/src/ui/layerbrowser/layerbrowser.component.ts @@ -9,6 +9,7 @@ import { getViewer } from "src/util/fn"; import { INgLayerInterface } from "../../atlasViewer/atlasViewer.component"; import { FORCE_SHOW_SEGMENT, getNgIds, isDefined, REMOVE_NG_LAYER, safeFilter, ViewerStateInterface, IavRootStoreInterface } from "../../services/stateStore.service"; import { MatSliderChange } from "@angular/material/slider"; +import { ARIA_LABELS } from 'common/constants' @Component({ selector : 'layer-browser', @@ -21,6 +22,8 @@ import { MatSliderChange } from "@angular/material/slider"; export class LayerBrowser implements OnInit, OnDestroy { + public TOGGLE_SHOW_LAYER_CONTROL_ARIA_LABEL = ARIA_LABELS.TOGGLE_SHOW_LAYER_CONTROL + @Output() public nonBaseLayersChanged: EventEmitter<INgLayerInterface[]> = new EventEmitter() /** diff --git a/src/ui/layerbrowser/layerbrowser.template.html b/src/ui/layerbrowser/layerbrowser.template.html index 4c50cf9506371c4c236dc8d89dfd10e6cd1d6942..f34b2ea8b77315f7a2b54f39d237c21b469c4a5d 100644 --- a/src/ui/layerbrowser/layerbrowser.template.html +++ b/src/ui/layerbrowser/layerbrowser.template.html @@ -13,7 +13,7 @@ class="layer-expansion-unit" #expansionPanel> <mat-expansion-panel-header> - <div class="align-items-center d-flex flex-nowrap pr-4"> + <div class="align-items-center d-flex flex-nowrap pr-4 w-100"> <!-- toggle opacity --> <div matTooltip="opacity"> @@ -70,11 +70,12 @@ <mat-label [matTooltipPosition]="matTooltipPosition" [matTooltip]="ngLayer.name | getFilenamePipe " - [class]="((darktheme$ | async) ? 'text-light' : 'text-dark') + ' text-truncate'"> + [class]="((darktheme$ | async) ? 'text-light' : 'text-dark') + ' text-truncate flex-grow-1 flex-shrink-1'"> {{ ngLayer.name | getFilenamePipe }} </mat-label> <button mat-icon-button + [attr.aria-label]="TOGGLE_SHOW_LAYER_CONTROL_ARIA_LABEL" (click)="expansionPanel.toggle()"> <ng-container *ngIf="expansionPanel.expanded; else btnIconAlt"> <i class="fas fa-chevron-up"></i> diff --git a/src/ui/layerbrowser/ngLayerService.service.ts b/src/ui/layerbrowser/ngLayerService.service.ts index fefb5b7c4675cfac110a02c5008f462c0de5592f..baf59a9a4191c27ea5ddffb72629148cdb1a1ab9 100644 --- a/src/ui/layerbrowser/ngLayerService.service.ts +++ b/src/ui/layerbrowser/ngLayerService.service.ts @@ -1,22 +1,5 @@ import { Injectable } from "@angular/core"; -const setGetShaderFn = (normalizedIncomingColor) => (lowerThreshold, upperThreshold, brightness, contrast, removeBg: boolean) => ` -void main() { - float raw_x = toNormalized(getDataValue()); - float x = (raw_x - ${lowerThreshold.toFixed(5)}) / (${(upperThreshold - lowerThreshold).toFixed(5)}) ${brightness > 0 ? '+' : '-'} ${Math.abs(brightness).toFixed(5)}; - - ${ removeBg ? 'if(x>1.0){ emitTransparent(); }else if (x<0.0){ emitTransparent(); }else{' : '' } - - emitRGB(vec3( - x * ${normalizedIncomingColor[0].toFixed(5)}, x * ${normalizedIncomingColor[1].toFixed(5)}, x * ${normalizedIncomingColor[2].toFixed(5)}) - * exp(${contrast.toFixed(5)}) - ); - - ${ removeBg ? '}' : '' } - -} -` - @Injectable({ providedIn: 'root' }) @@ -27,5 +10,4 @@ export class NgLayersService{ public brightnessMap: Map<string, number> = new Map() public contrastMap: Map<string, number> = new Map() public removeBgMap: Map<string, boolean> = new Map() - public getShader: (low: number, high: number, brightness: number, contrast: number, removeBg: boolean) => string = setGetShaderFn([1, 1, 1]) } diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html index 4d538936d97e6f385673e486683d3f263b06ca5f..d4f008803fae1a52450fa862010319c168e7a609 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html @@ -138,7 +138,7 @@ </mat-list-item> <!-- connectivity --> - <div iav-switch #connectivitySwitch="iavSwitch"> + <div *ngIf="hasConnectivity" iav-switch #connectivitySwitch="iavSwitch"> <mat-list-item mat-ripple (click)="connectivitySwitch.toggle()" diff --git a/src/util/constants.ts b/src/util/constants.ts index bfb83d130f37e87b74f201971016e38012127395..8a56a7ceaa97c2c654be25e3d49df77dd191d1f4 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -66,4 +66,39 @@ export const getHttpHeader: () => HttpHeaders = () => { const header = new HttpHeaders() header.set('referrer', getScopedReferer()) return header -} \ No newline at end of file +} + +const CM_MATLAB_JET = `float r;if( x < 0.7 ){r = 4.0 * x - 1.5;} else {r = -4.0 * x + 4.5;}float g;if (x < 0.5) {g = 4.0 * x - 0.5;} else {g = -4.0 * x + 3.5;}float b;if (x < 0.3) {b = 4.0 * x + 0.5;} else {b = -4.0 * x + 2.5;}float a = 1.0;` +const CM_DEFAULT = `float r = x; float g = x; float b = x;` +export const COLORMAP_IS_JET = `// iav-colormap-is-jet` +export const COLORMAP_IS_DEFAULT = `// iav-colormap-default` + +export const getShader = ({ + colormap = null, + lowThreshold = 0, + highThreshold = 1, + brightness = 0, + contrast = 0, + removeBg = false +} = {}): string => { + const header = colormap === 'jet' ? COLORMAP_IS_JET : COLORMAP_IS_DEFAULT + const colormapGlsl = colormap === 'jet' ? CM_MATLAB_JET : CM_DEFAULT + return `${header} +void main() { + float raw_x = toNormalized(getDataValue()); + float x = (raw_x - ${lowThreshold.toFixed(5)}) / (${highThreshold - lowThreshold}) ${ brightness > 0 ? '+' : '-' } ${Math.abs(brightness).toFixed(5)}; + + ${ removeBg ? 'if(x>1.0){emitTransparent();}else if(x<0.0){emitTransparent();}else{' : '' } + ${colormapGlsl} + + emitRGB(vec3(r, g, b)*exp(${contrast.toFixed(5)})); + ${ removeBg ? '}' : '' } +} +` +} + +export const PMAP_DEFAULT_CONFIG = { + colormap: 'jet', + lowThreshold: 0.05, + removeBg: true +}