diff --git a/angular.json b/angular.json index 7d4fcb6a0e5a971095069f821056fcfab9ea3679..9797b5413cb83b570a3695fe1a1bdca44b6a2de7 100644 --- a/angular.json +++ b/angular.json @@ -48,6 +48,10 @@ "input": "worker/worker-nifti.js", "inject": false, "bundleName": "worker-nifti" + },{ + "input": "worker/worker-typedarray.js", + "inject": false, + "bundleName": "worker-typedarray" }, { diff --git a/docs/releases/v2.6.5.md b/docs/releases/v2.6.5.md new file mode 100644 index 0000000000000000000000000000000000000000..9cf83b085c6deffc754bbad8c84744256def20d7 --- /dev/null +++ b/docs/releases/v2.6.5.md @@ -0,0 +1,5 @@ +# v2.6.4 + +## Feature + +- Re-enabled autoradiographs for receptor datasets diff --git a/mkdocs.yml b/mkdocs.yml index aa75e34ae6aca364251d8e362e8d6a1c81197858..d56285fcb2696c7884fb90844282f9ba87e94d2b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ pages: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.6.5: 'releases/v2.6.5.md' - v2.6.4: 'releases/v2.6.4.md' - v2.6.3: 'releases/v2.6.3.md' - v2.6.2: 'releases/v2.6.2.md' diff --git a/package.json b/package.json index 7f15a78d4750687a3c76dbea7db733d534920db4..4ef7b728e472e70f049ec71e0eb77e7893154581 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interactive-viewer", - "version": "2.6.4", + "version": "2.6.5", "description": "HBP interactive atlas viewer. Integrating KG query, dataset previews & more. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", "scripts": { "build-aot": "ng build && node ./third_party/matomo/processMatomo.js", diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.component.ts index bb28ba3fa4847ffa13101fcae7c1ed25cea73fde..83054ac26c45903d6e5cef7c64015a57454f2cf1 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.component.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.component.ts @@ -1,8 +1,9 @@ -import { Component, Input, OnChanges } from "@angular/core"; +import { Component, ElementRef, Input, OnChanges, ViewChild } from "@angular/core"; import { BsFeatureReceptorBase } from "../base"; import { CONST } from 'common/constants' import { TBSDetail } from "../type"; import { environment } from 'src/environments/environment' +import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; const { RECEPTOR_AR_CAPTION } = CONST @@ -27,12 +28,18 @@ export class BsFeatureReceptorAR extends BsFeatureReceptorBase implements OnChan @Input() bsLabel: string - public imgUrl: string + @ViewChild('arContainer', { read: ElementRef }) + arContainer: ElementRef - constructor(){ + private renderBuffer: Uint8ClampedArray + private width: number + private height: number + private pleaseRender = false + + constructor(private worker: AtlasWorkerService){ super() } - ngOnChanges(){ + async ngOnChanges(){ this.error = null this.urls = [] if (!this.bsFeature) { @@ -45,16 +52,69 @@ export class BsFeatureReceptorAR extends BsFeatureReceptorBase implements OnChan } try { - const url = this.bsFeature.__data.__autoradiographs[this.bsLabel] - - if (!url) throw new Error(`autoradiograph cannot be found`) - this.urls = [{ url }] - const query = url.replace('https://object.cscs.ch/v1', '') - this.imgUrl = `${this.DS_PREVIEW_URL}/imageProxy/v1?u=${encodeURIComponent(query)}` - + const { + "x-channel": channel, + "x-height": height, + "x-width": width, + content_type: contentType, + content_encoding: contentEncoding, + content, + } = this.bsFeature.__data.__autoradiographs[this.bsLabel] + + if (contentType !== "application/octet-stream") { + throw new Error(`contentType expected to be application/octet-stream, but is instead ${contentType}`) + } + if (contentEncoding !== "gzip; base64") { + throw new Error(`contentEncoding expected to be gzip; base64, but is ${contentEncoding} instead.`) + } + + const bin = atob(content) + const { pako } = (window as any).export_nehuba + const uint8array: Uint8Array = pako.inflate(bin) + + this.width = width + this.height = height + + const rgbaBuffer = await this.worker.sendMessage({ + method: "PROCESS_TYPED_ARRAY", + param: { + inputArray: uint8array, + width, + height, + channel + }, + transfers: [ uint8array.buffer ] + }) + + this.renderBuffer = rgbaBuffer.result.buffer + this.renderCanvas() } catch (e) { this.error = e.toString() } - + } + + private renderCanvas(){ + if (!this.arContainer) { + this.pleaseRender = true + return + } + + const arContainer = (this.arContainer.nativeElement as HTMLElement) + while (arContainer.firstChild) { + arContainer.removeChild(arContainer.firstChild) + } + + const canvas = document.createElement("canvas") + canvas.height = this.height + canvas.width = this.width + arContainer.appendChild(canvas) + const ctx = canvas.getContext("2d") + const imgData = ctx.createImageData(this.width, this.height) + imgData.data.set(this.renderBuffer) + ctx.putImageData(imgData, 0, 0) + } + + ngAfterViewChecked(){ + if (this.pleaseRender) this.renderCanvas() } } \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.style.css b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.style.css index 523e0a8d190409673b7c3b5d74a28d519554f1eb..c7ebabfec67ff4346377d1dfed4a4ce63a40c066 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.style.css +++ b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.style.css @@ -1,4 +1,5 @@ -.ar-container > img +/* canvas created by createElement does not have encapsulation applied */ +.ar-container >>> canvas { width: 100%; } \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.template.html index 1714d15fa96583c3aa2fcd4c19a474c5d42a6936..ad467adc5a5f15bf37343c49a0c02fbb96a2c4d7 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.template.html +++ b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/ar/autoradiograph.template.html @@ -16,7 +16,6 @@ <figcaption class="text-muted"> Autoradiograph: {{ RECEPTOR_AR_CAPTION }} </figcaption> - <div class="ar-container"> - <img [src]="imgUrl"> + <div class="ar-container" #arContainer> </div> </figure> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/type.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/type.ts index 6d3222c8f78d086602d5bb2dccb91257bb11de6d..9ef1efe8b17727ded7f382784c035d780d323fc6 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/type.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/type.ts @@ -47,7 +47,14 @@ export type TBSDetail = TBSSummary & { [key: string]: TProfile } __autoradiographs: { - [key: string]: string + [key: string]: { + content_type: string + content_encoding: string + ['x-width']: number + ['x-height']: number + ['x-channel']: number + content: string + } } __fingerprint: TBSFingerprint } diff --git a/src/atlasViewer/atlasViewer.workerService.service.ts b/src/atlasViewer/atlasViewer.workerService.service.ts index 653764d97a7670d16ca8805c1033f09775de0cb0..9a17115534cae61a899a29c7d32bc4602599fd19 100644 --- a/src/atlasViewer/atlasViewer.workerService.service.ts +++ b/src/atlasViewer/atlasViewer.workerService.service.ts @@ -28,10 +28,10 @@ export class AtlasWorkerService { ...data }, transfers) const message = await fromEvent(this.worker, 'message').pipe( - filter((message: MessageEvent) => message.data.id && message.data.id === newUuid), + filter((msg: MessageEvent) => msg.data.id && msg.data.id === newUuid), take(1) ).toPromise() - + const { data: returnData } = message as MessageEvent const { id, error, ...rest } = returnData if (error) { diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts index 130ee0d1dbc0ba1b820dfe0e7c915e5e2e8b6138..51406f461875ce46e737629f8f5f87c0c64beeae 100644 --- a/src/util/pureConstant.service.ts +++ b/src/util/pureConstant.service.ts @@ -17,7 +17,7 @@ import { MatSnackBar } from "@angular/material/snack-bar"; import { TTemplateImage } from "./interfaces"; export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version' -export const SIIBRA_API_VERSION = '0.1.9' +export const SIIBRA_API_VERSION = '0.1.10' const validVolumeType = new Set([ 'neuroglancer/precomputed', diff --git a/worker/worker-typedarray.js b/worker/worker-typedarray.js new file mode 100644 index 0000000000000000000000000000000000000000..e501bfa19de2db4892f542131403e2db1a63ab4d --- /dev/null +++ b/worker/worker-typedarray.js @@ -0,0 +1,37 @@ +(function(exports){ + exports.typedArray = { + fortranToRGBA(inputArray, width, height, channel) { + if (channel !== 1 && channel !== 3) { + throw new Error(`channel must be either 1 or 3`) + } + const greyScale = (channel === 1) + const dim = width * height + if (channel === 1 && inputArray.length !== dim) { + throw new Error(`for single channel, expect width * height === inputArray.length, but ${width} * ${height} !== ${inputArray.length}`) + } + if (channel === 3 && inputArray.length !== (dim * 3)) { + throw new Error(`for 3 channel, expect 3 * width * height === inputArray.length, but 3 * ${width} * ${height} !== ${inputArray.length}`) + } + const _ = new ArrayBuffer(width * height * 4) + const buffer = new Uint8ClampedArray(_) + for (let i = 0; i < width; i ++) { + for (let j = 0; j < height; j ++) { + for (let k = 0; k < 4; k ++) { + const toIndex = (j * width + i) * 4 + k + const fromValue = k === 3 + ? 255 + : greyScale + ? inputArray[width * j + i] + : inputArray[dim * k + width * j + i] + buffer[toIndex] = fromValue + } + } + } + return buffer + } + } +})( + typeof exports === 'undefined' + ? self + : exports +) \ No newline at end of file diff --git a/worker/worker.js b/worker/worker.js index 51156b1b95fe6ac2dd8d064156c0bf36ca6d56e9..04a3db58bd8cbac500f2ab0dedc896ea2de7f979 100644 --- a/worker/worker.js +++ b/worker/worker.js @@ -9,6 +9,7 @@ globalThis.constants = { if (typeof self.importScripts === 'function') self.importScripts('./worker-plotly.js') 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 @@ -24,11 +25,13 @@ const validTypes = [ const VALID_METHOD = { PROCESS_PLOTLY: `PROCESS_PLOTLY`, PROCESS_NIFTI: 'PROCESS_NIFTI', + PROCESS_TYPED_ARRAY: `PROCESS_TYPED_ARRAY`, } const VALID_METHODS = [ VALID_METHOD.PROCESS_PLOTLY, VALID_METHOD.PROCESS_NIFTI, + VALID_METHOD.PROCESS_TYPED_ARRAY, ] const validOutType = [ @@ -291,6 +294,27 @@ onmessage = (message) => { }) } } + if (message.data.method === VALID_METHOD.PROCESS_TYPED_ARRAY) { + try { + const { inputArray, width, height, channel } = message.data.param + const buffer = self.typedArray.fortranToRGBA(inputArray, width, height, channel) + + postMessage({ + id, + result: { + buffer + } + }, [ buffer.buffer ]) + } catch (e) { + postMessage({ + id, + error: { + code: 401, + message: `process typed array error: ${e.toString()}` + } + }) + } + } postMessage({ id, error: {