diff --git a/common/constants.js b/common/constants.js index d3f6f24546090062459459f82f8652db30691244..fb5a018be88f017fb2b1767e07a7305288fb7b03 100644 --- a/common/constants.js +++ b/common/constants.js @@ -108,7 +108,7 @@ These outlines are based on the authoritative Terms and Conditions are found <ht If you do not accept the Terms & Conditions you are not permitted to access or use the KG to search for, to submit, to post, or to download any materials found there-in. `, - NEHUBA_DRAG_DROP_TEXT: `Drag and drop any .nii.gz, .nii or .swc files.`, + NEHUBA_DRAG_DROP_TEXT: `Drag and drop any .nii.gz, .nii, .json or .swc files.`, LOADING_TXT: `Loading ...`, diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts index 776fb7848761df1bc48e99403ded8b3607cdba0a..be1b3fe8ed9f57747c9ec1c27788c37173524edf 100644 --- a/src/atlasComponents/annotations/annotation.service.ts +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -61,6 +61,13 @@ interface NgAnnotationLayer { visible: boolean } +export const ID_AFFINE = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], +] + export class AnnotationLayer { static Map = new Map<string, AnnotationLayer>() static Get(name: string, color: string){ @@ -79,7 +86,8 @@ export class AnnotationLayer { private idset = new Set<string>() constructor( private name: string = getUuid(), - private color="#ffffff" + private color="#ffffff", + affine=ID_AFFINE, ){ const layerSpec = this.viewer.layerSpecification.getLayer( this.name, @@ -88,12 +96,7 @@ export class AnnotationLayer { "annotationColor": this.color, "annotations": [], name: this.name, - transform: [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1], - ] + transform: affine, } ) this.nglayer = this.viewer.layerManager.addManagedLayer(layerSpec) @@ -134,19 +137,35 @@ export class AnnotationLayer { } } - async addAnnotation(spec: AnnotationSpec){ + /** + * Unsafe method. Caller should ensure this.nglayer.isReady() + * + * @param spec + */ + #addSingleAnn(spec: AnnotationSpec) { + const localAnnotations = this.nglayer.layer.localAnnotations + this.idset.add(spec.id) + const annSpec = this.parseNgSpecType(spec) + localAnnotations.add( + annSpec + ) + } + + async addAnnotation(spec: AnnotationSpec|AnnotationSpec[]){ if (!this.nglayer) { throw new Error(`layer has already been disposed`) } 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 - ) + if (Array.isArray(spec)) { + for (const item of spec) { + this.#addSingleAnn(item) + } + } else { + this.#addSingleAnn(spec) + } + return true } return false @@ -162,8 +181,13 @@ export class AnnotationLayer { localAnnotations.references.delete(spec.id) } } - async updateAnnotation(spec: AnnotationSpec) { - await waitFor(() => !!this.nglayer?.layer?.localAnnotations) + + /** + * Unsafe method. Caller should ensure this.nglayer.layer is defined + * + * @param spec + */ + #updateSingleAnn(spec: AnnotationSpec) { const { localAnnotations } = this.nglayer.layer const ref = localAnnotations.references.get(spec.id) const _spec = this.parseNgSpecType(spec) @@ -178,6 +202,17 @@ export class AnnotationLayer { } } + async updateAnnotation(spec: AnnotationSpec|AnnotationSpec[]) { + await waitFor(() => !!this.nglayer?.layer?.localAnnotations) + if (Array.isArray(spec)) { + for (const item of spec){ + this.#updateSingleAnn(item) + } + return + } + this.#updateSingleAnn(spec) + } + private get viewer() { if ((window as any).viewer) return (window as any).viewer throw new Error(`window.viewer not defined`) diff --git a/src/atlasComponents/sapi/core/space/interspaceLinearXform.ts b/src/atlasComponents/sapi/core/space/interspaceLinearXform.ts index 44c93c74186567e3967d2224cb4937ee391de215..74bf8078cb9ae9765684ec78e5b038d02d4b3c15 100644 --- a/src/atlasComponents/sapi/core/space/interspaceLinearXform.ts +++ b/src/atlasComponents/sapi/core/space/interspaceLinearXform.ts @@ -1,29 +1,33 @@ -export const VALID_LINEAR_XFORM_SRC = { - CCF: "Allen Common Coordination Framework" -} - -export const VALID_LINEAR_XFORM_DST = { - NEHUBA: "nehuba" -} +const VALID_XFORM_SRC = ["CCF_V2_5", "QUICKNII"] as const +const VALID_XFORM_DST = ["NEHUBA"] as const -export type TVALID_LINEAR_XFORM_SRC = keyof typeof VALID_LINEAR_XFORM_SRC -export type TVALID_LINEAR_XFORM_DST = keyof typeof VALID_LINEAR_XFORM_DST +export type TVALID_LINEAR_XFORM_SRC = typeof VALID_XFORM_SRC[number] +export type TVALID_LINEAR_XFORM_DST = typeof VALID_XFORM_DST[number] type TLinearXform = number[][] const _linearXformDict: Record< - keyof typeof VALID_LINEAR_XFORM_SRC, + TVALID_LINEAR_XFORM_SRC, Record< - keyof typeof VALID_LINEAR_XFORM_DST, + TVALID_LINEAR_XFORM_DST, TLinearXform >> = { - CCF: { + CCF_V2_5: { NEHUBA: [ [-1e3, 0, 0, 11400000 - 5737500], // [0, 0, -1e3, 13200000 - 6637500], // [0, -1e3, 0, 8000000 - 4037500], // [0, 0, 0, 1], ] + }, + // see https://www.nitrc.org/plugins/mwiki/index.php?title=quicknii:Coordinate_systems + QUICKNII: { + NEHUBA: [ + [2.5e4, 0, 0, -5737500], // + [0, 2.5e4, 0, -6637500], // + [0, 0, 2.5e4, -4037500], // + [0, 0, 0, 1], + ] } } @@ -48,13 +52,13 @@ export const linearXformDict = getProxyXform(_linearXformDict, (value: Record<st return defaultXform }) }) as Record< - keyof typeof VALID_LINEAR_XFORM_SRC, +TVALID_LINEAR_XFORM_SRC, Record< - keyof typeof VALID_LINEAR_XFORM_DST, + TVALID_LINEAR_XFORM_DST, TLinearXform >> -export const linearTransform = async (srcTmplName: keyof typeof VALID_LINEAR_XFORM_SRC, targetTmplName: keyof typeof VALID_LINEAR_XFORM_DST) => { +export const linearTransform = async (srcTmplName: TVALID_LINEAR_XFORM_SRC, targetTmplName: TVALID_LINEAR_XFORM_DST) => { return linearXformDict[srcTmplName][targetTmplName] } diff --git a/src/atlasComponents/userAnnotations/tools/line.ts b/src/atlasComponents/userAnnotations/tools/line.ts index 9223b1d989aacccab83e470f9145aca1b81674a9..37299eb4e820d2696e7ed6e61b510c1b3cd7af1f 100644 --- a/src/atlasComponents/userAnnotations/tools/line.ts +++ b/src/atlasComponents/userAnnotations/tools/line.ts @@ -23,7 +23,6 @@ export type TLineJsonSpec = { } & TBaseAnnotationGeomtrySpec export class Line extends IAnnotationGeometry{ - public id: string public annotationType = 'Line' public points: Point[] = [] diff --git a/src/atlasComponents/userAnnotations/tools/point.ts b/src/atlasComponents/userAnnotations/tools/point.ts index 5086a30459665423439da1b8ee6cbb2fe51d35df..dea8847d747aec28f0f8d852dcdf6c030de8c6c1 100644 --- a/src/atlasComponents/userAnnotations/tools/point.ts +++ b/src/atlasComponents/userAnnotations/tools/point.ts @@ -11,7 +11,6 @@ export type TPointJsonSpec = { } & TBaseAnnotationGeomtrySpec export class Point extends IAnnotationGeometry { - id: string x: number y: number z: number diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts index 4bc86182aa6849161b384923cabd24d76534f502..01dada57995f76dd898547980faa8c9dd9cefb10 100644 --- a/src/atlasComponents/userAnnotations/tools/poly.ts +++ b/src/atlasComponents/userAnnotations/tools/poly.ts @@ -12,7 +12,6 @@ export type TPolyJsonSpec = { } & TBaseAnnotationGeomtrySpec export class Polygon extends IAnnotationGeometry{ - public id: string public annotationType = 'Polygon' public points: Point[] = [] diff --git a/src/viewerModule/nehuba/userLayers/service.ts b/src/viewerModule/nehuba/userLayers/service.ts index 5937a106ed53e1fa3682f2900446bba171ffcdb5..0a960972f7f7b2770826ae1bdd970fa250e6ef3d 100644 --- a/src/viewerModule/nehuba/userLayers/service.ts +++ b/src/viewerModule/nehuba/userLayers/service.ts @@ -1,5 +1,5 @@ import { Injectable, OnDestroy } from "@angular/core" -import { MatDialog } from "@angular/material/dialog" +import { MatDialog, MatDialogRef } from "@angular/material/dialog" import { select, Store } from "@ngrx/store" import { forkJoin, from, Subscription } from "rxjs" import { distinctUntilChanged, filter } from "rxjs/operators" @@ -17,6 +17,8 @@ import { getExportNehuba, getUuid } from "src/util/fn" import { UserLayerInfoCmp } from "./userlayerInfo/userlayerInfo.component" import { translateV3Entities } from "src/atlasComponents/sapi/translateV3" import { MetaV1Schema } from "src/atlasComponents/sapi/typeV3" +import { AnnotationLayer } from "src/atlasComponents/annotations" +import { rgbToHex } from 'common/util' type OmitKeys = "clType" | "id" | "source" type LayerOption = Omit<atlasAppearance.const.NgLayerCustomLayer, OmitKeys> @@ -32,9 +34,9 @@ const SUPPORTED_PREFIX = ["nifti://", "precomputed://", "swc://", "deepzoom://"] type ValidProtocol = typeof SUPPORTED_PREFIX[number] type ValidInputTypes = File|string -type ProcessorOutput = {option: LayerOption, url: string, protocol: ValidProtocol, meta: Meta, cleanup: () => void} +type ProcessorOutput = {option?: LayerOption, url?: string, protocol?: ValidProtocol, meta: Meta, cleanup: () => void} type ProcessResource = { - matcher: (input: ValidInputTypes) => boolean + matcher: (input: ValidInputTypes) => Promise<boolean> processor: (input: ValidInputTypes) => Promise<ProcessorOutput> } @@ -52,6 +54,7 @@ function RegisterSource(matcher: ProcessResource['matcher']) { @Injectable() export class UserLayerService implements OnDestroy { #idToCleanup = new Map<string, () => void>() + #dialogRef: MatDialogRef<unknown> static VerifyUrl(source: string) { for (const prefix of SUPPORTED_PREFIX) { @@ -63,7 +66,7 @@ export class UserLayerService implements OnDestroy { } @RegisterSource( - input => input instanceof File && input.name.endsWith(".swc") + async input => input instanceof File && input.name.endsWith(".swc") ) async processSwc(file: File): ReturnType<ProcessResource['processor']> { let message = `The swc rendering is experimental. Please contact us on any feedbacks. ` @@ -71,7 +74,7 @@ export class UserLayerService implements OnDestroy { let src: TVALID_LINEAR_XFORM_SRC const dst: TVALID_LINEAR_XFORM_DST = "NEHUBA" if (/ccf/i.test(swcText)) { - src = "CCF" + src = "CCF_V2_5" message += `CCF detected, applying known transformation.` } if (!src) { @@ -123,7 +126,7 @@ export class UserLayerService implements OnDestroy { } @RegisterSource( - input => input instanceof File && input.name.endsWith(".nii") + async input => input instanceof File && input.name.endsWith(".nii") ) async processNifti(file: File){ const buf = await file.arrayBuffer() @@ -131,7 +134,7 @@ export class UserLayerService implements OnDestroy { } @RegisterSource( - input => input instanceof File && input.name.endsWith(".nii.gz") + async input => input instanceof File && input.name.endsWith(".nii.gz") ) async processNiiGz(file: File) { const buf = await file.arrayBuffer() @@ -146,7 +149,7 @@ export class UserLayerService implements OnDestroy { } @RegisterSource( - input => typeof input === "string" && input.startsWith(OVERLAY_LAYER_PROTOCOL) + async input => typeof input === "string" && input.startsWith(OVERLAY_LAYER_PROTOCOL) ) async processOverlayPath(source: string) { const strippedSrc = source.replace(OVERLAY_LAYER_PROTOCOL, "") @@ -161,7 +164,7 @@ export class UserLayerService implements OnDestroy { } @RegisterSource( - input => typeof input === "string" && input.startsWith("precomputed://") + async input => typeof input === "string" && input.startsWith("precomputed://") ) async processPrecomputed(source: string): Promise<ProcessorOutput>{ const url = source.replace("precomputed://", "") @@ -190,7 +193,7 @@ export class UserLayerService implements OnDestroy { } @RegisterSource( - input => typeof input === "string" && input.startsWith("deepzoom://") + async input => typeof input === "string" && input.startsWith("deepzoom://") ) async processDzi(source: string): Promise<ProcessorOutput> { const url = source.replace("deepzoom://", "") @@ -214,13 +217,82 @@ export class UserLayerService implements OnDestroy { } } + @RegisterSource(async input => { + if (input instanceof File && input.name.endsWith(".json")) { + const JSON_KEYS = [ + // "b", + // "count", + // "g", + // "idx", + // "name", + // "r", + "triplets" + ] + const text = await input.text() + const arr = JSON.parse(text) + + // must be array + if (!Array.isArray(arr)) { + return false + } + // can only deal with length 1 for now + if (arr.length !== 1) { + return false + } + const item = arr[0] + for (const key of JSON_KEYS) { + if (!item[key]) { + console.log(`Parsing PCJson failed. ${key} does not exist`) + return false + } + } + return true + } + return false + }) + async processPCJson(file: File): Promise<ProcessorOutput>{ + const arr = JSON.parse(await file.text()) + const item = arr[0] + const { r, g, b } = item + + const rgbString = [r, g, b].every(v => Number.isInteger(v)) + ? rgbToHex([r, g, b]) + : "#ff0000" + + const id = getUuid() + const src = "QUICKNII" + const dst = "NEHUBA" + const xform = await linearTransform(src, dst) + const layer = new AnnotationLayer(id, rgbString, xform) + + const triplets: number[][] = [item.triplets.slice(0, 3)] + for (const num of item.triplets as number[]) { + if (triplets.at(-1).length === 3) { + triplets.push([num]) + continue + } + triplets.at(-1).push(num) + } + + layer.addAnnotation(triplets.map((triplet, idx) => ({ + id: `${id}-${idx}`, + type: 'point', + point: triplet.map(v => v) as [number, number, number] + }))) + return { + cleanup: () => layer.dispose(), + meta: { + filename: file.name, + } + } + } + async #processInput(input: ValidInputTypes): Promise<ProcessorOutput> { for (const { matcher, processor } of SOURCE_PROCESSOR) { - if (matcher(input)) { + if (await matcher(input)) { return await processor.apply(this, [input]) } } - debugger const inputStr = input instanceof File ? input.name : input @@ -236,7 +308,7 @@ export class UserLayerService implements OnDestroy { this.#idToCleanup.set(id, cleanup) this.addUserLayer( id, - `${protocol}${url}`, + protocol && url &&`${protocol}${url}`, meta, option, ) @@ -245,24 +317,30 @@ export class UserLayerService implements OnDestroy { addUserLayer( id: string, - source: string, + source: string|null|undefined, meta: Meta, options: LayerOption = {} ) { - UserLayerService.VerifyUrl(source) - const layer = { - id, - clType: "customlayer/nglayer" as const, - source, - ...options, + if (source) { + UserLayerService.VerifyUrl(source) + const layer = { + id, + clType: "customlayer/nglayer" as const, + source, + ...options, + } + this.store$.dispatch( + atlasAppearance.actions.addCustomLayer({ + customLayer: layer, + }) + ) } - this.store$.dispatch( - atlasAppearance.actions.addCustomLayer({ - customLayer: layer, - }) - ) - this.dialog.open(UserLayerInfoCmp, { + if (this.#dialogRef) { + this.#dialogRef.close() + this.#dialogRef = null + } + this.#dialogRef = this.dialog.open(UserLayerInfoCmp, { data: { layerName: id, filename: meta.filename, @@ -276,15 +354,16 @@ export class UserLayerService implements OnDestroy { autoFocus: false, panelClass: ["no-padding-dialog", "w-100"], }) - .afterClosed() - .subscribe(() => { - this.store$.dispatch(atlasAppearance.actions.removeCustomLayer({ id })) + + this.#dialogRef.afterClosed().subscribe(() => { const cleanup = this.#idToCleanup.get(id) - if (!cleanup) { - console.warn(`idToCleanup ${id} could not be found! ${meta.filename}`) - return + cleanup && cleanup() + if (source) { + this.store$.dispatch( + atlasAppearance.actions.removeCustomLayer({ id }) + ) } - cleanup() + this.#idToCleanup.delete(id) }) } diff --git a/tsconfig.json b/tsconfig.json index bfd3effbebe1bbe4804212fd1188169c72d3937e..77502016e3320cade026e538bf74cf842a976c72 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,9 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "moduleResolution": "node", + "useDefineForClassFields": false, "module": "esnext", - "target": "es2020", + "target": "es2022", "sourceMap": false, "baseUrl": ".", "paths": { @@ -25,4 +26,4 @@ "annotateForClosureCompiler" : true, "strictTemplates": true } -} \ No newline at end of file +}