+ Your computer does not have enough GPU power to support the default GPU
+ rendering mode. OHIF has switched to CPU rendering mode. Please note
+ that CPU rendering does not support all features such as Volume
+ Rendering, Multiplanar Reconstruction, and Segmentation Overlays.
+
+
+ );
+}
+
+function _showCPURenderingModal(UIModalService, HangingProtocolService) {
+ const callback = progress => {
+ if (progress === 100) {
+ UIModalService.show({
+ content: CPUModal,
+ title: 'OHIF Fell Back to CPU Rendering',
+ });
+
+ return true;
+ }
+ };
+
+ const { unsubscribe } = HangingProtocolService.subscribe(
+ HangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
+ ({ progress }) => {
+ const done = callback(progress);
+
+ if (done) {
+ unsubscribe();
+ }
+ }
+ );
}
diff --git a/extensions/cornerstone/src/initCornerstoneTools.js b/extensions/cornerstone/src/initCornerstoneTools.js
index a092976e678..635b7b77eb5 100644
--- a/extensions/cornerstone/src/initCornerstoneTools.js
+++ b/extensions/cornerstone/src/initCornerstoneTools.js
@@ -12,6 +12,7 @@ import {
BidirectionalTool,
ArrowAnnotateTool,
DragProbeTool,
+ ProbeTool,
AngleTool,
MagnifyTool,
CrosshairsTool,
@@ -19,6 +20,7 @@ import {
init,
addTool,
annotation,
+ ReferenceLinesTool,
} from '@cornerstonejs/tools';
export default function initCornerstoneTools(configuration = {}) {
@@ -28,6 +30,7 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(StackScrollMouseWheelTool);
addTool(StackScrollTool);
addTool(ZoomTool);
+ addTool(ProbeTool);
addTool(VolumeRotateMouseWheelTool);
addTool(MIPJumpToClickTool);
addTool(LengthTool);
@@ -40,6 +43,7 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(MagnifyTool);
addTool(CrosshairsTool);
addTool(SegmentationDisplayTool);
+ addTool(ReferenceLinesTool);
// Modify annotation tools to use dashed lines on SR
const annotationStyle = {
@@ -67,6 +71,7 @@ const toolNames = {
MipJumpToClick: MIPJumpToClickTool.toolName,
Length: LengthTool.toolName,
DragProbe: DragProbeTool.toolName,
+ Probe: ProbeTool.toolName,
RectangleROI: RectangleROITool.toolName,
EllipticalROI: EllipticalROITool.toolName,
Bidirectional: BidirectionalTool.toolName,
@@ -74,6 +79,7 @@ const toolNames = {
Magnify: MagnifyTool.toolName,
Crosshairs: CrosshairsTool.toolName,
SegmentationDisplay: SegmentationDisplayTool.toolName,
+ ReferenceLines: ReferenceLinesTool.toolName,
};
export { toolNames };
diff --git a/extensions/cornerstone/src/initSegmentationService.js b/extensions/cornerstone/src/initSegmentationService.js
deleted file mode 100644
index 3ea099fcec5..00000000000
--- a/extensions/cornerstone/src/initSegmentationService.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import { eventTarget, cache, triggerEvent } from '@cornerstonejs/core';
-import * as csTools from '@cornerstonejs/tools';
-import { Enums as csToolsEnums } from '@cornerstonejs/tools';
-import Labelmap from './utils/segmentationServiceMappings/Labelmap';
-
-function initSegmentationService(
- SegmentationService,
- CornerstoneViewportService
-) {
- connectToolsToSegmentationService(
- SegmentationService,
- CornerstoneViewportService
- );
-
- connectSegmentationServiceToTools(
- SegmentationService,
- CornerstoneViewportService
- );
-}
-
-function connectToolsToSegmentationService(
- SegmentationService,
- CornerstoneViewportService
-) {
- connectSegmentationServiceToTools(
- SegmentationService,
- CornerstoneViewportService
- );
- const segmentationUpdated = csToolsEnums.Events.SEGMENTATION_MODIFIED;
-
- eventTarget.addEventListener(segmentationUpdated, evt => {
- const { segmentationId } = evt.detail;
- const segmentationState = csTools.segmentation.state.getSegmentation(
- segmentationId
- );
-
- if (!segmentationState) {
- return;
- }
-
- if (
- !Object.keys(segmentationState.representationData).includes(
- csToolsEnums.SegmentationRepresentations.Labelmap
- )
- ) {
- throw new Error('Non-labelmap representations are not supported yet');
- }
-
- // Todo: handle other representations when available in cornerstone3D
- const segmentationSchema = Labelmap.toSegmentation(segmentationState);
-
- try {
- SegmentationService.addOrUpdateSegmentation(
- segmentationId,
- segmentationSchema
- );
- } catch (error) {
- console.warn(
- `Failed to add/update segmentation ${segmentationId}`,
- error
- );
- }
- });
-}
-
-function connectSegmentationServiceToTools(
- SegmentationService,
- CornerstoneViewportService
-) {
- const {
- SEGMENTATION_UPDATED,
- SEGMENTATION_REMOVED,
- } = SegmentationService.EVENTS;
-
- SegmentationService.subscribe(SEGMENTATION_REMOVED, ({ id }) => {
- // Todo: This should be from the configuration
- const removeFromCache = true;
-
- const sourceSegState = csTools.segmentation.state.getSegmentation(id);
-
- if (!sourceSegState) {
- return;
- }
-
- const toolGroupIds = csTools.segmentation.state.getToolGroupsWithSegmentation(
- id
- );
-
- toolGroupIds.forEach(toolGroupId => {
- const segmentationRepresentations = csTools.segmentation.state.getSegmentationRepresentations(
- toolGroupId
- );
-
- const UIDsToRemove = [];
- segmentationRepresentations.forEach(representation => {
- if (representation.segmentationId === id) {
- UIDsToRemove.push(representation.segmentationRepresentationUID);
- }
- });
-
- csTools.segmentation.removeSegmentationsFromToolGroup(
- toolGroupId,
- UIDsToRemove
- );
- });
-
- // cleanup the segmentation state too
- csTools.segmentation.state.removeSegmentation(id);
-
- if (removeFromCache) {
- cache.removeVolumeLoadObject(id);
- }
- });
-
- SegmentationService.subscribe(
- SEGMENTATION_UPDATED,
- ({ id, segmentation, notYetUpdatedAtSource }) => {
- if (notYetUpdatedAtSource === false) {
- return;
- }
- const { label, text } = segmentation;
-
- const sourceSegmentation = csTools.segmentation.state.getSegmentation(id);
-
- // Update the label in the source if necessary
- if (sourceSegmentation.label !== label) {
- sourceSegmentation.label = label;
- }
-
- if (sourceSegmentation.text !== text) {
- sourceSegmentation.text = text;
- }
-
- triggerEvent(eventTarget, csTools.Enums.Events.SEGMENTATION_MODIFIED, {
- segmentationId: id,
- });
- }
- );
-}
-
-export default initSegmentationService;
diff --git a/extensions/cornerstone/src/initWADOImageLoader.js b/extensions/cornerstone/src/initWADOImageLoader.js
index df8357a3e02..a7a8ec0f86c 100644
--- a/extensions/cornerstone/src/initWADOImageLoader.js
+++ b/extensions/cornerstone/src/initWADOImageLoader.js
@@ -1,7 +1,9 @@
import * as cornerstone from '@cornerstonejs/core';
import { volumeLoader } from '@cornerstonejs/core';
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
-import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
+import cornerstoneWADOImageLoader, {
+ webWorkerManager,
+} from 'cornerstone-wado-image-loader';
import dicomParser from 'dicom-parser';
import { errorHandler } from '@ohif/core';
@@ -80,3 +82,13 @@ export default function initWADOImageLoader(
initWebWorkers(appConfig);
}
+
+export function destroy() {
+ // Note: we don't want to call .terminate on the webWorkerManager since
+ // that resets the config
+ const webWorkers = webWorkerManager.webWorkers;
+ for (let i = 0; i < webWorkers.length; i++) {
+ webWorkers[i].worker.terminate();
+ }
+ webWorkers.length = 0;
+}
diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts
new file mode 100644
index 00000000000..caef8ebac90
--- /dev/null
+++ b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts
@@ -0,0 +1,252 @@
+import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
+import { utils } from '@ohif/core';
+
+import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
+import {
+ StackViewportData,
+ VolumeViewportData,
+} from '../../types/CornerstoneCacheService';
+
+const VOLUME_IMAGE_LOADER_SCHEME = 'streaming-wadors';
+const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
+
+class CornerstoneCacheService {
+ stackImageIds: Map = new Map();
+ volumeImageIds: Map = new Map();
+
+ constructor(servicesManager) {
+ this.servicesManager = servicesManager;
+ }
+
+ public getCacheSize() {
+ return cs3DCache.getCacheSize();
+ }
+
+ public getCacheFreeSpace() {
+ return cs3DCache.getBytesAvailable();
+ }
+
+ public async createViewportData(
+ displaySets: unknown[],
+ viewportOptions: Record,
+ dataSource: unknown,
+ initialImageIndex?: number
+ ): Promise {
+ let viewportType = viewportOptions.viewportType as string;
+
+ // Todo: Since Cornerstone 3D currently doesn't support segmentation
+ // on stack viewport, we should check if whether the the displaySets
+ // that are about to be displayed are referenced in a segmentation
+ // as a reference volume, if so, we should hang a volume viewport
+ // instead of a stack viewport
+ if (this._shouldRenderSegmentation(displaySets)) {
+ viewportType = 'volume';
+
+ // update viewportOptions to reflect the new viewport type
+ viewportOptions.viewportType = viewportType;
+ }
+
+ const cs3DViewportType = getCornerstoneViewportType(viewportType);
+ let viewportData: StackViewportData | VolumeViewportData;
+
+ if (cs3DViewportType === Enums.ViewportType.STACK) {
+ viewportData = await this._getStackViewportData(
+ dataSource,
+ displaySets,
+ initialImageIndex
+ );
+ }
+
+ if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
+ viewportData = await this._getVolumeViewportData(dataSource, displaySets);
+ }
+
+ viewportData.viewportType = cs3DViewportType;
+
+ return viewportData;
+ }
+
+ public async invalidateViewportData(
+ viewportData: VolumeViewportData,
+ invalidatedDisplaySetInstanceUID: string,
+ dataSource,
+ DisplaySetService
+ ) {
+ if (viewportData.viewportType === Enums.ViewportType.STACK) {
+ throw new Error('Invalidation of StackViewport is not supported yet');
+ }
+
+ // Todo: grab the volume and get the id from the viewport itself
+ const volumeId = `${VOLUME_LOADER_SCHEME}:${invalidatedDisplaySetInstanceUID}`;
+
+ const volume = cs3DCache.getVolume(volumeId);
+
+ if (volume) {
+ cs3DCache.removeVolumeLoadObject(volumeId);
+ }
+
+ const displaySets = viewportData.data.map(({ displaySetInstanceUID }) =>
+ DisplaySetService.getDisplaySetByUID(displaySetInstanceUID)
+ );
+
+ const newViewportData = await this._getVolumeViewportData(
+ dataSource,
+ displaySets
+ );
+
+ return newViewportData;
+ }
+
+ private _getStackViewportData(
+ dataSource,
+ displaySets,
+ initialImageIndex
+ ): StackViewportData {
+ // For Stack Viewport we don't have fusion currently
+ const displaySet = displaySets[0];
+
+ let stackImageIds = this.stackImageIds.get(
+ displaySet.displaySetInstanceUID
+ );
+
+ if (!stackImageIds) {
+ stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
+ this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
+ }
+
+ const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
+
+ const StackViewportData: StackViewportData = {
+ viewportType: Enums.ViewportType.STACK,
+ data: {
+ StudyInstanceUID,
+ displaySetInstanceUID,
+ imageIds: stackImageIds,
+ },
+ };
+
+ if (typeof initialImageIndex === 'number') {
+ StackViewportData.data.initialImageIndex = initialImageIndex;
+ }
+
+ return StackViewportData;
+ }
+
+ private async _getVolumeViewportData(
+ dataSource,
+ displaySets
+ ): Promise {
+ // Todo: Check the cache for multiple scenarios to see if we need to
+ // decache the volume data from other viewports or not
+
+ const volumeData = [];
+
+ for (const displaySet of displaySets) {
+ // Don't create volumes for the displaySets that have custom load
+ // function (e.g., SEG, RT, since they rely on the reference volumes
+ // and they take care of their own loading after they are created in their
+ // getSOPClassHandler method
+
+ if (displaySet.load && displaySet.load instanceof Function) {
+ const { UserAuthenticationService } = this.servicesManager.services;
+ const headers = UserAuthenticationService.getAuthorizationHeader();
+ await displaySet.load({ headers });
+
+ volumeData.push({
+ studyInstanceUID: displaySet.StudyInstanceUID,
+ displaySetInstanceUID: displaySet.displaySetInstanceUID,
+ });
+
+ // Todo: do some cache check and empty the cache if needed
+ continue;
+ }
+
+ const volumeLoaderSchema =
+ displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME;
+
+ const volumeId = `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
+
+ let volumeImageIds = this.volumeImageIds.get(
+ displaySet.displaySetInstanceUID
+ );
+
+ let volume = cs3DCache.getVolume(volumeId);
+
+ if (!volumeImageIds || !volume) {
+ volumeImageIds = this._getCornerstoneVolumeImageIds(
+ displaySet,
+ dataSource
+ );
+
+ volume = await volumeLoader.createAndCacheVolume(volumeId, {
+ imageIds: volumeImageIds,
+ });
+
+ this.volumeImageIds.set(
+ displaySet.displaySetInstanceUID,
+ volumeImageIds
+ );
+ }
+
+ volumeData.push({
+ StudyInstanceUID: displaySet.StudyInstanceUID,
+ displaySetInstanceUID: displaySet.displaySetInstanceUID,
+ volume,
+ volumeId,
+ imageIds: volumeImageIds,
+ });
+ }
+
+ return {
+ viewportType: Enums.ViewportType.ORTHOGRAPHIC,
+ data: volumeData,
+ };
+ }
+
+ private _shouldRenderSegmentation(displaySets) {
+ const { SegmentationService } = this.servicesManager.services;
+
+ const viewportDisplaySetInstanceUIDs = displaySets.map(
+ ({ displaySetInstanceUID }) => displaySetInstanceUID
+ );
+
+ // check inside segmentations if any of them are referencing the displaySets
+ // that are about to be displayed
+ const segmentations = SegmentationService.getSegmentations();
+
+ for (const segmentation of segmentations) {
+ const segDisplaySetInstanceUID = segmentation.displaySetInstanceUID;
+
+ const shouldDisplaySeg = SegmentationService.shouldRenderSegmentation(
+ viewportDisplaySetInstanceUIDs,
+ segDisplaySetInstanceUID
+ );
+
+ if (shouldDisplaySeg) {
+ return true;
+ }
+ }
+ }
+
+ private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
+ return dataSource.getImageIdsForDisplaySet(displaySet);
+ }
+
+ private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
+ const stackImageIds = this._getCornerstoneStackImageIds(
+ displaySet,
+ dataSource
+ );
+
+ if (stackImageIds[0].startsWith('dicomfile')) {
+ return stackImageIds;
+ }
+
+ return stackImageIds.map(imageId => {
+ const imageURI = utils.imageIdToURI(imageId);
+ return `${VOLUME_IMAGE_LOADER_SCHEME}:${imageURI}`;
+ });
+ }
+}
+
+export default CornerstoneCacheService;
diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/index.js b/extensions/cornerstone/src/services/CornerstoneCacheService/index.js
new file mode 100644
index 00000000000..4386dcddede
--- /dev/null
+++ b/extensions/cornerstone/src/services/CornerstoneCacheService/index.js
@@ -0,0 +1,10 @@
+import CornerstoneCacheService from './CornerstoneCacheService';
+
+export default function ExtendedCornerstoneCacheService(serviceManager) {
+ return {
+ name: 'CornerstoneCacheService',
+ create: ({ configuration = {} }) => {
+ return new CornerstoneCacheService(serviceManager);
+ },
+ };
+}
diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts
new file mode 100644
index 00000000000..478e0195bed
--- /dev/null
+++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts
@@ -0,0 +1,1985 @@
+import cloneDeep from 'lodash.clonedeep';
+
+import { pubSubServiceInterface } from '@ohif/core';
+import {
+ utilities as cstUtils,
+ segmentation as cstSegmentation,
+ CONSTANTS as cstConstants,
+ Enums as csToolsEnums,
+ Types as cstTypes,
+} from '@cornerstonejs/tools';
+import {
+ eventTarget,
+ cache,
+ utilities as csUtils,
+ volumeLoader,
+ Types,
+ metaData,
+ getEnabledElementByIds,
+} from '@cornerstonejs/core';
+import isEqual from 'lodash.isequal';
+import { easeInOutBell } from '../../utils/transitions';
+import {
+ Segmentation,
+ SegmentationConfig,
+ SegmentationSchema,
+} from './SegmentationServiceTypes';
+
+const { COLOR_LUT } = cstConstants;
+const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap;
+
+const EVENTS = {
+ // fired when the segmentation is updated (e.g. when a segment is added, removed, or modified, locked, visibility changed etc.)
+ SEGMENTATION_UPDATED: 'event::segmentation_updated',
+ // fired when the segmentation data (e.g., labelmap pixels) is modified
+ SEGMENTATION_DATA_MODIFIED: 'event::segmentation_data_modified',
+ // fired when the segmentation is added to the cornerstone
+ SEGMENTATION_ADDED: 'event::segmentation_added',
+ // fired when the segmentation is removed
+ SEGMENTATION_REMOVED: 'event::segmentation_removed',
+ // fired when the configuration for the segmentation is changed (e.g., brush size, render fill, outline thickness, etc.)
+ SEGMENTATION_CONFIGURATION_CHANGED:
+ 'event::segmentation_configuration_changed',
+ SEGMENT_PIXEL_DATA_CREATED: 'event::segment_pixel_data_created',
+ // for all segments
+ SEGMENTATION_PIXEL_DATA_CREATED: 'event::segmentation_pixel_data_created',
+};
+
+const VALUE_TYPES = {};
+
+class SegmentationService {
+ listeners = {};
+ segmentations: Record;
+ servicesManager = null;
+ highlightIntervalId = null;
+ _broadcastEvent: (eventName: string, callbackProps: any) => void;
+ readonly EVENTS = EVENTS;
+
+ constructor({ servicesManager }) {
+ this.segmentations = {};
+ this.listeners = {};
+
+ Object.assign(this, pubSubServiceInterface);
+ this.servicesManager = servicesManager;
+
+ this._initSegmentationService();
+ }
+
+ public destroy = () => {
+ eventTarget.removeEventListener(
+ csToolsEnums.Events.SEGMENTATION_MODIFIED,
+ this._onSegmentationModified
+ );
+
+ eventTarget.removeEventListener(
+ csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED,
+ this._onSegmentationDataModified
+ );
+
+ // remove the segmentations from the cornerstone
+ Object.keys(this.segmentations).forEach(segmentationId => {
+ this._removeSegmentationFromCornerstone(segmentationId);
+ });
+
+ this.segmentations = {};
+ this.listeners = {};
+ };
+
+ /**
+ * It adds a segment to a segmentation, basically just setting the properties for
+ * the segment
+ * @param segmentationId - The ID of the segmentation you want to add a
+ * segment to.
+ * @param segmentIndex - The index of the segment to add.
+ * @param properties - The properties of the segment to add including
+ * -- label: the label of the segment
+ * -- color: the color of the segment
+ * -- opacity: the opacity of the segment
+ * -- visibility: the visibility of the segment (boolean)
+ * -- isLocked: whether the segment is locked for editing
+ * -- active: whether the segment is currently the active segment to be edited
+ */
+ public addSegment(
+ segmentationId: string,
+ segmentIndex: number,
+ toolGroupId?: string,
+ properties?: {
+ label?: string;
+ color?: Types.Point3;
+ opacity?: number;
+ visibility?: boolean;
+ isLocked?: boolean;
+ active?: boolean;
+ }
+ ): void {
+ if (segmentIndex === 0) {
+ throw new Error('Segment index 0 is reserved for "no label"');
+ }
+
+ toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
+
+ const {
+ segmentationRepresentationUID,
+ segmentation,
+ } = this._getSegmentationInfo(segmentationId, toolGroupId);
+
+ if (this._getSegmentInfo(segmentation, segmentIndex)) {
+ throw new Error(`Segment ${segmentIndex} already exists`);
+ }
+
+ const rgbaColor = cstSegmentation.config.color.getColorForSegmentIndex(
+ toolGroupId,
+ segmentationRepresentationUID,
+ segmentIndex
+ );
+
+ segmentation.segments[segmentIndex] = {
+ label: properties.label,
+ segmentIndex: segmentIndex,
+ color: [rgbaColor[0], rgbaColor[1], rgbaColor[2]],
+ opacity: rgbaColor[3],
+ isVisible: true,
+ isLocked: false,
+ };
+
+ segmentation.segmentCount++;
+
+ const suppressEvents = true;
+ if (properties !== undefined) {
+ const {
+ color: newColor,
+ opacity,
+ isLocked,
+ visibility,
+ active,
+ } = properties;
+
+ if (newColor !== undefined) {
+ this._setSegmentColor(
+ segmentationId,
+ segmentIndex,
+ newColor,
+ toolGroupId,
+ suppressEvents
+ );
+ }
+
+ if (opacity !== undefined) {
+ this._setSegmentOpacity(
+ segmentationId,
+ segmentIndex,
+ opacity,
+ toolGroupId,
+ suppressEvents
+ );
+ }
+
+ if (visibility !== undefined) {
+ this._setSegmentVisibility(
+ segmentationId,
+ segmentIndex,
+ visibility,
+ toolGroupId,
+ suppressEvents
+ );
+ }
+
+ if (active !== undefined) {
+ this._setActiveSegment(segmentationId, segmentIndex, suppressEvents);
+ }
+
+ if (isLocked !== undefined) {
+ this._setSegmentLocked(
+ segmentationId,
+ segmentIndex,
+ isLocked,
+ suppressEvents
+ );
+ }
+ }
+
+ if (segmentation.activeSegmentIndex === null) {
+ this._setActiveSegment(segmentationId, segmentIndex, suppressEvents);
+ }
+
+ // Todo: this includes nonhydrated segmentations which might not be
+ // persisted in the store
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+
+ public removeSegment(segmentationId: string, segmentIndex: number): void {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ if (segmentIndex === 0) {
+ throw new Error('Segment index 0 is reserved for "no label"');
+ }
+
+ if (!this._getSegmentInfo(segmentation, segmentIndex)) {
+ return;
+ }
+
+ segmentation.segmentCount--;
+
+ segmentation.segments[segmentIndex] = null;
+
+ // Get volume and delete the labels
+ // Todo: handle other segmentations other than labelmap
+ const labelmapVolume = this.getLabelmapVolume(segmentationId);
+
+ const { scalarData, dimensions } = labelmapVolume;
+
+ // Set all values of this segment to zero and get which frames have been edited.
+ const frameLength = dimensions[0] * dimensions[1];
+ const numFrames = dimensions[2];
+
+ let voxelIndex = 0;
+
+ const modifiedFrames = new Set() as Set;
+
+ for (let frame = 0; frame < numFrames; frame++) {
+ for (let p = 0; p < frameLength; p++) {
+ if (scalarData[voxelIndex] === segmentIndex) {
+ scalarData[voxelIndex] = 0;
+ modifiedFrames.add(frame);
+ }
+
+ voxelIndex++;
+ }
+ }
+
+ const modifiedFramesArray: number[] = Array.from(modifiedFrames);
+
+ // Trigger texture update of modified segmentation frames.
+ cstSegmentation.triggerSegmentationEvents.triggerSegmentationDataModified(
+ segmentationId,
+ modifiedFramesArray
+ );
+
+ if (segmentation.activeSegmentIndex === segmentIndex) {
+ const segmentIndices = Object.keys(segmentation.segments);
+
+ const newActiveSegmentIndex = segmentIndices.length
+ ? Number(segmentIndices[0])
+ : 1;
+
+ this._setActiveSegment(segmentationId, newActiveSegmentIndex, true);
+ }
+
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+
+ public setSegmentVisibility(
+ segmentationId: string,
+ segmentIndex: number,
+ isVisible: boolean,
+ toolGroupId?: string,
+ suppressEvents = false
+ ): void {
+ this._setSegmentVisibility(
+ segmentationId,
+ segmentIndex,
+ isVisible,
+ toolGroupId,
+ suppressEvents
+ );
+ }
+
+ public setSegmentLockedForSegmentation(
+ segmentationId: string,
+ segmentIndex: number,
+ isLocked: boolean
+ ): void {
+ const suppressEvents = false;
+ this._setSegmentLocked(
+ segmentationId,
+ segmentIndex,
+ isLocked,
+ suppressEvents
+ );
+ }
+
+ public setSegmentLabel(
+ segmentationId: string,
+ segmentIndex: number,
+ segmentLabel: string
+ ): void {
+ this._setSegmentLabel(segmentationId, segmentIndex, segmentLabel);
+ }
+
+ public setSegmentColor(
+ segmentationId: string,
+ segmentIndex: number,
+ color: Types.Point3,
+ toolGroupId?: string
+ ): void {
+ this._setSegmentColor(segmentationId, segmentIndex, color, toolGroupId);
+ }
+
+ public setSegmentRGBA = (
+ segmentationId: string,
+ segmentIndex: number,
+ rgbaColor: cstTypes.Color,
+ toolGroupId?: string
+ ): void => {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ const suppressEvents = true;
+ this._setSegmentOpacity(
+ segmentationId,
+ segmentIndex,
+ rgbaColor[3],
+ toolGroupId,
+ suppressEvents
+ );
+
+ this._setSegmentColor(
+ segmentationId,
+ segmentIndex,
+ [rgbaColor[0], rgbaColor[1], rgbaColor[2]],
+ toolGroupId,
+ suppressEvents
+ );
+
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ };
+
+ public setSegmentOpacity(
+ segmentationId: string,
+ segmentIndex: number,
+ opacity: number,
+ toolGroupId?: string
+ ): void {
+ this._setSegmentOpacity(segmentationId, segmentIndex, opacity, toolGroupId);
+ }
+
+ public setActiveSegmentationForToolGroup(
+ segmentationId: string,
+ toolGroupId?: string
+ ): void {
+ toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
+
+ const suppressEvents = false;
+ this._setActiveSegmentationForToolGroup(
+ segmentationId,
+ toolGroupId,
+ suppressEvents
+ );
+ }
+
+ public setActiveSegmentForSegmentation(
+ segmentationId: string,
+ segmentIndex: number
+ ): void {
+ this._setActiveSegment(segmentationId, segmentIndex, false);
+ }
+
+ /**
+ * Get all segmentations.
+ *
+ * * @param filterNonHydratedSegmentations - If true, only return hydrated segmentations
+ * hydrated segmentations are those that have been loaded and persisted
+ * in the state, but non hydrated segmentations are those that are
+ * only created for the SEG displayset (SEG viewport) and the user might not
+ * have loaded them yet fully.
+ *
+
+ * @return Array of segmentations
+ */
+ public getSegmentations(
+ filterNonHydratedSegmentations = true
+ ): Segmentation[] {
+ const segmentations = this._getSegmentations();
+
+ return (
+ segmentations &&
+ segmentations.filter(segmentation => {
+ return !filterNonHydratedSegmentations || segmentation.hydrated;
+ })
+ );
+ }
+
+ private _getSegmentations(): Segmentation[] {
+ const segmentations = this.arrayOfObjects(this.segmentations);
+ return (
+ segmentations &&
+ segmentations.map(m => this.segmentations[Object.keys(m)[0]])
+ );
+ }
+
+ /**
+ * Get specific segmentation by its id.
+ *
+ * @param segmentationId If of the segmentation
+ * @return segmentation instance
+ */
+ public getSegmentation(segmentationId: string): Segmentation {
+ return this.segmentations[segmentationId];
+ }
+
+ public addOrUpdateSegmentation(
+ segmentationSchema: SegmentationSchema,
+ suppressEvents = false,
+ notYetUpdatedAtSource = false
+ ): string {
+ const { id: segmentationId } = segmentationSchema;
+ let segmentation = this.segmentations[segmentationId];
+ if (segmentation) {
+ // Update the segmentation (mostly for assigning metadata/labels)
+ Object.assign(segmentation, segmentationSchema);
+
+ this._updateCornerstoneSegmentations({
+ segmentationId,
+ notYetUpdatedAtSource,
+ });
+
+ if (!suppressEvents) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+
+ return segmentationId;
+ }
+
+ // Add the segmentation otherwise
+ cstSegmentation.addSegmentations([
+ {
+ segmentationId,
+ representation: {
+ type: LABELMAP,
+ // Todo: need to be generalized
+ data: {
+ volumeId: segmentationId,
+ },
+ },
+ },
+ ]);
+
+ // Define a new color LUT and associate it with this segmentation.
+
+ // Todo: need to be generalized to accept custom color LUTs
+ const newColorLUT = this.generateNewColorLUT();
+ const newColorLUTIndex = this.getNextColorLUTIndex();
+
+ cstSegmentation.config.color.addColorLUT(newColorLUT, newColorLUTIndex);
+
+ if (
+ segmentationSchema.label === undefined ||
+ segmentationSchema.label === ''
+ ) {
+ segmentationSchema.label = 'Segmentation';
+ }
+
+ this.segmentations[segmentationId] = {
+ ...segmentationSchema,
+ segments: segmentationSchema.segments || [null],
+ activeSegmentIndex: segmentationSchema.activeSegmentIndex ?? null,
+ segmentCount: segmentationSchema.segmentCount ?? 0,
+ isActive: false,
+ colorLUTIndex: newColorLUTIndex,
+ isVisible: true,
+ };
+
+ segmentation = this.segmentations[segmentationId];
+
+ this._updateCornerstoneSegmentations({
+ segmentationId,
+ notYetUpdatedAtSource: true,
+ });
+
+ if (!suppressEvents) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, {
+ segmentation,
+ });
+ }
+
+ return segmentation.id;
+ }
+
+ public async createSegmentationForSEGDisplaySet(
+ segDisplaySet,
+ segmentationId?: string,
+ suppressEvents = false
+ ): Promise {
+ segmentationId = segmentationId ?? segDisplaySet.displaySetInstanceUID;
+ const { segments, referencedVolumeId } = segDisplaySet;
+
+ if (!segments || !referencedVolumeId) {
+ throw new Error(
+ 'To create the segmentation from SEG displaySet, the displaySet should be loaded first, you can perform segDisplaySet.load() before calling this method.'
+ );
+ }
+
+ const segmentationSchema: SegmentationSchema = {
+ id: segmentationId,
+ volumeId: segmentationId,
+ displaySetInstanceUID: segDisplaySet.displaySetInstanceUID,
+ referencedVolumeURI: segDisplaySet.referencedVolumeURI,
+ activeSegmentIndex: 1,
+ cachedStats: {},
+ label: '',
+ segmentsLocked: [],
+ type: LABELMAP,
+ displayText: [],
+ hydrated: false, // by default we don't hydrate the segmentation for SEG displaySets
+ segmentCount: 0,
+ segments: [],
+ };
+
+ const labelmap = this.getLabelmapVolume(segmentationId);
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (labelmap && segmentation) {
+ // if the labalemap with the same segmentationId already exists, we can
+ // just assume that the segmentation is already created and move on with
+ // updating the state
+ return this.addOrUpdateSegmentation(
+ Object.assign(segmentationSchema, segmentation),
+ suppressEvents
+ );
+ }
+
+ // if the labelmap doesn't exist, we need to create it first from the
+ // DICOM SEG displaySet data
+
+ const referencedVolume = cache.getVolume(referencedVolumeId);
+
+ if (!referencedVolume) {
+ throw new Error(
+ `No volume found for referencedVolumeId: ${referencedVolumeId}`
+ );
+ }
+
+ // Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so
+ // it is easily compressible in worker thread.
+ const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(
+ referencedVolumeId,
+ {
+ volumeId: segmentationId,
+ targetBuffer: {
+ type: 'Uint8Array',
+ sharedArrayBuffer: true,
+ },
+ }
+ );
+ const [rows, columns] = derivedVolume.dimensions;
+ const derivedVolumeScalarData = derivedVolume.scalarData;
+
+ const { imageIds } = referencedVolume;
+ const sopUIDImageIdIndexMap = imageIds.reduce((acc, imageId, index) => {
+ const { sopInstanceUid } = metaData.get('generalImageModule', imageId);
+ acc[sopInstanceUid] = index;
+ return acc;
+ }, {} as { [sopUID: string]: number });
+
+ const numSegments = Object.keys(segments).length;
+ // Note: ideally we could use the TypedArray set method, but since each
+ // slice can have multiple segments, we need to loop over each slice and
+ // set the segment value for each segment.
+ const _segmentInfoUpdate = (segmentInfo, segmentIndex) => {
+ const { pixelData: segPixelData } = segmentInfo;
+
+ let segmentX = 0;
+ let segmentY = 0;
+ let segmentZ = 0;
+ let count = 0;
+
+ for (const [
+ functionalGroupIndex,
+ functionalGroup,
+ ] of segmentInfo.functionalGroups.entries()) {
+ const {
+ ReferencedSOPInstanceUID,
+ } = functionalGroup.DerivationImageSequence.SourceImageSequence;
+
+ const imageIdIndex = sopUIDImageIdIndexMap[ReferencedSOPInstanceUID];
+
+ if (imageIdIndex === -1) {
+ return;
+ }
+
+ const step = rows * columns;
+
+ // we need a faster way to get the pixel data for the current
+ // functional group, which we use typed array view
+
+ const functionGroupPixelData = new Uint8Array(
+ segPixelData.buffer,
+ functionalGroupIndex * step,
+ step
+ );
+
+ const functionalGroupStartIndex = imageIdIndex * step;
+ const functionalGroupEndIndex = (imageIdIndex + 1) * step;
+
+ // Note: this for loop is not optimized, since DICOM SEG stores
+ // each segment as a separate labelmap so if there is a slice
+ // that has multiple segments, we will have to loop over each
+ // segment and we cannot use the TypedArray set method.
+ for (
+ let i = functionalGroupStartIndex, j = 0;
+ i < functionalGroupEndIndex;
+ i++, j++
+ ) {
+ if (functionGroupPixelData[j] !== 0) {
+ derivedVolumeScalarData[i] = segmentIndex;
+
+ // centroid calculations
+ segmentX += i % columns;
+ segmentY += Math.floor(i / columns) % rows;
+ segmentZ += Math.floor(i / (columns * rows));
+ count++;
+ }
+ }
+ }
+
+ // centroid calculations
+ const x = Math.floor(segmentX / count);
+ const y = Math.floor(segmentY / count);
+ const z = Math.floor(segmentZ / count);
+
+ const centerWorld = derivedVolume.imageData.indexToWorld([x, y, z]);
+
+ segmentationSchema.cachedStats = {
+ ...segmentationSchema.cachedStats,
+ segmentCenter: {
+ ...segmentationSchema.cachedStats.segmentCenter,
+ [segmentIndex]: {
+ center: {
+ image: [x, y, z],
+ world: centerWorld,
+ },
+ modifiedTime: Date.now(),
+ },
+ },
+ };
+
+ this._broadcastEvent(EVENTS.SEGMENT_PIXEL_DATA_CREATED, {
+ segmentIndex: Number(segmentIndex),
+ numSegments,
+ });
+ };
+
+ for (const segmentIndex in segments) {
+ const segmentInfo = segments[segmentIndex];
+
+ // Important: we need a non-blocking way to update the segmentation
+ // state, otherwise the UI will freeze and the user will not be able
+ // to interact with the app or progress bars will not be updated.
+ const promise = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ _segmentInfoUpdate(segmentInfo, segmentIndex);
+ resolve();
+ }, 0);
+ });
+
+ await promise;
+ }
+
+ segmentationSchema.segmentCount = Object.keys(segments).length;
+ segmentationSchema.segments = [null]; // segment 0
+
+ Object.keys(segments).forEach(segmentIndex => {
+ const segmentInfo = segments[segmentIndex];
+ const segIndex = Number(segmentIndex);
+
+ segmentationSchema.segments[segIndex] = {
+ label: segmentInfo.label || `Segment ${segIndex}`,
+ segmentIndex: Number(segmentIndex),
+ color: [
+ segmentInfo.color[0],
+ segmentInfo.color[1],
+ segmentInfo.color[2],
+ ],
+ opacity: segmentInfo.color[3],
+ isVisible: true,
+ isLocked: false,
+ };
+ });
+
+ segDisplaySet.isLoaded = true;
+
+ this._broadcastEvent(EVENTS.SEGMENTATION_PIXEL_DATA_CREATED, {
+ segmentationId,
+ segDisplaySet,
+ });
+
+ return this.addOrUpdateSegmentation(segmentationSchema, suppressEvents);
+ }
+
+ public jumpToSegmentCenter(
+ segmentationId: string,
+ segmentIndex: number,
+ toolGroupId?: string,
+ highlightAlpha = 0.9,
+ highlightSegment = true,
+ animationLength = 750,
+ highlightHideOthers = false,
+ highlightFunctionType: 'ease-in-out' // todo: make animation functions configurable from outside
+ ): void {
+ const { ToolGroupService } = this.servicesManager.services;
+ const center = this._getSegmentCenter(segmentationId, segmentIndex);
+
+ const { world } = center;
+
+ // todo: generalize
+ toolGroupId =
+ toolGroupId || this._getToolGroupIdsWithSegmentation(segmentationId);
+
+ const toolGroups = [];
+
+ if (Array.isArray(toolGroupId)) {
+ toolGroupId.forEach(toolGroup => {
+ toolGroups.push(ToolGroupService.getToolGroup(toolGroup));
+ });
+ } else {
+ toolGroups.push(ToolGroupService.getToolGroup(toolGroupId));
+ }
+
+ toolGroups.forEach(toolGroup => {
+ const viewportsInfo = toolGroup.getViewportsInfo();
+
+ // @ts-ignore
+ for (const { viewportId, renderingEngineId } of viewportsInfo) {
+ const { viewport } = getEnabledElementByIds(
+ viewportId,
+ renderingEngineId
+ );
+ cstUtils.viewport.jumpToWorld(viewport, world);
+ }
+
+ if (highlightSegment) {
+ this.highlightSegment(
+ segmentationId,
+ segmentIndex,
+ toolGroup.id,
+ highlightAlpha,
+ animationLength,
+ highlightHideOthers,
+ highlightFunctionType
+ );
+ }
+ });
+ }
+
+ public highlightSegment(
+ segmentationId: string,
+ segmentIndex: number,
+ toolGroupId?: string,
+ alpha = 0.9,
+ animationLength = 750,
+ hideOthers = true,
+ highlightFunctionType: 'ease-in-out'
+ ): void {
+ if (this.highlightIntervalId) {
+ clearInterval(this.highlightIntervalId);
+ }
+
+ const segmentation = this.getSegmentation(segmentationId);
+ toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
+
+ const segmentationRepresentation = this._getSegmentationRepresentation(
+ segmentationId,
+ toolGroupId
+ );
+
+ const { segments } = segmentation;
+
+ const newSegmentSpecificConfig = {
+ [segmentIndex]: {
+ LABELMAP: {
+ fillAlpha: alpha,
+ },
+ },
+ };
+
+ if (hideOthers) {
+ for (let i = 0; i < segments.length; i++) {
+ if (i !== segmentIndex) {
+ newSegmentSpecificConfig[i] = {
+ LABELMAP: {
+ fillAlpha: 0,
+ },
+ };
+ }
+ }
+ }
+
+ const { fillAlpha } = this.getConfiguration(toolGroupId);
+
+ let count = 0;
+ const intervalTime = 16;
+ const numberOfFrames = Math.ceil(animationLength / intervalTime);
+
+ this.highlightIntervalId = setInterval(() => {
+ const x = (count * intervalTime) / animationLength;
+ cstSegmentation.config.setSegmentSpecificConfig(
+ toolGroupId,
+ segmentationRepresentation.segmentationRepresentationUID,
+ {
+ [segmentIndex]: {
+ LABELMAP: {
+ fillAlpha: easeInOutBell(x, fillAlpha),
+ },
+ },
+ }
+ );
+
+ count++;
+
+ if (count === numberOfFrames) {
+ clearInterval(this.highlightIntervalId);
+ cstSegmentation.config.setSegmentSpecificConfig(
+ toolGroupId,
+ segmentationRepresentation.segmentationRepresentationUID,
+ {}
+ );
+
+ this.highlightIntervalId = null;
+ }
+ }, intervalTime);
+ }
+
+ public createSegmentationForDisplaySet = async (
+ displaySetInstanceUID: string,
+ options?: {
+ segmentationId: string;
+ label: string;
+ }
+ ): Promise => {
+ const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use
+ const volumeId = `${volumeLoaderScheme}:${displaySetInstanceUID}`; // VolumeId with loader id + volume id
+
+ const segmentationId = options?.segmentationId ?? `${csUtils.uuidv4()}`;
+
+ // Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so
+ // it is easily compressible in worker thread.
+ await volumeLoader.createAndCacheDerivedVolume(volumeId, {
+ volumeId: segmentationId,
+ targetBuffer: {
+ type: 'Uint8Array',
+ sharedArrayBuffer: true,
+ },
+ });
+
+ const segmentationSchema: SegmentationSchema = {
+ id: segmentationId,
+ volumeId: segmentationId,
+ displaySetInstanceUID,
+ referencedVolumeURI: volumeId.split(':')[0], // Todo: this is so ugly
+ activeSegmentIndex: 1,
+ cachedStats: {},
+ label: options?.label,
+ segmentsLocked: [],
+ type: LABELMAP,
+ displayText: [],
+ hydrated: false,
+ segmentCount: 0,
+ segments: [],
+ };
+
+ this.addOrUpdateSegmentation(segmentationSchema);
+
+ return segmentationId;
+ };
+
+ /**
+ * Toggles the visibility of a segmentation in the state, and broadcasts the event.
+ * Note: this method does not update the segmentation state in the source. It only
+ * updates the state, and there should be separate listeners for that.
+ * @param ids segmentation ids
+ */
+ public toggleSegmentationVisibility = (segmentationId: string): void => {
+ this._toggleSegmentationVisibility(segmentationId, false);
+ };
+
+ public addSegmentationRepresentationToToolGroup = async (
+ toolGroupId: string,
+ segmentationId: string,
+ hydrateSegmentation = false,
+ representationType = csToolsEnums.SegmentationRepresentations.Labelmap
+ ): Promise => {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (!segmentation) {
+ throw new Error(
+ `Segmentation with segmentationId ${segmentationId} not found.`
+ );
+ }
+
+ if (hydrateSegmentation) {
+ // hydrate the segmentation if it's not hydrated yet
+ segmentation.hydrated = true;
+ }
+
+ const { colorLUTIndex } = segmentation;
+
+ // Based on the segmentationId, set the colorLUTIndex.
+ const segmentationRepresentationUIDs = await cstSegmentation.addSegmentationRepresentations(
+ toolGroupId,
+ [
+ {
+ segmentationId,
+ type: representationType,
+ },
+ ]
+ );
+
+ // set the latest segmentation representation as active one
+ this._setActiveSegmentationForToolGroup(
+ segmentationId,
+ toolGroupId,
+ segmentationRepresentationUIDs[0]
+ );
+
+ cstSegmentation.config.color.setColorLUT(
+ toolGroupId,
+ segmentationRepresentationUIDs[0],
+ colorLUTIndex
+ );
+
+ // add the segmentation segments properly
+ for (const segment of segmentation.segments) {
+ if (segment === null || segment === undefined) {
+ continue;
+ }
+
+ const {
+ segmentIndex,
+ color,
+ isLocked,
+ isVisible: visibility,
+ opacity,
+ } = segment;
+
+ const suppressEvents = true;
+
+ if (color !== undefined) {
+ this._setSegmentColor(
+ segmentationId,
+ segmentIndex,
+ color,
+ toolGroupId,
+ suppressEvents
+ );
+ }
+
+ if (opacity !== undefined) {
+ this._setSegmentOpacity(
+ segmentationId,
+ segmentIndex,
+ opacity,
+ toolGroupId,
+ suppressEvents
+ );
+ }
+
+ if (visibility !== undefined) {
+ this._setSegmentVisibility(
+ segmentationId,
+ segmentIndex,
+ visibility,
+ toolGroupId,
+ suppressEvents
+ );
+ }
+
+ if (isLocked !== undefined) {
+ this._setSegmentLocked(
+ segmentationId,
+ segmentIndex,
+ isLocked,
+ suppressEvents
+ );
+ }
+ }
+ };
+
+ public setSegmentRGBAColorForSegmentation = (
+ segmentationId: string,
+ segmentIndex: number,
+ rgbaColor,
+ toolGroupId?: string
+ ) => {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ this._setSegmentOpacity(
+ segmentationId,
+ segmentIndex,
+ rgbaColor[3],
+ toolGroupId, // toolGroupId
+ true
+ );
+ this._setSegmentColor(
+ segmentationId,
+ segmentIndex,
+ [rgbaColor[0], rgbaColor[1], rgbaColor[2]],
+ toolGroupId, // toolGroupId
+ true
+ );
+
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ };
+
+ public getToolGroupIdsWithSegmentation = (
+ segmentationId: string
+ ): string[] => {
+ const toolGroupIds = cstSegmentation.state.getToolGroupIdsWithSegmentation(
+ segmentationId
+ );
+ return toolGroupIds;
+ };
+
+ public hydrateSegmentation = (
+ segmentationId: string,
+ suppressEvents = false
+ ): void => {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (!segmentation) {
+ throw new Error(
+ `Segmentation with segmentationId ${segmentationId} not found.`
+ );
+ }
+
+ segmentation.hydrated = true;
+
+ if (!suppressEvents) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+ };
+
+ public removeSegmentationRepresentationFromToolGroup(
+ toolGroupId: string,
+ segmentationIds?: string[]
+ ): void {
+ segmentationIds =
+ segmentationIds ??
+ cstSegmentation.state
+ .getSegmentationRepresentations(toolGroupId)
+ .map(rep => rep.segmentationRepresentationUID);
+
+ cstSegmentation.removeSegmentationsFromToolGroup(
+ toolGroupId,
+ segmentationIds,
+ true // immediate render
+ );
+ }
+
+ /**
+ * Removes a segmentation and broadcasts the removed event.
+ *
+ * @param {string} segmentationId The segmentation id
+ */
+ public remove(segmentationId: string): void {
+ const segmentation = this.segmentations[segmentationId];
+ const wasActive = segmentation.isActive;
+
+ if (!segmentationId || !segmentation) {
+ console.warn(
+ `No segmentationId provided, or unable to find segmentation by id.`
+ );
+ return;
+ }
+
+ const { colorLUTIndex } = segmentation;
+
+ this._removeSegmentationFromCornerstone(segmentationId);
+
+ // Delete associated colormap
+ // Todo: bring this back
+ cstSegmentation.state.removeColorLUT(colorLUTIndex);
+
+ delete this.segmentations[segmentationId];
+
+ // If this segmentation was active, and there is another segmentation, set another one active.
+
+ if (wasActive) {
+ const remainingSegmentations = this._getSegmentations();
+
+ if (remainingSegmentations.length) {
+ const { id } = remainingSegmentations[0];
+
+ this._setActiveSegmentationForToolGroup(
+ id,
+ this._getFirstToolGroupId(),
+ false
+ );
+ }
+ }
+
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, {
+ segmentationId,
+ });
+ }
+
+ public getConfiguration = (toolGroupId?: string): SegmentationConfig => {
+ toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
+
+ const brushSize = 1;
+ // const brushSize = cstUtils.segmentation.getBrushSizeForToolGroup(
+ // toolGroupId
+ // );
+
+ const brushThresholdGate = 1;
+ // const brushThresholdGate = cstUtils.segmentation.getBrushThresholdForToolGroup(
+ // toolGroupId
+ // );
+
+ const config = cstSegmentation.config.getGlobalConfig();
+ const { renderInactiveSegmentations } = config;
+
+ const labelmapRepresentationConfig = config.representations.LABELMAP;
+
+ const {
+ renderOutline,
+ outlineWidthActive,
+ renderFill,
+ fillAlpha,
+ fillAlphaInactive,
+ outlineOpacity,
+ outlineOpacityInactive,
+ } = labelmapRepresentationConfig;
+
+ return {
+ brushSize,
+ brushThresholdGate,
+ fillAlpha,
+ fillAlphaInactive,
+ outlineWidthActive,
+ renderFill,
+ renderInactiveSegmentations,
+ renderOutline,
+ outlineOpacity,
+ outlineOpacityInactive,
+ };
+ };
+
+ public setConfiguration = (configuration: SegmentationConfig): void => {
+ const {
+ brushSize,
+ brushThresholdGate,
+ fillAlpha,
+ fillAlphaInactive,
+ outlineWidthActive,
+ outlineOpacity,
+ renderFill,
+ renderInactiveSegmentations,
+ renderOutline,
+ } = configuration;
+
+ if (renderOutline !== undefined) {
+ this._setLabelmapConfigValue('renderOutline', renderOutline);
+ }
+
+ if (outlineWidthActive !== undefined) {
+ this._setLabelmapConfigValue('outlineWidthActive', outlineWidthActive);
+ // this._setLabelmapConfigValue('outlineWidthInactive', outlineWidthActive);
+ }
+
+ if (outlineOpacity !== undefined) {
+ this._setLabelmapConfigValue('outlineOpacity', outlineOpacity / 100);
+ }
+
+ if (fillAlpha !== undefined) {
+ this._setLabelmapConfigValue('fillAlpha', fillAlpha / 100);
+ }
+
+ if (renderFill !== undefined) {
+ this._setLabelmapConfigValue('renderFill', renderFill);
+ }
+
+ if (renderInactiveSegmentations !== undefined) {
+ const config = cstSegmentation.config.getGlobalConfig();
+
+ config.renderInactiveSegmentations = renderInactiveSegmentations;
+ cstSegmentation.config.setGlobalConfig(config);
+ }
+
+ if (fillAlphaInactive !== undefined) {
+ this._setLabelmapConfigValue(
+ 'fillAlphaInactive',
+ fillAlphaInactive / 100
+ );
+
+ // we assume that if the user changes the inactive fill alpha, they
+ // want the inactive outline to be also changed
+ this._setLabelmapConfigValue(
+ 'outlineOpacityInactive',
+ Math.max(0.75, fillAlphaInactive / 100) // don't go below 0.7 for outline
+ );
+ }
+
+ // if (brushSize !== undefined) {
+ // const { ToolGroupService } = this.servicesManager.services;
+
+ // const toolGroupIds = ToolGroupService.getToolGroupIds();
+
+ // toolGroupIds.forEach(toolGroupId => {
+ // cstUtils.segmentation.setBrushSizeForToolGroup(toolGroupId, brushSize);
+ // });
+ // }
+
+ // if (brushThresholdGate !== undefined) {
+ // const { ToolGroupService } = this.servicesManager.services;
+
+ // const toolGroupIds = ToolGroupService.getFirstToolGroupIds();
+
+ // toolGroupIds.forEach(toolGroupId => {
+ // cstUtils.segmentation.setBrushThresholdForToolGroup(
+ // toolGroupId,
+ // brushThresholdGate
+ // );
+ // });
+ // }
+
+ this._broadcastEvent(
+ this.EVENTS.SEGMENTATION_CONFIGURATION_CHANGED,
+ this.getConfiguration()
+ );
+ };
+
+ public getLabelmapVolume = (segmentationId: string) => {
+ return cache.getVolume(segmentationId);
+ };
+
+ public getSegmentationRepresentationsForToolGroup = toolGroupId => {
+ return cstSegmentation.state.getSegmentationRepresentations(toolGroupId);
+ };
+
+ public setSegmentLabelForSegmentation(
+ segmentationId: string,
+ segmentIndex: number,
+ label: string
+ ) {
+ this._setSegmentLabelForSegmentation(segmentationId, segmentIndex, label);
+ }
+
+ private _setSegmentLabelForSegmentation(
+ segmentationId: string,
+ segmentIndex: number,
+ label: string,
+ suppressEvents = false
+ ) {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ const segmentInfo = segmentation.segments[segmentIndex];
+
+ if (segmentInfo === undefined) {
+ throw new Error(
+ `Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
+ );
+ }
+
+ segmentInfo.label = label;
+
+ if (suppressEvents === false) {
+ // this._setSegmentationModified(segmentationId);
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+ }
+ public shouldRenderSegmentation(
+ viewportDisplaySetInstanceUIDs,
+ segDisplaySetInstanceUID
+ ) {
+ if (
+ !viewportDisplaySetInstanceUIDs ||
+ !viewportDisplaySetInstanceUIDs.length
+ ) {
+ return false;
+ }
+
+ const { DisplaySetService } = this.servicesManager.services;
+
+ let shouldDisplaySeg = false;
+
+ const segDisplaySet = DisplaySetService.getDisplaySetByUID(
+ segDisplaySetInstanceUID
+ );
+
+ const {
+ FrameOfReferenceUID: segFrameOfReferenceUID,
+ } = segDisplaySet.instance;
+
+ viewportDisplaySetInstanceUIDs.forEach(displaySetInstanceUID => {
+ // check if the displaySet is sharing the same frameOfReferenceUID
+ // with the new segmentation
+ const displaySet = DisplaySetService.getDisplaySetByUID(
+ displaySetInstanceUID
+ );
+
+ // Todo: this might not be ideal for use cases such as 4D, since we
+ // don't want to show the segmentation for all the frames
+ if (
+ displaySet.isReconstructable &&
+ displaySet?.images?.[0]?.FrameOfReferenceUID === segFrameOfReferenceUID
+ ) {
+ shouldDisplaySeg = true;
+ }
+ });
+
+ return shouldDisplaySeg;
+ }
+
+ private _setActiveSegmentationForToolGroup(
+ segmentationId: string,
+ toolGroupId: string,
+ suppressEvents = false
+ ) {
+ const segmentations = this._getSegmentations();
+ const targetSegmentation = this.getSegmentation(segmentationId);
+
+ if (targetSegmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ segmentations.forEach(segmentation => {
+ segmentation.isActive = segmentation.id === segmentationId;
+ });
+
+ const representation = this._getSegmentationRepresentation(
+ segmentationId,
+ toolGroupId
+ );
+
+ cstSegmentation.activeSegmentation.setActiveSegmentationRepresentation(
+ toolGroupId,
+ representation.segmentationRepresentationUID
+ );
+
+ if (suppressEvents === false) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation: targetSegmentation,
+ });
+ }
+ }
+
+ private _toggleSegmentationVisibility = (
+ segmentationId: string,
+ suppressEvents = false
+ ) => {
+ const segmentation = this.segmentations[segmentationId];
+
+ if (!segmentation) {
+ throw new Error(
+ `Segmentation with segmentationId ${segmentationId} not found.`
+ );
+ }
+
+ segmentation.isVisible = !segmentation.isVisible;
+
+ this._updateCornerstoneSegmentationVisibility(segmentationId);
+
+ if (suppressEvents === false) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+ };
+
+ private _setActiveSegment(
+ segmentationId: string,
+ segmentIndex: number,
+ suppressEvents = false
+ ) {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ cstSegmentation.segmentIndex.setActiveSegmentIndex(
+ segmentationId,
+ segmentIndex
+ );
+
+ segmentation.activeSegmentIndex = segmentIndex;
+
+ if (suppressEvents === false) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+ }
+
+ private _getSegmentInfo(segmentation: Segmentation, segmentIndex: number) {
+ const segments = segmentation.segments;
+
+ if (!segments) {
+ return;
+ }
+
+ if (segments && segments.length > 0) {
+ return segments[segmentIndex];
+ }
+ }
+
+ private _setSegmentColor = (
+ segmentationId: string,
+ segmentIndex: number,
+ color: Types.Point3,
+ toolGroupId?: string,
+ suppressEvents = false
+ ) => {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
+
+ if (segmentInfo === undefined) {
+ throw new Error(
+ `Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
+ );
+ }
+
+ toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
+
+ const segmentationRepresentation = this._getSegmentationRepresentation(
+ segmentationId,
+ toolGroupId
+ );
+
+ if (!segmentationRepresentation) {
+ throw new Error(
+ 'Must add representation to toolgroup before setting segments'
+ );
+ }
+ const { segmentationRepresentationUID } = segmentationRepresentation;
+
+ const rgbaColor = cstSegmentation.config.color.getColorForSegmentIndex(
+ toolGroupId,
+ segmentationRepresentationUID,
+ segmentIndex
+ );
+
+ cstSegmentation.config.color.setColorForSegmentIndex(
+ toolGroupId,
+ segmentationRepresentationUID,
+ segmentIndex,
+ [...color, rgbaColor[3]]
+ );
+
+ segmentInfo.color = color;
+
+ if (suppressEvents === false) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+ };
+
+ private _getSegmentCenter(segmentationId, segmentIndex) {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (!segmentation) {
+ return;
+ }
+
+ const { cachedStats } = segmentation;
+
+ if (!cachedStats) {
+ return;
+ }
+
+ const { segmentCenter } = cachedStats;
+
+ if (!segmentCenter) {
+ return;
+ }
+
+ const { center } = segmentCenter[segmentIndex];
+
+ return center;
+ }
+
+ private _setSegmentLocked(
+ segmentationId: string,
+ segmentIndex: number,
+ isLocked: boolean,
+ suppressEvents = false
+ ) {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
+
+ if (segmentInfo === undefined) {
+ throw new Error(
+ `Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
+ );
+ }
+
+ segmentInfo.isLocked = isLocked;
+
+ cstSegmentation.segmentLocking.setSegmentIndexLocked(
+ segmentationId,
+ segmentIndex,
+ isLocked
+ );
+
+ if (suppressEvents === false) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+ }
+
+ private _setSegmentVisibility(
+ segmentationId: string,
+ segmentIndex: number,
+ isVisible: boolean,
+ toolGroupId?: string,
+ suppressEvents = false
+ ) {
+ toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
+
+ const {
+ segmentationRepresentationUID,
+ segmentation,
+ } = this._getSegmentationInfo(segmentationId, toolGroupId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
+
+ if (segmentInfo === undefined) {
+ throw new Error(
+ `Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
+ );
+ }
+
+ segmentInfo.isVisible = isVisible;
+
+ cstSegmentation.config.visibility.setVisibilityForSegmentIndex(
+ toolGroupId,
+ segmentationRepresentationUID,
+ segmentIndex,
+ isVisible
+ );
+
+ if (suppressEvents === false) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+ }
+
+ private _setSegmentOpacity = (
+ segmentationId: string,
+ segmentIndex: number,
+ opacity: number,
+ toolGroupId?: string,
+ suppressEvents = false
+ ) => {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
+
+ if (segmentInfo === undefined) {
+ throw new Error(
+ `Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
+ );
+ }
+
+ toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
+
+ const segmentationRepresentation = this._getSegmentationRepresentation(
+ segmentationId,
+ toolGroupId
+ );
+
+ if (!segmentationRepresentation) {
+ throw new Error(
+ 'Must add representation to toolgroup before setting segments'
+ );
+ }
+ const { segmentationRepresentationUID } = segmentationRepresentation;
+
+ const rgbaColor = cstSegmentation.config.color.getColorForSegmentIndex(
+ toolGroupId,
+ segmentationRepresentationUID,
+ segmentIndex
+ );
+
+ cstSegmentation.config.color.setColorForSegmentIndex(
+ toolGroupId,
+ segmentationRepresentationUID,
+ segmentIndex,
+ [rgbaColor[0], rgbaColor[1], rgbaColor[2], opacity]
+ );
+
+ segmentInfo.opacity = opacity;
+
+ if (suppressEvents === false) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+ };
+
+ private _setSegmentLabel(
+ segmentationId: string,
+ segmentIndex: number,
+ segmentLabel: string,
+ suppressEvents = false
+ ) {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+
+ const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
+
+ if (segmentInfo === undefined) {
+ throw new Error(
+ `Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
+ );
+ }
+
+ segmentInfo.label = segmentLabel;
+
+ if (suppressEvents === false) {
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
+ segmentation,
+ });
+ }
+ }
+
+ private _getSegmentationRepresentation(segmentationId, toolGroupId) {
+ const segmentationRepresentations = this.getSegmentationRepresentationsForToolGroup(
+ toolGroupId
+ );
+
+ if (segmentationRepresentations.length === 0) {
+ return;
+ }
+
+ // Todo: this finds the first segmentation representation that matches the segmentationId
+ // If there are two labelmap representations from the same segmentation, this will not work
+ const representation = segmentationRepresentations.find(
+ representation => representation.segmentationId === segmentationId
+ );
+
+ return representation;
+ }
+
+ private _setLabelmapConfigValue = (property, value) => {
+ const { CornerstoneViewportService } = this.servicesManager.services;
+
+ const config = cstSegmentation.config.getGlobalConfig();
+
+ config.representations.LABELMAP[property] = value;
+
+ // Todo: add non global (representation specific config as well)
+ cstSegmentation.config.setGlobalConfig(config);
+
+ const renderingEngine = CornerstoneViewportService.getRenderingEngine();
+ const viewportIds = CornerstoneViewportService.getViewportIds();
+
+ renderingEngine.renderViewports(viewportIds);
+ };
+
+ private _initSegmentationService() {
+ // Connect Segmentation Service to Cornerstone3D.
+ eventTarget.addEventListener(
+ csToolsEnums.Events.SEGMENTATION_MODIFIED,
+ this._onSegmentationModified
+ );
+
+ eventTarget.addEventListener(
+ csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED,
+ this._onSegmentationDataModified
+ );
+ }
+
+ private _onSegmentationDataModified = evt => {
+ const { segmentationId } = evt.detail;
+
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ // Part of add operation, not update operation, exit early.
+ return;
+ }
+
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_DATA_MODIFIED, {
+ segmentation,
+ });
+ };
+
+ private _onSegmentationModified = evt => {
+ const { segmentationId } = evt.detail;
+
+ const segmentation = this.segmentations[segmentationId];
+
+ if (segmentation === undefined) {
+ // Part of add operation, not update operation, exit early.
+ return;
+ }
+
+ const segmentationState = cstSegmentation.state.getSegmentation(
+ segmentationId
+ );
+
+ if (!segmentationState) {
+ return;
+ }
+
+ if (!Object.keys(segmentationState.representationData).includes(LABELMAP)) {
+ throw new Error('Non-labelmap representations are not supported yet');
+ }
+
+ const {
+ activeSegmentIndex,
+ cachedStats,
+ segmentsLocked,
+ representationData,
+ label,
+ type,
+ } = segmentationState;
+
+ const labelmapRepresentationData = representationData[LABELMAP];
+
+ // TODO: handle other representations when available in cornerstone3D
+ const segmentationSchema = {
+ activeSegmentIndex,
+ cachedStats,
+ displayText: [],
+ id: segmentationId,
+ label,
+ segmentsLocked,
+ type,
+ volumeId: labelmapRepresentationData.volumeId,
+ };
+
+ try {
+ this.addOrUpdateSegmentation(segmentationSchema);
+ } catch (error) {
+ console.warn(
+ `Failed to add/update segmentation ${segmentationId}`,
+ error
+ );
+ }
+ };
+
+ private _getSegmentationInfo(segmentationId: string, toolGroupId: string) {
+ const segmentation = this.getSegmentation(segmentationId);
+
+ if (segmentation === undefined) {
+ throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
+ }
+ const segmentationRepresentation = this._getSegmentationRepresentation(
+ segmentationId,
+ toolGroupId
+ );
+
+ if (!segmentationRepresentation) {
+ throw new Error(
+ 'Must add representation to toolgroup before setting segments'
+ );
+ }
+
+ const { segmentationRepresentationUID } = segmentationRepresentation;
+
+ return { segmentationRepresentationUID, segmentation };
+ }
+
+ private _removeSegmentationFromCornerstone(segmentationId: string) {
+ // TODO: This should be from the configuration
+ const removeFromCache = true;
+ const segmentationState = cstSegmentation.state;
+ const sourceSegState = segmentationState.getSegmentation(segmentationId);
+
+ if (!sourceSegState) {
+ return;
+ }
+
+ const toolGroupIds = segmentationState.getToolGroupIdsWithSegmentation(
+ segmentationId
+ );
+
+ toolGroupIds.forEach(toolGroupId => {
+ const segmentationRepresentations = segmentationState.getSegmentationRepresentations(
+ toolGroupId
+ );
+
+ const UIDsToRemove = [];
+ segmentationRepresentations.forEach(representation => {
+ if (representation.segmentationId === segmentationId) {
+ UIDsToRemove.push(representation.segmentationRepresentationUID);
+ }
+ });
+
+ // remove segmentation representations
+ cstSegmentation.removeSegmentationsFromToolGroup(
+ toolGroupId,
+ UIDsToRemove,
+ true // immediate
+ );
+ });
+
+ // cleanup the segmentation state too
+ segmentationState.removeSegmentation(segmentationId);
+
+ if (removeFromCache) {
+ cache.removeVolumeLoadObject(segmentationId);
+ }
+ }
+
+ private _updateCornerstoneSegmentations({
+ segmentationId,
+ notYetUpdatedAtSource,
+ }) {
+ if (notYetUpdatedAtSource === false) {
+ return;
+ }
+ const segmentationState = cstSegmentation.state;
+ const sourceSegmentation = segmentationState.getSegmentation(
+ segmentationId
+ );
+ const segmentation = this.segmentations[segmentationId];
+ const { label, cachedStats } = segmentation;
+
+ // Update the label in the source if necessary
+ if (sourceSegmentation.label !== label) {
+ sourceSegmentation.label = label;
+ }
+
+ if (!isEqual(sourceSegmentation.cachedStats, cachedStats)) {
+ sourceSegmentation.cachedStats = cachedStats;
+ }
+ }
+
+ private _updateCornerstoneSegmentationVisibility = segmentationId => {
+ const segmentationState = cstSegmentation.state;
+ const toolGroupIds = segmentationState.getToolGroupIdsWithSegmentation(
+ segmentationId
+ );
+
+ toolGroupIds.forEach(toolGroupId => {
+ const segmentationRepresentations = cstSegmentation.state.getSegmentationRepresentations(
+ toolGroupId
+ );
+
+ if (segmentationRepresentations.length === 0) {
+ return;
+ }
+
+ // Todo: this finds the first segmentation representation that matches the segmentationId
+ // If there are two labelmap representations from the same segmentation, this will not work
+ const representation = segmentationRepresentations.find(
+ representation => representation.segmentationId === segmentationId
+ );
+
+ const visibility = cstSegmentation.config.visibility.getSegmentationVisibility(
+ toolGroupId,
+ representation.segmentationRepresentationUID
+ );
+
+ cstSegmentation.config.visibility.setSegmentationVisibility(
+ toolGroupId,
+ representation.segmentationRepresentationUID,
+ !visibility
+ );
+
+ // set all segments to visible as well
+ const segments = this.getSegmentation(segmentationId).segments;
+ Object.keys(segments).forEach(segmentIndex => {
+ if (segmentIndex !== '0') {
+ this._setSegmentVisibility(
+ segmentationId,
+ Number(segmentIndex),
+ !visibility,
+ toolGroupId
+ );
+ }
+ });
+ });
+ };
+
+ private _getToolGroupIdsWithSegmentation(segmentationId: string) {
+ const segmentationState = cstSegmentation.state;
+ const toolGroupIds = segmentationState.getToolGroupIdsWithSegmentation(
+ segmentationId
+ );
+
+ return toolGroupIds;
+ }
+
+ private _getFirstToolGroupId = () => {
+ const { ToolGroupService } = this.servicesManager.services;
+ const toolGroupIds = ToolGroupService.getToolGroupIds();
+
+ return toolGroupIds[0];
+ };
+
+ private getNextColorLUTIndex = (): number => {
+ let i = 0;
+ while (true) {
+ if (cstSegmentation.state.getColorLUT(i) === undefined) {
+ return i;
+ }
+
+ i++;
+ }
+ };
+
+ private generateNewColorLUT() {
+ const newColorLUT = cloneDeep(COLOR_LUT);
+
+ return newColorLUT;
+ }
+
+ /**
+ * Converts object of objects to array.
+ *
+ * @return {Array} Array of objects
+ */
+ private arrayOfObjects = obj => {
+ return Object.entries(obj).map(e => ({ [e[0]]: e[1] }));
+ };
+}
+
+export default SegmentationService;
+export { EVENTS, VALUE_TYPES };
diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts
new file mode 100644
index 00000000000..3d4066d096c
--- /dev/null
+++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts
@@ -0,0 +1,88 @@
+import { Enums as csToolsEnums, Types as cstTypes } from '@cornerstonejs/tools';
+import { Types } from '@cornerstonejs/core';
+
+type SegmentationConfig = cstTypes.LabelmapTypes.LabelmapConfig & {
+ renderInactiveSegmentations: boolean;
+ brushSize: number;
+ brushThresholdGate: number;
+};
+
+type Segment = {
+ // the label for the segment
+ label: string;
+ // the index of the segment in the segmentation
+ segmentIndex: number;
+ // the color of the segment
+ color: Types.Point3;
+ // the opacity of the segment
+ opacity: number;
+ // whether the segment is visible
+ isVisible: boolean;
+ // whether the segment is locked
+ isLocked: boolean;
+};
+
+type Segmentation = {
+ // active segment index is the index of the segment that is currently being edited.
+ activeSegmentIndex: number;
+ // colorLUTIndex is the index of the color LUT that is currently being used.
+ colorLUTIndex: number;
+ // if segmentation contains any data (often calculated from labelmap)
+ cachedStats: Record;
+ // displayText is the text that is displayed on the segmentation panel (often derived from the data)
+ displayText?: string[];
+ // the id of the segmentation
+ id: string;
+ // if the segmentation is the active segmentation being used in the viewer
+ isActive: boolean;
+ // if the segmentation is visible in the viewer
+ isVisible: boolean;
+ // the label of the segmentation
+ label: string;
+ // the number of segments in the segmentation
+ segmentCount: number;
+ // the array of segments with their details, [null, segment1, segment2, ...]
+ segments: Array;
+ // the set of segments that are locked
+ segmentsLocked: Array;
+ // the segmentation representation type
+ type: csToolsEnums.SegmentationRepresentations;
+ // if labelmap, the id of the volume that the labelmap is associated with
+ volumeId?: string;
+ // whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
+ // but hydrated SEG -> segmentation that is persisted in the store)
+ hydrated: boolean;
+};
+
+// Schema to generate a segmentation
+type SegmentationSchema = {
+ // active segment index for the segmentation
+ activeSegmentIndex: number;
+ // statistics that are derived from the segmentation
+ cachedStats: Record;
+ // the displayText for the segmentation in the panels
+ displayText?: string[];
+ // segmentation id
+ id: string;
+ // displaySetInstanceUID
+ displaySetInstanceUID: string;
+ // segmentation label
+ label: string;
+ // segment indices that are locked for the segmentation
+ segmentsLocked: Array;
+ // the type of the segmentation (e.g., Labelmap etc.)
+ type: csToolsEnums.SegmentationRepresentations;
+ // the volume id of the volume that the labelmap is associated with, this only exists for the labelmap representation
+ volumeId: string;
+ // the referenced volumeURI for the segmentation
+ referencedVolumeURI: string;
+ // whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
+ // but hydrated SEG -> segmentation that is persisted in the store)
+ hydrated: boolean;
+ // the number of segments in the segmentation
+ segmentCount: number;
+ // the array of segments with their details
+ segments: Array;
+};
+
+export { SegmentationConfig, Segment, Segmentation, SegmentationSchema };
diff --git a/extensions/cornerstone/src/services/SegmentationService/index.js b/extensions/cornerstone/src/services/SegmentationService/index.js
new file mode 100644
index 00000000000..3cc5767b6b1
--- /dev/null
+++ b/extensions/cornerstone/src/services/SegmentationService/index.js
@@ -0,0 +1,10 @@
+import SegmentationService from './SegmentationService';
+
+export default function ExtendedSegmentationService(servicesManager) {
+ return {
+ name: 'SegmentationService',
+ create: ({ configuration = {} }) => {
+ return new SegmentationService({ servicesManager });
+ },
+ };
+}
diff --git a/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts b/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts
index e4e6a6b79b0..6acfce70a83 100644
--- a/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts
+++ b/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts
@@ -31,6 +31,7 @@ export type SyncGroup = {
const POSITION = 'cameraposition';
const VOI = 'voi';
const ZOOMPAN = 'zoompan';
+const STACKIMAGE = 'stackimage';
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
@@ -43,6 +44,7 @@ export default class SyncGroupService {
[POSITION]: synchronizers.createCameraPositionSynchronizer,
[VOI]: synchronizers.createVOISynchronizer,
[ZOOMPAN]: synchronizers.createZoomPanSynchronizer,
+ [STACKIMAGE]: synchronizers.createStackImageSynchronizer,
};
constructor(serviceManager) {
@@ -72,7 +74,7 @@ export default class SyncGroupService {
* @param creator
*/
public setSynchronizer(type: string, creator: SyncCreator): void {
- this.synchronizerCreators[type] = creator;
+ this.synchronizerCreators[type.toLowerCase()] = creator;
}
protected _getOrCreateSynchronizer(
@@ -91,20 +93,29 @@ export default class SyncGroupService {
public addViewportToSyncGroup(
viewportId: string,
renderingEngineId: string,
- syncGroups?: (SyncGroup | string)[]
+ syncGroups?: SyncGroup | string | SyncGroup[] | string[]
): void {
- if (!syncGroups || !syncGroups.length) {
+ if (!syncGroups) {
return;
}
- syncGroups.forEach(syncGroup => {
+ const syncGroupsArray = Array.isArray(syncGroups)
+ ? syncGroups
+ : [syncGroups];
+
+ syncGroupsArray.forEach(syncGroup => {
const syncGroupObj = asSyncGroup(syncGroup);
- const { type, target = true, source = true, options = {} } = syncGroupObj;
- const { id = type } = syncGroupObj;
+ const {
+ type,
+ target = true,
+ source = true,
+ options = {},
+ id = type,
+ } = syncGroupObj;
const synchronizer = this._getOrCreateSynchronizer(type, id, options);
-
synchronizer.setOptions(viewportId, options);
+
const viewportInfo = { viewportId, renderingEngineId };
if (target && source) {
synchronizer.add(viewportInfo);
@@ -123,11 +134,16 @@ export default class SyncGroupService {
public removeViewportFromSyncGroup(
viewportId: string,
- renderingEngineId: string
+ renderingEngineId: string,
+ syncGroupId?: string
): void {
const synchronizers = SynchronizerManager.getAllSynchronizers();
- synchronizers.forEach(synchronizer => {
+ const filteredSynchronizers = syncGroupId
+ ? synchronizers.filter(s => s.id === syncGroupId)
+ : synchronizers;
+
+ filteredSynchronizers.forEach(synchronizer => {
if (!synchronizer) {
return;
}
diff --git a/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts b/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts
index 3e896a83896..96d9cd31ea1 100644
--- a/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts
+++ b/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts
@@ -72,7 +72,16 @@ export default class ToolGroupService {
this.toolGroupIds = new Set();
}
- public disable(viewportId: string, renderingEngineId: string): void {
+ public destroyToolGroup(toolGroupId: string) {
+ ToolGroupManager.destroyToolGroup(toolGroupId);
+ this.toolGroupIds.delete(toolGroupId);
+ }
+
+ public removeViewportFromToolGroup(
+ viewportId: string,
+ renderingEngineId: string,
+ deleteToolGroupIfEmpty?: boolean
+ ): void {
const toolGroup = ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
@@ -85,9 +94,10 @@ export default class ToolGroupService {
toolGroup.removeViewports(renderingEngineId, viewportId);
const viewportIds = toolGroup.getViewportIds();
- // if (viewportIds.length === 0) {
- // ToolGroupManager.destroyToolGroup(toolGroup.id);
- // }
+
+ if (viewportIds.length === 0 && deleteToolGroupIfEmpty) {
+ ToolGroupManager.destroyToolGroup(toolGroup.id);
+ }
}
public addViewportToToolGroup(
@@ -110,7 +120,10 @@ export default class ToolGroupService {
toolGroup.addViewport(viewportId, renderingEngineId);
}
- this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { viewportId, toolGroupId });
+ this._broadcastEvent(EVENTS.VIEWPORT_ADDED, {
+ viewportId,
+ toolGroupId,
+ });
}
public createToolGroup(toolGroupId: string): Types.IToolGroup {
@@ -122,7 +135,9 @@ export default class ToolGroupService {
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
this.toolGroupIds.add(toolGroupId);
- this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, { toolGroupId });
+ this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, {
+ toolGroupId,
+ });
return toolGroup;
}
@@ -195,9 +210,11 @@ export default class ToolGroupService {
private _getToolNames(toolGroupTools: Tools): string[] {
const toolNames = [];
- toolGroupTools.active.forEach(tool => {
- toolNames.push(tool.toolName);
- });
+ if (toolGroupTools.active) {
+ toolGroupTools.active.forEach(tool => {
+ toolNames.push(tool.toolName);
+ });
+ }
if (toolGroupTools.passive) {
toolGroupTools.passive.forEach(tool => {
toolNames.push(tool.toolName);
@@ -221,9 +238,12 @@ export default class ToolGroupService {
private _setToolsMode(toolGroup, tools) {
const { active, passive, enabled, disabled } = tools;
- active.forEach(({ toolName, bindings }) => {
- toolGroup.setToolActive(toolName, { bindings });
- });
+
+ if (active) {
+ active.forEach(({ toolName, bindings }) => {
+ toolGroup.setToolActive(toolName, { bindings });
+ });
+ }
if (passive) {
passive.forEach(({ toolName }) => {
diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneCacheService.ts
deleted file mode 100644
index 0e65bf5f72f..00000000000
--- a/extensions/cornerstone/src/services/ViewportService/CornerstoneCacheService.ts
+++ /dev/null
@@ -1,231 +0,0 @@
-import {
- cache as cs3DCache,
- Enums,
- Types,
- volumeLoader,
-} from '@cornerstonejs/core';
-import { utils, pubSubServiceInterface } from '@ohif/core';
-
-import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
-
-export type StackData = {
- StudyInstanceUID: string;
- displaySetInstanceUID: string;
- imageIds: string[];
- frameRate?: number;
- isClip?: boolean;
- initialImageIndex?: number | string | null;
- viewportType: Enums.ViewportType;
-};
-
-export type VolumeData = {
- StudyInstanceUID: string;
- displaySetInstanceUIDs: string[]; // can have more than one displaySet (fusion)
- imageIds: string[][]; // can have more than one imageId list (fusion)
- volumes: Types.IVolume[];
- viewportType: Enums.ViewportType;
-};
-
-const VOLUME_LOADER_SCHEME = 'streaming-wadors';
-
-const EVENTS = {
- VIEWPORT_DATA_CHANGED: 'event::cornerstone::viewportdatachanged',
-};
-
-export type IViewportData = StackData | VolumeData;
-
-class CornerstoneCacheService {
- stackImageIds: Map = new Map();
- volumeImageIds: Map = new Map();
- listeners: { [key: string]: (...args: any[]) => void } = {};
- EVENTS: { [key: string]: string };
-
- constructor() {
- this.listeners = {};
- this.EVENTS = EVENTS;
- Object.assign(this, pubSubServiceInterface);
- }
-
- public getCacheSize() {
- return cs3DCache.getCacheSize();
- }
-
- public getCacheFreeSpace() {
- return cs3DCache.getBytesAvailable();
- }
-
- public async getViewportData(
- viewportIndex: number,
- displaySets: unknown[],
- viewportType: string,
- dataSource: unknown,
- callback: (val: IViewportData) => unknown,
- initialImageIndex?: number
- ): Promise {
- const cs3DViewportType = getCornerstoneViewportType(viewportType);
- let viewportData: IViewportData;
-
- if (cs3DViewportType === Enums.ViewportType.STACK) {
- viewportData = await this._getStackViewportData(
- dataSource,
- displaySets,
- initialImageIndex
- );
- }
-
- if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
- viewportData = await this._getVolumeViewportData(dataSource, displaySets);
- }
-
- viewportData.viewportType = cs3DViewportType;
-
- await callback(viewportData);
-
- this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
- viewportData,
- viewportIndex,
- });
-
- return viewportData;
- }
- public async invalidateViewportData(
- viewportData: VolumeData,
- invalidatedDisplaySetInstanceUID: string,
- dataSource,
- DisplaySetService
- ) {
- if (viewportData.viewportType === Enums.ViewportType.STACK) {
- throw new Error('Invalidation of StackViewport is not supported yet');
- }
-
- const volumeId = invalidatedDisplaySetInstanceUID;
- const volume = cs3DCache.getVolume(volumeId);
-
- if (volume) {
- cs3DCache.removeVolumeLoadObject(volumeId);
- }
-
- const displaySets = viewportData.displaySetInstanceUIDs.map(
- DisplaySetService.getDisplaySetByUID
- );
-
- const newViewportData = await this._getVolumeViewportData(
- dataSource,
- displaySets
- );
-
- return newViewportData;
- }
-
- private _getStackViewportData(
- dataSource,
- displaySets,
- initialImageIndex
- ): StackData {
- // For Stack Viewport we don't have fusion currently
- const displaySet = displaySets[0];
-
- let stackImageIds = this.stackImageIds.get(
- displaySet.displaySetInstanceUID
- );
-
- if (!stackImageIds) {
- stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
- this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
- }
-
- const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
-
- const stackData: StackData = {
- StudyInstanceUID,
- displaySetInstanceUID,
- viewportType: Enums.ViewportType.STACK,
- imageIds: stackImageIds,
- };
-
- if (typeof initialImageIndex === 'number') {
- stackData.initialImageIndex = initialImageIndex;
- }
-
- return stackData;
- }
-
- private async _getVolumeViewportData(
- dataSource,
- displaySets
- ): Promise {
- // Check the cache for multiple scenarios to see if we need to
- // decache the volume data from other viewports or not
-
- const volumeImageIdsArray = [];
- const volumes = [];
-
- for (const displaySet of displaySets) {
- const volumeId = displaySet.displaySetInstanceUID;
-
- let volumeImageIds = this.volumeImageIds.get(
- displaySet.displaySetInstanceUID
- );
-
- let volume = cs3DCache.getVolume(volumeId);
-
- if (!volumeImageIds || !volume) {
- volumeImageIds = this._getCornerstoneVolumeImageIds(
- displaySet,
- dataSource
- );
-
- volume = await volumeLoader.createAndCacheVolume(volumeId, {
- imageIds: volumeImageIds,
- });
-
- this.volumeImageIds.set(
- displaySet.displaySetInstanceUID,
- volumeImageIds
- );
- }
-
- volumeImageIdsArray.push(volumeImageIds);
- volumes.push(volume);
- }
-
- // assert displaySets are from the same study
- const { StudyInstanceUID } = displaySets[0];
- const displaySetInstanceUIDs = [];
-
- displaySets.forEach(displaySet => {
- if (displaySet.StudyInstanceUID !== StudyInstanceUID) {
- throw new Error('Display sets are not from the same study');
- }
-
- displaySetInstanceUIDs.push(displaySet.displaySetInstanceUID);
- });
-
- return {
- StudyInstanceUID,
- displaySetInstanceUIDs,
- imageIds: volumeImageIdsArray,
- viewportType: Enums.ViewportType.ORTHOGRAPHIC,
- volumes,
- };
- }
-
- private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
- return dataSource.getImageIdsForDisplaySet(displaySet);
- }
-
- private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
- const stackImageIds = this._getCornerstoneStackImageIds(
- displaySet,
- dataSource
- );
-
- return stackImageIds.map(imageId => {
- const imageURI = utils.imageIdToURI(imageId);
- return `${VOLUME_LOADER_SCHEME}:${imageURI}`;
- });
- }
-}
-
-const CacheService = new CornerstoneCacheService();
-export default CacheService;
diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts
index 85c6a4a55f0..6b25b43fe03 100644
--- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts
+++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts
@@ -2,7 +2,6 @@ import { pubSubServiceInterface } from '@ohif/core';
import {
RenderingEngine,
StackViewport,
- Enums,
Types,
getRenderingEngine,
utilities as csUtils,
@@ -18,7 +17,10 @@ import ViewportInfo, {
DisplaySetOptions,
PublicViewportOptions,
} from './Viewport';
-import { StackData, VolumeData } from './CornerstoneCacheService';
+import {
+ StackViewportData,
+ VolumeViewportData,
+} from '../../types/CornerstoneCacheService';
import {
setColormap,
setLowerUpperColorTransferFunction,
@@ -27,8 +29,8 @@ import {
import JumpPresets from '../../utils/JumpPresets';
const EVENTS = {
- VIEWPORT_INFO_CREATED:
- 'event::cornerstone::viewportservice:viewportinfocreated',
+ VIEWPORT_DATA_CHANGED:
+ 'event::cornerstoneViewportService:viewportDataChanged',
};
/**
@@ -39,8 +41,7 @@ class CornerstoneViewportService implements IViewportService {
renderingEngine: Types.IRenderingEngine | null;
viewportsInfo: Map;
viewportGridResizeObserver: ResizeObserver | null;
- // TODO - get the right type here.
- hangingProtocolService: object;
+ viewportsDisplaySets: Map = new Map();
/**
* Service-specific
@@ -52,6 +53,7 @@ class CornerstoneViewportService implements IViewportService {
enableResizeDetector: true;
resizeRefreshRateMs: 200;
resizeRefreshMode: 'debounce';
+ servicesManager = null;
constructor(servicesManager) {
this.renderingEngine = null;
@@ -60,8 +62,7 @@ class CornerstoneViewportService implements IViewportService {
//
this.listeners = {};
this.EVENTS = EVENTS;
- const { HangingProtocolService } = servicesManager.services;
- this.hangingProtocolService = HangingProtocolService;
+ this.servicesManager = servicesManager;
Object.assign(this, pubSubServiceInterface);
//
}
@@ -71,7 +72,7 @@ class CornerstoneViewportService implements IViewportService {
* @param {*} viewportIndex
* @param {*} elementRef
*/
- public enableElement(
+ public enableViewport(
viewportIndex: number,
viewportOptions: PublicViewportOptions,
elementRef: HTMLDivElement
@@ -87,6 +88,16 @@ class CornerstoneViewportService implements IViewportService {
return `viewport-${viewportIndex}`;
}
+ public getViewportIds(): string[] {
+ const viewportIds = [];
+
+ this.viewportsInfo.forEach(viewportInfo => {
+ viewportIds.push(viewportInfo.getViewportId());
+ });
+
+ return viewportIds;
+ }
+
/**
* It retrieves the renderingEngine if it does exist, or creates one otherwise
* @returns {RenderingEngine} rendering engine
@@ -112,10 +123,9 @@ class CornerstoneViewportService implements IViewportService {
*/
public resize() {
const immediate = true;
- const resetPan = false;
- const resetZoom = false;
+ const keepCamera = true;
- this.renderingEngine.resize(immediate, resetPan, resetZoom);
+ this.renderingEngine.resize(immediate, keepCamera);
this.renderingEngine.render();
}
@@ -126,8 +136,9 @@ class CornerstoneViewportService implements IViewportService {
this._removeResizeObserver();
this.viewportGridResizeObserver = null;
this.renderingEngine.destroy();
+ this.viewportsDisplaySets.clear();
this.renderingEngine = null;
- cache.purgeVolumeCache();
+ cache.purgeCache();
}
/**
@@ -142,12 +153,11 @@ class CornerstoneViewportService implements IViewportService {
}
const viewportId = viewportInfo.getViewportId();
- this.renderingEngine.disableElement(viewportId);
- this.viewportsInfo.delete(viewportIndex);
- if (this.viewportsInfo.size === 0) {
- this.destroy();
- }
+ this.renderingEngine && this.renderingEngine.disableElement(viewportId);
+
+ this.viewportsInfo.get(viewportIndex).destroy();
+ this.viewportsInfo.delete(viewportIndex);
}
/**
@@ -158,9 +168,9 @@ class CornerstoneViewportService implements IViewportService {
* @param {*} dataSource
* @returns
*/
- public setViewportDisplaySets(
+ public setViewportData(
viewportIndex: number,
- viewportData: StackData | VolumeData,
+ viewportData: StackViewportData | VolumeViewportData,
publicViewportOptions: PublicViewportOptions,
publicDisplaySetOptions: DisplaySetOptions[]
): void {
@@ -179,8 +189,12 @@ class CornerstoneViewportService implements IViewportService {
viewportInfo.setViewportOptions(viewportOptions);
viewportInfo.setDisplaySetOptions(displaySetOptions);
+ viewportInfo.setViewportData(viewportData);
- this._broadcastEvent(EVENTS.VIEWPORT_INFO_CREATED, viewportInfo);
+ this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
+ viewportData,
+ viewportIndex,
+ });
const viewportId = viewportInfo.getViewportId();
const element = viewportInfo.getElement();
@@ -203,6 +217,7 @@ class CornerstoneViewportService implements IViewportService {
// renderingEngine is designed to be used like this. This will trigger
// ENABLED_ELEMENT again and again, which will run onEnableElement callbacks
renderingEngine.enableElement(viewportInput);
+
this._setDisplaySets(viewportId, viewportData, viewportInfo);
}
@@ -265,11 +280,18 @@ class CornerstoneViewportService implements IViewportService {
_setStackViewport(
viewport: Types.IStackViewport,
- viewportData: StackData,
+ viewportData: StackViewportData,
viewportInfo: ViewportInfo
) {
const displaySetOptions = viewportInfo.getDisplaySetOptions();
- const { imageIds, initialImageIndex } = viewportData;
+
+ const {
+ imageIds,
+ initialImageIndex,
+ displaySetInstanceUID,
+ } = viewportData.data;
+
+ this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]);
let initialImageIndexToUse = initialImageIndex;
@@ -331,7 +353,15 @@ class CornerstoneViewportService implements IViewportService {
}
if (preset === JumpPresets.Middle) {
- return Math.floor(lastSliceIndex / 2);
+ // Note: this is a simple but yet very important formula.
+ // since viewport reset works with the middle slice
+ // if the below formula is not correct, on a viewport reset
+ // it will jump to a different slice than the middle one which
+ // was the initial slice, and we have some tools such as Crosshairs
+ // which rely on a relative camera modifications and those will break.
+ return lastSliceIndex % 2 === 0
+ ? lastSliceIndex / 2
+ : (lastSliceIndex + 1) / 2;
}
return 0;
@@ -339,7 +369,7 @@ class CornerstoneViewportService implements IViewportService {
async _setVolumeViewport(
viewport: Types.IVolumeViewport,
- viewportData: VolumeData,
+ viewportData: VolumeViewportData,
viewportInfo: ViewportInfo
): Promise {
// TODO: We need to overhaul the way data sources work so requests can be made
@@ -355,21 +385,24 @@ class CornerstoneViewportService implements IViewportService {
// (This call may or may not create sub-requests for series metadata)
const volumeInputArray = [];
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
- const { hangingProtocolService } = this;
+ const { HangingProtocolService } = this.servicesManager.services;
- for (let i = 0; i < viewportData.imageIds.length; i++) {
- const imageIds = viewportData.imageIds[i];
- const displaySetInstanceUID = viewportData.displaySetInstanceUIDs[i];
- const displaySetOptions = displaySetOptionsArray[i];
+ const volumeToLoad = [];
+ const displaySetInstanceUIDs = [];
- const volumeId = displaySetInstanceUID;
+ for (const [index, data] of viewportData.data.entries()) {
+ const { volume, imageIds, displaySetInstanceUID } = data;
- // if (displaySet.needsRerendering) {
- // console.warn('Removing volume from cache', volumeId);
- // cache.removeVolumeLoadObject(volumeId);
- // displaySet.needsRerendering = false;
- // this.displaySetsNeedRerendering.add(displaySet.displaySetInstanceUID);
- // }
+ displaySetInstanceUIDs.push(displaySetInstanceUID);
+
+ if (!volume) {
+ continue;
+ }
+
+ volumeToLoad.push(volume);
+
+ const displaySetOptions = displaySetOptionsArray[index];
+ const { volumeId } = volume;
const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions);
@@ -386,18 +419,20 @@ class CornerstoneViewportService implements IViewportService {
});
}
+ this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
+
if (
- hangingProtocolService.hasCustomImageLoadStrategy() &&
- !hangingProtocolService.customImageLoadPerformed
+ HangingProtocolService.hasCustomImageLoadStrategy() &&
+ !HangingProtocolService.customImageLoadPerformed
) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
- return hangingProtocolService.runImageLoadStrategy({
+ return HangingProtocolService.runImageLoadStrategy({
viewportId: viewport.id,
volumeInputArray,
});
}
- viewportData.volumes.forEach(volume => {
+ volumeToLoad.forEach(volume => {
volume.load();
});
@@ -406,9 +441,98 @@ class CornerstoneViewportService implements IViewportService {
}
public async setVolumesForViewport(viewport, volumeInputArray) {
+ const {
+ DisplaySetService,
+ SegmentationService,
+ ToolGroupService,
+ } = this.servicesManager.services;
+
await viewport.setVolumes(volumeInputArray);
+ // load any secondary displaySets
+ const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
+
+ const segDisplaySet = displaySetInstanceUIDs
+ .map(DisplaySetService.getDisplaySetByUID)
+ .find(displaySet => displaySet && displaySet.Modality === 'SEG');
+
+ if (segDisplaySet) {
+ const { referencedVolumeId } = segDisplaySet;
+ const referencedVolume = cache.getVolume(referencedVolumeId);
+ const segmentationId = segDisplaySet.displaySetInstanceUID;
+
+ const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id);
+
+ if (referencedVolume) {
+ SegmentationService.addSegmentationRepresentationToToolGroup(
+ toolGroup.id,
+ segmentationId
+ );
+ }
+ } else {
+ const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id);
+ const toolGroupSegmentationRepresentations =
+ SegmentationService.getSegmentationRepresentationsForToolGroup(
+ toolGroup.id
+ ) || [];
+
+ // csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
+ // If the displaySet is not a SEG displaySet we assume it is a primary displaySet
+ // and we can look into hydrated segmentations to check if any of them are
+ // associated with the primary displaySet
+ // get segmentations only returns the hydrated segmentations
+ const segmentations = SegmentationService.getSegmentations();
+
+ for (const segmentation of segmentations) {
+ // if there is already a segmentation representation for this segmentation
+ // for this toolGroup, don't bother at all
+ if (
+ toolGroupSegmentationRepresentations.find(
+ representation => representation.segmentationId === segmentation.id
+ )
+ ) {
+ continue;
+ }
+
+ // otherwise, check if the hydrated segmentations are in the same FOR
+ // as the primary displaySet, if so add the representation (since it was not there)
+ const { id: segDisplaySetInstanceUID } = segmentation;
+
+ const segFrameOfReferenceUID = this._getFrameOfReferenceUID(
+ segDisplaySetInstanceUID
+ );
+
+ let shouldDisplaySeg = false;
+
+ for (const displaySetInstanceUID of displaySetInstanceUIDs) {
+ const primaryFrameOfReferenceUID = this._getFrameOfReferenceUID(
+ displaySetInstanceUID
+ );
+
+ if (segFrameOfReferenceUID === primaryFrameOfReferenceUID) {
+ shouldDisplaySeg = true;
+ break;
+ }
+ }
+
+ if (shouldDisplaySeg) {
+ const toolGroup = ToolGroupService.getToolGroupForViewport(
+ viewport.id
+ );
+
+ SegmentationService.addSegmentationRepresentationToToolGroup(
+ toolGroup.id,
+ segmentation.id
+ );
+ }
+ }
+ }
+
const viewportInfo = this.getViewportInfo(viewport.id);
+
+ const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id);
+ csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
+
const initialImageOptions = viewportInfo.getInitialImageOptions();
if (
@@ -493,17 +617,21 @@ class CornerstoneViewportService implements IViewportService {
_setDisplaySets(
viewportId: string,
- viewportData: StackData | VolumeData,
+ viewportData: StackViewportData | VolumeViewportData,
viewportInfo: ViewportInfo
): void {
const viewport = this.getCornerstoneViewport(viewportId);
if (viewport instanceof StackViewport) {
- this._setStackViewport(viewport, viewportData as StackData, viewportInfo);
+ this._setStackViewport(
+ viewport,
+ viewportData as StackViewportData,
+ viewportInfo
+ );
} else if (viewport instanceof VolumeViewport) {
this._setVolumeViewport(
viewport,
- viewportData as VolumeData,
+ viewportData as VolumeViewportData,
viewportInfo
);
} else {
@@ -559,6 +687,10 @@ class CornerstoneViewportService implements IViewportService {
} {
const viewportIndex = viewportInfo.getViewportIndex();
+ if (!publicViewportOptions.viewportId) {
+ publicViewportOptions.viewportId = this.getViewportId(viewportIndex);
+ }
+
// Creating a temporary viewportInfo to handle defaults
const newViewportInfo = new ViewportInfo(
viewportIndex,
@@ -578,6 +710,31 @@ class CornerstoneViewportService implements IViewportService {
displaySetOptions: newDisplaySetOptions,
};
}
+
+ _getFrameOfReferenceUID(displaySetInstanceUID) {
+ const { DisplaySetService } = this.servicesManager.services;
+ const displaySet = DisplaySetService.getDisplaySetByUID(
+ displaySetInstanceUID
+ );
+
+ if (!displaySet) {
+ return;
+ }
+
+ if (displaySet.frameOfReferenceUID) {
+ return displaySet.frameOfReferenceUID;
+ }
+
+ if (displaySet.Modality === 'SEG') {
+ const { instance } = displaySet;
+ return instance.FrameOfReferenceUID;
+ }
+
+ const { images } = displaySet;
+ if (images && images.length) {
+ return images[0].FrameOfReferenceUID;
+ }
+ }
}
export default function ExtendedCornerstoneViewportService(serviceManager) {
diff --git a/extensions/cornerstone/src/services/ViewportService/IViewportService.ts b/extensions/cornerstone/src/services/ViewportService/IViewportService.ts
index ce47f28fcad..5fe88ba63f7 100644
--- a/extensions/cornerstone/src/services/ViewportService/IViewportService.ts
+++ b/extensions/cornerstone/src/services/ViewportService/IViewportService.ts
@@ -1,5 +1,5 @@
import { Types } from '@cornerstonejs/core';
-import { StackData, VolumeData } from './CornerstoneCacheService';
+import { StackData, VolumeData } from '../../types/CornerstoneCacheService';
import {
DisplaySetOptions,
PublicViewportOptions,
@@ -31,7 +31,7 @@ export interface IViewportService {
* @param {*} viewportIndex
* @param {*} elementRef
*/
- enableElement(
+ enableViewport(
viewportIndex: number,
viewportOptions: ViewportOptions,
elementRef: HTMLDivElement
@@ -65,7 +65,7 @@ export interface IViewportService {
* @param {*} dataSource
* @returns
*/
- setViewportDisplaySets(
+ setViewportData(
viewportIndex: number,
viewportData: StackData | VolumeData,
publicViewportOptions: PublicViewportOptions,
diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts
index f537ce123e4..ff1e75bfc50 100644
--- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts
+++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts
@@ -4,6 +4,10 @@ import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import JumpPresets from '../../utils/JumpPresets';
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
+import {
+ StackViewportData,
+ VolumeViewportData,
+} from './CornerstoneCacheService';
export type InitialImageOptions = {
index?: number;
@@ -67,6 +71,7 @@ class ViewportInfo {
private element: HTMLDivElement;
private viewportOptions: ViewportOptions;
private displaySetOptions: Array;
+ private viewportData: StackViewportData | VolumeViewportData;
private renderingEngineId: string;
constructor(viewportIndex: number, viewportId: string) {
@@ -76,6 +81,13 @@ class ViewportInfo {
this.setPublicDisplaySetOptions([{}]);
}
+ public destroy = (): void => {
+ this.element = null;
+ this.viewportData = null;
+ this.viewportOptions = null;
+ this.displaySetOptions = null;
+ };
+
public setRenderingEngineId(renderingEngineId: string): void {
this.renderingEngineId = renderingEngineId;
}
@@ -95,6 +107,16 @@ class ViewportInfo {
this.element = element;
}
+ public setViewportData(
+ viewportData: StackViewportData | VolumeViewportData
+ ): void {
+ this.viewportData = viewportData;
+ }
+
+ public getViewportData(): StackViewportData | VolumeViewportData {
+ return this.viewportData;
+ }
+
public getViewportIndex(): number {
return this.viewportIndex;
}
@@ -118,6 +140,23 @@ class ViewportInfo {
this.setDisplaySetOptions(displaySetOptions);
}
+ public hasDisplaySet(displaySetInstanceUID: string): boolean {
+ // Todo: currently this does not work for non image & referenceImage displaySets.
+ // Since SEG and other derived displaySets are loaded in a different way, and not
+ // via cornerstoneViewportService
+ let viewportData = this.getViewportData();
+
+ if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
+ viewportData = viewportData as VolumeViewportData;
+ return viewportData.data.some(
+ ({ displaySetInstanceUID: dsUID }) => dsUID === displaySetInstanceUID
+ );
+ }
+
+ viewportData = viewportData as StackViewportData;
+ return viewportData.data.displaySetInstanceUID === displaySetInstanceUID;
+ }
+
public setPublicViewportOptions(
viewportOptionsEntry: PublicViewportOptions
): void {
@@ -201,12 +240,21 @@ class ViewportInfo {
const displaySetOptions: Array = [];
publicDisplaySetOptions.forEach(option => {
+ if (!option) {
+ option = {
+ blendMode: undefined,
+ slabThickness: undefined,
+ colormap: undefined,
+ voi: {},
+ voiInverted: false,
+ };
+ }
const blendMode = getCornerstoneBlendMode(option.blendMode);
displaySetOptions.push({
- voi: option.voi || ({} as VOI),
- voiInverted: option.voiInverted || false,
- colormap: option.colormap || undefined,
+ voi: option.voi,
+ voiInverted: option.voiInverted,
+ colormap: option.colormap,
slabThickness: option.slabThickness,
blendMode,
});
diff --git a/extensions/cornerstone/src/state.ts b/extensions/cornerstone/src/state.ts
index 6c6aa3d7d01..e1d67a56159 100644
--- a/extensions/cornerstone/src/state.ts
+++ b/extensions/cornerstone/src/state.ts
@@ -31,4 +31,8 @@ const getEnabledElement = viewportIndex => {
return state.enabledElements[viewportIndex];
};
-export { setEnabledElement, getEnabledElement };
+const reset = () => {
+ state.enabledElements = {};
+};
+
+export { setEnabledElement, getEnabledElement, reset };
diff --git a/extensions/cornerstone/src/types/CornerstoneCacheService.ts b/extensions/cornerstone/src/types/CornerstoneCacheService.ts
new file mode 100644
index 00000000000..2a877e9c00b
--- /dev/null
+++ b/extensions/cornerstone/src/types/CornerstoneCacheService.ts
@@ -0,0 +1,38 @@
+import {
+ Enums,
+ Types,
+} from '@cornerstonejs/core';
+
+
+type StackData = {
+ StudyInstanceUID: string;
+ displaySetInstanceUID: string;
+ imageIds: string[];
+ frameRate?: number;
+ isClip?: boolean;
+ initialImageIndex?: number | string | null;
+};
+
+type VolumeData = {
+ studyInstanceUID: string;
+ displaySetInstanceUID: string;
+ volume?: Types.IVolume;
+ imageIds?: string[];
+};
+
+ type StackViewportData = {
+ viewportType: Enums.ViewportType;
+ data: StackData;
+};
+
+ type VolumeViewportData = {
+ viewportType: Enums.ViewportType;
+ data: VolumeData[];
+};
+
+export type {
+ StackViewportData,
+ VolumeViewportData,
+ StackData,
+ VolumeData,
+};
diff --git a/extensions/cornerstone/src/types/index.ts b/extensions/cornerstone/src/types/index.ts
new file mode 100644
index 00000000000..8c8e32e9832
--- /dev/null
+++ b/extensions/cornerstone/src/types/index.ts
@@ -0,0 +1,5 @@
+import * as CornerstoneCacheService from './CornerstoneCacheService'
+
+export type {
+ CornerstoneCacheService
+}
diff --git a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx
index f29aa7c4a3d..973bd9dbe25 100644
--- a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx
+++ b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx
@@ -1,11 +1,11 @@
-import React from 'react';
-import domtoimage from 'dom-to-image';
-
+import React, { useEffect, useState } from 'react';
+import html2canvas from 'html2canvas';
import {
Enums,
getEnabledElement,
getOrCreateCanvas,
StackViewport,
+ VolumeViewport,
} from '@cornerstonejs/core';
import { ToolGroupManager } from '@cornerstonejs/tools';
import PropTypes from 'prop-types';
@@ -17,7 +17,6 @@ const MINIMUM_SIZE = 100;
const DEFAULT_SIZE = 512;
const MAX_TEXTURE_SIZE = 10000;
const VIEWPORT_ID = 'cornerstone-viewport-download-form';
-const TOOLGROUP_ID = 'cornerstone-viewport-download-form-toolgroup';
const CornerstoneViewportDownloadForm = ({
onClose,
@@ -26,15 +25,57 @@ const CornerstoneViewportDownloadForm = ({
}) => {
const enabledElement = OHIFgetEnabledElement(activeViewportIndex);
const activeViewportElement = enabledElement?.element;
+ const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
+
+ const {
+ viewportId: activeViewportId,
+ renderingEngineId,
+ } = activeViewportEnabledElement;
+
+ const toolGroup = ToolGroupManager.getToolGroupForViewport(
+ activeViewportId,
+ renderingEngineId
+ );
+
+ const toolModeAndBindings = Object.keys(toolGroup.toolOptions).reduce(
+ (acc, toolName) => {
+ const tool = toolGroup.toolOptions[toolName];
+ const { mode, bindings } = tool;
+
+ return {
+ ...acc,
+ [toolName]: {
+ mode,
+ bindings,
+ },
+ };
+ },
+ {}
+ );
+
+ useEffect(() => {
+ return () => {
+ Object.keys(toolModeAndBindings).forEach(toolName => {
+ const { mode, bindings } = toolModeAndBindings[toolName];
+ toolGroup.setToolMode(toolName, mode, { bindings });
+ });
+ };
+ }, []);
const enableViewport = viewportElement => {
if (viewportElement) {
- const renderingEngine = CornerstoneViewportService.getRenderingEngine();
+ const { renderingEngine, viewport } = getEnabledElement(
+ activeViewportElement
+ );
const viewportInput = {
viewportId: VIEWPORT_ID,
element: viewportElement,
- type: Enums.ViewportType.STACK,
+ type: viewport.type,
+ defaultOptions: {
+ background: viewport.defaultOptions.background,
+ orientation: viewport.defaultOptions.orientation,
+ },
};
renderingEngine.enableElement(viewportInput);
@@ -43,11 +84,9 @@ const CornerstoneViewportDownloadForm = ({
const disableViewport = viewportElement => {
if (viewportElement) {
- const renderingEngine = CornerstoneViewportService.getRenderingEngine();
-
+ const { renderingEngine } = getEnabledElement(viewportElement);
return new Promise(resolve => {
renderingEngine.disableElement(VIEWPORT_ID);
- ToolGroupManager.destroyToolGroup(TOOLGROUP_ID);
});
}
};
@@ -115,26 +154,39 @@ const CornerstoneViewportDownloadForm = ({
const { viewport } = activeViewportEnabledElement;
- if (!(viewport instanceof StackViewport)) {
- throw new Error('Viewport is not a StackViewport');
- }
-
- const imageId = viewport.getCurrentImageId();
-
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
- const downloadViewport = renderingEngine.getViewport(
- VIEWPORT_ID
- ) as StackViewport;
+ const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
- downloadViewport.setStack([imageId]).then(() => {
+ if (downloadViewport instanceof StackViewport) {
+ const imageId = viewport.getCurrentImageId();
const properties = viewport.getProperties();
- downloadViewport.setProperties(properties);
+
+ downloadViewport.setStack([imageId]).then(() => {
+ downloadViewport.setProperties(properties);
+
+ const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
+ const newHeight = Math.min(
+ height || image.height,
+ MAX_TEXTURE_SIZE
+ );
+
+ resolve({ width: newWidth, height: newHeight });
+ });
+ } else if (downloadViewport instanceof VolumeViewport) {
+ const actors = viewport.getActors();
+ // downloadViewport.setActors(actors);
+ actors.forEach(actor => {
+ downloadViewport.addActor(actor);
+ });
+
+ downloadViewport.setCamera(viewport.getCamera());
+ downloadViewport.render();
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
resolve({ width: newWidth, height: newHeight });
- });
+ }
}
});
@@ -164,31 +216,20 @@ const CornerstoneViewportDownloadForm = ({
renderingEngineId
);
- let downloadToolGroup = ToolGroupManager.getToolGroupForViewport(
- downloadViewportId,
- renderingEngineId
- );
-
- if (downloadToolGroup === undefined) {
- downloadToolGroup = ToolGroupManager.createToolGroup(TOOLGROUP_ID);
-
- // what tools were in the active viewport?
- // make them all enabled instances so that they can not be interacted
- // with in the download viewport
- Object.values(toolGroup._toolInstances).forEach(tool => {
- downloadToolGroup.addTool(tool.getToolName());
- });
-
- // add the viewport to the toolGroup
- downloadToolGroup.addViewport(downloadViewportId);
- }
-
- Object.values(downloadToolGroup._toolInstances).forEach(tool => {
- const toolName = tool.getToolName();
- if (toggle) {
- downloadToolGroup.setToolEnabled(toolName);
+ // add the viewport to the toolGroup
+ toolGroup.addViewport(downloadViewportId);
+
+ Object.keys(toolGroup._toolInstances).forEach(toolName => {
+ // make all tools Enabled so that they can not be interacted with
+ // in the download viewport
+ if (toggle && toolName !== 'Crosshairs') {
+ try {
+ toolGroup.setToolEnabled(toolName);
+ } catch (e) {
+ console.log(e);
+ }
} else {
- downloadToolGroup.setToolDisabled(toolName);
+ toolGroup.setToolDisabled(toolName);
}
});
};
@@ -199,10 +240,10 @@ const CornerstoneViewportDownloadForm = ({
`div[data-viewport-uid="${VIEWPORT_ID}"]`
);
- domtoimage.toPng(divForDownloadViewport).then(dataUrl => {
+ html2canvas(divForDownloadViewport).then(canvas => {
const link = document.createElement('a');
link.download = file;
- link.href = dataUrl;
+ link.href = canvas.toDataURL(fileType, 1.0);
link.click();
});
};
diff --git a/extensions/cornerstone/src/utils/calculateViewportRegistrations.ts b/extensions/cornerstone/src/utils/calculateViewportRegistrations.ts
new file mode 100644
index 00000000000..cbb976534be
--- /dev/null
+++ b/extensions/cornerstone/src/utils/calculateViewportRegistrations.ts
@@ -0,0 +1,30 @@
+import { Types, getRenderingEngine, utilities } from '@cornerstonejs/core';
+
+export default function calculateViewportRegistrations(
+ viewports: Types.IViewportId[]
+) {
+ const viewportPairs = _getViewportPairs(viewports);
+
+ for (const [viewport, nextViewport] of viewportPairs) {
+ // check if they are in the same Frame of Reference
+ const renderingEngine1 = getRenderingEngine(viewport.renderingEngineId);
+ const renderingEngine2 = getRenderingEngine(nextViewport.renderingEngineId);
+
+ const csViewport1 = renderingEngine1.getViewport(viewport.viewportId);
+ const csViewport2 = renderingEngine2.getViewport(nextViewport.viewportId);
+
+ utilities.calculateViewportsSpatialRegistration(csViewport1, csViewport2);
+ }
+}
+
+const _getViewportPairs = (viewports: Types.IViewportId[]) => {
+ const viewportPairs = [];
+
+ for (let i = 0; i < viewports.length; i++) {
+ for (let j = i + 1; j < viewports.length; j++) {
+ viewportPairs.push([viewports[i], viewports[j]]);
+ }
+ }
+
+ return viewportPairs;
+};
diff --git a/extensions/cornerstone/src/utils/dicomLoaderService.js b/extensions/cornerstone/src/utils/dicomLoaderService.js
index dbc5b1b8310..54836fc1277 100644
--- a/extensions/cornerstone/src/utils/dicomLoaderService.js
+++ b/extensions/cornerstone/src/utils/dicomLoaderService.js
@@ -38,6 +38,10 @@ const getImageInstance = dataset => {
return dataset && dataset.images && dataset.images[0];
};
+const getNonImageInstance = dataset => {
+ return dataset && dataset.instance;
+};
+
const getImageInstanceId = imageInstance => {
return getImageId(imageInstance);
};
@@ -89,19 +93,28 @@ const getImageLoaderType = imageId => {
class DicomLoaderService {
getLocalData(dataset, studies) {
- if (dataset && dataset.localFile) {
- // Use referenced imageInstance
- const imageInstance = getImageInstance(dataset);
- let imageId = getImageInstanceId(imageInstance);
-
- // or Try to get it from studies
- if (someInvalidStrings(imageId)) {
- imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUID);
- }
+ // Use referenced imageInstance
+ const imageInstance = getImageInstance(dataset);
+ const nonImageInstance = getNonImageInstance(dataset);
- if (!someInvalidStrings(imageId)) {
- return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
- }
+ if (
+ (!imageInstance && !nonImageInstance) ||
+ !nonImageInstance.imageId.startsWith('dicomfile')
+ ) {
+ return;
+ }
+
+ const instance = imageInstance || nonImageInstance;
+
+ let imageId = getImageInstanceId(instance);
+
+ // or Try to get it from studies
+ if (someInvalidStrings(imageId)) {
+ imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUID);
+ }
+
+ if (!someInvalidStrings(imageId)) {
+ return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
}
}
@@ -182,13 +195,14 @@ class DicomLoaderService {
}
}
- *getLoaderIterator(dataset, studies) {
+ *getLoaderIterator(dataset, studies, headers) {
yield this.getLocalData(dataset, studies);
yield this.getDataByImageType(dataset);
yield this.getDataByDatasetType(dataset);
}
- findDicomDataPromise(dataset, studies) {
+ findDicomDataPromise(dataset, studies, headers) {
+ dataset.authorizationHeaders = headers;
const loaderIterator = this.getLoaderIterator(dataset, studies);
// it returns first valid retriever method.
for (const loader of loaderIterator) {
diff --git a/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts b/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts
index 398db953522..1c91dfb1356 100644
--- a/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts
+++ b/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts
@@ -8,15 +8,19 @@ const CORONAL = 'coronal';
export default function getCornerstoneOrientation(
orientation: string
): Enums.OrientationAxis {
- switch (orientation.toLowerCase()) {
- case AXIAL:
- return Enums.OrientationAxis.AXIAL;
- case SAGITTAL:
- return Enums.OrientationAxis.SAGITTAL;
- case CORONAL:
- return Enums.OrientationAxis.CORONAL;
- default:
- log.wanr('Choosing acquisition plane orientation');
- return Enums.OrientationAxis.ACQUISITION;
+ if (orientation) {
+ switch (orientation.toLowerCase()) {
+ case AXIAL:
+ return Enums.OrientationAxis.AXIAL;
+ case SAGITTAL:
+ return Enums.OrientationAxis.SAGITTAL;
+ case CORONAL:
+ return Enums.OrientationAxis.CORONAL;
+ default:
+ log.wanr('Choosing acquisition plane orientation');
+ return Enums.OrientationAxis.ACQUISITION;
+ }
}
+
+ return Enums.OrientationAxis.ACQUISITION;
}
diff --git a/extensions/cornerstone/src/utils/getProtocolViewportStructureFromGridViewports.ts b/extensions/cornerstone/src/utils/getProtocolViewportStructureFromGridViewports.ts
new file mode 100644
index 00000000000..331c9019d24
--- /dev/null
+++ b/extensions/cornerstone/src/utils/getProtocolViewportStructureFromGridViewports.ts
@@ -0,0 +1,74 @@
+/**
+ * Given the ViewportGridService state it will re create the protocol viewport structure
+ * that was used at the hanging protocol creation time. This is used to re create the
+ * viewport structure when the user decides to go back to a previous cached
+ * layout in the viewport grid.
+ *
+ *
+ * viewportGrid's viewports look like
+ *
+ * viewports = [
+ * {
+ * displaySetInstanceUIDs: string[],
+ * displaySetOptions: [],
+ * viewportOptions: {}
+ * height: number,
+ * width: number,
+ * x: number,
+ * y: number
+ * },
+ * ]
+ *
+ * and hanging protocols viewport structure looks like
+ *
+ * viewportStructure: {
+ * layoutType: 'grid',
+ * properties: {
+ * rows: 3,
+ * columns: 4,
+ * layoutOptions: [
+ * {
+ * x: 0,
+ * y: 0,
+ * width: 1 / 4,
+ * height: 1 / 3,
+ * },
+ * {
+ * x: 1 / 4,
+ * y: 0,
+ * width: 1 / 4,
+ * height: 1 / 3,
+ * },
+ * ],
+ * },
+ * },
+ */
+export default function getProtocolViewportStructureFromGridViewports({
+ numRows,
+ numCols,
+ viewports,
+}: {
+ numRows: number;
+ numCols: number;
+ viewports: any[];
+}) {
+ const viewportStructure = {
+ layoutType: 'grid',
+ properties: {
+ rows: numRows,
+ columns: numCols,
+ layoutOptions: [],
+ },
+ };
+
+ viewports.forEach(viewport => {
+ viewportStructure.properties.layoutOptions.push({
+ x: viewport.x,
+ y: viewport.y,
+ width: viewport.width,
+ height: viewport.height,
+ });
+ });
+
+ return viewportStructure;
+}
diff --git a/extensions/cornerstone/src/utils/interleaveCenterLoader.ts b/extensions/cornerstone/src/utils/interleaveCenterLoader.ts
index d0ea08fbc77..0047f0ac0ab 100644
--- a/extensions/cornerstone/src/utils/interleaveCenterLoader.ts
+++ b/extensions/cornerstone/src/utils/interleaveCenterLoader.ts
@@ -51,7 +51,7 @@ export default function interleaveCenterLoader({
* listen to it and as the other viewports are created we can set the volumes for them
* since volumes are already started loading.
*/
- if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
+ if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
return;
}
diff --git a/extensions/cornerstone/src/utils/interleaveTopToBottom.ts b/extensions/cornerstone/src/utils/interleaveTopToBottom.ts
index 1f1bed3f08a..162bb5588cf 100644
--- a/extensions/cornerstone/src/utils/interleaveTopToBottom.ts
+++ b/extensions/cornerstone/src/utils/interleaveTopToBottom.ts
@@ -50,7 +50,7 @@ export default function interleaveTopToBottom({
* listen to it and as the other viewports are created we can set the volumes for them
* since volumes are already started loading.
*/
- if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
+ if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
return;
}
diff --git a/extensions/cornerstone/src/utils/removeToolGroupSegmentationRepresentations.ts b/extensions/cornerstone/src/utils/removeToolGroupSegmentationRepresentations.ts
new file mode 100644
index 00000000000..513321b32ff
--- /dev/null
+++ b/extensions/cornerstone/src/utils/removeToolGroupSegmentationRepresentations.ts
@@ -0,0 +1,20 @@
+import { segmentation } from '@cornerstonejs/tools';
+
+function removeToolGroupSegmentationRepresentations(toolGroupId) {
+ const representations = segmentation.state.getSegmentationRepresentations(
+ toolGroupId
+ );
+
+ if (!representations || !representations.length) {
+ return;
+ }
+
+ representations.forEach(representation => {
+ segmentation.state.removeSegmentationRepresentation(
+ toolGroupId,
+ representation.segmentationRepresentationUID
+ );
+ });
+}
+
+export default removeToolGroupSegmentationRepresentations;
diff --git a/extensions/cornerstone/src/utils/segmentationServiceMappings/Labelmap.js b/extensions/cornerstone/src/utils/segmentationServiceMappings/Labelmap.js
deleted file mode 100644
index cb058b06e62..00000000000
--- a/extensions/cornerstone/src/utils/segmentationServiceMappings/Labelmap.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Enums as csToolsEnums } from '@cornerstonejs/tools';
-
-const Labelmap = {
- toSegmentation: segmentationState => {
- const {
- activeSegmentIndex,
- cachedStats: data,
- segmentsLocked,
- representationData,
- label,
- segmentationId,
- text,
- } = segmentationState;
-
- const labelmapRepresentationData =
- representationData[csToolsEnums.SegmentationRepresentations.Labelmap];
-
- return {
- id: segmentationId,
- activeSegmentIndex,
- segmentsLocked,
- data,
- label,
- volumeId: labelmapRepresentationData.volumeId,
- displayText: text || [],
- };
- },
-};
-
-export default Labelmap;
diff --git a/extensions/cornerstone/src/utils/segmentationServiceMappings/segmentationServiceMappingsFactory.js b/extensions/cornerstone/src/utils/segmentationServiceMappings/segmentationServiceMappingsFactory.js
deleted file mode 100644
index ea248829abb..00000000000
--- a/extensions/cornerstone/src/utils/segmentationServiceMappings/segmentationServiceMappingsFactory.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Labelmap from './Labelmap';
-
-const segmentationServiceMappingsFactory = (
- SegmentationService,
- DisplaySetService
-) => {
- return {
- Labelmap: {
- matchingCriteria: {},
- toSegmentation: csToolsSegmentation =>
- Labelmap.toSegmentation(csToolsSegmentation, DisplaySetService),
- },
- };
-};
-
-export default segmentationServiceMappingsFactory;
diff --git a/extensions/cornerstone/src/utils/transitions.ts b/extensions/cornerstone/src/utils/transitions.ts
new file mode 100644
index 00000000000..2fa96794d09
--- /dev/null
+++ b/extensions/cornerstone/src/utils/transitions.ts
@@ -0,0 +1,22 @@
+/**
+ * It is a bell curved function that uses ease in out quadratic for css
+ * transition timing function for each side of the curve.
+ *
+ * @param {number} x - The current time, in the range [0, 1].
+ * @param {number} baseline - The baseline value to start from and return to.
+ * @returns the value of the transition at time x.
+ */
+export function easeInOutBell(x: number, baseline: number): number {
+ const alpha = 1 - baseline;
+
+ // prettier-ignore
+ if (x < 1 / 4) {
+ return 4 * Math.pow(2 * x, 3) * alpha + baseline;
+ } else if (x < 1 / 2) {
+ return (1 - Math.pow(-4 * x + 2, 3) / 2) * alpha + baseline;
+ } else if (x < 3 / 4) {
+ return (1 - Math.pow(4 * x - 2, 3) / 2) * alpha + baseline;
+ } else {
+ return (- 4 * Math.pow(2 * x - 2, 3)) * alpha + baseline;
+ }
+}
diff --git a/extensions/default/package.json b/extensions/default/package.json
index 34858832a01..a894ef8d9d8 100644
--- a/extensions/default/package.json
+++ b/extensions/default/package.json
@@ -43,6 +43,6 @@
},
"dependencies": {
"@babel/runtime": "7.16.3",
- "@cornerstonejs/calculate-suv": "1.0.2"
+ "@cornerstonejs/calculate-suv": "^1.0.3"
}
}
diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js
index faa2b95ae74..1e29238bf16 100644
--- a/extensions/default/src/DicomLocalDataSource/index.js
+++ b/extensions/default/src/DicomLocalDataSource/index.js
@@ -166,7 +166,6 @@ function createDicomLocalApi(dicomLocalConfig) {
displaySet.images.forEach(instance => {
const NumberOfFrames = instance.NumberOfFrames;
-
if (NumberOfFrames > 1) {
for (let i = 0; i < NumberOfFrames; i++) {
const imageId = this.getImageIdsForInstance({
@@ -190,9 +189,14 @@ function createDicomLocalApi(dicomLocalConfig) {
SeriesInstanceUID,
SOPInstanceUID
);
- if (storedInstance.url) {
- return storedInstance.url;
+
+ let imageId = storedInstance.url;
+
+ if (frame !== undefined) {
+ imageId += `&frame=${frame}`;
}
+
+ return imageId;
},
deleteStudyMetadataPromise() {
console.log('deleteStudyMetadataPromise not implemented');
diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js
index d18a13f87e0..6f7eb76f962 100644
--- a/extensions/default/src/DicomWebDataSource/index.js
+++ b/extensions/default/src/DicomWebDataSource/index.js
@@ -423,6 +423,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
// any implementation that stores static copies of the metadata
StudyInstanceUID: naturalized.StudyInstanceUID,
};
+ // Todo: this needs to be from wado dicom web client
return qidoDicomWebClient.retrieveBulkData(options).then(val => {
const ret = (val && val[0]) || undefined;
value.Value = ret;
@@ -440,6 +441,9 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
// Adding instanceMetadata to OHIF MetadataProvider
naturalizedInstances.forEach((instance, index) => {
+ instance.wadoRoot = dicomWebConfig.wadoRoot;
+ instance.wadoUri = dicomWebConfig.wadoUri;
+
const imageId = implementation.getImageIdsForInstance({
instance,
});
diff --git a/extensions/default/src/Panels/PanelMeasurementTable.tsx b/extensions/default/src/Panels/PanelMeasurementTable.tsx
index c076692ca80..fb890117828 100644
--- a/extensions/default/src/Panels/PanelMeasurementTable.tsx
+++ b/extensions/default/src/Panels/PanelMeasurementTable.tsx
@@ -155,7 +155,6 @@ export default function PanelMeasurementTable({
>
Studies --> DisplaySets --> Thumbnails
@@ -34,10 +39,25 @@ function PanelStudyBrowser({
const isMounted = useRef(true);
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
- viewportGridService.setDisplaySetsForViewport({
- viewportIndex: activeViewportIndex,
- displaySetInstanceUIDs: [displaySetInstanceUID],
- });
+ let updatedViewports = [];
+ const viewportIndex = activeViewportIndex;
+ try {
+ updatedViewports = HangingProtocolService.getViewportsRequireUpdate(
+ viewportIndex,
+ displaySetInstanceUID
+ );
+ } catch (error) {
+ console.warn(error);
+ UINotificationService.show({
+ title: 'Thumbnail Double Click',
+ message:
+ 'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.',
+ type: 'info',
+ duration: 3000,
+ });
+ }
+
+ viewportGridService.setDisplaySetsForViewports(updatedViewports);
};
// ~~ studyDisplayList
@@ -227,12 +247,7 @@ function PanelStudyBrowser({
}
PanelStudyBrowser.propTypes = {
- DisplaySetService: PropTypes.shape({
- EVENTS: PropTypes.object.isRequired,
- activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired,
- getDisplaySetByUID: PropTypes.func.isRequired,
- subscribe: PropTypes.func.isRequired,
- }).isRequired,
+ servicesManager: PropTypes.object.isRequired,
dataSource: PropTypes.shape({
getImageIdsForDisplaySet: PropTypes.func.isRequired,
}).isRequired,
diff --git a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx
index 7ae799a865e..237b5553b7e 100644
--- a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx
+++ b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx
@@ -35,7 +35,7 @@ function WrappedPanelStudyBrowser({
return (
{
+ const { unsubscribe: unsub1 } = ToolBarService.subscribe(
+ ToolBarService.EVENTS.TOOL_BAR_MODIFIED,
+ () => setToolbarButtons(ToolBarService.getButtonSection('primary'))
+ );
+ const { unsubscribe: unsub2 } = ToolBarService.subscribe(
+ ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
+ () => setButtonState({ ...ToolBarService.state })
+ );
+
+ return () => {
+ unsub1();
+ unsub2();
+ };
+ }, [ToolBarService]);
+
+ return (
+ <>
+ {toolbarButtons.map((toolDef, index) => {
+ const { id, Component, componentProps } = toolDef;
+ // TODO: ...
+
+ // isActive if:
+ // - id is primary?
+ // - id is in list of "toggled on"?
+ let isActive;
+ if (componentProps.type === 'toggle') {
+ isActive = buttonState.toggles[id];
+ }
+ // Also need... to filter list for splitButton, and set primary based on most recently clicked
+ // Also need to kill the radioGroup button's magic logic
+ // Everything should be reactive off these props, so commands can inform ToolbarService
+
+ // These can... Trigger toolbar events based on updates?
+ // Then sync using useEffect, or simply modify the state here?
+ return (
+ ToolBarService.recordInteraction(args)}
+ servicesManager={servicesManager}
+ />
+ );
+ })}
+ >
+ );
+}
diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx
index 78377ea9e95..a2e4509cf30 100644
--- a/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx
+++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx
@@ -1,20 +1,43 @@
import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
import {
LayoutSelector as OHIFLayoutSelector,
ToolbarButton,
useViewportGrid,
} from '@ohif/ui';
-function LayoutSelector({rows, columns}) {
+function LayoutSelector({ rows, columns, servicesManager }) {
const [isOpen, setIsOpen] = useState(false);
+ const [disableSelector, setDisableSelector] = useState(false);
const [viewportGridState, viewportGridService] = useViewportGrid();
+ const { HangingProtocolService } = servicesManager.services;
+
const closeOnOutsideClick = () => {
if (isOpen) {
setIsOpen(false);
}
};
+ useEffect(() => {
+ const { unsubscribe } = HangingProtocolService.subscribe(
+ HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
+ evt => {
+ const { protocol } = evt;
+
+ if (protocol.id === 'mpr') {
+ setDisableSelector(true);
+ } else {
+ setDisableSelector(false);
+ }
+ }
+ );
+
+ return () => {
+ unsubscribe();
+ };
+ }, [HangingProtocolService]);
+
useEffect(() => {
window.addEventListener('click', closeOnOutsideClick);
return () => {
@@ -44,15 +67,27 @@ function LayoutSelector({rows, columns}) {
rows={rows}
columns={columns}
onSelection={({ numRows, numCols }) => {
- viewportGridService.setLayout({ numCols, numRows });
+ viewportGridService.setLayout({ numRows, numCols });
}}
/>
)
}
- isActive={isOpen}
+ isActive={disableSelector ? false : isOpen}
type="toggle"
/>
);
}
+LayoutSelector.propTypes = {
+ rows: PropTypes.number,
+ columns: PropTypes.number,
+ onLayoutChange: PropTypes.func,
+};
+
+LayoutSelector.defaultProps = {
+ rows: 3,
+ columns: 3,
+ onLayoutChange: () => {},
+};
+
export default LayoutSelector;
diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx
index 4326ece4f8f..562760b0435 100644
--- a/extensions/default/src/ViewerLayout/index.tsx
+++ b/extensions/default/src/ViewerLayout/index.tsx
@@ -9,74 +9,16 @@ import {
AboutModal,
Header,
useModal,
+ LoadingIndicatorProgress,
} from '@ohif/ui';
import i18n from '@ohif/i18n';
import { hotkeys } from '@ohif/core';
import { useAppConfig } from '@state';
+import Toolbar from '../Toolbar/Toolbar';
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
-function Toolbar({ servicesManager }) {
- const { ToolBarService } = servicesManager.services;
- const [toolbarButtons, setToolbarButtons] = useState([]);
- const [buttonState, setButtonState] = useState({
- primaryToolId: '',
- toggles: {},
- groups: {},
- });
-
- // Could track buttons and state separately...?
- useEffect(() => {
- const { unsubscribe: unsub1 } = ToolBarService.subscribe(
- ToolBarService.EVENTS.TOOL_BAR_MODIFIED,
- () => setToolbarButtons(ToolBarService.getButtonSection('primary'))
- );
- const { unsubscribe: unsub2 } = ToolBarService.subscribe(
- ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
- () => setButtonState({ ...ToolBarService.state })
- );
-
- return () => {
- unsub1();
- unsub2();
- };
- }, [ToolBarService]);
-
- return (
- <>
- {toolbarButtons.map((toolDef, index) => {
- const { id, Component, componentProps } = toolDef;
- // TODO: ...
-
- // isActive if:
- // - id is primary?
- // - id is in list of "toggled on"?
- let isActive;
- if (componentProps.type === 'toggle') {
- isActive = buttonState.toggles[id];
- }
- // Also need... to filter list for splitButton, and set primary based on most recently clicked
- // Also need to kill the radioGroup button's magic logic
- // Everything should be reactive off these props, so commands can inform ToolbarService
-
- // These can... Trigger toolbar events based on updates?
- // Then sync using useEffect, or simply modify the state here?
- return (
- ToolBarService.recordInteraction(args)}
- />
- );
- })}
- >
- );
-}
-
function ViewerLayout({
// From Extension Module Params
extensionManager,
@@ -84,12 +26,12 @@ function ViewerLayout({
hotkeysManager,
commandsManager,
// From Modes
- leftPanels,
- rightPanels,
- leftPanelDefaultClosed,
- rightPanelDefaultClosed,
viewports,
ViewportGridComp,
+ leftPanels = [],
+ rightPanels = [],
+ leftPanelDefaultClosed = false,
+ rightPanelDefaultClosed = false,
}) {
const [appConfig] = useAppConfig();
const navigate = useNavigate();
@@ -101,6 +43,12 @@ function ViewerLayout({
const { t } = useTranslation();
const { show, hide } = useModal();
+ const [showLoadingIndicator, setShowLoadingIndicator] = useState(
+ appConfig.showLoadingIndicator
+ );
+
+ const { HangingProtocolService } = servicesManager.services;
+
const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
const versionNumber = process.env.VERSION_NUMBER;
const buildNumber = process.env.BUILD_NUM;
@@ -153,8 +101,10 @@ function ViewerLayout({
title: t('Header:Logout'),
icon: 'power-off',
onClick: async () => {
- navigate(`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`);
- }
+ navigate(
+ `/logout?redirect_uri=${encodeURIComponent(window.location.href)}`
+ );
+ },
});
}
@@ -186,6 +136,25 @@ function ViewerLayout({
};
};
+ useEffect(() => {
+ const { unsubscribe } = HangingProtocolService.subscribe(
+ HangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
+
+ // Todo: right now to set the loading indicator to false, we need to wait for the
+ // HangingProtocolService to finish applying the viewport matching to each viewport,
+ // however, this might not be the only approach to set the loading indicator to false. we need to explore this further.
+ ({ progress }) => {
+ if (progress === 100) {
+ setShowLoadingIndicator(false);
+ }
+ }
+ );
+
+ return () => {
+ unsubscribe();
+ };
+ }, [HangingProtocolService]);
+
const getViewportComponentData = viewportComponent => {
const entry = extensionManager.getModuleEntry(viewportComponent.namespace);
@@ -213,44 +182,45 @@ function ViewerLayout({
+
+ {isTracked ? (
+ <>
+ Series is
+ tracked and
+ can be viewed in the measurement panel
+ >
+ ) : (
+ <>
+ Measurements for
+ untracked
+ series will not be shown in the measurements
+ panel
+ >
+ )}
+
+