UNPKG

ng-cornerstone

Version:

[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.git

949 lines (935 loc) 127 kB
import { coerceBooleanProperty, _isNumberValue, coerceCssPixelValue } from '@angular/cdk/coercion'; import * as cornerstone from '@cornerstonejs/core'; import { utilities, volumeLoader, cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader, imageLoader, init, RenderingEngine, eventTarget, cache, metaData, StackViewport, Enums, getRenderingEngine, setVolumesForViewports, CONSTANTS } from '@cornerstonejs/core'; import { cornerstoneNiftiImageLoader, createNiftiImageIdsAndCacheMetadata } from '@cornerstonejs/nifti-volume-loader'; import * as polySeg from '@cornerstonejs/polymorphic-segmentation'; import * as i0 from '@angular/core'; import { Injectable, Input, ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Output, HostListener, ContentChildren, Inject, Optional, NgModule, SkipSelf, ElementRef, ViewChild, Directive, ViewChildren, ViewEncapsulation, APP_INITIALIZER } from '@angular/core'; import { init as init$1, destroy, StackScrollTool, WindowLevelTool, PanTool, ZoomTool, TrackballRotateTool, EllipticalROITool, RectangleROITool, LengthTool, AngleTool, ArrowAnnotateTool, ToolGroupManager, state, addTool, Enums as Enums$1, segmentation } from '@cornerstonejs/tools'; import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'; import { api } from 'dicomweb-client'; import dcmjs from 'dcmjs'; import { calculateSUVScalingFactors } from '@cornerstonejs/calculate-suv'; import { __decorate } from 'tslib'; import { Subject, from, merge, filter, startWith, debounceTime, BehaviorSubject, combineLatest, map } from 'rxjs'; import * as i2 from '@angular/common'; import { DOCUMENT, CommonModule } from '@angular/common'; import { takeUntil, switchMap } from 'rxjs/operators'; var RequestSchema; (function (RequestSchema) { RequestSchema["wadoRs"] = "wadors:"; RequestSchema["wadoUri"] = "wadouri:"; RequestSchema["nifti"] = "nifti:"; })(RequestSchema || (RequestSchema = {})); var VolumeLoaderSchema; (function (VolumeLoaderSchema) { VolumeLoaderSchema["stream"] = "cornerstoneStreamingImageVolume"; VolumeLoaderSchema["dynamicStream"] = "cornerstoneStreamingDynamicImageVolume"; VolumeLoaderSchema["nifti"] = "nifti"; })(VolumeLoaderSchema || (VolumeLoaderSchema = {})); function imageInfoToUniqueId(imageInfo) { if (imageInfo) { if (imageInfo.schema === RequestSchema.wadoRs) { return RequestSchema.wadoRs + imageInfo?.studyInstanceUID + imageInfo?.seriesInstanceUID; } else if (imageInfo.schema === RequestSchema.nifti) { return RequestSchema.nifti + imageInfo.urlRoot; } else { console.error('Unsupported request schema'); return ''; } } return ''; } /** * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ function toBoolean(value) { return coerceBooleanProperty(value); } function toNumber(value, fallbackValue = 0) { return _isNumberValue(value) ? Number(value) : fallbackValue; } function toCssPixel(value) { return coerceCssPixelValue(value); } // eslint-disable no-invalid-this /** * Get the function-property type's value */ function valueFunctionProp(prop, ...args) { return typeof prop === 'function' ? prop(...args) : prop; } function propDecoratorFactory(name, fallback) { function propDecorator(target, propName, originalDescriptor) { const privatePropName = `$$__zorroPropDecorator__${propName}`; if (Object.prototype.hasOwnProperty.call(target, privatePropName)) { console.warn(`The prop "${privatePropName}" is already exist, it will be overrided by ${name} decorator.`); } Object.defineProperty(target, privatePropName, { configurable: true, writable: true, }); return { get() { return originalDescriptor && originalDescriptor.get ? originalDescriptor.get.bind(this)() : this[privatePropName]; }, set(value) { if (originalDescriptor && originalDescriptor.set) { originalDescriptor.set.bind(this)(fallback(value)); } this[privatePropName] = fallback(value); }, }; } return propDecorator; } /** * Input decorator that handle a prop to do get/set automatically with toBoolean * * Why not using @InputBoolean alone without @Input? AOT needs @Input to be visible * * @howToUse * ``` * @Input() @InputBoolean() visible: boolean = false; * * // Act as below: * // @Input() * // get visible() { return this.__visible; } * // set visible(value) { this.__visible = value; } * // __visible = false; * ``` */ function InputBoolean() { return propDecoratorFactory('InputBoolean', toBoolean); } function InputCssPixel() { return propDecoratorFactory('InputCssPixel', toCssPixel); } function InputNumber(fallbackValue) { return propDecoratorFactory('InputNumber', (value) => toNumber(value, fallbackValue)); } const scalingPerImageId = {}; function addInstance(imageId, scalingMetaData) { const imageURI = utilities.imageIdToURI(imageId); scalingPerImageId[imageURI] = scalingMetaData; } function get(type, imageId) { if (type === 'scalingModule') { const imageURI = utilities.imageIdToURI(imageId); return scalingPerImageId[imageURI]; } } var ptScalingMetaDataProvider = { addInstance, get }; const { calibratedPixelSpacingMetadataProvider } = cornerstone.utilities; function initProviders() { cornerstone.metaData.addProvider(ptScalingMetaDataProvider.get.bind(ptScalingMetaDataProvider), 10000); cornerstone.metaData.addProvider(calibratedPixelSpacingMetadataProvider.get.bind(calibratedPixelSpacingMetadataProvider), 11000); } function initVolumeLoader() { volumeLoader.registerUnknownVolumeLoader(cornerstoneStreamingImageVolumeLoader); volumeLoader.registerVolumeLoader(VolumeLoaderSchema.stream, cornerstoneStreamingImageVolumeLoader); volumeLoader.registerVolumeLoader(VolumeLoaderSchema.dynamicStream, cornerstoneStreamingDynamicImageVolumeLoader); imageLoader.registerImageLoader(VolumeLoaderSchema.nifti, cornerstoneNiftiImageLoader); } class CornerstoneService { constructor() { this.renderingEngineId = 'RENDERING_ENGINE_ID'; this.initialized = false; } async init() { try { if (this.initialized) { return; } initProviders(); cornerstoneDICOMImageLoader.init(); initVolumeLoader(); await Promise.all([ init(), init$1({ addons: { polySeg: polySeg, }, }), ]); this.renderingEngine = new RenderingEngine(this.renderingEngineId); this.initialized = true; console.debug('CornerstoneService initialized'); } catch (error) { console.error('Failed to initialize CornerstoneService:', error); throw error; } } checkInitialized() { if (!this.initialized) { throw new Error('CornerstoneService not initialized'); } } setToolGroup(toolGroup) { this.toolGroup = toolGroup; } getRenderingEngine() { this.checkInitialized(); return this.renderingEngine; } getRenderingEngineId() { return this.renderingEngineId; } getToolGroup() { return this.toolGroup; } ngOnDestroy() { this.renderingEngine.destroy(); eventTarget.reset(); cache.purgeCache(); destroy(); metaData.removeAllProviders(); imageLoader.unregisterAllImageLoaders(); console.debug('cs service destroyed'); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CornerstoneService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CornerstoneService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CornerstoneService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [] }); // See https://github.com/OHIF/Viewers/blob/94a9067fe3d291d30e25a1bda5913511388edea2/platform/core/src/utils/metadataProvider/getPixelSpacingInformation.js function getPixelSpacingInformation(instance) { // See http://gdcm.sourceforge.net/wiki/index.php/Imager_Pixel_Spacing // TODO: Add Ultrasound region spacing // TODO: Add manual calibration // TODO: Use ENUMS from dcmjs const projectionRadiographSOPClassUIDs = [ '1.2.840.10008.5.1.4.1.1.1', // CR Image Storage '1.2.840.10008.5.1.4.1.1.1.1', // Digital X-Ray Image Storage – for Presentation '1.2.840.10008.5.1.4.1.1.1.1.1', // Digital X-Ray Image Storage – for Processing '1.2.840.10008.5.1.4.1.1.1.2', // Digital Mammography X-Ray Image Storage – for Presentation '1.2.840.10008.5.1.4.1.1.1.2.1', // Digital Mammography X-Ray Image Storage – for Processing '1.2.840.10008.5.1.4.1.1.1.3', // Digital Intra – oral X-Ray Image Storage – for Presentation '1.2.840.10008.5.1.4.1.1.1.3.1', // Digital Intra – oral X-Ray Image Storage – for Processing '1.2.840.10008.5.1.4.1.1.12.1', // X-Ray Angiographic Image Storage '1.2.840.10008.5.1.4.1.1.12.1.1', // Enhanced XA Image Storage '1.2.840.10008.5.1.4.1.1.12.2', // X-Ray Radiofluoroscopic Image Storage '1.2.840.10008.5.1.4.1.1.12.2.1', // Enhanced XRF Image Storage '1.2.840.10008.5.1.4.1.1.12.3', // X-Ray Angiographic Bi-plane Image Storage Retired ]; const { PixelSpacing, ImagerPixelSpacing, SOPClassUID, PixelSpacingCalibrationType, PixelSpacingCalibrationDescription, EstimatedRadiographicMagnificationFactor, SequenceOfUltrasoundRegions, } = instance; const isProjection = projectionRadiographSOPClassUIDs.includes(SOPClassUID); const TYPES = { NOT_APPLICABLE: 'NOT_APPLICABLE', UNKNOWN: 'UNKNOWN', CALIBRATED: 'CALIBRATED', DETECTOR: 'DETECTOR', }; if (!isProjection) { return PixelSpacing; } if (isProjection && !ImagerPixelSpacing) { // If only Pixel Spacing is present, and this is a projection radiograph, // PixelSpacing should be used, but the user should be informed that // what it means is unknown return { PixelSpacing, type: TYPES.UNKNOWN, isProjection, }; } else if (PixelSpacing && ImagerPixelSpacing && PixelSpacing === ImagerPixelSpacing) { // If Imager Pixel Spacing and Pixel Spacing are present and they have the same values, // then the user should be informed that the measurements are at the detector plane return { PixelSpacing, type: TYPES.DETECTOR, isProjection, }; } else if (PixelSpacing && ImagerPixelSpacing && PixelSpacing !== ImagerPixelSpacing) { // If Imager Pixel Spacing and Pixel Spacing are present and they have different values, // then the user should be informed that these are "calibrated" // (in some unknown manner if Pixel Spacing Calibration Type and/or // Pixel Spacing Calibration Description are absent) return { PixelSpacing, type: TYPES.CALIBRATED, isProjection, PixelSpacingCalibrationType, PixelSpacingCalibrationDescription, }; } else if (!PixelSpacing && ImagerPixelSpacing) { let CorrectedImagerPixelSpacing = ImagerPixelSpacing; if (EstimatedRadiographicMagnificationFactor) { // Note that in IHE Mammo profile compliant displays, the value of Imager Pixel Spacing is required to be corrected by // Estimated Radiographic Magnification Factor and the user informed of that. // TODO: should this correction be done before all of this logic? CorrectedImagerPixelSpacing = ImagerPixelSpacing.map((pixelSpacing) => pixelSpacing / EstimatedRadiographicMagnificationFactor); } else { console.warn('EstimatedRadiographicMagnificationFactor was not present. Unable to correct ImagerPixelSpacing.'); } return { PixelSpacing: CorrectedImagerPixelSpacing, isProjection, }; } else if (SequenceOfUltrasoundRegions && typeof SequenceOfUltrasoundRegions === 'object') { const { PhysicalDeltaX, PhysicalDeltaY } = SequenceOfUltrasoundRegions; const USPixelSpacing = [PhysicalDeltaX * 10, PhysicalDeltaY * 10]; return { PixelSpacing: USPixelSpacing, }; } else if (SequenceOfUltrasoundRegions && Array.isArray(SequenceOfUltrasoundRegions) && SequenceOfUltrasoundRegions.length > 1) { console.warn('Sequence of Ultrasound Regions > one entry. This is not yet implemented, all measurements will be shown in pixels.'); } else if (!isProjection && !ImagerPixelSpacing) { // If only Pixel Spacing is present, and this is not a projection radiograph, // we can stop here return { PixelSpacing, type: TYPES.NOT_APPLICABLE, isProjection, }; } console.warn('Unknown combination of PixelSpacing and ImagerPixelSpacing identified. Unable to determine spacing.'); } function getPTImageIdInstanceMetadata(imageId) { const petSequenceModule = metaData.get('petIsotopeModule', imageId); const generalSeriesModule = metaData.get('generalSeriesModule', imageId); const patientStudyModule = metaData.get('patientStudyModule', imageId); const ptSeriesModule = metaData.get('petSeriesModule', imageId); const ptImageModule = metaData.get('petImageModule', imageId); if (!petSequenceModule) { throw new Error('petSequenceModule metadata is required'); } const radiopharmaceuticalInfo = petSequenceModule.radiopharmaceuticalInfo; const { seriesDate, seriesTime, acquisitionDate, acquisitionTime } = generalSeriesModule; const { patientWeight } = patientStudyModule; const { correctedImage, units, decayCorrection } = ptSeriesModule; if (seriesDate === undefined || seriesTime === undefined || patientWeight === undefined || acquisitionDate === undefined || acquisitionTime === undefined || correctedImage === undefined || units === undefined || decayCorrection === undefined || radiopharmaceuticalInfo.radionuclideTotalDose === undefined || radiopharmaceuticalInfo.radionuclideHalfLife === undefined || (radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime === undefined && seriesDate === undefined && radiopharmaceuticalInfo.radiopharmaceuticalStartTime === undefined) // ) { throw new Error('required metadata are missing'); } const instanceMetadata = { CorrectedImage: correctedImage, Units: units, RadionuclideHalfLife: radiopharmaceuticalInfo.radionuclideHalfLife, RadionuclideTotalDose: radiopharmaceuticalInfo.radionuclideTotalDose, DecayCorrection: decayCorrection, PatientWeight: patientWeight, SeriesDate: seriesDate, SeriesTime: seriesTime, AcquisitionDate: acquisitionDate, AcquisitionTime: acquisitionTime, }; if (radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime && radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime !== undefined && typeof radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime === 'string') { instanceMetadata.RadiopharmaceuticalStartDateTime = radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime; } if (radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime && radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime !== undefined && typeof radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime !== 'string') { const dateString = convertInterfaceDateToString(radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime); instanceMetadata.RadiopharmaceuticalStartDateTime = dateString; } if (instanceMetadata.AcquisitionDate && instanceMetadata.AcquisitionDate !== undefined && typeof instanceMetadata.AcquisitionDate !== 'string') { const dateString = convertInterfaceDateToString(instanceMetadata.AcquisitionDate); instanceMetadata.AcquisitionDate = dateString; } if (instanceMetadata.SeriesDate && instanceMetadata.SeriesDate !== undefined && typeof instanceMetadata.SeriesDate !== 'string') { const dateString = convertInterfaceDateToString(instanceMetadata.SeriesDate); instanceMetadata.SeriesDate = dateString; } if (radiopharmaceuticalInfo.radiopharmaceuticalStartTime && radiopharmaceuticalInfo.radiopharmaceuticalStartTime !== undefined && typeof radiopharmaceuticalInfo.radiopharmaceuticalStartTime === 'string') { instanceMetadata.RadiopharmaceuticalStartTime = radiopharmaceuticalInfo.radiopharmaceuticalStartTime; } if (radiopharmaceuticalInfo.radiopharmaceuticalStartTime && radiopharmaceuticalInfo.radiopharmaceuticalStartTime !== undefined && typeof radiopharmaceuticalInfo.radiopharmaceuticalStartTime !== 'string') { const timeString = convertInterfaceTimeToString(radiopharmaceuticalInfo.radiopharmaceuticalStartTime); instanceMetadata.RadiopharmaceuticalStartTime = timeString; } if (instanceMetadata.AcquisitionTime && instanceMetadata.AcquisitionTime !== undefined && typeof instanceMetadata.AcquisitionTime !== 'string') { const timeString = convertInterfaceTimeToString(instanceMetadata.AcquisitionTime); instanceMetadata.AcquisitionTime = timeString; } if (instanceMetadata.SeriesTime && instanceMetadata.SeriesTime !== undefined && typeof instanceMetadata.SeriesTime !== 'string') { const timeString = convertInterfaceTimeToString(instanceMetadata.SeriesTime); instanceMetadata.SeriesTime = timeString; } if (ptImageModule.frameReferenceTime && ptImageModule.frameReferenceTime !== undefined) { instanceMetadata.FrameReferenceTime = ptImageModule.frameReferenceTime; } if (ptImageModule.actualFrameDuration && ptImageModule.actualFrameDuration !== undefined) { instanceMetadata.ActualFrameDuration = ptImageModule.actualFrameDuration; } if (patientStudyModule.patientSex && patientStudyModule.patientSex !== undefined) { instanceMetadata.PatientSex = patientStudyModule.patientSex; } if (patientStudyModule.patientSize && patientStudyModule.patientSize !== undefined) { instanceMetadata.PatientSize = patientStudyModule.patientSize; } // Todo: add private tags // if ( // dicomMetaData['70531000'] || // dicomMetaData['70531000'] !== undefined || // dicomMetaData['70531009'] || // dicomMetaData['70531009'] !== undefined // ) { // const philipsPETPrivateGroup: PhilipsPETPrivateGroup = { // SUVScaleFactor: dicomMetaData['70531000'], // ActivityConcentrationScaleFactor: dicomMetaData['70531009'], // }; // instanceMetadata.PhilipsPETPrivateGroup = philipsPETPrivateGroup; // } // if (dicomMetaData['0009100d'] && dicomMetaData['0009100d'] !== undefined) { // instanceMetadata.GEPrivatePostInjectionDateTime = dicomMetaData['0009100d']; // } return instanceMetadata; } function convertInterfaceTimeToString(time) { const hours = `${time.hours || '00'}`.padStart(2, '0'); const minutes = `${time.minutes || '00'}`.padStart(2, '0'); const seconds = `${time.seconds || '00'}`.padStart(2, '0'); const fractionalSeconds = `${time.fractionalSeconds || '000000'}`.padEnd(6, '0'); const timeString = `${hours}${minutes}${seconds}.${fractionalSeconds}`; return timeString; } function convertInterfaceDateToString(date) { const month = `${date.month}`.padStart(2, '0'); const day = `${date.day}`.padStart(2, '0'); const dateString = `${date.year}${month}${day}`; return dateString; } const { DicomMetaDictionary } = dcmjs.data; class ImageIdService { constructor() { } async wadoRsCreateImageIdsAndCacheMetaData(imageInfo) { const MODALITY = '00080060'; const SOP_INSTANCE_UID = '00080018'; const { urlRoot, studyInstanceUID, seriesInstanceUID } = imageInfo; const client = new api.DICOMwebClient({ url: urlRoot }); const instances = await client.retrieveSeriesMetadata({ studyInstanceUID, seriesInstanceUID, }); const modality = instances[0][MODALITY].Value[0]; let sopInstanceUIDSet; if (imageInfo?.sopInstanceUIDs && imageInfo?.sopInstanceUIDs.length >= 0) { sopInstanceUIDSet = new Set(imageInfo?.sopInstanceUIDs); } else { const instanceUIDs = instances.map((instanceMetaData) => { return instanceMetaData[SOP_INSTANCE_UID].Value[0]; }); sopInstanceUIDSet = new Set(instanceUIDs); imageInfo.sopInstanceUIDs = instanceUIDs; } let imageIds = []; instances.forEach((instanceMetaData) => { const sopInstanceUID = instanceMetaData[SOP_INSTANCE_UID].Value[0]; if (sopInstanceUIDSet.has(sopInstanceUID)) { const imageId = ImageIdService.wadoRsCreateImageIds({ ...imageInfo, sopInstanceUIDs: [sopInstanceUID], }); imageIds.push(...imageId); // only one image id cornerstoneDICOMImageLoader.wadors.metaDataManager.add(imageId[0], instanceMetaData); } }); imageIds = this.convertMultiframeImageIds(imageIds); this.pixelSpacingProvider(imageIds); if (modality === 'PT') { this.ptProvider(imageIds); } return imageIds; } static createImageIds(imageInfo) { if (imageInfo?.schema === RequestSchema.wadoRs) { return ImageIdService.wadoRsCreateImageIds(imageInfo); } else if (imageInfo?.schema === RequestSchema.wadoUri) { return ImageIdService.wadoURICreateImageIds(imageInfo); } return []; } static wadoURICreateImageIds(imageInfo) { if (!imageInfo.sopInstanceUIDs || imageInfo.sopInstanceUIDs.length === 0) { console.error('Invalid image info', imageInfo); return []; } const { urlRoot, studyInstanceUID, seriesInstanceUID, sopInstanceUIDs } = imageInfo; const wadoURIRoot = `${RequestSchema.wadoUri}${urlRoot}?requestType=WADO&studyUID=${studyInstanceUID}&seriesUID=${seriesInstanceUID}&contentType=application%2Fdicom`; return sopInstanceUIDs.map((uid) => { return `${wadoURIRoot}&objectUID=${uid}`; }); } static wadoRsCreateImageIds(imageInfo) { if (!imageInfo.sopInstanceUIDs || imageInfo.sopInstanceUIDs.length === 0) { console.error('Invalid image info', imageInfo); return []; } const { urlRoot, studyInstanceUID, seriesInstanceUID, sopInstanceUIDs } = imageInfo; const wadoURIRoot = `${RequestSchema.wadoRs}${urlRoot}/studies/${studyInstanceUID}/series/${seriesInstanceUID}`; return sopInstanceUIDs.map((uid) => { return `${wadoURIRoot}/instances/${uid}/frames/1`; }); } convertMultiframeImageIds(imageIds) { const newImageIds = []; imageIds.forEach((imageId) => { const { imageIdFrameless } = this.getFrameInformation(imageId); const instanceMetaData = metaData.get('multiframeModule', imageId); if (instanceMetaData && instanceMetaData.NumberOfFrames && instanceMetaData.NumberOfFrames > 1) { const NumberOfFrames = instanceMetaData.NumberOfFrames; for (let i = 0; i < NumberOfFrames; i++) { const newImageId = imageIdFrameless + (i + 1); newImageIds.push(newImageId); } } else newImageIds.push(imageId); }); return newImageIds; } getFrameInformation(imageId) { if (imageId.includes('wadors:')) { const frameIndex = imageId.indexOf('/frames/'); const imageIdFrameless = frameIndex > 0 ? imageId.slice(0, frameIndex + 8) : imageId; return { frameIndex, imageIdFrameless, }; } else { const frameIndex = imageId.indexOf('&frame='); let imageIdFrameless = frameIndex > 0 ? imageId.slice(0, frameIndex + 7) : imageId; if (!imageIdFrameless.includes('&frame=')) { imageIdFrameless = imageIdFrameless + '&frame='; } return { frameIndex, imageIdFrameless, }; } } pixelSpacingProvider(imageIds) { imageIds.forEach((imageId) => { let instanceMetaData = cornerstoneDICOMImageLoader.wadors.metaDataManager.get(imageId); // It was using JSON.parse(JSON.stringify(...)) before but it is 8x slower instanceMetaData = this.removeInvalidTags(instanceMetaData); if (instanceMetaData) { // Add calibrated pixel spacing const metadata = DicomMetaDictionary.naturalizeDataset(instanceMetaData); const pixelSpacing = getPixelSpacingInformation(metadata); if (pixelSpacing) { utilities.calibratedPixelSpacingMetadataProvider.add(imageId, pixelSpacing.map((s) => parseFloat(s))); } } }); } ptProvider(imageIds) { const instanceMetadataArray = []; imageIds.forEach((imageId) => { const instanceMetadata = getPTImageIdInstanceMetadata(imageId); // TODO: Temporary fix because static-wado is producing a string, not an array of values // (or maybe dcmjs isn't parsing it correctly?) // It's showing up like 'DECY\\ATTN\\SCAT\\DTIM\\RAN\\RADL\\DCAL\\SLSENS\\NORM' // but calculate-suv expects ['DECY', 'ATTN', ...] if (typeof instanceMetadata.CorrectedImage === 'string') { instanceMetadata.CorrectedImage = instanceMetadata.CorrectedImage.split('\\'); } if (instanceMetadata) { instanceMetadataArray.push(instanceMetadata); } }); if (instanceMetadataArray.length) { const suvScalingFactors = calculateSUVScalingFactors(instanceMetadataArray); instanceMetadataArray.forEach((instanceMetadata, index) => { ptScalingMetaDataProvider.addInstance(imageIds[index], suvScalingFactors[index]); }); } } removeInvalidTags(srcMetadata) { // Object.create(null) make it ~9% faster const dstMetadata = Object.create(null); const tagIds = Object.keys(srcMetadata); let tagValue; tagIds.forEach((tagId) => { tagValue = srcMetadata[tagId]; if (tagValue !== undefined && tagValue !== null) { dstMetadata[tagId] = tagValue; } }); return dstMetadata; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImageIdService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImageIdService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImageIdService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [] }); const windowWidth = 400; const windowCenter = 40; const lower = windowCenter - windowWidth / 2.0; const upper = windowCenter + windowWidth / 2.0; const ctVoiRange = { lower, upper }; function setCtTransferFunctionForVolumeActor({ volumeActor }) { volumeActor.getProperty().getRGBTransferFunction(0).setMappingRange(lower, upper); } /** * Similar to {@link addVolumesToViewports} it adds volumes to viewports; however, * this method will Set the volumes on the viewports which means that the previous * volumes will be removed. * * @param renderingEngine - The rendering engine to use to get viewports from * @param volumeInputs - Array of volume inputs including volumeId. Other properties * such as visibility, callback, blendMode, slabThickness are optional * @param viewportIds - Array of viewport IDs to add the volume to * @param immediateRender - If true, the volumes will be rendered immediately * @returns A promise that resolves when all volumes have been added */ async function setStacksForViewports(renderingEngine, viewportIds, imageIds, currentImageIdIndex) { // Check if all viewports are StackViewport viewportIds.forEach((viewportId) => { const viewport = renderingEngine.getViewport(viewportId); if (!viewport) { throw new Error(`Viewport with Id ${viewportId} does not exist`); } // if not instance of VolumeViewport, throw if (!(viewport instanceof StackViewport)) { throw new Error('setStacksForViewports only supports StackViewport'); } }); const setStackPromises = viewportIds.map(async (viewportId) => { const viewport = renderingEngine.getViewport(viewportId); await viewport.setStack(imageIds, currentImageIdIndex); // Set the VOI of the stack viewport.setProperties({ voiRange: ctVoiRange }); // Render the image viewport.render(); }); await Promise.all(setStackPromises); return; } var LayoutEnum; (function (LayoutEnum) { LayoutEnum[LayoutEnum["LAYOUT_1x1"] = 0] = "LAYOUT_1x1"; LayoutEnum[LayoutEnum["LAYOUT_1x2"] = 1] = "LAYOUT_1x2"; LayoutEnum[LayoutEnum["LAYOUT_1x3"] = 2] = "LAYOUT_1x3"; LayoutEnum[LayoutEnum["LAYOUT_2x2"] = 3] = "LAYOUT_2x2"; })(LayoutEnum || (LayoutEnum = {})); const STACK_VIEWPORT_INPUTS = [ { viewportId: 'viewport-stack', type: Enums.ViewportType.STACK, defaultOptions: { background: [0, 0, 0], }, }, ]; const SAGITTAL_VIEWPORT_INPUTS = [ { viewportId: 'viewport-mpr-sagittal', type: Enums.ViewportType.ORTHOGRAPHIC, defaultOptions: { background: [0, 0, 0], orientation: Enums.OrientationAxis.SAGITTAL, }, }, ]; const CORONAL_VIEWPORT_INPUTS = [ { viewportId: 'viewport-mpr-coronal', type: Enums.ViewportType.ORTHOGRAPHIC, defaultOptions: { background: [0, 0, 0], orientation: Enums.OrientationAxis.CORONAL, }, }, ]; const AXIAL_VIEWPORT_INPUTS = [ { viewportId: 'viewport-mpr-axial', type: Enums.ViewportType.ORTHOGRAPHIC, defaultOptions: { background: [0, 0, 0], orientation: Enums.OrientationAxis.AXIAL, }, }, ]; const VOLUME_3D_VIEWPORT_INPUTS = [ { viewportId: 'viewport-volume', type: Enums.ViewportType.VOLUME_3D, defaultOptions: { background: [0.2, 0, 0.2], }, }, ]; function generateViewportInputs(layout, suffix, imageInfo) { const viewportType = imageInfo?.viewportType || Enums.ViewportType.STACK; const isStack = viewportType === Enums.ViewportType.STACK; const result = []; switch (layout) { case LayoutEnum.LAYOUT_1x1: // 1x1 layout: set viewportType according to imageType if (viewportType === Enums.ViewportType.STACK) { const viewportInput = structuredClone(STACK_VIEWPORT_INPUTS[0]); viewportInput.viewportId = `viewport-stack-${suffix}`; result.push(viewportInput); } else if (viewportType === Enums.ViewportType.VOLUME_3D) { const viewportInput = structuredClone(VOLUME_3D_VIEWPORT_INPUTS[0]); viewportInput.viewportId = `viewport-volume-${suffix}`; result.push(viewportInput); } else { const viewportInput = structuredClone(SAGITTAL_VIEWPORT_INPUTS[0]); viewportInput.viewportId = `viewport-sagittal-${suffix}`; result.push(viewportInput); } break; case LayoutEnum.LAYOUT_1x2: // 1x2 layout: all stack when imageType is stack, otherwise one volume3D one sagittal if (isStack) { for (let i = 0; i < 2; i++) { const viewportInput = structuredClone(STACK_VIEWPORT_INPUTS[0]); viewportInput.viewportId = `viewport-stack-${i + 1}${suffix}`; result.push(viewportInput); } } else { const sagittalViewport = structuredClone(SAGITTAL_VIEWPORT_INPUTS[0]); sagittalViewport.viewportId = `viewport-sagittal-${suffix}`; result.push(sagittalViewport); const volumeViewport = structuredClone(VOLUME_3D_VIEWPORT_INPUTS[0]); volumeViewport.viewportId = `viewport-volume-${suffix}`; result.push(volumeViewport); } break; case LayoutEnum.LAYOUT_1x3: // 1x3 layout: all stack when imageType is stack, otherwise one volume3D one sagittal one axial if (isStack) { for (let i = 0; i < 3; i++) { const viewportInput = structuredClone(STACK_VIEWPORT_INPUTS[0]); viewportInput.viewportId = `viewport-stack-${i + 1}${suffix}`; result.push(viewportInput); } } else { const sagittalViewport = structuredClone(SAGITTAL_VIEWPORT_INPUTS[0]); sagittalViewport.viewportId = `viewport-sagittal-${suffix}`; result.push(sagittalViewport); const axialViewport = structuredClone(AXIAL_VIEWPORT_INPUTS[0]); axialViewport.viewportId = `viewport-axial-${suffix}`; result.push(axialViewport); const volumeViewport = structuredClone(VOLUME_3D_VIEWPORT_INPUTS[0]); volumeViewport.viewportId = `viewport-volume-${suffix}`; result.push(volumeViewport); } break; case LayoutEnum.LAYOUT_2x2: // 2x2 layout: all stack when imageType is stack, otherwise one volume3D one sagittal one axial one coronal if (isStack) { for (let i = 0; i < 4; i++) { const viewportInput = structuredClone(STACK_VIEWPORT_INPUTS[0]); viewportInput.viewportId = `viewport-stack-${i + 1}${suffix}`; result.push(viewportInput); } } else { const sagittalViewport = structuredClone(SAGITTAL_VIEWPORT_INPUTS[0]); sagittalViewport.viewportId = `viewport-sagittal-${suffix}`; result.push(sagittalViewport); const axialViewport = structuredClone(AXIAL_VIEWPORT_INPUTS[0]); axialViewport.viewportId = `viewport-axial-${suffix}`; result.push(axialViewport); const coronalViewport = structuredClone(CORONAL_VIEWPORT_INPUTS[0]); coronalViewport.viewportId = `viewport-coronal-${suffix}`; result.push(coronalViewport); const volumeViewport = structuredClone(VOLUME_3D_VIEWPORT_INPUTS[0]); volumeViewport.viewportId = `viewport-volume-${suffix}`; result.push(volumeViewport); } break; } return result; } function generateRandomString(length = 6) { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { const randomIndex = Math.floor(Math.random() * characters.length); result += characters[randomIndex]; } return '-' + result; } class IconComponent { constructor() { this.iconfont = ''; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: IconComponent, selector: "i[nc-icon]", inputs: { iconfont: "iconfont" }, ngImport: i0, template: ` <svg class="icon" aria-hidden="true"> <use attr.xlink:href="#{{ iconfont }}"></use> </svg> `, isInline: true, styles: [":host{display:inline-block;line-height:0}.icon{fill:currentColor;height:1em;overflow:hidden;vertical-align:-.15em;width:1em}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: IconComponent, decorators: [{ type: Component, args: [{ selector: 'i[nc-icon]', template: ` <svg class="icon" aria-hidden="true"> <use attr.xlink:href="#{{ iconfont }}"></use> </svg> `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:inline-block;line-height:0}.icon{fill:currentColor;height:1em;overflow:hidden;vertical-align:-.15em;width:1em}\n"] }] }], propDecorators: { iconfont: [{ type: Input }] } }); class ButtonComponent { onClick(event) { if (this.disabled) { event.preventDefault(); event.stopImmediatePropagation(); } else if (this.isToggle) { this.pressed = !this.pressed; } } get pressedChange$() { return this._pressedChange$.asObservable(); } /** * Controls button pressed state **/ get pressed() { return this._pressed; } set pressed(value) { if (this.pressed !== coerceBooleanProperty(value)) { this._pressed = !this.pressed; this.pressedChange.emit(this.pressed); this._pressedChange$.next({ source: this, pressed: this.pressed }); } } constructor(_elementRef, cd) { this._elementRef = _elementRef; this.cd = cd; this.disabled = false; this.tabIndex = null; this.type = 'default'; this.shape = 'default'; this.content = 'default'; this.size = 'default'; this.isToggle = false; this.icon = null; this.text = null; /** * Emits whenever button pressed state change **/ this.pressedChange = new EventEmitter(); this._pressedChange$ = new Subject(); this._pressed = false; this.destroy$ = new Subject(); } _getHostElement() { return this._elementRef.nativeElement; } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } /** * @docs-private */ _updatePressed(value) { this.pressed = value; this.cd.markForCheck(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ButtonComponent, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ButtonComponent, selector: "button[nc-button], a[nc-button]", inputs: { disabled: "disabled", tabIndex: "tabIndex", type: "type", shape: "shape", content: "content", size: "size", isToggle: "isToggle", icon: "icon", text: "text", value: "value" }, outputs: { pressedChange: "pressedChange" }, host: { listeners: { "click": "onClick($event)" }, properties: { "class.btn-type-primary": "type === 'primary'", "class.btn-type-default": "type === 'default'", "class.btn-content-icon-text": "content === 'icon-text'", "class.btn-content-icon": "content === 'icon'", "class.btn-content-text": "content === 'text'", "class.btn-shape-round": "shape === 'round'", "class.btn-shape-circle": "shape === 'circle'", "class.btn-size-lg": "size === 'large'", "class.btn-size-sm": "size === 'small'", "attr.tabindex": "disabled ? -1 : (tabIndex === null ? null : tabIndex)", "attr.disabled": "disabled || null", "class.pressed": "this.pressed" }, classAttribute: "nc-btn" }, exportAs: ["ncButton"], ngImport: i0, template: "<ng-container [ngSwitch]=\"content\">\n <ng-container *ngSwitchCase=\"'icon-text'\">\n <i nc-icon [iconfont]=\"icon\"></i>\n <span class=\"text\">{{ text }}</span>\n </ng-container>\n <div *ngSwitchCase=\"'text'\">\n <span>{{ text }}</span>\n </div>\n <div *ngSwitchCase=\"'icon'\">\n <i nc-icon [iconfont]=\"icon\"></i>\n </div>\n <ng-content *ngSwitchDefault></ng-content>\n</ng-container>\n", styles: ["@charset \"UTF-8\";.h1,h1,.headline{font-size:24px;line-height:32px;font-weight:400;font-family:roboto,Noto Sans SC,sans-serif;letter-spacing:normal}.h2,h2,.title{font-size:20px;line-height:32px;font-weight:500;font-family:roboto,Noto Sans SC,sans-serif;letter-spacing:normal}.h3,h3,.subheading{font-size:16px;line-height:28px;font-weight:400;font-family:roboto,Noto Sans SC,sans-serif;letter-spacing:normal}.h4,h4,.body{font-size:14px;line-height:24px;font-weight:500;font-family:roboto,Noto Sans SC,sans-serif;letter-spacing:normal}.h5,h5,.button,:host.nc-btn{font-size:14px;line-height:14px;font-weight:500;font-family:roboto,Noto Sans SC,sans-serif;letter-spacing:normal}:host.nc-btn{cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:transparent;box-sizing:border-box;position:relative;appearance:none;text-align:center;display:inline-flex;align-items:center;justify-content:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;user-select:none;text-decoration:none;margin:0;min-width:0;padding:2px;overflow:visible;border-radius:0;border-width:1px;border-style:solid;border-color:transparent;font-size:16px;line-height:20px}:host.nc-btn::-moz-focus-inner{border:0}:host.nc-btn:disabled{cursor:not-allowed;color:#595959d9}:host.nc-btn.transition{transition-duration:.15s;transition-property:background-color,border-color,box-shadow,color;transition-timing-function:ease-in}:host.btn-type-primary{color:#f2f2f2d9;background-color:#3bdbdf}:host.btn-type-primary:hover:not(:disabled){background-color:#93ebed}:host.btn-type-primary:active:not(:disabled){background-color:#3bdbdf}:host.btn-type-primary.pressed:not(:disabled){background-color:#93ebed}:host.btn-type-default{color:#f2f2f2d9;background-color:transparent}:host.btn-type-default:hover:not(:disabled){border:1px transparent solid!important;color:#3bdbdfd9}:host.btn-type-default:focus:not(:disabled){border:1px rgba(59,219,223,.85) solid}:host.btn-type-default:active:not(:disabled){border:1px transparent solid!important;color:#3bdbdfd9}:host.btn-type-default.pressed:not(:disabled){border:1px transparent solid!important;color:#3bdbdfd9}:host.btn-content-icon-text{flex-flow:column nowrap;align-items:center;justify-content:space-evenly;height:42px}:host.btn-content-icon-text i{font-size:20px}:host.btn-content-icon-text span{font-size:12px;line-height:16px}:host.btn-content-icon{font-size:20px;line-height:24px}:host.btn-content-text{font-size:16px;line-height:20px}:host.btn-shape-round{border-radius:2px}:host.btn-shape-circle{border-radius:50%}\n"], dependencies: [{ kind: "directive", type: i2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: IconComponent, selector: "i[nc-icon]", inputs: ["iconfont"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } __decorate([ InputBoolean() ], ButtonComponent.prototype, "disabled", void 0); __decorate([ InputBoolean() ], ButtonComponent.prototype, "isToggle", void 0); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ButtonComponent, decorators: [{ type: Component, args: [{ selector: 'button[nc-button], a[nc-button]', exportAs: 'ncButton', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'nc-btn', '[class.btn-type-primary]': `type === 'primary'`, '[class.btn-type-default]': `type === 'default'`, '[class.btn-content-icon-text]': `content === 'icon-text'`, '[class.btn-content-icon]': `content === 'icon'`, '[class.btn-content-text]': `content === 'text'`, '[class.btn-shape-round]': `shape === 'round'`, '[class.btn-shape-circle]': `shape === 'circle'`, '[class.btn-size-lg]': `size === 'large'`, '[class.btn-size-sm]': `size === 'small'`, '[attr.tabindex]': 'disabled ? -1 : (tabIndex === null ? null : tabIndex)', '[attr.disabled]': 'disabled || null', }, template: "<ng-container [ngSwitch]=\"content\">\n <ng-container *ngSwitchCase=\"'icon-text'\">\n <i nc-icon [iconfont]=\"icon\"></i>\n <span class=\"text\">{{ text }}</span>\n </ng-container>\n <div *ngSwitchCase=\"'text'\">\n <span>{{ text }}</span>\n </div>\n <div *ngSwitchCase=\"'icon'\">\n <i nc-icon [iconfont]=\"icon\"></i>\n </div>\n <ng-content *ngSwitchDefault></ng-content>\n</ng-container>\n", styles: ["@charset \"UTF-8\";.h1,h1,.headline{font-size:24px;line-height:32px;font-weight:400;font-family:roboto,Noto Sans SC,sans-serif;letter-spacing:normal}.h2,h2,.title{font-size:20px;line-height:32px;font-weight:500;font-family:roboto,Noto Sans SC,sans-serif;letter-spacing:normal}.h3,h3,.subheading{font-size:16px;line-height:28px;font-weight:400;font-family:roboto,Noto Sans SC,sans-serif;letter-spacing:normal}.h4,h4,.body{font-size:14px;line-height:24px;font-weight:500;font-family:roboto,Noto Sans SC,sans-serif;letter-spacing:normal}.h5,h5,.button,:host.nc-btn{font-size:14px;line-height:14px;font-weight:500;font-family:roboto,Noto Sans SC,sans-serif;letter-spacing:normal}:host.nc-btn{cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:transparent;box-sizing:border-box;position:relative;appearance:none;text-align:center;display:inline-flex;align-items:center;justify-content:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;user-select:none;text-decoration:none;margin:0;min-width:0;padding:2px;overflow:visible;border-radius:0;border-width:1px;border-style:solid;border-color:transparent;font-size:16px;line-height:20px}:host.nc-btn::-moz-focus-inner{border:0}:host.nc-btn:disabled{cursor:not-allowed;color:#595959d9}:host.nc-btn.transition{transition-duration:.15s;transition-property:background-color,border-color,box-shadow,color;transition-timing-function:ease-in}:host.btn-type-primary{color:#f2f2f2d9;background-color:#3bdbdf}:host.btn-type-primary:hover:not(:disabled){background-color:#93ebed}:host.btn-type-primary:active:not(:disabled){background-color:#3bdbdf}:host.btn-type-primary.pressed:not(:disabled){background-color:#93ebed}:host.btn-type-default{color:#f2f2f2d9;background-color:transparent}:host.btn-type-default:hover:not(:disabled){border:1px transparent solid!important;color:#3bdbdfd9}:host.btn-type-default:focus:not(:disabled){border:1px rgba(59,219,223,.85) solid}:host.btn-type-default:active:not(:disabled){border:1px transparent solid!important;color:#3bdbdfd9}:host.btn-type-default.pressed:not(:disabled){border:1px transparent solid!important;color:#3bdbdfd9}:host.btn-content-icon-text{flex-flow:column nowrap;align-items:center;justify-content:space-evenly;height:42px}:host.btn-content-icon-text i{font-size:20px}:host.btn-content-icon-text span{font-size:12px;line-height:16px}:host.btn-content-icon{font-size:20px;line-height:24px}:host.btn-content-text{font-size:16px;line-height:20px}:host.btn-shape-round{border-radius:2px}:host.btn-shape-circle{border-radius:50%}\n"] }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }], propDecorators: { disabled: [{ type: Input }], tabIndex: [{ type: Input }], type: [{ type: Input }], shape: [{ type: Input }], content: [{ type: Input }], size: [{ type: Input }], isToggle: [{ type: Input }], icon: [{ type: Input }], text: [{ type: Input }], value: [{ type: Input }], onClick: [{ type: HostListener, args: ['click', ['$event']] }], pressedChang