-
-
+
+
+
+
+
+
);
diff --git a/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx b/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx
index 8a72dd593..00eac97da 100644
--- a/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx
+++ b/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react';
import {
- InputDoubleRange,
Button,
PanelSection,
ButtonGroup,
@@ -10,6 +9,8 @@ import {
Tooltip,
} from '@ohif/ui';
+import { DoubleSlider } from '@ohif/ui-next';
+
import { Enums } from '@cornerstonejs/core';
const controlClassNames = {
@@ -55,21 +56,15 @@ const DynamicVolumeControls = ({
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
- const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
-
- useEffect(() => {
- setSliderRangeValues([framesLength / 4, framesLength / 2]);
- }, [framesLength]);
+ const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]);
const handleSliderChange = newValues => {
onDoubleRangeChange(newValues);
-
- if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
- return;
- }
setSliderRangeValues(newValues);
};
+ const formatLabel = value => Math.round(value);
+
return (
-
-
+
);
+
+ );
}
/**
diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx
index cd219b20e..6edc50665 100644
--- a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx
+++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx
@@ -1,7 +1,6 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
-import { Enums, VolumeViewport3D } from '@cornerstonejs/core';
-import { utilities as csToolsUtils } from '@cornerstonejs/tools';
+import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import { ImageScrollbar } from '@ohif/ui';
function CornerstoneImageScrollbar({
@@ -12,7 +11,9 @@ function CornerstoneImageScrollbar({
setImageSliceData,
scrollbarHeight,
servicesManager,
-}: withAppTypes) {
+}: withAppTypes<{
+ element: HTMLElement;
+}>) {
const { cineService, cornerstoneViewportService } = servicesManager.services;
const onImageScrollbarChange = (imageIndex, viewportId) => {
@@ -26,7 +27,7 @@ function CornerstoneImageScrollbar({
cineService.setCine({ id: viewportId, isPlaying: false });
}
- csToolsUtils.jumpToSlice(viewport.element, {
+ csUtils.jumpToSlice(viewport.element, {
imageIndex,
debounceLoading: true,
});
@@ -64,7 +65,9 @@ function CornerstoneImageScrollbar({
const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
- if(!viewport || viewport instanceof VolumeViewport3D) return
+ if (!viewport || viewport instanceof VolumeViewport3D) {
+ return;
+ }
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
const numberOfSlices = viewport.getNumberOfSlices();
// find the index of imageId in the imageIds
diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx
index 178705f40..69be2be75 100644
--- a/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx
+++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx
@@ -1,14 +1,7 @@
import React, { useEffect, useState, useMemo } from 'react';
import classNames from 'classnames';
-import {
- metaData,
- Enums,
- Types,
- getEnabledElement,
- utilities as csUtils,
-} from '@cornerstonejs/core';
+import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
-import PropTypes from 'prop-types';
import { vec3 } from 'gl-matrix';
import './ViewportOrientationMarkers.css';
@@ -31,8 +24,9 @@ function ViewportOrientationMarkers({
useEffect(() => {
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
- const { rotation, previousCamera, camera } = evt.detail;
+ const { previousCamera, camera } = evt.detail;
+ const { rotation } = camera;
if (rotation !== undefined) {
setRotation(rotation);
}
@@ -74,7 +68,12 @@ function ViewportOrientationMarkers({
return false;
}
- ({ rowCosines, columnCosines, isDefaultValueSetForColumnCosine, isDefaultValueSetForColumnCosine } = metaData.get('imagePlaneModule', imageId) || {});
+ ({
+ rowCosines,
+ columnCosines,
+ isDefaultValueSetForColumnCosine,
+ isDefaultValueSetForColumnCosine,
+ } = metaData.get('imagePlaneModule', imageId) || {});
} else {
if (!element || !getEnabledElement(element)) {
return '';
@@ -90,7 +89,13 @@ function ViewportOrientationMarkers({
rowCosines = viewRight;
}
- if (!rowCosines || !columnCosines || rotation === undefined || isDefaultValueSetForRowCosine || isDefaultValueSetForColumnCosine) {
+ if (
+ !rowCosines ||
+ !columnCosines ||
+ rotation === undefined ||
+ isDefaultValueSetForRowCosine ||
+ isDefaultValueSetForColumnCosine
+ ) {
return '';
}
diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts
index 69f51168a..e4d1c2e67 100644
--- a/extensions/cornerstone/src/commandsModule.ts
+++ b/extensions/cornerstone/src/commandsModule.ts
@@ -12,15 +12,23 @@ import {
utilities as cstUtils,
ReferenceLinesTool,
} from '@cornerstonejs/tools';
+
import { Types as OhifTypes } from '@ohif/core';
+import {
+ callLabelAutocompleteDialog,
+ showLabelAnnotationPopup,
+ createReportAsync,
+ callInputDialog,
+ colorPickerDialog,
+} from '@ohif/extension-default';
import { vec3, mat4 } from 'gl-matrix';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
-import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/callInputDialog';
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
+import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
@@ -29,6 +37,7 @@ const toggleSyncFunctions = {
function commandsModule({
servicesManager,
+ extensionManager,
commandsManager,
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
const {
@@ -101,7 +110,49 @@ function commandsModule({
commandsManager.run(options, optionsToUse);
},
+ updateStoredSegmentationPresentation: ({ displaySet, type }) => {
+ const { setSegmentationPresentation } = useSegmentationPresentationStore.getState();
+ const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID;
+ setSegmentationPresentation(referencedDisplaySetInstanceUID, [
+ {
+ segmentationId: displaySet.displaySetInstanceUID,
+ hydrated: true,
+ type,
+ },
+ ]);
+ },
+ updateStoredPositionPresentation: ({ viewportId, displaySetInstanceUID }) => {
+ const presentations = cornerstoneViewportService.getPresentations(viewportId);
+ const { positionPresentationStore, setPositionPresentation, getPositionPresentationId } =
+ usePositionPresentationStore.getState();
+
+ // Look inside positionPresentationStore and find the key that includes the displaySetInstanceUID
+ // and the value has viewportId as activeViewportId.
+ const previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find(
+ ([key, value]) => key.includes(displaySetInstanceUID) && value.viewportId === viewportId
+ )?.[0];
+
+ if (previousReferencedDisplaySetStoreKey) {
+ setPositionPresentation(
+ previousReferencedDisplaySetStoreKey,
+ presentations.positionPresentation
+ );
+
+ return;
+ }
+
+ // if not found means we have not visited that referencedDisplaySetInstanceUID before
+ // so we need to grab the positionPresentationId directly from the store,
+ // Todo: this is really hacky, we should have a better way for this
+
+ const positionPresentationId = getPositionPresentationId({
+ displaySetInstanceUIDs: [displaySetInstanceUID],
+ viewportId,
+ });
+
+ setPositionPresentation(positionPresentationId, presentations.positionPresentation);
+ },
getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates);
},
@@ -435,9 +486,10 @@ function commandsModule({
viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 });
viewport.render();
} else if (viewport.getRotation !== undefined) {
- const currentRotation = viewport.getRotation();
+ const presentation = viewport.getViewPresentation();
+ const { rotation: currentRotation } = presentation;
const newRotation = (currentRotation + rotation) % 360;
- viewport.setProperties({ rotation: newRotation });
+ viewport.setViewPresentation({ rotation: newRotation });
viewport.render();
}
},
@@ -554,7 +606,7 @@ function commandsModule({
// Set slice to last slice
const options = { imageIndex: jumpIndex };
- cstUtils.jumpToSlice(viewport.element, options);
+ csUtils.jumpToSlice(viewport.element, options);
},
scroll: ({ direction }) => {
const enabledElement = _getActiveViewportEnabledElement();
@@ -566,7 +618,7 @@ function commandsModule({
const { viewport } = enabledElement;
const options = { delta: direction };
- cstUtils.scroll(viewport, options);
+ csUtils.scroll(viewport, options);
},
setViewportColormap: ({
viewportId,
@@ -577,7 +629,6 @@ function commandsModule({
}) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
- const actorEntries = viewport.getActors();
let hpOpacity;
// Retrieve active protocol's viewport match details
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
@@ -596,14 +647,8 @@ function commandsModule({
// HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || opacity };
- const setViewportProperties = (viewport, uid) => {
- const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
- const { actor: volumeActor, uid: volumeId } = actorEntry;
- viewport.setProperties({ colormap, volumeActor }, volumeId);
- };
-
if (viewport instanceof StackViewport) {
- setViewportProperties(viewport, viewportId);
+ viewport.setProperties({ colormap });
}
if (viewport instanceof VolumeViewport) {
@@ -611,7 +656,9 @@ function commandsModule({
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
}
- setViewportProperties(viewport, displaySetInstanceUID);
+
+ const volumeId = viewport.getVolumeId();
+ viewport.setProperties({ colormap }, volumeId);
}
if (immediate) {
@@ -827,6 +874,339 @@ function commandsModule({
ins?.resetCrosshairs();
});
},
+ /**
+ * Creates a labelmap for the active viewport
+ */
+ createLabelmapForViewport: async ({ viewportId, options = {} }) => {
+ const { viewportGridService, displaySetService, segmentationService } =
+ servicesManager.services;
+ const { viewports } = viewportGridService.getState();
+ const targetViewportId = viewportId;
+
+ const viewport = viewports.get(targetViewportId);
+
+ // Todo: add support for multiple display sets
+ const displaySetInstanceUID =
+ options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0];
+
+ const segs = segmentationService.getSegmentations();
+
+ const label = options.label || `Segmentation ${segs.length + 1}`;
+ const segmentationId = options.segmentationId || `${csUtils.uuidv4()}`;
+
+ const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
+
+ const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet(
+ displaySet,
+ {
+ label,
+ segmentationId,
+ segments: options.createInitialSegment
+ ? {
+ 1: {
+ label: 'Segment 1',
+ active: true,
+ },
+ }
+ : {},
+ }
+ );
+
+ await segmentationService.addSegmentationRepresentation(viewportId, {
+ segmentationId,
+ type: Enums.SegmentationRepresentations.Labelmap,
+ });
+
+ return generatedSegmentationId;
+ },
+
+ /**
+ * Sets the active segmentation for a viewport
+ * @param props.segmentationId - The ID of the segmentation to set as active
+ */
+ setActiveSegmentation: ({ segmentationId }) => {
+ const { viewportGridService, segmentationService } = servicesManager.services;
+ segmentationService.setActiveSegmentation(
+ viewportGridService.getActiveViewportId(),
+ segmentationId
+ );
+ },
+
+ /**
+ * Adds a new segment to a segmentation
+ * @param props.segmentationId - The ID of the segmentation to add the segment to
+ */
+ addSegmentCommand: ({ segmentationId }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.addSegment(segmentationId);
+ },
+
+ /**
+ * Sets the active segment and jumps to its center
+ * @param props.segmentationId - The ID of the segmentation
+ * @param props.segmentIndex - The index of the segment to activate
+ */
+ setActiveSegmentAndCenterCommand: ({ segmentationId, segmentIndex }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.setActiveSegment(segmentationId, segmentIndex);
+ segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex);
+ },
+
+ /**
+ * Toggles the visibility of a segment
+ * @param props.segmentationId - The ID of the segmentation
+ * @param props.segmentIndex - The index of the segment
+ * @param props.type - The type of visibility to toggle
+ */
+ toggleSegmentVisibilityCommand: ({ segmentationId, segmentIndex, type }) => {
+ const { segmentationService, viewportGridService } = servicesManager.services;
+ segmentationService.toggleSegmentVisibility(
+ viewportGridService.getActiveViewportId(),
+ segmentationId,
+ segmentIndex,
+ type
+ );
+ },
+
+ /**
+ * Toggles the lock state of a segment
+ * @param props.segmentationId - The ID of the segmentation
+ * @param props.segmentIndex - The index of the segment
+ */
+ toggleSegmentLockCommand: ({ segmentationId, segmentIndex }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
+ },
+
+ /**
+ * Toggles the visibility of a segmentation representation
+ * @param props.segmentationId - The ID of the segmentation
+ * @param props.type - The type of representation
+ */
+ toggleSegmentationVisibilityCommand: ({ segmentationId, type }) => {
+ const { segmentationService, viewportGridService } = servicesManager.services;
+ segmentationService.toggleSegmentationRepresentationVisibility(
+ viewportGridService.getActiveViewportId(),
+ { segmentationId, type }
+ );
+ },
+
+ /**
+ * Downloads a segmentation
+ * @param props.segmentationId - The ID of the segmentation to download
+ */
+ downloadSegmentationCommand: ({ segmentationId }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.downloadSegmentation(segmentationId);
+ },
+
+ /**
+ * Stores a segmentation and shows it in the viewport
+ * @param props.segmentationId - The ID of the segmentation to store
+ */
+ storeSegmentationCommand: async ({ segmentationId }) => {
+ const { segmentationService, viewportGridService } = servicesManager.services;
+ const datasources = extensionManager.getActiveDataSource();
+
+ const displaySetInstanceUIDs = await createReportAsync({
+ servicesManager,
+ getReport: () =>
+ commandsManager.runCommand('storeSegmentation', {
+ segmentationId,
+ dataSource: datasources[0],
+ }),
+ reportType: 'Segmentation',
+ });
+
+ if (displaySetInstanceUIDs) {
+ segmentationService.remove(segmentationId);
+ viewportGridService.setDisplaySetsForViewport({
+ viewportId: viewportGridService.getActiveViewportId(),
+ displaySetInstanceUIDs,
+ });
+ }
+ },
+
+ /**
+ * Downloads a segmentation as RTSS
+ * @param props.segmentationId - The ID of the segmentation
+ */
+ downloadRTSSCommand: ({ segmentationId }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.downloadRTSS(segmentationId);
+ },
+
+ /**
+ * Sets the style for a segmentation
+ * @param props.segmentationId - The ID of the segmentation
+ * @param props.type - The type of style
+ * @param props.key - The style key to set
+ * @param props.value - The style value
+ */
+ setSegmentationStyleCommand: ({ segmentationId, type, key, value }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.setStyle({ type }, { [key]: value });
+ },
+
+ /**
+ * Deletes a segment from a segmentation
+ * @param props.segmentationId - The ID of the segmentation
+ * @param props.segmentIndex - The index of the segment to delete
+ */
+ deleteSegmentCommand: ({ segmentationId, segmentIndex }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.removeSegment(segmentationId, segmentIndex);
+ },
+
+ /**
+ * Deletes an entire segmentation
+ * @param props.segmentationId - The ID of the segmentation to delete
+ */
+ deleteSegmentationCommand: ({ segmentationId }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.remove(segmentationId);
+ },
+
+ /**
+ * Removes a segmentation from the viewport
+ * @param props.segmentationId - The ID of the segmentation to remove
+ */
+ removeSegmentationFromViewportCommand: ({ segmentationId }) => {
+ const { segmentationService, viewportGridService } = servicesManager.services;
+ segmentationService.removeSegmentationRepresentations(
+ viewportGridService.getActiveViewportId(),
+ { segmentationId }
+ );
+ },
+
+ /**
+ * Toggles rendering of inactive segmentations
+ */
+ toggleRenderInactiveSegmentationsCommand: () => {
+ const { segmentationService, viewportGridService } = servicesManager.services;
+ const viewportId = viewportGridService.getActiveViewportId();
+ const renderInactive = segmentationService.getRenderInactiveSegmentations(viewportId);
+ segmentationService.setRenderInactiveSegmentations(viewportId, !renderInactive);
+ },
+
+ /**
+ * Sets the fill alpha value for a segmentation type
+ * @param props.type - The type of segmentation
+ * @param props.value - The alpha value to set
+ */
+ setFillAlphaCommand: ({ type, value }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.setStyle({ type }, { fillAlpha: value });
+ },
+
+ /**
+ * Sets the outline width for a segmentation type
+ * @param props.type - The type of segmentation
+ * @param props.value - The width value to set
+ */
+ setOutlineWidthCommand: ({ type, value }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.setStyle({ type }, { outlineWidth: value });
+ },
+
+ /**
+ * Sets whether to render fill for a segmentation type
+ * @param props.type - The type of segmentation
+ * @param props.value - Whether to render fill
+ */
+ setRenderFillCommand: ({ type, value }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.setStyle({ type }, { renderFill: value });
+ },
+
+ /**
+ * Sets whether to render outline for a segmentation type
+ * @param props.type - The type of segmentation
+ * @param props.value - Whether to render outline
+ */
+ setRenderOutlineCommand: ({ type, value }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.setStyle({ type }, { renderOutline: value });
+ },
+
+ /**
+ * Sets the fill alpha for inactive segmentations
+ * @param props.type - The type of segmentation
+ * @param props.value - The alpha value to set
+ */
+ setFillAlphaInactiveCommand: ({ type, value }) => {
+ const { segmentationService } = servicesManager.services;
+ segmentationService.setStyle({ type }, { fillAlphaInactive: value });
+ },
+
+ editSegmentLabel: ({ segmentationId, segmentIndex }) => {
+ const { segmentationService, uiDialogService } = servicesManager.services;
+ const segmentation = segmentationService.getSegmentation(segmentationId);
+
+ if (!segmentation) {
+ return;
+ }
+
+ const segment = segmentation.segments[segmentIndex];
+ const { label } = segment;
+
+ callInputDialog(uiDialogService, label, (label, actionId) => {
+ if (label === '') {
+ return;
+ }
+
+ segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
+ });
+ },
+
+ editSegmentationLabel: ({ segmentationId }) => {
+ const { segmentationService, uiDialogService } = servicesManager.services;
+ const segmentation = segmentationService.getSegmentation(segmentationId);
+
+ if (!segmentation) {
+ return;
+ }
+
+ const { label } = segmentation;
+
+ callInputDialog(uiDialogService, label, (label, actionId) => {
+ if (label === '') {
+ return;
+ }
+
+ segmentationService.addOrUpdateSegmentation({ segmentationId, label });
+ });
+ },
+
+ editSegmentColor: ({ segmentationId, segmentIndex }) => {
+ const { segmentationService, uiDialogService, viewportGridService } =
+ servicesManager.services;
+ const viewportId = viewportGridService.getActiveViewportId();
+ const color = segmentationService.getSegmentColor(viewportId, segmentationId, segmentIndex);
+
+ const rgbaColor = {
+ r: color[0],
+ g: color[1],
+ b: color[2],
+ a: color[3] / 255.0,
+ };
+
+ colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
+ if (actionId === 'cancel') {
+ return;
+ }
+
+ const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
+ segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
+ });
+ },
+
+ getRenderInactiveSegmentations: () => {
+ const { segmentationService, viewportGridService } = servicesManager.services;
+ return segmentationService.getRenderInactiveSegmentations(
+ viewportGridService.getActiveViewportId()
+ );
+ },
};
const definitions = {
@@ -988,6 +1368,84 @@ function commandsModule({
toggleActiveDisabledToolbar: {
commandFn: actions.toggleActiveDisabledToolbar,
},
+ updateStoredPositionPresentation: {
+ commandFn: actions.updateStoredPositionPresentation,
+ },
+ updateStoredSegmentationPresentation: {
+ commandFn: actions.updateStoredSegmentationPresentation,
+ },
+ createLabelmapForViewport: {
+ commandFn: actions.createLabelmapForViewport,
+ },
+ setActiveSegmentation: {
+ commandFn: actions.setActiveSegmentation,
+ },
+ addSegment: {
+ commandFn: actions.addSegmentCommand,
+ },
+ setActiveSegmentAndCenter: {
+ commandFn: actions.setActiveSegmentAndCenterCommand,
+ },
+ toggleSegmentVisibility: {
+ commandFn: actions.toggleSegmentVisibilityCommand,
+ },
+ toggleSegmentLock: {
+ commandFn: actions.toggleSegmentLockCommand,
+ },
+ toggleSegmentationVisibility: {
+ commandFn: actions.toggleSegmentationVisibilityCommand,
+ },
+ downloadSegmentation: {
+ commandFn: actions.downloadSegmentationCommand,
+ },
+ storeSegmentation: {
+ commandFn: actions.storeSegmentationCommand,
+ },
+ downloadRTSS: {
+ commandFn: actions.downloadRTSSCommand,
+ },
+ setSegmentationStyle: {
+ commandFn: actions.setSegmentationStyleCommand,
+ },
+ deleteSegment: {
+ commandFn: actions.deleteSegmentCommand,
+ },
+ deleteSegmentation: {
+ commandFn: actions.deleteSegmentationCommand,
+ },
+ removeSegmentationFromViewport: {
+ commandFn: actions.removeSegmentationFromViewportCommand,
+ },
+ toggleRenderInactiveSegmentations: {
+ commandFn: actions.toggleRenderInactiveSegmentationsCommand,
+ },
+ setFillAlpha: {
+ commandFn: actions.setFillAlphaCommand,
+ },
+ setOutlineWidth: {
+ commandFn: actions.setOutlineWidthCommand,
+ },
+ setRenderFill: {
+ commandFn: actions.setRenderFillCommand,
+ },
+ setRenderOutline: {
+ commandFn: actions.setRenderOutlineCommand,
+ },
+ setFillAlphaInactive: {
+ commandFn: actions.setFillAlphaInactiveCommand,
+ },
+ editSegmentLabel: {
+ commandFn: actions.editSegmentLabel,
+ },
+ editSegmentationLabel: {
+ commandFn: actions.editSegmentationLabel,
+ },
+ editSegmentColor: {
+ commandFn: actions.editSegmentColor,
+ },
+ getRenderInactiveSegmentations: {
+ commandFn: actions.getRenderInactiveSegmentations,
+ },
};
return {
diff --git a/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx b/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx
index af78066ab..06b292e3c 100644
--- a/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx
+++ b/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx
@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { CinePlayer, useCine } from '@ohif/ui';
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
-import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
import { useAppConfig } from '@state';
function WrappedCinePlayer({
@@ -101,7 +100,7 @@ function WrappedCinePlayer({
return;
}
- eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
+ enabledVPElement.addEventListener(Enums.Events.VIEWPORT_NEW_IMAGE_SET, newDisplaySetHandler);
// this doesn't makes sense that we are listening to this event on viewport element
enabledVPElement.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
@@ -111,7 +110,10 @@ function WrappedCinePlayer({
return () => {
cineService.setCine({ id: viewportId, isPlaying: false });
- eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
+ enabledVPElement.removeEventListener(
+ Enums.Events.VIEWPORT_NEW_IMAGE_SET,
+ newDisplaySetHandler
+ );
enabledVPElement.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
newDisplaySetHandler
@@ -181,13 +183,13 @@ function RenderCinePlayer({
};
eventTarget.addEventListener(
- StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
+ Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange
);
return () => {
eventTarget.removeEventListener(
- StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
+ Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange
);
};
@@ -199,7 +201,7 @@ function RenderCinePlayer({
}
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
- const volume = cache.getVolume(volumeId);
+ const volume = cache.getVolume(volumeId, true);
volume.timePointIndex = timePointIndex;
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
@@ -207,7 +209,7 @@ function RenderCinePlayer({
const updateDynamicInfo = useCallback(props => {
const { volumeId, timePointIndex } = props;
- const volume = cache.getVolume(volumeId);
+ const volume = cache.getVolume(volumeId, true);
volume.timePointIndex = timePointIndex;
}, []);
@@ -223,6 +225,7 @@ function RenderCinePlayer({
isPlaying: false,
});
cineService.setIsCineEnabled(false);
+ cineService.setViewportCineClosed(viewportId);
}}
onPlayPauseChange={isPlaying => {
cineService.setCine({
diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx
new file mode 100644
index 000000000..93f9c5864
--- /dev/null
+++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx
@@ -0,0 +1,138 @@
+import React, { useEffect, useState } from 'react';
+import { Button, Icons, Separator } from '@ohif/ui-next';
+import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
+
+function ViewportSegmentationMenu({
+ viewportId,
+ servicesManager,
+}: withAppTypes<{ viewportId: string }>) {
+ const { segmentationService } = servicesManager.services;
+ const [activeSegmentations, setActiveSegmentations] = useState([]);
+ const [availableSegmentations, setAvailableSegmentations] = useState([]);
+
+ useEffect(() => {
+ const updateSegmentations = () => {
+ const active = segmentationService.getSegmentationRepresentations(viewportId);
+ setActiveSegmentations(active);
+
+ const all = segmentationService.getSegmentations();
+ const available = all.filter(
+ seg => !active.some(activeSeg => activeSeg.segmentationId === seg.segmentationId)
+ );
+ setAvailableSegmentations(available);
+ };
+
+ updateSegmentations();
+
+ const subscriptions = [
+ segmentationService.EVENTS.SEGMENTATION_MODIFIED,
+ segmentationService.EVENTS.SEGMENTATION_REMOVED,
+ segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
+ ].map(event => segmentationService.subscribe(event, updateSegmentations));
+
+ return () => {
+ subscriptions.forEach(subscription => subscription.unsubscribe());
+ };
+ }, [segmentationService, viewportId]);
+
+ const toggleSegmentationRepresentationVisibility = (
+ segmentationId,
+ type = SegmentationRepresentations.Labelmap
+ ) => {
+ segmentationService.toggleSegmentationRepresentationVisibility(viewportId, {
+ segmentationId,
+ type,
+ });
+ };
+
+ const addSegmentationToViewport = segmentationId => {
+ segmentationService.addSegmentationRepresentation(viewportId, { segmentationId });
+ };
+
+ const removeSegmentationFromViewport = segmentationId => {
+ segmentationService.removeSegmentationRepresentations(viewportId, {
+ segmentationId,
+ });
+ };
+
+ return (
+
+
Current Viewport
+
+ {activeSegmentations.map(segmentation => (
+ -
+
+ {segmentation.label}
+ {segmentation.visible ? (
+
+ ) : (
+
+ )}
+
+ ))}
+
+ {availableSegmentations.length > 0 && (
+ <>
+
+
Available
+
+ {availableSegmentations.map(segmentation => (
+ -
+
+ {segmentation.segmentation.label}
+
+ ))}
+
+ >
+ )}
+
+ );
+}
+
+export default ViewportSegmentationMenu;
diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx
new file mode 100644
index 000000000..8fcd5cf34
--- /dev/null
+++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx
@@ -0,0 +1,101 @@
+import React, { ReactNode, useEffect, useState } from 'react';
+import { Button, Icons, Popover, PopoverContent, PopoverTrigger } from '@ohif/ui-next';
+import ViewportSegmentationMenu from './ViewportSegmentationMenu';
+import classNames from 'classnames';
+
+export function ViewportSegmentationMenuWrapper({
+ viewportId,
+ displaySets,
+ servicesManager,
+ commandsManager,
+ location,
+}: withAppTypes<{
+ viewportId: string;
+ element: HTMLElement;
+}>): ReactNode {
+ const { segmentationService, viewportActionCornersService, viewportGridService } =
+ servicesManager.services;
+ const [representations, setRepresentations] = useState(null);
+
+ useEffect(() => {
+ const representations = segmentationService.getSegmentationRepresentations(viewportId);
+ setRepresentations(representations);
+ }, [viewportId, segmentationService]);
+
+ useEffect(() => {
+ const { unsubscribe } = segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
+ () => {
+ const representations = segmentationService.getSegmentationRepresentations(viewportId);
+ setRepresentations(representations);
+ }
+ );
+
+ return () => {
+ unsubscribe();
+ };
+ }, [viewportId, segmentationService]);
+
+ const activeViewportId = viewportGridService.getActiveViewportId();
+ const isActiveViewport = viewportId === activeViewportId;
+
+ const { align, side } = getAlignAndSide(viewportActionCornersService, location);
+
+ if (!representations?.length) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+const getAlignAndSide = (viewportActionCornersService, location) => {
+ const ViewportActionCornersLocations = viewportActionCornersService.LOCATIONS;
+
+ switch (location) {
+ case ViewportActionCornersLocations.topLeft:
+ return { align: 'start', side: 'bottom' };
+ case ViewportActionCornersLocations.topRight:
+ return { align: 'end', side: 'bottom' };
+ case ViewportActionCornersLocations.bottomLeft:
+ return { align: 'start', side: 'top' };
+ case ViewportActionCornersLocations.bottomRight:
+ return { align: 'end', side: 'top' };
+ default:
+ console.debug('Unknown location, defaulting to bottom-start');
+ return { align: 'start', side: 'bottom' };
+ }
+};
diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx
new file mode 100644
index 000000000..5933a82cd
--- /dev/null
+++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx
@@ -0,0 +1,11 @@
+import React, { ReactNode } from 'react';
+import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper';
+
+export function getViewportDataOverlaySettingsMenu(
+ props: withAppTypes<{
+ viewportId: string;
+ element: HTMLElement;
+ }>
+): ReactNode {
+ return
;
+}
diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx b/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx
index 522022965..3810d5467 100644
--- a/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx
+++ b/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx
@@ -1,10 +1,15 @@
-import React, { useEffect, useCallback, useState, ReactElement } from 'react';
+import React, { useEffect, useCallback, useState, ReactElement, useMemo } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import { PanelSection, WindowLevel } from '@ohif/ui';
-import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
-import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core';
-import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
+import { Enums, eventTarget } from '@cornerstonejs/core';
+import { useActiveViewportDisplaySets } from '@ohif/core';
+import {
+ getNodeOpacity,
+ isPetVolumeWithDefaultOpacity,
+ isVolumeWithConstantOpacity,
+ getWindowLevelsData,
+} from './utils';
const { Events } = Enums;
@@ -16,19 +21,16 @@ const ViewportWindowLevel = ({
}>): ReactElement => {
const { cornerstoneViewportService } = servicesManager.services;
const [windowLevels, setWindowLevels] = useState([]);
- const [cachedHistograms, setCachedHistograms] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const displaySets = useActiveViewportDisplaySets({ servicesManager });
- /**
- * Looks for all viewports that has exactly all volumeIds passed as parameter.
- */
const getViewportsWithVolumeIds = useCallback(
(volumeIds: string[]) => {
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewports = renderingEngine.getVolumeViewports();
return viewports.filter(vp => {
- const viewportVolumeIds = vp.getActors().map(actor => actor.uid);
-
+ const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId);
return (
volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
@@ -38,97 +40,10 @@ const ViewportWindowLevel = ({
[cornerstoneViewportService]
);
- const getNodeOpacity = (volumeActor, nodeIndex) => {
- const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
- const nodeValue = [];
-
- volumeOpacity.getNodeValue(nodeIndex, nodeValue);
-
- return nodeValue[1];
- };
-
- /**
- * Checks if the opacity applied to the PET volume is something like
- * [{x: 0, y: 0}, {x: 0.1, y: [C]}, {x: [ANY], y: [C]}] where C is a
- * constant opacity value for all x's greater than 0.1
- */
- const isPetVolumeWithDefaultOpacity = (volumeId, volumeActor) => {
- const volume = cs3DCache.getVolume(volumeId);
-
- if (!volume) {
- return false;
- }
-
- const modality = volume.metadata.Modality;
-
- if (modality !== 'PT') {
- return false;
- }
-
- const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
-
- // It must have at least two points (0 and 0.1)
- if (volumeOpacity.getSize() < 2) {
- return false;
- }
-
- const node1Value = [];
- const node2Value = [];
-
- volumeOpacity.getNodeValue(0, node1Value);
- volumeOpacity.getNodeValue(1, node2Value);
-
- // First node must be (x:0, y:0} and the second one {x:0.1, y:any}
- if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
- return false;
- }
-
- const expectedOpacity = node2Value[1];
- const opacitySize = volumeOpacity.getSize();
- const currentNodeValue = [];
-
- // Any point after 0.1 must have the same opacity
- for (let i = 2; i < opacitySize; i++) {
- volumeOpacity.getNodeValue(i, currentNodeValue);
-
- if (currentNodeValue[1] !== expectedOpacity) {
- return false;
- }
- }
-
- return true;
- };
-
- /**
- * Checks if the opacity function has a constance opacity value for all x's
- */
- const isVolumeWithConstantOpacity = volumeActor => {
- const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
- const opacitySize = volumeOpacity.getSize();
- const firstNodeValue = [];
-
- volumeOpacity.getNodeValue(0, firstNodeValue);
-
- const firstNodeOpacity = firstNodeValue[1];
-
- for (let i = 0; i < opacitySize; i++) {
- const currentNodeValue = [];
-
- volumeOpacity.getNodeValue(0, currentNodeValue);
-
- if (currentNodeValue[1] !== firstNodeOpacity) {
- return false;
- }
- }
-
- return true;
- };
-
const getVolumeOpacity = useCallback((viewport, volumeId) => {
- const volumeActor = viewport.getActor(volumeId).actor;
+ const volumeActor = viewport.getActors().find(actor => actor.referencedId === volumeId)?.actor;
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
- // Get the opacity from the second node at 0.1
return getNodeOpacity(volumeActor, 1);
} else if (isVolumeWithConstantOpacity(volumeActor)) {
return getNodeOpacity(volumeActor, 0);
@@ -137,85 +52,14 @@ const ViewportWindowLevel = ({
return undefined;
}, []);
- const getWindowLevelsData = useCallback(
- async (viewportId: number) => {
- const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
-
- if (!viewport) {
- return [];
- }
-
- const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
-
- const volumeIds = viewport.getActors().map(actor => actor.uid);
- const viewportProperties = viewport.getProperties();
- const { voiRange } = viewportProperties;
- const viewportVoi = voiRange
- ? {
- windowWidth: voiRange.upper - voiRange.lower,
- windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2,
- }
- : undefined;
-
- const windowLevels = await Promise.all(
- volumeIds.map(async (volumeId, volumeIndex) => {
- const volume = cs3DCache.getVolume(volumeId);
-
- if (!volume) {
- return null;
- }
-
- const opacity = getVolumeOpacity(viewport, volumeId);
- const { metadata, scaling } = volume;
- const modality = metadata.Modality;
-
- // TODO: find a proper way to fix the histogram
- const options = {
- min: modality === 'PT' ? 0.1 : -999,
- max: modality === 'PT' ? 5 : 10000,
- };
-
- const histogram =
- cachedHistograms[volumeId] ??
- (await getViewportVolumeHistogram(viewport, volume, options));
- const { voi: displaySetVOI, colormap: displaySetColormap } =
- viewportInfo.displaySetOptions[volumeIndex];
- let colormap;
- if (displaySetColormap) {
- colormap =
- csUtils.colormap.getColormap(displaySetColormap.name) ??
- vtkColorMaps.getPresetByName(displaySetColormap.name);
- }
-
- const voi = !volumeIndex ? viewportVoi ?? displaySetVOI : displaySetVOI;
-
- return {
- viewportId,
- modality,
- volumeId,
- volumeIndex,
- voi,
- histogram,
- colormap,
- step: scaling?.PT ? 0.05 : 1,
- opacity,
- showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
- };
- })
- );
-
- const data = windowLevels.filter(Boolean);
- return data;
- },
-
- [cachedHistograms, cornerstoneViewportService, getVolumeOpacity]
- );
-
const updateViewportHistograms = useCallback(() => {
- getWindowLevelsData(viewportId).then(windowLevels => {
- setWindowLevels(windowLevels);
+ const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
+ const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
+
+ getWindowLevelsData(viewport, viewportInfo, getVolumeOpacity).then(data => {
+ setWindowLevels(data);
});
- }, [viewportId, getWindowLevelsData]);
+ }, [viewportId, cornerstoneViewportService, getVolumeOpacity]);
const handleCornerstoneVOIModified = useCallback(
e => {
@@ -252,8 +96,8 @@ const ViewportWindowLevel = ({
[windowLevels]
);
- const debouncedHandleCornerstoneVOIModified = useCallback(
- debounce(handleCornerstoneVOIModified, 100),
+ const debouncedHandleCornerstoneVOIModified = useMemo(
+ () => debounce(handleCornerstoneVOIModified, 100),
[handleCornerstoneVOIModified]
);
@@ -280,7 +124,7 @@ const ViewportWindowLevel = ({
return;
}
- const viewportVolumeIds = viewport.getActors().map(actor => actor.uid);
+ const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId);
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => {
@@ -291,49 +135,50 @@ const ViewportWindowLevel = ({
[getViewportsWithVolumeIds, cornerstoneViewportService]
);
- // Listen to windowLevels changes and caches all the new ones
+ // New function to handle image volume loading completion
+ const handleImageVolumeLoadingCompleted = useCallback(() => {
+ setIsLoading(false);
+ updateViewportHistograms();
+ }, [updateViewportHistograms]);
+
+ // Listen to cornerstone events and set up interval for histogram updates
useEffect(() => {
- const newVolumeHistograms = windowLevels
- .filter(windowLevel => !cachedHistograms[windowLevel.volumeId] && windowLevel.histogram)
- .reduce((volumeHistograms, windowLevel) => {
- // If the histogram exists, add it to the volumeHistograms object
- if (windowLevel?.histogram) {
- volumeHistograms[windowLevel.volumeId] = windowLevel.histogram;
- }
- return volumeHistograms;
- }, {});
-
- if (Object.keys(newVolumeHistograms).length) {
- setCachedHistograms(prev => ({ ...prev, ...newVolumeHistograms }));
- }
- }, [windowLevels, cachedHistograms]);
-
- // Updates the histogram when the viewport index prop has changed
- useEffect(() => updateViewportHistograms(), [viewportId, updateViewportHistograms]);
-
- // Listen to cornerstone events on "eventTarget" and at the document level
- useEffect(() => {
- eventTarget.addEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, updateViewportHistograms);
-
document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true);
+ eventTarget.addEventListener(
+ Events.IMAGE_VOLUME_LOADING_COMPLETED,
+ handleImageVolumeLoadingCompleted
+ );
+
+ const intervalId = setInterval(() => {
+ if (isLoading) {
+ updateViewportHistograms();
+ }
+ }, 1000);
return () => {
- eventTarget.removeEventListener(
- Events.IMAGE_VOLUME_LOADING_COMPLETED,
- updateViewportHistograms
- );
-
document.removeEventListener(
Events.VOI_MODIFIED,
debouncedHandleCornerstoneVOIModified,
true
);
+ eventTarget.removeEventListener(
+ Events.IMAGE_VOLUME_LOADING_COMPLETED,
+ handleImageVolumeLoadingCompleted
+ );
+ clearInterval(intervalId);
};
- }, [updateViewportHistograms, debouncedHandleCornerstoneVOIModified]);
+ }, [
+ updateViewportHistograms,
+ debouncedHandleCornerstoneVOIModified,
+ handleImageVolumeLoadingCompleted,
+ isLoading,
+ ]);
+
+ // Create a memoized version of displaySet IDs for comparison
+ const displaySetIds = useMemo(() => {
+ return displaySets?.map(ds => ds.displaySetInstanceUID).sort() || [];
+ }, [displaySets]);
- // Updates the viewport when the context of the viewport has changed. This is
- // necessary when moving across different stages because the viewport index
- // may not change but the volumes loaded on it may change.
useEffect(() => {
const { unsubscribe } = cornerstoneViewportService.subscribe(
cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED,
@@ -344,10 +189,15 @@ const ViewportWindowLevel = ({
}
);
+ // Only update if displaySets actually changed and are loaded
+ if (displaySetIds.length && !isLoading) {
+ updateViewportHistograms();
+ }
+
return () => {
unsubscribe();
};
- }, [viewportId, cornerstoneViewportService, updateViewportHistograms]);
+ }, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
return (
diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts b/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts
index 167d49f8a..f3911e42b 100644
--- a/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts
+++ b/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts
@@ -20,10 +20,6 @@ const workerFn = () => {
const getViewportVolumeHistogram = async (viewport, volume, options?) => {
workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions);
- if (!volume?.loadStatus.loaded) {
- return undefined;
- }
-
const volumeImageData = viewport.getImageData(volume.volumeId);
if (!volumeImageData) {
@@ -32,12 +28,15 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
let scalarData = volume.scalarData;
- let prevTimePoint;
if (volume.numTimePoints > 1) {
- prevTimePoint = volume.timePointIndex;
- const middleTimePoint = Math.round(volume.numTimePoints / 2);
- volume.timePointIndex = middleTimePoint;
- scalarData = volume.getScalarData(middleTimePoint);
+ const targetTimePoint = volume.numTimePoints - 1; // or any other time point you need
+ scalarData = volume.voxelManager.getTimePointScalarData(targetTimePoint);
+ } else {
+ scalarData = volume.voxelManager.getCompleteScalarDataArray();
+ }
+
+ if (!scalarData?.length) {
+ return undefined;
}
const { dimensions, origin, direction, spacing } = volume;
@@ -50,11 +49,12 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
scalarData,
});
- // after we calculate the range let's reset the timePointIndex
- if (volume.numTimePoints > 1) {
- volume.timePointIndex = prevTimePoint;
- }
const { minimum: min, maximum: max } = range;
+
+ if (min === Infinity || max === -Infinity) {
+ return undefined;
+ }
+
const calcHistOptions = {
numBins: 256,
min: Math.max(min, options?.min ?? min),
diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/utils.ts b/extensions/cornerstone/src/components/ViewportWindowLevel/utils.ts
new file mode 100644
index 000000000..8dcbf509c
--- /dev/null
+++ b/extensions/cornerstone/src/components/ViewportWindowLevel/utils.ts
@@ -0,0 +1,153 @@
+import { cache as cs3DCache, Types } from '@cornerstonejs/core';
+import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
+import { utilities as csUtils } from '@cornerstonejs/core';
+import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
+
+/**
+ * Gets node opacity from volume actor
+ */
+export const getNodeOpacity = (volumeActor, nodeIndex) => {
+ const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
+ const nodeValue = [];
+
+ volumeOpacity.getNodeValue(nodeIndex, nodeValue);
+
+ return nodeValue[1];
+};
+
+/**
+ * Checks if the opacity applied to the PET volume follows a specific pattern
+ */
+export const isPetVolumeWithDefaultOpacity = (volumeId: string, volumeActor) => {
+ const volume = cs3DCache.getVolume(volumeId);
+
+ if (!volume || volume.metadata.Modality !== 'PT') {
+ return false;
+ }
+
+ const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
+
+ if (volumeOpacity.getSize() < 2) {
+ return false;
+ }
+
+ const node1Value = [];
+ const node2Value = [];
+
+ volumeOpacity.getNodeValue(0, node1Value);
+ volumeOpacity.getNodeValue(1, node2Value);
+
+ if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
+ return false;
+ }
+
+ const expectedOpacity = node2Value[1];
+ const opacitySize = volumeOpacity.getSize();
+ const currentNodeValue = [];
+
+ for (let i = 2; i < opacitySize; i++) {
+ volumeOpacity.getNodeValue(i, currentNodeValue);
+ if (currentNodeValue[1] !== expectedOpacity) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+/**
+ * Checks if volume has constant opacity
+ */
+export const isVolumeWithConstantOpacity = volumeActor => {
+ const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
+ const opacitySize = volumeOpacity.getSize();
+ const firstNodeValue = [];
+
+ volumeOpacity.getNodeValue(0, firstNodeValue);
+ const firstNodeOpacity = firstNodeValue[1];
+
+ for (let i = 0; i < opacitySize; i++) {
+ const currentNodeValue = [];
+ volumeOpacity.getNodeValue(0, currentNodeValue);
+ if (currentNodeValue[1] !== firstNodeOpacity) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+/**
+ * Gets window levels data for a viewport
+ */
+export const getWindowLevelsData = async (
+ viewport: Types.IStackViewport | Types.IVolumeViewport,
+ viewportInfo: any,
+ getVolumeOpacity: (viewport: any, volumeId: string) => number | undefined
+) => {
+ if (!viewport) {
+ return [];
+ }
+
+ const volumeIds = (viewport as Types.IBaseVolumeViewport).getAllVolumeIds();
+ const viewportProperties = viewport.getProperties();
+ const { voiRange } = viewportProperties;
+ const viewportVoi = voiRange
+ ? {
+ windowWidth: voiRange.upper - voiRange.lower,
+ windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2,
+ }
+ : undefined;
+
+ const windowLevels = await Promise.all(
+ volumeIds.map(async (volumeId, volumeIndex) => {
+ const volume = cs3DCache.getVolume(volumeId);
+
+ const opacity = getVolumeOpacity(viewport, volumeId);
+ const { metadata, scaling } = volume;
+ const modality = metadata.Modality;
+
+ const options = {
+ min: modality === 'PT' ? 0.1 : -999,
+ max: modality === 'PT' ? 5 : 10000,
+ };
+
+ const histogram = await getViewportVolumeHistogram(viewport, volume, options);
+
+ if (!histogram || histogram.range.min === histogram.range.max) {
+ return null;
+ }
+
+ if (!viewportInfo.displaySetOptions || !viewportInfo.displaySetOptions[volumeIndex]) {
+ return null;
+ }
+
+ const { voi: displaySetVOI, colormap: displaySetColormap } =
+ viewportInfo.displaySetOptions[volumeIndex];
+
+ let colormap;
+ if (displaySetColormap) {
+ colormap =
+ csUtils.colormap.getColormap(displaySetColormap.name) ??
+ vtkColorMaps.getPresetByName(displaySetColormap.name);
+ }
+
+ const voi = !volumeIndex ? (viewportVoi ?? displaySetVOI) : displaySetVOI;
+
+ return {
+ viewportId: viewportInfo.viewportId,
+ modality,
+ volumeId,
+ volumeIndex,
+ voi,
+ histogram,
+ colormap,
+ step: scaling?.PT ? 0.05 : 1,
+ opacity,
+ showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
+ };
+ })
+ );
+
+ return windowLevels.filter(Boolean);
+};
diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx
index 51764c956..93c2ec481 100644
--- a/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx
+++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx
@@ -1,6 +1,6 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui';
-import { StackViewport } from '@cornerstonejs/core';
+import { StackViewport, Types } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap';
export function Colormap({
@@ -54,8 +54,10 @@ export function Colormap({
return colormap;
}
const actorEntries = viewport.getActors();
- const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
- const { colormap } = viewport.getProperties(actorEntry.uid);
+ const actorEntry = actorEntries?.find(entry =>
+ entry.referencedId.includes(displaySetInstanceUID)
+ );
+ const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId);
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx
index 55b4d53b9..c4196f9d7 100644
--- a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx
+++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx
@@ -15,12 +15,12 @@ import { ViewportPreset } from '../../types/ViewportPresets';
import { VolumeViewport3D } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core';
+export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
+
export type WindowLevelActionMenuProps = {
viewportId: string;
element: HTMLElement;
presets: Array>>;
- verticalDirection: AllInOneMenu.VerticalDirection;
- horizontalDirection: AllInOneMenu.HorizontalDirection;
colorbarProperties: ColorbarProperties;
displaySets: Array;
volumeRenderingPresets: Array;
@@ -53,8 +53,6 @@ export function WindowLevelActionMenu({
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
- const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
-
const { t } = useTranslation('WindowLevelActionMenu');
const [viewportGrid] = useViewportGrid();
@@ -119,7 +117,7 @@ export function WindowLevelActionMenu({
horizontalDirection={horizontalDirection}
iconClassName={classNames(
// Visible on hover and for the active viewport
- activeViewportId === viewportId ? 'visible' : 'invisible group-hover:visible',
+ activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible',
'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light',
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60'
)}
@@ -133,7 +131,7 @@ export function WindowLevelActionMenu({
{!is3DVolume && (
!nonImageModalities.includes(ds.Modality))}
+ displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager}
servicesManager={servicesManager}
colorbarProperties={colorbarProperties}
@@ -149,7 +147,7 @@ export function WindowLevelActionMenu({
!nonImageModalities.includes(ds.Modality))}
+ displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager}
servicesManager={servicesManager}
/>
diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx
index c18176a58..d8d2cbda8 100644
--- a/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx
+++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx
@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
-import { WindowLevelActionMenu } from './WindowLevelActionMenu';
+import { nonWLModalities, WindowLevelActionMenu } from './WindowLevelActionMenu';
export function getWindowLevelActionMenu({
viewportId,
@@ -9,7 +9,11 @@ export function getWindowLevelActionMenu({
commandsManager,
verticalDirection,
horizontalDirection,
-}: withAppTypes): ReactNode {
+}: withAppTypes<{
+ viewportId: string;
+ element: HTMLElement;
+ displaySets: AppTypes.DisplaySet[];
+}>): ReactNode {
const { customizationService } = servicesManager.services;
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
@@ -24,6 +28,14 @@ export function getWindowLevelActionMenu({
return { [displaySet.Modality]: presets[displaySet.Modality] };
});
+ const modalities = displaySets
+ .map(displaySet => displaySet.Modality)
+ .filter(modality => !nonWLModalities.includes(modality));
+
+ if (modalities.length === 0) {
+ return null;
+ }
+
return (
{
switch (action.type) {
- case 'SET_ACTION_COMPONENT': {
- const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
+ case 'ADD_ACTION_COMPONENT': {
+ const { viewportId, id, component, location, indexPriority } = action.payload;
// Get the components at the specified location of the specified viewport.
let locationComponents = state?.[viewportId]?.[location]
? [...state[viewportId][location]]
@@ -56,27 +56,48 @@ export function ViewportActionCornersProvider({ children, service }) {
// Insert the component from the payload but
// do not insert an undefined or null component.
if (component) {
- const insertionIndex = locationComponents.findIndex(
- component => indexPriority <= component.indexPriority
- );
+ let insertionIndex;
+ const isRightSide =
+ location === ViewportActionCornersLocations.topRight ||
+ location === ViewportActionCornersLocations.bottomRight;
+
+ if (indexPriority === undefined) {
+ // If no indexPriority is provided, add it to the appropriate end
+ insertionIndex = isRightSide ? 0 : locationComponents.length;
+ } else {
+ if (isRightSide) {
+ insertionIndex = locationComponents.findIndex(
+ component => indexPriority > component.indexPriority
+ );
+ } else {
+ insertionIndex = locationComponents.findIndex(
+ component => indexPriority <= component.indexPriority
+ );
+ }
+ if (insertionIndex === -1) {
+ // If no suitable position found, add to the appropriate end
+ insertionIndex = isRightSide ? 0 : locationComponents.length;
+ }
+ }
+
+ const defaultPriority = isRightSide ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
+
locationComponents = [
...locationComponents.slice(0, insertionIndex),
{
id,
component,
- indexPriority,
+ indexPriority: indexPriority ?? defaultPriority,
},
- ...locationComponents.slice(insertionIndex + 1),
+ ...locationComponents.slice(insertionIndex),
];
}
return {
...state,
- ...{
- [viewportId]: {
- ...state[viewportId],
- [location]: locationComponents,
- },
+ [viewportId]: {
+ ...state[viewportId],
+ [location]: locationComponents,
},
};
}
@@ -100,17 +121,17 @@ export function ViewportActionCornersProvider({ children, service }) {
return viewportActionCornersState;
}, [viewportActionCornersState]);
- const setComponent = useCallback(
+ const addComponent = useCallback(
(actionComponentInfo: ActionComponentInfo) => {
- dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo });
+ dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo });
},
[dispatch]
);
- const setComponents = useCallback(
+ const addComponents = useCallback(
(actionComponentInfos: Array) => {
actionComponentInfos.forEach(actionComponentInfo =>
- dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo })
+ dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo })
);
},
[dispatch]
@@ -120,28 +141,28 @@ export function ViewportActionCornersProvider({ children, service }) {
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
[dispatch]
);
+
useEffect(() => {
if (service) {
service.setServiceImplementation({
getState,
- setComponent,
- setComponents,
+ addComponent,
+ addComponents,
clear,
});
}
- }, [getState, service, setComponent, setComponents]);
+ }, [getState, service, addComponent, addComponents, clear]);
- // run many of the calls through the service itself since we want to publish events
- const api = {
+ const viewportCornerActions = {
getState,
- setComponent: props => service.setComponent(props),
- setComponents: props => service.setComponents(props),
+ addComponent: props => service.addComponent(props),
+ addComponents: props => service.addComponents(props),
clear: props => service.clear(props),
};
const contextValue = useMemo(
- () => [viewportActionCornersState, api],
- [viewportActionCornersState, api]
+ () => [viewportActionCornersState, viewportCornerActions],
+ [viewportActionCornersState, viewportCornerActions]
);
return (
diff --git a/extensions/cornerstone/src/getCustomizationModule.ts b/extensions/cornerstone/src/getCustomizationModule.ts
index 02cc7a0f7..cf00156fe 100644
--- a/extensions/cornerstone/src/getCustomizationModule.ts
+++ b/extensions/cornerstone/src/getCustomizationModule.ts
@@ -1,6 +1,5 @@
import { Enums } from '@cornerstonejs/tools';
import { toolNames } from './initCornerstoneTools';
-import DicomUpload from './components/DicomUpload/DicomUpload';
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
import { colormaps } from './utils/colormaps';
import { CONSTANTS } from '@cornerstonejs/core';
@@ -23,10 +22,12 @@ const tools = {
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
- { toolName: toolNames.StackScrollMouseWheel, bindings: [] },
+ {
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
+ },
],
enabled: [
- { toolName: toolNames.SegmentationDisplay },
{
toolName: toolNames.PlanarFreehandContourSegmentation,
configuration: {
@@ -38,13 +39,6 @@ const tools = {
function getCustomizationModule() {
return [
- {
- name: 'cornerstoneDicomUploadComponent',
- value: {
- id: 'dicomUploadComponent',
- component: DicomUpload,
- },
- },
{
name: 'default',
value: [
@@ -120,7 +114,7 @@ function getCustomizationModule() {
type: 'value',
},
{
- value: 'areaUnit',
+ value: 'areaUnits',
for: ['area'],
type: 'unit',
},
@@ -140,7 +134,7 @@ function getCustomizationModule() {
},
{
displayName: 'Unit',
- value: 'areaUnit',
+ value: 'areaUnits',
type: 'value',
},
],
@@ -170,12 +164,12 @@ function getCustomizationModule() {
type: 'value',
},
{
- value: 'modalityUnit',
+ value: 'pixelValueUnits',
for: ['mean', 'max' /** 'stdDev **/],
type: 'unit',
},
{
- value: 'areaUnit',
+ value: 'areaUnits',
for: ['area'],
type: 'unit',
},
diff --git a/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx b/extensions/cornerstone/src/getPanelModule.tsx
similarity index 68%
rename from extensions/cornerstone-dicom-seg/src/getPanelModule.tsx
rename to extensions/cornerstone/src/getPanelModule.tsx
index 1a135e152..48cbd6eaa 100644
--- a/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx
+++ b/extensions/cornerstone/src/getPanelModule.tsx
@@ -1,15 +1,11 @@
import React from 'react';
-import { useAppConfig } from '@state';
import { Toolbox } from '@ohif/ui-next';
import PanelSegmentation from './panels/PanelSegmentation';
+import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
- const { customizationService } = servicesManager.services;
-
const wrappedPanelSegmentation = ({ configuration }) => {
- const [appConfig] = useAppConfig();
-
return (
+ );
+ };
+
+ const wrappedPanelSegmentationNoHeader = ({ configuration }) => {
+ return (
+
);
};
const wrappedPanelSegmentationWithTools = ({ configuration }) => {
- const [appConfig] = useAppConfig();
-
return (
<>
>
@@ -54,6 +57,12 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
};
return [
+ {
+ name: 'activeViewportWindowLevel',
+ component: () => {
+ return ;
+ },
+ },
{
name: 'panelSegmentation',
iconName: 'tab-segmentation',
@@ -61,6 +70,13 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
label: 'Segmentation',
component: wrappedPanelSegmentation,
},
+ {
+ name: 'panelSegmentationNoHeader',
+ iconName: 'tab-segmentation',
+ iconLabel: 'Segmentation',
+ label: 'Segmentation',
+ component: wrappedPanelSegmentationNoHeader,
+ },
{
name: 'panelSegmentationWithTools',
iconName: 'tab-segmentation',
diff --git a/extensions/cornerstone/src/getSopClassHandlerModule.js b/extensions/cornerstone/src/getSopClassHandlerModule.js
index e3b5fae21..f88dc66ae 100644
--- a/extensions/cornerstone/src/getSopClassHandlerModule.js
+++ b/extensions/cornerstone/src/getSopClassHandlerModule.js
@@ -89,7 +89,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
const displaySet = {
plugin: 'microscopy',
Modality: 'SM',
- viewportType: csEnums.ViewportType.WholeSlide,
+ viewportType: csEnums.ViewportType.WHOLE_SLIDE,
altImageText: 'Microscopy',
displaySetInstanceUID: utils.guid(),
SOPInstanceUID,
diff --git a/extensions/cornerstone/src/getToolbarModule.tsx b/extensions/cornerstone/src/getToolbarModule.tsx
index 6c99065eb..e94eb895f 100644
--- a/extensions/cornerstone/src/getToolbarModule.tsx
+++ b/extensions/cornerstone/src/getToolbarModule.tsx
@@ -6,6 +6,12 @@ const getToggledClassName = (isToggled: boolean) => {
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
};
+const getDisabledState = (disabledText?: string) => ({
+ disabled: true,
+ className: '!text-common-bright ohif-disabled',
+ disabledText: disabledText ?? 'Not available on the current viewport',
+});
+
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
const {
toolGroupService,
@@ -20,6 +26,52 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
return [
// functions/helpers to be used by the toolbar buttons to decide if they should
// enabled or not
+ {
+ name: 'evaluate.viewport.supported',
+ evaluate: ({ viewportId, unsupportedViewportTypes, disabledText }) => {
+ const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
+
+ if (viewport && unsupportedViewportTypes?.includes(viewport.type)) {
+ return getDisabledState(disabledText);
+ }
+
+ return undefined;
+ },
+ },
+ {
+ name: 'evaluate.modality.supported',
+ evaluate: ({ viewportId, unsupportedModalities, supportedModalities, disabledText }) => {
+ const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
+
+ if (!displaySetUIDs?.length) {
+ return;
+ }
+
+ const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
+
+ // Check for unsupported modalities (exclusion)
+ if (unsupportedModalities?.length) {
+ const hasUnsupportedModality = displaySets.some(displaySet =>
+ unsupportedModalities.includes(displaySet?.Modality)
+ );
+
+ if (hasUnsupportedModality) {
+ return getDisabledState(disabledText);
+ }
+ }
+
+ // Check for supported modalities (inclusion)
+ if (supportedModalities?.length) {
+ const hasAnySupportedModality = displaySets.some(displaySet =>
+ supportedModalities.includes(displaySet?.Modality)
+ );
+
+ if (!hasAnySupportedModality) {
+ return getDisabledState(disabledText || 'Tool not available for this modality');
+ }
+ }
+ },
+ },
{
name: 'evaluate.cornerstoneTool',
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
@@ -32,11 +84,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) {
- return {
- disabled: true,
- className: '!text-common-bright ohif-disabled',
- disabledText: disabledText ?? 'Not available on the current viewport',
- };
+ return getDisabledState(disabledText);
}
const isPrimaryActive = toolNames
@@ -172,40 +220,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
};
},
},
- {
- name: 'evaluate.not3D',
- evaluate: ({ viewportId, disabledText }) => {
- const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
-
- if (viewport?.type === 'volume3d') {
- return {
- disabled: true,
- className: '!text-common-bright ohif-disabled',
- disabledText: disabledText ?? 'Not available on the current viewport',
- };
- }
- },
- },
- {
- name: 'evaluate.isUS',
- evaluate: ({ viewportId, disabledText }) => {
- const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
-
- if (!displaySetUIDs?.length) {
- return;
- }
-
- const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
- const isUS = displaySets.some(displaySet => displaySet?.Modality === 'US');
- if (!isUS) {
- return {
- disabled: true,
- className: '!text-common-bright ohif-disabled',
- disabledText: disabledText ?? 'Not available on the current viewport',
- };
- }
- },
- },
{
name: 'evaluate.viewportProperties.toggle',
evaluate: ({ viewportId, button }) => {
@@ -254,11 +268,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
});
if (!areReconstructable) {
- return {
- disabled: true,
- className: '!text-common-bright ohif-disabled',
- disabledText: disabledText ?? 'Not available on the current viewport',
- };
+ return getDisabledState(disabledText);
}
const isMpr = protocol?.id === 'mpr';
@@ -288,11 +298,7 @@ function _evaluateToggle({
const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup.hasTool(toolName)) {
- return {
- disabled: true,
- className: '!text-common-bright ohif-disabled',
- disabledText: disabledText ?? 'Not available on the current viewport',
- };
+ return getDisabledState(disabledText);
}
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
diff --git a/extensions/cornerstone/src/hooks/useActiveViewportSegmentationRepresentations.ts b/extensions/cornerstone/src/hooks/useActiveViewportSegmentationRepresentations.ts
new file mode 100644
index 000000000..0f1c26a40
--- /dev/null
+++ b/extensions/cornerstone/src/hooks/useActiveViewportSegmentationRepresentations.ts
@@ -0,0 +1,219 @@
+import { useState, useEffect } from 'react';
+import debounce from 'lodash.debounce';
+import { roundNumber } from '@ohif/core/src/utils';
+import {
+ SegmentationData,
+ SegmentationRepresentation,
+} from '../services/SegmentationService/SegmentationService';
+
+const excludedModalities = ['SM', 'OT', 'DOC', 'ECG'];
+
+function mapSegmentationToDisplay(segmentation, customizationService) {
+ const { label, segments } = segmentation;
+
+ // Get the readable text mapping once
+ const { readableText: readableTextMap } = customizationService.getCustomization(
+ 'PanelSegmentation.readableText',
+ {}
+ );
+
+ // Helper function to recursively map cachedStats to readable display text
+ function mapStatsToDisplay(stats, indent = 0) {
+ const primary = [];
+ const indentation = ' '.repeat(indent);
+
+ for (const key in stats) {
+ if (Object.prototype.hasOwnProperty.call(stats, key)) {
+ const value = stats[key];
+ const readableText = readableTextMap?.[key];
+
+ if (!readableText) {
+ continue;
+ }
+
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+ // Add empty row before category (except for the first category)
+ if (primary.length > 0) {
+ primary.push('');
+ }
+ // Add category title
+ primary.push(`${indentation}${readableText}`);
+ // Recursively handle nested objects
+ primary.push(...mapStatsToDisplay(value, indent + 1));
+ } else {
+ // For non-nested values, don't add empty rows
+ primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
+ }
+ }
+ }
+
+ return primary;
+ }
+
+ // Get customization for display text mapping
+ const displayTextMapper = segment => {
+ const defaultDisplay = {
+ primary: [],
+ secondary: [],
+ };
+
+ // If the segment has cachedStats, map it to readable text
+ if (segment.cachedStats) {
+ const primary = mapStatsToDisplay(segment.cachedStats);
+ defaultDisplay.primary = primary;
+ }
+
+ return defaultDisplay;
+ };
+
+ const updatedSegments = {};
+
+ Object.entries(segments).forEach(([segmentIndex, segment]) => {
+ updatedSegments[segmentIndex] = {
+ ...segment,
+ displayText: displayTextMapper(segment),
+ };
+ });
+
+ // Map the segments and apply the display text mapper
+ return {
+ ...segmentation,
+ label,
+ segments: updatedSegments,
+ };
+}
+
+/**
+ * Represents the combination of segmentation data and its representation in a viewport.
+ */
+type ViewportSegmentationRepresentation = {
+ segmentationsWithRepresentations: {
+ representation: SegmentationRepresentation;
+ segmentation: SegmentationData;
+ }[];
+ disabled: boolean;
+};
+
+/**
+ * Custom hook that provides segmentation data and their representations for the active viewport.
+ * @param options - The options object.
+ * @param options.servicesManager - The services manager object.
+ * @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
+ * @param options.debounceTime - Debounce time in milliseconds for updates.
+ * @returns An array of segmentation data and their representations for the active viewport.
+ */
+export function useActiveViewportSegmentationRepresentations({
+ servicesManager,
+ subscribeToDataModified = false,
+ debounceTime = 0,
+}: withAppTypes<{ debounceTime?: number }>): ViewportSegmentationRepresentation {
+ const { segmentationService, viewportGridService, customizationService, displaySetService } =
+ servicesManager.services;
+
+ const [segmentationsWithRepresentations, setSegmentationsWithRepresentations] =
+ useState({
+ segmentationsWithRepresentations: [],
+ disabled: false,
+ });
+
+ useEffect(() => {
+ const update = () => {
+ const viewportId = viewportGridService.getActiveViewportId();
+ const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
+
+ if (!displaySetUIDs?.length) {
+ return;
+ }
+
+ const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
+
+ if (!displaySet) {
+ return;
+ }
+
+ if (excludedModalities.includes(displaySet.Modality)) {
+ setSegmentationsWithRepresentations(prev => ({
+ segmentationsWithRepresentations: [],
+ disabled: true,
+ }));
+ return;
+ }
+
+ const segmentations = segmentationService.getSegmentations();
+
+ if (!segmentations?.length) {
+ setSegmentationsWithRepresentations(prev => ({
+ segmentationsWithRepresentations: [],
+ disabled: false,
+ }));
+ return;
+ }
+
+ const representations = segmentationService.getSegmentationRepresentations(viewportId);
+
+ const newSegmentationsWithRepresentations = representations.map(representation => {
+ const segmentation = segmentationService.getSegmentation(representation.segmentationId);
+ const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService);
+ return {
+ representation,
+ segmentation: mappedSegmentation,
+ };
+ });
+
+ setSegmentationsWithRepresentations({
+ segmentationsWithRepresentations: newSegmentationsWithRepresentations,
+ disabled: false,
+ });
+ };
+
+ const debouncedUpdate =
+ debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
+
+ update();
+
+ const subscriptions = [
+ segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_MODIFIED,
+ debouncedUpdate
+ ),
+ segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_REMOVED,
+ debouncedUpdate
+ ),
+ segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
+ debouncedUpdate
+ ),
+ viewportGridService.subscribe(
+ viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
+ debouncedUpdate
+ ),
+ viewportGridService.subscribe(viewportGridService.EVENTS.GRID_STATE_CHANGED, debouncedUpdate),
+ ];
+
+ if (subscribeToDataModified) {
+ subscriptions.push(
+ segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
+ debouncedUpdate
+ )
+ );
+ }
+
+ return () => {
+ subscriptions.forEach(subscription => subscription.unsubscribe());
+ if (debounceTime > 0) {
+ debouncedUpdate.cancel();
+ }
+ };
+ }, [
+ segmentationService,
+ viewportGridService,
+ customizationService,
+ displaySetService,
+ debounceTime,
+ subscribeToDataModified,
+ ]);
+
+ return segmentationsWithRepresentations;
+}
diff --git a/extensions/cornerstone/src/hooks/useMeasurements.ts b/extensions/cornerstone/src/hooks/useMeasurements.ts
new file mode 100644
index 000000000..ba659a35a
--- /dev/null
+++ b/extensions/cornerstone/src/hooks/useMeasurements.ts
@@ -0,0 +1,103 @@
+import { useState, useEffect } from 'react';
+import debounce from 'lodash.debounce';
+
+function mapMeasurementToDisplay(measurement, displaySetService) {
+ const { referenceSeriesUID } = measurement;
+
+ const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID);
+
+ if (!displaySets[0]?.instances) {
+ throw new Error('The tracked measurements panel should only be tracking "stack" displaySets.');
+ }
+
+ const { findingSites, finding, label: baseLabel, displayText: baseDisplayText } = measurement;
+
+ const firstSite = findingSites?.[0];
+ const label = baseLabel || finding?.text || firstSite?.text || '(empty)';
+
+ // Initialize displayText with the structure used in Length.ts and CobbAngle.ts
+ const displayText = {
+ primary: [],
+ secondary: baseDisplayText?.secondary || [],
+ };
+
+ // Add baseDisplayText to primary if it exists
+ if (baseDisplayText) {
+ displayText.primary.push(...baseDisplayText.primary);
+ }
+
+ // Add finding sites to primary
+ if (findingSites) {
+ findingSites.forEach(site => {
+ if (site?.text && site.text !== label) {
+ displayText.primary.push(site.text);
+ }
+ });
+ }
+
+ // Add finding to primary if it's different from the label
+ if (finding && finding.text && finding.text !== label) {
+ displayText.primary.push(finding.text);
+ }
+
+ return {
+ ...measurement,
+ displayText,
+ label,
+ };
+}
+
+/**
+ * A custom hook that provides mapped measurements based on the given services and filters.
+ *
+ * @param {Object} servicesManager - The services manager object.
+ * @param {Object} options - The options for filtering and mapping measurements.
+ * @param {Function} options.measurementFilter - Optional function to filter measurements.
+ * @param {Object} options.valueTypes - The value types for mapping measurements.
+ * @returns {Array} An array of mapped and filtered measurements.
+ */
+export function useMeasurements(servicesManager, { measurementFilter }) {
+ const { measurementService, displaySetService } = servicesManager.services;
+ const [displayMeasurements, setDisplayMeasurements] = useState([]);
+
+ useEffect(() => {
+ const updateDisplayMeasurements = () => {
+ let measurements = measurementService.getMeasurements();
+ if (measurementFilter) {
+ measurements = measurements.filter(measurementFilter);
+ }
+ const mappedMeasurements = measurements.map(m =>
+ mapMeasurementToDisplay(m, displaySetService)
+ );
+ setDisplayMeasurements(prevMeasurements => {
+ if (JSON.stringify(prevMeasurements) !== JSON.stringify(mappedMeasurements)) {
+ return mappedMeasurements;
+ }
+ return prevMeasurements;
+ });
+ };
+
+ const debouncedUpdate = debounce(updateDisplayMeasurements, 100);
+
+ updateDisplayMeasurements();
+
+ const events = [
+ measurementService.EVENTS.MEASUREMENT_ADDED,
+ measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
+ measurementService.EVENTS.MEASUREMENT_UPDATED,
+ measurementService.EVENTS.MEASUREMENT_REMOVED,
+ measurementService.EVENTS.MEASUREMENTS_CLEARED,
+ ];
+
+ const subscriptions = events.map(
+ evt => measurementService.subscribe(evt, debouncedUpdate).unsubscribe
+ );
+
+ return () => {
+ subscriptions.forEach(unsub => unsub());
+ debouncedUpdate.cancel();
+ };
+ }, [measurementService, measurementFilter, displaySetService]);
+
+ return displayMeasurements;
+}
diff --git a/extensions/cornerstone/src/hooks/useSegmentations.ts b/extensions/cornerstone/src/hooks/useSegmentations.ts
new file mode 100644
index 000000000..835444c81
--- /dev/null
+++ b/extensions/cornerstone/src/hooks/useSegmentations.ts
@@ -0,0 +1,148 @@
+import { useState, useEffect } from 'react';
+import debounce from 'lodash.debounce';
+import { roundNumber } from '@ohif/core/src/utils';
+import { SegmentationData } from '../services/SegmentationService/SegmentationService';
+
+function mapSegmentationToDisplay(segmentation, customizationService) {
+ const { label, segments } = segmentation;
+
+ // Get the readable text mapping once
+ const { readableText: readableTextMap } = customizationService.getCustomization(
+ 'PanelSegmentation.readableText',
+ {}
+ );
+
+ // Helper function to recursively map cachedStats to readable display text
+ function mapStatsToDisplay(stats, indent = 0) {
+ const primary = [];
+ const indentation = ' '.repeat(indent);
+
+ for (const key in stats) {
+ if (Object.prototype.hasOwnProperty.call(stats, key)) {
+ const value = stats[key];
+ const readableText = readableTextMap?.[key];
+
+ if (!readableText) {
+ continue;
+ }
+
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+ // Add empty row before category (except for the first category)
+ if (primary.length > 0) {
+ primary.push('');
+ }
+ // Add category title
+ primary.push(`${indentation}${readableText}`);
+ // Recursively handle nested objects
+ primary.push(...mapStatsToDisplay(value, indent + 1));
+ } else {
+ // For non-nested values, don't add empty rows
+ primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
+ }
+ }
+ }
+
+ return primary;
+ }
+
+ // Get customization for display text mapping
+ const displayTextMapper = segment => {
+ const defaultDisplay = {
+ primary: [],
+ secondary: [],
+ };
+
+ // If the segment has cachedStats, map it to readable text
+ if (segment.cachedStats) {
+ const primary = mapStatsToDisplay(segment.cachedStats);
+ defaultDisplay.primary = primary;
+ }
+
+ return defaultDisplay;
+ };
+
+ const updatedSegments = {};
+
+ Object.entries(segments).forEach(([segmentIndex, segment]) => {
+ updatedSegments[segmentIndex] = {
+ ...segment,
+ displayText: displayTextMapper(segment),
+ };
+ });
+
+ // Map the segments and apply the display text mapper
+ return {
+ ...segmentation,
+ label,
+ segments: updatedSegments,
+ };
+}
+
+/**
+ * Custom hook that provides segmentation data.
+ * @param options - The options object.
+ * @param options.servicesManager - The services manager object.
+ * @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
+ * @param options.debounceTime - Debounce time in milliseconds for updates.
+ * @returns An array of segmentation data.
+ */
+export function useSegmentations({
+ servicesManager,
+ subscribeToDataModified = false,
+ debounceTime = 0,
+}: withAppTypes<{ debounceTime?: number }>): SegmentationData[] {
+ const { segmentationService, customizationService } = servicesManager.services;
+
+ const [segmentations, setSegmentations] = useState([]);
+
+ useEffect(() => {
+ const update = () => {
+ const segmentations = segmentationService.getSegmentations();
+
+ if (!segmentations?.length) {
+ setSegmentations([]);
+ return;
+ }
+
+ const mappedSegmentations = segmentations.map(segmentation =>
+ mapSegmentationToDisplay(segmentation, customizationService)
+ );
+
+ setSegmentations(mappedSegmentations);
+ };
+
+ const debouncedUpdate =
+ debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
+
+ update();
+
+ const subscriptions = [
+ segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_MODIFIED,
+ debouncedUpdate
+ ),
+ segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_REMOVED,
+ debouncedUpdate
+ ),
+ ];
+
+ if (subscribeToDataModified) {
+ subscriptions.push(
+ segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
+ debouncedUpdate
+ )
+ );
+ }
+
+ return () => {
+ subscriptions.forEach(subscription => subscription.unsubscribe());
+ if (debounceTime > 0) {
+ debouncedUpdate.cancel();
+ }
+ };
+ }, [segmentationService, customizationService, debounceTime, subscribeToDataModified]);
+
+ return segmentations;
+}
diff --git a/extensions/cornerstone/src/hps/mpr.ts b/extensions/cornerstone/src/hps/mpr.ts
index fa54ffde6..99a12e038 100644
--- a/extensions/cornerstone/src/hps/mpr.ts
+++ b/extensions/cornerstone/src/hps/mpr.ts
@@ -1,5 +1,25 @@
import { Types } from '@ohif/core';
+const VOI_SYNC_GROUP = {
+ type: 'voi',
+ id: 'mpr',
+ source: true,
+ target: true,
+ options: {
+ syncColormap: true,
+ },
+};
+
+const HYDRATE_SEG_SYNC_GROUP = {
+ type: 'hydrateseg',
+ id: 'sameFORId',
+ source: true,
+ target: true,
+ options: {
+ matchingRules: ['sameFOR'],
+ },
+};
+
export const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
name: 'MPR',
@@ -10,7 +30,6 @@ export const mpr: Types.HangingProtocol.Protocol = {
modifiedDate: '2023-08-15',
availableTo: {},
editableBy: {},
- // Unknown number of priors referenced - so just match any study
numberOfPriorsReferenced: 0,
protocolMatchingRules: [],
imageLoadStrategy: 'nth',
@@ -71,17 +90,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
initialImageOptions: {
preset: 'middle',
},
- syncGroups: [
- {
- type: 'voi',
- id: 'mpr',
- source: true,
- target: true,
- options: {
- syncColormap: true,
- },
- },
- ],
+ syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
},
displaySets: [
{
@@ -98,17 +107,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
initialImageOptions: {
preset: 'middle',
},
- syncGroups: [
- {
- type: 'voi',
- id: 'mpr',
- source: true,
- target: true,
- options: {
- syncColormap: true,
- },
- },
- ],
+ syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
},
displaySets: [
{
@@ -125,17 +124,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
initialImageOptions: {
preset: 'middle',
},
- syncGroups: [
- {
- type: 'voi',
- id: 'mpr',
- source: true,
- target: true,
- options: {
- syncColormap: true,
- },
- },
- ],
+ syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
},
displaySets: [
{
diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx
index bb6dc8bed..47d630c10 100644
--- a/extensions/cornerstone/src/index.tsx
+++ b/extensions/cornerstone/src/index.tsx
@@ -6,7 +6,6 @@ import {
imageLoadPoolManager,
imageRetrievalPoolManager,
} from '@cornerstonejs/core';
-import * as csStreamingImageVolumeLoader from '@cornerstonejs/streaming-image-volume-loader';
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
import { Types } from '@ohif/core';
import Enums from './enums';
@@ -33,17 +32,28 @@ import { id } from './id';
import { measurementMappingUtils } from './utils/measurementServiceMappings';
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
-import { showLabelAnnotationPopup } from './utils/callInputDialog';
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
-import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import { findNearbyToolData } from './utils/findNearbyToolData';
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
import { getSopClassHandlerModule } from './getSopClassHandlerModule';
+import { getDynamicVolumeInfo } from '@cornerstonejs/core/utilities';
+import {
+ useLutPresentationStore,
+ usePositionPresentationStore,
+ useSegmentationPresentationStore,
+ useSynchronizersStore,
+} from './stores';
+import { useToggleOneUpViewportGridStore } from '../../default/src/stores/useToggleOneUpViewportGridStore';
+import { useActiveViewportSegmentationRepresentations } from './hooks/useActiveViewportSegmentationRepresentations';
+import { useMeasurements } from './hooks/useMeasurements';
+import getPanelModule from './getPanelModule';
+import PanelSegmentation from './panels/PanelSegmentation';
+import PanelMeasurement from './panels/PanelMeasurement';
+import DicomUpload from './components/DicomUpload/DicomUpload';
+import { useSegmentations } from './hooks/useSegmentations';
-const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader;
-const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {};
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
const Component = React.lazy(() => {
@@ -84,9 +94,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
]);
toolbarService.registerEventForToolbarUpdate(segmentationService, [
- segmentationService.EVENTS.SEGMENTATION_ADDED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
- segmentationService.EVENTS.SEGMENTATION_UPDATED,
+ segmentationService.EVENTS.SEGMENTATION_MODIFIED,
]);
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
@@ -108,9 +117,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
// how to define them.
imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions);
},
-
+ getPanelModule,
onModeExit: ({ servicesManager }: withAppTypes): void => {
- const { cineService } = servicesManager.services;
+ const { cineService, segmentationService } = servicesManager.services;
// Empty out the image load and retrieval pools to prevent memory leaks
// on the mode exits
Object.values(cs3DEnums.RequestType).forEach(type => {
@@ -121,6 +130,13 @@ const cornerstoneExtension: Types.Extensions.Extension = {
cineService.setIsCineEnabled(false);
enabledElementReset();
+
+ useLutPresentationStore.getState().clearLutPresentationStore();
+ usePositionPresentationStore.getState().clearPositionPresentationStore();
+ useSynchronizersStore.getState().clearSynchronizersStore();
+ useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore();
+ useSegmentationPresentationStore.getState().clearSegmentationPresentationStore();
+ segmentationService.removeAllSegmentations();
},
/**
@@ -145,20 +161,12 @@ const cornerstoneExtension: Types.Extensions.Extension = {
const { syncGroupService } = servicesManager.services;
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
+ servicesManager.services.customizationService.setGlobalCustomization('dicomUploadComponent', {
+ component: props => ,
+ });
return init.call(this, props);
},
-
getToolbarModule,
- getPanelModule({ servicesManager }) {
- return [
- {
- name: 'activeViewportWindowLevel',
- component: () => {
- return ;
- },
- },
- ];
- },
getHangingProtocolModule,
getViewportModule({ servicesManager, commandsManager }) {
const ExtendedOHIFCornerstoneViewport = props => {
@@ -196,7 +204,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
},
getEnabledElement,
dicomLoaderService,
- showLabelAnnotationPopup,
},
},
{
@@ -235,6 +242,17 @@ export {
ImageOverlayViewerTool,
getSOPInstanceAttributes,
dicomLoaderService,
+ // Export all stores
+ useLutPresentationStore,
+ usePositionPresentationStore,
+ useSegmentationPresentationStore,
+ useSynchronizersStore,
Enums,
+ useMeasurements,
+ useActiveViewportSegmentationRepresentations,
+ useSegmentations,
+ PanelSegmentation,
+ PanelMeasurement,
+ DicomUpload,
};
export default cornerstoneExtension;
diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx
index df0dc07c5..4b9c13d54 100644
--- a/extensions/cornerstone/src/init.tsx
+++ b/extensions/cornerstone/src/init.tsx
@@ -1,4 +1,4 @@
-import OHIF, { Types, errorHandler } from '@ohif/core';
+import OHIF, { errorHandler } from '@ohif/core';
import React from 'react';
import * as cornerstone from '@cornerstonejs/core';
@@ -13,12 +13,13 @@ import {
getEnabledElement,
Settings,
utilities as csUtilities,
- Enums as csEnums,
} from '@cornerstonejs/core';
import {
cornerstoneStreamingImageVolumeLoader,
cornerstoneStreamingDynamicImageVolumeLoader,
-} from '@cornerstonejs/streaming-image-volume-loader';
+} from '@cornerstonejs/core/loaders';
+
+import RequestTypes from '@cornerstonejs/core/enums/RequestType';
import initWADOImageLoader from './initWADOImageLoader';
import initCornerstoneTools from './initCornerstoneTools';
@@ -33,6 +34,10 @@ import initContextMenu from './initContextMenu';
import initDoubleClick from './initDoubleClick';
import initViewTiming from './utils/initViewTiming';
import { colormaps } from './utils/colormaps';
+import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
+import { useLutPresentationStore } from './stores/useLutPresentationStore';
+import { usePositionPresentationStore } from './stores/usePositionPresentationStore';
+import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore';
const { registerColormap } = csUtilities.colormap;
@@ -46,27 +51,12 @@ export default async function init({
servicesManager,
commandsManager,
extensionManager,
- appConfig
-}: Types.Extensions.ExtensionParams): Promise {
+ appConfig,
+}: withAppTypes): Promise {
// Note: this should run first before initializing the cornerstone
// DO NOT CHANGE THE ORDER
- const value = appConfig.useSharedArrayBuffer;
- let sharedArrayBufferDisabled = false;
-
- if (value === 'AUTO') {
- cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.AUTO);
- } else if (value === 'FALSE' || value === false) {
- cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.FALSE);
- sharedArrayBufferDisabled = true;
- } else {
- cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.TRUE);
- }
await cs3DInit({
- rendering: {
- preferSizeOverAccuracy: Boolean(appConfig.preferSizeOverAccuracy),
- useNorm16Texture: Boolean(appConfig.useNorm16Texture),
- },
peerImport: appConfig.peerImport,
});
@@ -99,63 +89,39 @@ export default async function init({
cornerstoneViewportService,
hangingProtocolService,
viewportGridService,
- stateSyncService,
- studyPrefetcherService,
} = servicesManager.services;
window.services = servicesManager.services;
window.extensionManager = extensionManager;
window.commandsManager = commandsManager;
- if (
- appConfig.showWarningMessageForCrossOrigin &&
- !window.crossOriginIsolated &&
- !sharedArrayBufferDisabled
- ) {
- uiNotificationService.show({
- title: 'Cross Origin Isolation',
- message:
- 'Cross Origin Isolation is not enabled, read more about it here: https://docs.ohif.org/faq/',
- type: 'warning',
- });
- }
-
if (appConfig.showCPUFallbackMessage && cornerstone.getShouldUseCPURendering()) {
_showCPURenderingModal(uiModalService, hangingProtocolService);
}
+ const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState();
- // Stores a map from `lutPresentationId` to a Presentation object so that
- // an OHIFCornerstoneViewport can be redisplayed with the same LUT
- stateSyncService.register('lutPresentationStore', { clearOnModeExit: true });
+ const { getPresentationId: getSegmentationPresentationId } =
+ useSegmentationPresentationStore.getState();
- // Stores synchronizers state to be restored
- stateSyncService.register('synchronizersStore', { clearOnModeExit: true });
+ const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState();
- // Stores a map from `positionPresentationId` to a Presentation object so that
- // an OHIFCornerstoneViewport can be redisplayed with the same position
- stateSyncService.register('positionPresentationStore', {
- clearOnModeExit: true,
- });
+ // register presentation id providers
+ viewportGridService.addPresentationIdProvider(
+ 'positionPresentationId',
+ getPositionPresentationId
+ );
+ viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId);
+ viewportGridService.addPresentationIdProvider(
+ 'segmentationPresentationId',
+ getSegmentationPresentationId
+ );
- // Stores the entire ViewportGridService getState when toggling to one up
- // (e.g. via a double click) so that it can be restored when toggling back.
- stateSyncService.register('toggleOneUpViewportGridStore', {
- clearOnModeExit: true,
- });
-
- const labelmapRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
- const contourRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Contour;
-
- cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(labelmapRepresentation, {
- fillAlpha: 0.5,
- fillAlphaInactive: 0.2,
- outlineOpacity: 1,
- outlineOpacityInactive: 0.65,
- });
-
- cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(contourRepresentation, {
- renderFill: false,
- });
+ cornerstoneTools.segmentation.config.style.setStyle(
+ { type: SegmentationRepresentations.Contour },
+ {
+ renderFill: false,
+ }
+ );
const metadataProvider = OHIF.classes.MetadataProvider;
@@ -184,9 +150,10 @@ export default async function init({
// These are set reasonably low to allow for interleaved retrieves and slower
// connections.
imageLoadPoolManager.maxNumRequests = {
- interaction: appConfig?.maxNumRequests?.interaction || 10,
- thumbnail: appConfig?.maxNumRequests?.thumbnail || 5,
- prefetch: appConfig?.maxNumRequests?.prefetch || 5,
+ [RequestTypes.Interaction]: appConfig?.maxNumRequests?.interaction || 10,
+ [RequestTypes.Thumbnail]: appConfig?.maxNumRequests?.thumbnail || 5,
+ [RequestTypes.Prefetch]: appConfig?.maxNumRequests?.prefetch || 5,
+ [RequestTypes.Compute]: appConfig?.maxNumRequests?.compute || 10,
};
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
@@ -201,17 +168,23 @@ export default async function init({
hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
volumeInputArrayMap => {
+ const { lutPresentationStore } = useLutPresentationStore.getState();
+ const { segmentationPresentationStore } = useSegmentationPresentationStore.getState();
+ const { positionPresentationStore } = usePositionPresentationStore.getState();
+
for (const entry of volumeInputArrayMap.entries()) {
const [viewportId, volumeInputArray] = entry;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
- const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
const { presentationIds } = ohifViewport.getViewportOptions();
+
const presentations = {
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
+ segmentationPresentation:
+ segmentationPresentationStore[presentationIds?.segmentationPresentationId],
};
cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations);
@@ -249,10 +222,6 @@ export default async function init({
handler(detail.error);
};
- eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
- const { element } = evt.detail;
- cornerstoneTools.utilities.stackContextPrefetch.enable(element);
- });
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler);
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler);
@@ -269,26 +238,11 @@ export default async function init({
commandsManager.runCommand('resetCrosshairs', { viewportId });
});
- // eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
-
initViewTiming({ element });
}
- function elementDisabledHandler(evt) {
- const { element } = evt.detail;
-
- // element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
-
- // TODO - consider removing the callback when all elements are gone
- // eventTarget.removeEventListener(
- // EVENTS.STACK_VIEWPORT_NEW_STACK,
- // newStackCallback
- // );
- }
-
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
- eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
colormaps.forEach(registerColormap);
// Event listener
@@ -301,8 +255,45 @@ export default async function init({
type: 'error',
});
},
- 1000
+ 100
);
+
+ // Call this function when initializing
+ initializeWebWorkerProgressHandler(servicesManager.services.uiNotificationService);
+}
+
+function initializeWebWorkerProgressHandler(uiNotificationService) {
+ const activeToasts = new Map();
+
+ eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => {
+ const { progress, type, id } = detail;
+
+ const cacheKey = `${type}-${id}`;
+ if (progress === 0 && !activeToasts.has(cacheKey)) {
+ const progressPromise = new Promise((resolve, reject) => {
+ activeToasts.set(cacheKey, { resolve, reject });
+ });
+
+ uiNotificationService.show({
+ id: cacheKey,
+ title: `${type}`,
+ message: `${type}: ${progress}%`,
+ autoClose: false,
+ promise: progressPromise,
+ promiseMessages: {
+ loading: `Computing...`,
+ success: `Completed successfully`,
+ error: 'Web Worker failed',
+ },
+ });
+ } else {
+ if (progress === 100) {
+ const { resolve } = activeToasts.get(cacheKey);
+ resolve({ progress, type });
+ activeToasts.delete(cacheKey);
+ }
+ }
+ });
}
function CPUModal() {
diff --git a/extensions/cornerstone/src/initCineService.ts b/extensions/cornerstone/src/initCineService.ts
index b98c2f281..f5fbfb998 100644
--- a/extensions/cornerstone/src/initCineService.ts
+++ b/extensions/cornerstone/src/initCineService.ts
@@ -1,12 +1,9 @@
-import { cache } from '@cornerstonejs/core';
+import { cache, Types } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
-function _getVolumesFromViewport(viewport) {
- return viewport ? viewport.getActors().map(actor => cache.getVolume(actor.uid)) : [];
-}
-
-function _getVolumeFromViewport(viewport) {
- const volumes = _getVolumesFromViewport(viewport).filter(volume => volume);
+function _getVolumeFromViewport(viewport: Types.IBaseVolumeViewport) {
+ const volumeIds = viewport.getAllVolumeIds();
+ const volumes = volumeIds.map(id => cache.getVolume(id));
const dynamicVolume = volumes.find(volume => volume.isDynamicVolume());
return dynamicVolume ?? volumes[0];
diff --git a/extensions/cornerstone/src/initCornerstoneTools.js b/extensions/cornerstone/src/initCornerstoneTools.js
index bfabc4a01..dd1aedc0f 100644
--- a/extensions/cornerstone/src/initCornerstoneTools.js
+++ b/extensions/cornerstone/src/initCornerstoneTools.js
@@ -2,9 +2,8 @@ import {
PanTool,
WindowLevelTool,
StackScrollTool,
- StackScrollMouseWheelTool,
+ VolumeRotateTool,
ZoomTool,
- VolumeRotateMouseWheelTool,
MIPJumpToClickTool,
LengthTool,
RectangleROITool,
@@ -19,7 +18,6 @@ import {
CobbAngleTool,
MagnifyTool,
CrosshairsTool,
- SegmentationDisplayTool,
RectangleScissorsTool,
SphereScissorsTool,
CircleScissorsTool,
@@ -47,15 +45,15 @@ export default function initCornerstoneTools(configuration = {}) {
CrosshairsTool.isAnnotation = false;
ReferenceLinesTool.isAnnotation = false;
AdvancedMagnifyTool.isAnnotation = false;
+ PlanarFreehandContourSegmentationTool.isAnnotation = false;
init(configuration);
addTool(PanTool);
addTool(WindowLevelTool);
- addTool(StackScrollMouseWheelTool);
addTool(StackScrollTool);
+ addTool(VolumeRotateTool);
addTool(ZoomTool);
addTool(ProbeTool);
- addTool(VolumeRotateMouseWheelTool);
addTool(MIPJumpToClickTool);
addTool(LengthTool);
addTool(RectangleROITool);
@@ -69,7 +67,6 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(CobbAngleTool);
addTool(MagnifyTool);
addTool(CrosshairsTool);
- addTool(SegmentationDisplayTool);
addTool(RectangleScissorsTool);
addTool(SphereScissorsTool);
addTool(CircleScissorsTool);
@@ -108,9 +105,8 @@ const toolNames = {
ArrowAnnotate: ArrowAnnotateTool.toolName,
WindowLevel: WindowLevelTool.toolName,
StackScroll: StackScrollTool.toolName,
- StackScrollMouseWheel: StackScrollMouseWheelTool.toolName,
Zoom: ZoomTool.toolName,
- VolumeRotateMouseWheel: VolumeRotateMouseWheelTool.toolName,
+ VolumeRotate: VolumeRotateTool.toolName,
MipJumpToClick: MIPJumpToClickTool.toolName,
Length: LengthTool.toolName,
DragProbe: DragProbeTool.toolName,
@@ -124,7 +120,6 @@ const toolNames = {
CobbAngle: CobbAngleTool.toolName,
Magnify: MagnifyTool.toolName,
Crosshairs: CrosshairsTool.toolName,
- SegmentationDisplay: SegmentationDisplayTool.toolName,
Brush: BrushTool.toolName,
PaintFill: PaintFillTool.toolName,
ReferenceLines: ReferenceLinesTool.toolName,
diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts
index 19ccac95d..1672ea79f 100644
--- a/extensions/cornerstone/src/initMeasurementService.ts
+++ b/extensions/cornerstone/src/initMeasurementService.ts
@@ -1,4 +1,4 @@
-import { eventTarget } from '@cornerstonejs/core';
+import { eventTarget, Types } from '@cornerstonejs/core';
import { Enums, annotation } from '@cornerstonejs/tools';
import { DicomMetadataStore } from '@ohif/core';
@@ -7,6 +7,7 @@ import { toolNames } from './initCornerstoneTools';
import { onCompletedCalibrationLine } from './tools/CalibrationLineTool';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
+import { triggerAnnotationRenderForViewportIds } from '@cornerstonejs/tools/utilities';
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { removeAnnotation } = annotation.state;
@@ -344,8 +345,7 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
return;
}
- const { uid, label } = measurement;
-
+ const { uid, label, isLocked, isVisible } = measurement;
const sourceAnnotation = annotation.state.getAnnotation(uid);
const { data, metadata } = sourceAnnotation;
@@ -361,7 +361,23 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
data.text = label;
}
- // Todo: trigger render for annotation
+ // update the isLocked state
+ annotation.locking.setAnnotationLocked(uid, isLocked);
+
+ // update the isVisible state
+ annotation.visibility.setAnnotationVisibility(uid, isVisible);
+
+ // annotation.config.style.setAnnotationStyles(uid, {
+ // color: `rgb(${color[0]}, ${color[1]}, ${color[2]})`,
+ // });
+
+ // I don't like this but will fix later
+ const renderingEngine =
+ cornerstoneViewportService.getRenderingEngine() as Types.IRenderingEngine;
+ // Note: We could do a better job by triggering the render on the
+ // viewport itself, but the removeAnnotation does not include that info...
+ const viewportIds = renderingEngine.getViewports().map(viewport => viewport.id);
+ triggerAnnotationRenderForViewportIds(viewportIds);
}
);
diff --git a/extensions/cornerstone/src/initWADOImageLoader.js b/extensions/cornerstone/src/initWADOImageLoader.js
index 5a200849f..9f9922f7d 100644
--- a/extensions/cornerstone/src/initWADOImageLoader.js
+++ b/extensions/cornerstone/src/initWADOImageLoader.js
@@ -1,47 +1,18 @@
-import * as cornerstone from '@cornerstonejs/core';
import { volumeLoader } from '@cornerstonejs/core';
import {
cornerstoneStreamingImageVolumeLoader,
cornerstoneStreamingDynamicImageVolumeLoader,
-} from '@cornerstonejs/streaming-image-volume-loader';
-import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader';
-import dicomParser from 'dicom-parser';
+} from '@cornerstonejs/core/loaders';
+import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
import { errorHandler, utils } from '@ohif/core';
const { registerVolumeLoader } = volumeLoader;
-let initialized = false;
-
-function initWebWorkers(appConfig) {
- const config = {
- maxWebWorkers: Math.min(
- Math.max(navigator.hardwareConcurrency - 1, 1),
- appConfig.maxNumberOfWebWorkers
- ),
- startWebWorkersOnDemand: true,
- taskConfiguration: {
- decodeTask: {
- initializeCodecsOnStartup: false,
- usePDFJS: false,
- strict: false,
- },
- },
- };
-
- if (!initialized) {
- dicomImageLoader.webWorkerManager.initialize(config);
- initialized = true;
- }
-}
-
export default function initWADOImageLoader(
userAuthenticationService,
appConfig,
extensionManager
) {
- dicomImageLoader.external.cornerstone = cornerstone;
- dicomImageLoader.external.dicomParser = dicomParser;
-
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
registerVolumeLoader(
@@ -49,17 +20,11 @@ export default function initWADOImageLoader(
cornerstoneStreamingDynamicImageVolumeLoader
);
- dicomImageLoader.configure({
- decodeConfig: {
- // !! IMPORTANT !!
- // We should set this flag to false, since, by default @cornerstonejs/dicom-image-loader
- // will convert everything to integers (to be able to work with cornerstone-2d).
- // Until the default is set to true (which is the case for cornerstone3D),
- // we should set this flag to false.
- convertFloatPixelDataToInt: false,
- use16BitDataType:
- Boolean(appConfig.useNorm16Texture) || Boolean(appConfig.preferSizeOverAccuracy),
- },
+ dicomImageLoader.init({
+ maxWebWorkers: Math.min(
+ Math.max(navigator.hardwareConcurrency - 1, 1),
+ appConfig.maxNumberOfWebWorkers
+ ),
beforeSend: function (xhr) {
//TODO should be removed in the future and request emitted by DicomWebDataSource
const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {};
@@ -84,16 +49,8 @@ export default function initWADOImageLoader(
errorHandler.getHTTPErrorHandler(error);
},
});
-
- 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;
+ console.debug('Destroying WADO Image Loader');
}
diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx
new file mode 100644
index 000000000..3652b31bb
--- /dev/null
+++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx
@@ -0,0 +1,147 @@
+import React, { useEffect, useRef } from 'react';
+import { useViewportGrid } from '@ohif/ui';
+import { MeasurementTable } from '@ohif/ui-next';
+import debounce from 'lodash.debounce';
+import { useMeasurements } from '../hooks/useMeasurements';
+import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-default';
+
+export default function PanelMeasurementTable({
+ servicesManager,
+ customHeader,
+ measurementFilter,
+}: withAppTypes): React.ReactNode {
+ const measurementsPanelRef = useRef(null);
+
+ const [viewportGrid] = useViewportGrid();
+ const { measurementService, customizationService, uiDialogService } = servicesManager.services;
+
+ const displayMeasurements = useMeasurements(servicesManager, {
+ measurementFilter,
+ });
+
+ useEffect(() => {
+ if (displayMeasurements.length > 0) {
+ debounce(() => {
+ measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight;
+ }, 300)();
+ }
+ }, [displayMeasurements.length]);
+
+ const onMeasurementItemClickHandler = (uid: string, isActive: boolean) => {
+ if (isActive) {
+ return;
+ }
+
+ const measurements = [...displayMeasurements];
+ const measurement = measurements.find(m => m.uid === uid);
+
+ measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true));
+ measurement.isActive = true;
+ };
+
+ const jumpToImage = (uid: string) => {
+ measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid);
+ onMeasurementItemClickHandler(uid, true);
+ };
+
+ const removeMeasurement = (uid: string) => {
+ measurementService.remove(uid);
+ };
+
+ const renameMeasurement = (uid: string) => {
+ jumpToImage(uid);
+ const labelConfig = customizationService.get('measurementLabels');
+ const measurement = measurementService.getMeasurement(uid);
+ showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => {
+ measurementService.update(
+ uid,
+ {
+ ...val,
+ },
+ true
+ );
+ });
+ };
+
+ const changeColorMeasurement = (uid: string) => {
+ const { color } = measurementService.getMeasurement(uid);
+ const rgbaColor = {
+ r: color[0],
+ g: color[1],
+ b: color[2],
+ a: color[3] / 255.0,
+ };
+ colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
+ if (actionId === 'cancel') {
+ return;
+ }
+
+ const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
+ // segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
+ });
+ };
+
+ const toggleLockMeasurement = (uid: string) => {
+ measurementService.toggleLockMeasurement(uid);
+ };
+
+ const toggleVisibilityMeasurement = (uid: string) => {
+ measurementService.toggleVisibilityMeasurement(uid);
+ };
+
+ const measurements = displayMeasurements.filter(
+ dm => dm.measurementType !== measurementService.VALUE_TYPES.POINT && dm.referencedImageId
+ );
+ const additionalFindings = displayMeasurements.filter(
+ dm => dm.measurementType === measurementService.VALUE_TYPES.POINT && dm.referencedImageId
+ );
+
+ return (
+ <>
+
+
+
+ {customHeader && (
+ <>
+ {typeof customHeader === 'function'
+ ? customHeader({
+ additionalFindings,
+ measurements,
+ })
+ : customHeader}
+ >
+ )}
+
+
+
+ {additionalFindings.length > 0 && (
+
+
+
+ )}
+
+ >
+ );
+}
diff --git a/extensions/cornerstone/src/panels/PanelSegmentation.tsx b/extensions/cornerstone/src/panels/PanelSegmentation.tsx
new file mode 100644
index 000000000..b9e9ffcd2
--- /dev/null
+++ b/extensions/cornerstone/src/panels/PanelSegmentation.tsx
@@ -0,0 +1,230 @@
+import React from 'react';
+import { SegmentationTable } from '@ohif/ui-next';
+import { useActiveViewportSegmentationRepresentations } from '../hooks/useActiveViewportSegmentationRepresentations';
+import { metaData } from '@cornerstonejs/core';
+
+export default function PanelSegmentation({
+ servicesManager,
+ commandsManager,
+ extensionManager,
+ configuration,
+ children,
+}: withAppTypes) {
+ const { customizationService, viewportGridService, displaySetService } = servicesManager.services;
+
+ const { segmentationsWithRepresentations, disabled } =
+ useActiveViewportSegmentationRepresentations({
+ servicesManager,
+ });
+
+ const handlers = {
+ onSegmentationAdd: async () => {
+ const viewportId = viewportGridService.getState().activeViewportId;
+ commandsManager.run('createLabelmapForViewport', { viewportId });
+ },
+
+ onSegmentationClick: (segmentationId: string) => {
+ commandsManager.run('setActiveSegmentation', { segmentationId });
+ },
+
+ onSegmentAdd: segmentationId => {
+ commandsManager.run('addSegment', { segmentationId });
+ },
+
+ onSegmentClick: (segmentationId, segmentIndex) => {
+ commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex });
+ },
+
+ onSegmentEdit: (segmentationId, segmentIndex) => {
+ commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex });
+ },
+
+ onSegmentationEdit: segmentationId => {
+ commandsManager.run('editSegmentationLabel', { segmentationId });
+ },
+
+ onSegmentColorClick: (segmentationId, segmentIndex) => {
+ commandsManager.run('editSegmentColor', { segmentationId, segmentIndex });
+ },
+
+ onSegmentDelete: (segmentationId, segmentIndex) => {
+ commandsManager.run('deleteSegment', { segmentationId, segmentIndex });
+ },
+
+ onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => {
+ commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type });
+ },
+
+ onToggleSegmentLock: (segmentationId, segmentIndex) => {
+ commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex });
+ },
+
+ onToggleSegmentationRepresentationVisibility: (segmentationId, type) => {
+ commandsManager.run('toggleSegmentationVisibility', { segmentationId, type });
+ },
+
+ onSegmentationDownload: segmentationId => {
+ commandsManager.run('downloadSegmentation', { segmentationId });
+ },
+
+ storeSegmentation: async segmentationId => {
+ commandsManager.run('storeSegmentation', { segmentationId });
+ },
+
+ onSegmentationDownloadRTSS: segmentationId => {
+ commandsManager.run('downloadRTSS', { segmentationId });
+ },
+
+ setStyle: (segmentationId, type, key, value) => {
+ commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value });
+ },
+
+ toggleRenderInactiveSegmentations: () => {
+ commandsManager.run('toggleRenderInactiveSegmentations');
+ },
+
+ onSegmentationRemoveFromViewport: segmentationId => {
+ commandsManager.run('removeSegmentationFromViewport', { segmentationId });
+ },
+
+ onSegmentationDelete: segmentationId => {
+ commandsManager.run('deleteSegmentation', { segmentationId });
+ },
+
+ setFillAlpha: (type, value) => {
+ commandsManager.run('setFillAlpha', { type, value });
+ },
+
+ setOutlineWidth: (type, value) => {
+ commandsManager.run('setOutlineWidth', { type, value });
+ },
+
+ setRenderFill: (type, value) => {
+ commandsManager.run('setRenderFill', { type, value });
+ },
+
+ setRenderOutline: (type, value) => {
+ commandsManager.run('setRenderOutline', { type, value });
+ },
+
+ setFillAlphaInactive: (type, value) => {
+ commandsManager.run('setFillAlphaInactive', { type, value });
+ },
+
+ getRenderInactiveSegmentations: () => {
+ return commandsManager.run('getRenderInactiveSegmentations');
+ },
+ };
+
+ const { mode: SegmentationTableMode } = customizationService.getCustomization(
+ 'PanelSegmentation.tableMode',
+ {
+ id: 'default.segmentationTable.mode',
+ mode: 'collapsed',
+ }
+ );
+
+ // custom onSegmentationAdd if provided
+ const { onSegmentationAdd } = customizationService.getCustomization(
+ 'PanelSegmentation.onSegmentationAdd',
+ {
+ id: 'segmentation.onSegmentationAdd',
+ onSegmentationAdd: handlers.onSegmentationAdd,
+ }
+ );
+
+ const { disableEditing } = customizationService.getCustomization(
+ 'PanelSegmentation.disableEditing',
+ {
+ id: 'default.disableEditing',
+ disableEditing: false,
+ }
+ );
+
+ const { showAddSegment } = customizationService.getCustomization(
+ 'PanelSegmentation.showAddSegment',
+ {
+ id: 'default.showAddSegment',
+ showAddSegment: true,
+ }
+ );
+
+ const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
+ const { representationData, segmentationId } = segmentation;
+ const { Labelmap } = representationData;
+ const referencedImageIds = Labelmap.referencedImageIds;
+ const firstImageId = referencedImageIds[0];
+
+ const instance = metaData.get('instance', firstImageId);
+ const { SOPInstanceUID, SeriesInstanceUID } = instance;
+
+ const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
+ SOPInstanceUID,
+ SeriesInstanceUID
+ );
+ const isExportable = displaySet.isReconstructable;
+
+ return {
+ segmentationId,
+ isExportable,
+ };
+ });
+
+ return (
+ <>
+
+ {children}
+
+
+
+ {SegmentationTableMode === 'collapsed' ? (
+
+
+
+
+
+ ) : (
+
+
+ {/* */}
+
+
+ )}
+
+ >
+ );
+}
diff --git a/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts b/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts
index a8a4620ac..9c62ff492 100644
--- a/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts
+++ b/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts
@@ -48,6 +48,17 @@ export default class ColorbarService extends PubSubService {
super(ColorbarService.EVENTS);
}
+ /**
+ * Gets the volume ID for a given identifier by searching through the viewport's volume IDs.
+ * @param viewport - The viewport instance to search volumes in
+ * @param searchId - The identifier to search for within volume IDs
+ * @returns The matching volume ID if found, null otherwise
+ */
+ private getVolumeIdForIdentifier(viewport, searchId: string): string | null {
+ const volumeIds = viewport.getAllVolumeIds?.() || [];
+ return volumeIds.length > 0 ? volumeIds.find(id => id.includes(searchId)) || null : null;
+ }
+
/**
* Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`.
* This method sets up the colorbar, associates it with the viewport, and applies initial configurations based on the provided options.
@@ -59,8 +70,18 @@ export default class ColorbarService extends PubSubService {
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
const viewport = renderingEngine.getViewport(viewportId);
+
+ if (!viewport) {
+ return;
+ }
+
const { element } = viewport;
const actorEntries = viewport.getActors();
+
+ if (!actorEntries || actorEntries.length === 0) {
+ return;
+ }
+
const { position, width: thickness, activeColormapName, colormaps } = options;
const numContainers = displaySetInstanceUIDs.length;
@@ -74,11 +95,9 @@ export default class ColorbarService extends PubSubService {
);
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
- const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
- const volumeId = actorEntry?.uid;
+ const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID);
const properties = viewport?.getProperties(volumeId);
const colormap = properties?.colormap;
- // if there's an initial colormap set, and no colormap on the viewport, set it
if (activeColormapName && !colormap) {
this.setViewportColormap(
viewportId,
@@ -196,9 +215,8 @@ export default class ColorbarService extends PubSubService {
return;
}
const setViewportProperties = (viewport, uid) => {
- const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
- const { actor: volumeActor, uid: volumeId } = actorEntry;
- viewport.setProperties({ colormap, volumeActor }, volumeId);
+ const volumeId = this.getVolumeIdForIdentifier(viewport, uid);
+ viewport.setProperties({ colormap }, volumeId);
};
if (viewport instanceof StackViewport) {
diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts
index 80454ac98..b185f6cdd 100644
--- a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts
+++ b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts
@@ -1,5 +1,5 @@
import { Types } from '@ohif/core';
-import { cache as cs3DCache, Enums, volumeLoader, utilities as utils } from '@cornerstonejs/core';
+import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
@@ -33,27 +33,11 @@ class CornerstoneCacheService {
public async createViewportData(
displaySets: Types.DisplaySet[],
- viewportOptions: Record,
+ viewportOptions: AppTypes.ViewportGrid.GridViewportOptions,
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)) {
- // if the viewport type is volume 3D, we should let it be as it is
- // Todo: in future here we should kick start the conversion of the
- // segmentation to closed surface
- viewportType =
- viewportType === Enums.ViewportType.VOLUME_3D ? Enums.ViewportType.VOLUME_3D : 'volume';
-
- // update viewportOptions to reflect the new viewport type
- viewportOptions.viewportType = viewportType;
- }
+ const viewportType = viewportOptions.viewportType as string;
const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets);
let viewportData: StackViewportData | VolumeViewportData;
@@ -86,7 +70,7 @@ class CornerstoneCacheService {
}
public async invalidateViewportData(
- viewportData: VolumeViewportData,
+ viewportData: VolumeViewportData | StackViewportData,
invalidatedDisplaySetInstanceUID: string,
dataSource,
displaySetService
@@ -176,9 +160,6 @@ class CornerstoneCacheService {
): Promise {
const { uiNotificationService } = this.servicesManager.services;
const overlayDisplaySets = displaySets.filter(ds => ds.isOverlayDisplaySet);
- const nonOverlayDisplaySets = displaySets.filter(ds => !ds.isOverlayDisplaySet);
-
- // load overlays if they are not loaded
for (const overlayDisplaySet of overlayDisplaySets) {
if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) {
const { userAuthenticationService } = this.servicesManager.services;
@@ -187,7 +168,7 @@ class CornerstoneCacheService {
await overlayDisplaySet.load({ headers });
} catch (e) {
uiNotificationService.show({
- title: 'Error loading overlayDisplaySet',
+ title: 'Error loading displaySet',
message: e.message,
type: 'error',
});
@@ -196,43 +177,43 @@ class CornerstoneCacheService {
}
}
- const displaySet = nonOverlayDisplaySets[0];
-
- if (displaySet.load && displaySet.load instanceof Function) {
- const { userAuthenticationService } = this.servicesManager.services;
- const headers = userAuthenticationService.getAuthorizationHeader();
-
- try {
- await displaySet.load({ headers });
- } catch (e) {
- uiNotificationService.show({
- title: 'Error loading displaySet',
- message: e.message,
- type: 'error',
- });
- console.error(e);
- }
- }
-
- let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
-
- if (!stackImageIds) {
- stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
- this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
- }
-
// Ensuring the first non-overlay `displaySet` is always the primary one
- const StackViewportData = [displaySet, ...overlayDisplaySets].map(ds => {
- const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = ds;
+ const StackViewportData = [];
+ for (const displaySet of displaySets) {
+ const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = displaySet;
- return {
+ if (displaySet.load && displaySet.load instanceof Function) {
+ const { userAuthenticationService } = this.servicesManager.services;
+ const headers = userAuthenticationService.getAuthorizationHeader();
+ try {
+ await displaySet.load({ headers });
+ } catch (e) {
+ uiNotificationService.show({
+ title: 'Error loading displaySet',
+ message: e.message,
+ type: 'error',
+ });
+ console.error(e);
+ }
+ }
+
+ let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
+
+ if (!stackImageIds) {
+ stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
+ // assign imageIds to the displaySet
+ displaySet.imageIds = stackImageIds;
+ this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
+ }
+
+ StackViewportData.push({
StudyInstanceUID,
displaySetInstanceUID,
isCompositeStack,
imageIds: stackImageIds,
initialImageIndex,
- };
- });
+ });
+ }
return {
viewportType,
@@ -253,6 +234,7 @@ class CornerstoneCacheService {
for (const displaySet of displaySets) {
const { Modality } = displaySet;
const isParametricMap = Modality === 'PMAP';
+ const isSeg = Modality === 'SEG';
// Don't create volumes for the displaySets that have custom load
// function (e.g., SEG, RT, since they rely on the reference volumes
@@ -295,7 +277,7 @@ class CornerstoneCacheService {
// Parametric maps do not have image ids but they already have volume data
// therefore a new volume should not be created.
- if (!isParametricMap && (!volumeImageIds || !volume)) {
+ if (!isParametricMap && !isSeg && (!volumeImageIds || !volume)) {
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
volume = await volumeLoader.createAndCacheVolume(volumeId, {
@@ -303,6 +285,9 @@ class CornerstoneCacheService {
});
this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds);
+
+ // Add imageIds to the displaySet for volumes
+ displaySet.imageIds = volumeImageIds;
}
volumeData.push({
@@ -321,39 +306,15 @@ class CornerstoneCacheService {
};
}
- private _shouldRenderSegmentation(displaySets) {
- const { segmentationService, displaySetService } = 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 segDisplaySet = displaySetService.getDisplaySetByUID(segDisplaySetInstanceUID);
-
- const instance = segDisplaySet.instances?.[0] || segDisplaySet.instance;
-
- const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
- viewportDisplaySetInstanceUIDs,
- instance?.FrameOfReferenceUID || segDisplaySet.FrameOfReferenceUID
- );
-
- if (shouldDisplaySeg) {
- return true;
- }
- }
- }
-
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
return dataSource.getImageIdsForDisplaySet(displaySet);
}
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
+ if (displaySet.imageIds) {
+ return displaySet.imageIds;
+ }
+
const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
return stackImageIds;
diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts
index af98398ff..40825c208 100644
--- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts
+++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts
@@ -1,40 +1,75 @@
-import { Types as OhifTypes, PubSubService } from '@ohif/core';
import {
cache,
Enums as csEnums,
- geometryLoader,
eventTarget,
- getEnabledElementByIds,
+ geometryLoader,
+ getEnabledElementByViewportId,
+ imageLoader,
+ Types as csTypes,
utilities as csUtils,
- volumeLoader,
- StackViewport,
} from '@cornerstonejs/core';
import {
Enums as csToolsEnums,
segmentation as cstSegmentation,
Types as cstTypes,
- utilities as cstUtils,
} from '@cornerstonejs/tools';
-import isEqual from 'lodash.isequal';
-import { Types as ohifTypes } from '@ohif/core';
+import { PubSubService, Types as OHIFTypes } from '@ohif/core';
import { easeInOutBell, reverseEaseInOutBell } from '../../utils/transitions';
-import { Segment, Segmentation, SegmentationConfig } from './SegmentationServiceTypes';
import { mapROIContoursToRTStructData } from './RTSTRUCT/mapROIContoursToRTStructData';
+import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
+import { addColorLUT } from '@cornerstonejs/tools/segmentation/addColorLUT';
+import { getNextColorLUTIndex } from '@cornerstonejs/tools/segmentation/getNextColorLUTIndex';
+import { Segment } from '@cornerstonejs/tools/types/SegmentationStateTypes';
+import { ContourStyle, LabelmapStyle, SurfaceStyle } from '@cornerstonejs/tools/types';
+import { ViewportType } from '@cornerstonejs/core/enums';
+import { SegmentationPresentation, SegmentationPresentationItem } from '../../types/Presentation';
+import { updateLabelmapSegmentationImageReferences } from '@cornerstonejs/tools/segmentation/updateLabelmapSegmentationImageReferences';
+import { triggerSegmentationRepresentationModified } from '@cornerstonejs/tools/segmentation/triggerSegmentationEvents';
+import { convertStackToVolumeLabelmap } from '@cornerstonejs/tools/segmentation/helpers/convertStackToVolumeLabelmap';
+import { getLabelmapImageIds } from '@cornerstonejs/tools/segmentation';
const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap;
const CONTOUR = csToolsEnums.SegmentationRepresentations.Contour;
+export type SegmentRepresentation = {
+ segmentIndex: number;
+ color: csTypes.Color;
+ opacity: number;
+ visible: boolean;
+};
+
+export type SegmentationData = cstTypes.Segmentation;
+
+export type SegmentationRepresentation = cstTypes.SegmentationRepresentation & {
+ viewportId: string;
+ id: string;
+ label: string;
+ styles: cstTypes.RepresentationStyle;
+ segments: {
+ [key: number]: SegmentRepresentation;
+ };
+};
+
+export type SegmentationInfo = {
+ segmentation: SegmentationData;
+ representation?: SegmentationRepresentation;
+};
+
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_MODIFIED: 'event::segmentation_modified',
+ // fired when the segmentation is added
SEGMENTATION_ADDED: 'event::segmentation_added',
+ //
+ SEGMENTATION_DATA_MODIFIED: 'event::segmentation_data_modified',
// 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',
+ //
+ // fired when segmentation representation is added
+ SEGMENTATION_REPRESENTATION_MODIFIED: 'event::segmentation_representation_modified',
+ // fired when segmentation representation is removed
+ SEGMENTATION_REPRESENTATION_REMOVED: 'event::segmentation_representation_removed',
+ //
+ // LOADING EVENTS
// fired when the active segment is loaded in SEG or RTSTRUCT
SEGMENT_LOADING_COMPLETE: 'event::segment_loading_complete',
// loading completed for all segments
@@ -43,37 +78,147 @@ const EVENTS = {
const VALUE_TYPES = {};
-const SEGMENT_CONSTANT = {
- opacity: 255,
- isVisible: true,
- isLocked: false,
-};
-
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
class SegmentationService extends PubSubService {
static REGISTRATION = {
name: 'segmentationService',
altName: 'SegmentationService',
- create: ({ servicesManager }: OhifTypes.Extensions.ExtensionParams): SegmentationService => {
+ create: ({ servicesManager }: OHIFTypes.Extensions.ExtensionParams): SegmentationService => {
return new SegmentationService({ servicesManager });
},
};
- segmentations: Record;
+ private _segmentationIdToColorLUTIndexMap: Map;
readonly servicesManager: AppTypes.ServicesManager;
highlightIntervalId = null;
readonly EVENTS = EVENTS;
constructor({ servicesManager }) {
super(EVENTS);
- this.segmentations = {};
+
+ this._segmentationIdToColorLUTIndexMap = new Map();
this.servicesManager = servicesManager;
this._initSegmentationService();
}
+ /**
+ * Retrieves a segmentation by its ID.
+ *
+ * @param segmentationId - The unique identifier of the segmentation to retrieve.
+ * @returns The segmentation object if found, or undefined if not found.
+ *
+ * @remarks
+ * This method directly accesses the cornerstone tools segmentation state to fetch
+ * the segmentation data. It's useful when you need to access specific properties
+ * or perform operations on a particular segmentation.
+ */
+ public getSegmentation(segmentationId: string): cstTypes.Segmentation | undefined {
+ return cstSegmentation.state.getSegmentation(segmentationId);
+ }
+
+ /**
+ * Retrieves all segmentations from the cornerstone tools segmentation state.
+ *
+ * @returns An array of all segmentations currently stored in the state
+ *
+ * @remarks
+ * This is a convenience method that directly accesses the cornerstone tools
+ * segmentation state to get all available segmentations. It returns the raw
+ * segmentation objects without any additional processing or filtering.
+ */
+ public getSegmentations(): cstTypes.Segmentation[] | [] {
+ return cstSegmentation.state.getSegmentations();
+ }
+
+ public getPresentation(viewportId: string): SegmentationPresentation {
+ const segmentationPresentations: SegmentationPresentation = [];
+ const segmentationsMap = new Map();
+
+ const representations = this.getSegmentationRepresentations(viewportId);
+ for (const representation of representations) {
+ const { segmentationId } = representation;
+
+ if (!representation) {
+ continue;
+ }
+
+ const { type } = representation;
+
+ segmentationsMap.set(segmentationId, {
+ segmentationId,
+ type,
+ hydrated: true,
+ config: representation.config || {},
+ });
+ }
+
+ // Check inside the removedDisplaySetAndRepresentationMaps to see if any of the representations are not hydrated
+ // const hydrationMap = this._segmentationRepresentationHydrationMaps.get(presentationId);
+
+ // if (hydrationMap) {
+ // hydrationMap.forEach(rep => {
+ // segmentationsMap.set(rep.segmentationId, {
+ // segmentationId: rep.segmentationId,
+ // type: rep.type,
+ // hydrated: rep.hydrated,
+ // config: rep.config || {},
+ // });
+ // });
+ // }
+
+ // // Convert the Map to an array
+ segmentationPresentations.push(...segmentationsMap.values());
+
+ return segmentationPresentations;
+ }
+
+ public getRepresentationsForSegmentation(
+ segmentationId: string
+ ): { viewportId: string; representations: any[] }[] {
+ const representations =
+ cstSegmentation.state.getSegmentationRepresentationsBySegmentationId(segmentationId);
+
+ return representations;
+ }
+
+ /**
+ * Retrieves segmentation representations (labelmap, contour, surface) based on specified criteria.
+ *
+ * @param viewportId - The ID of the viewport.
+ * @param specifier - An object containing optional `segmentationId` and `type` to filter the representations.
+ * @returns An array of `SegmentationRepresentation` matching the criteria, or an empty array if none are found.
+ *
+ * @remarks
+ * This method filters the segmentation representations according to the provided `specifier`:
+ * - **No `segmentationId` or `type` provided**: Returns all representations associated with the given `viewportId`.
+ * - **Only `segmentationId` provided**: Returns all representations with that `segmentationId`, regardless of `viewportId`.
+ * - **Only `type` provided**: Returns all representations of that `type` associated with the given `viewportId`.
+ * - **Both `segmentationId` and `type` provided**: Returns representations matching both criteria, regardless of `viewportId`.
+ */
+ public getSegmentationRepresentations(
+ viewportId: string,
+ specifier: {
+ segmentationId?: string;
+ type?: SegmentationRepresentations;
+ } = {}
+ ): SegmentationRepresentation[] {
+ // Get all representations for the viewportId
+ const representations = cstSegmentation.state.getSegmentationRepresentations(
+ viewportId,
+ specifier
+ );
+
+ // Map to our SegmentationRepresentation type
+ const ohifRepresentations = representations.map(repr =>
+ this._toOHIFSegmentationRepresentation(viewportId, repr)
+ );
+
+ return ohifRepresentations;
+ }
+
public destroy = () => {
eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_MODIFIED,
@@ -81,481 +226,198 @@ class SegmentationService extends PubSubService {
);
eventTarget.removeEventListener(
- csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED,
- this._onSegmentationDataModified
+ csToolsEnums.Events.SEGMENTATION_REMOVED,
+ this._onSegmentationModifiedFromSource
);
- // remove the segmentations from the cornerstone
- Object.keys(this.segmentations).forEach(segmentationId => {
- this._removeSegmentationFromCornerstone(segmentationId);
- });
+ eventTarget.removeEventListener(
+ csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED,
+ this._onSegmentationDataModifiedFromSource
+ );
+
+ eventTarget.removeEventListener(
+ csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED,
+ this._onSegmentationModifiedFromSource
+ );
+
+ eventTarget.removeEventListener(
+ csToolsEnums.Events.SEGMENTATION_ADDED,
+ this._onSegmentationAddedFromSource
+ );
- this.segmentations = {};
this.listeners = {};
};
- /**
- * Adds a new segment to the specified segmentation.
- * @param segmentationId - The ID of the segmentation to add the segment to.
- * @param config - An object containing the configuration options for the new segment.
- * - segmentIndex: (optional) The index of the segment to add. If not provided, the next available index will be used.
- * - toolGroupId: (optional) The ID of the tool group to associate the new segment with. If not provided, the first available tool group will be used.
- * - properties: (optional) An object containing the properties of the new segment.
- * - label: (optional) The label of the new segment. If not provided, a default label will be used.
- * - color: (optional) The color of the new segment in RGB format. If not provided, a default color will be used.
- * - opacity: (optional) The opacity of the new segment. If not provided, a default opacity will be used.
- * - visibility: (optional) Whether the new segment should be visible. If not provided, the segment will be visible by default.
- * - isLocked: (optional) Whether the new segment should be locked for editing. If not provided, the segment will not be locked by default.
- * - active: (optional) Whether the new segment should be the active segment to be edited. If not provided, the segment will not be active by default.
- */
- public addSegment(
- segmentationId: string,
- config: {
- segmentIndex?: number;
- toolGroupId?: string;
- properties?: {
- label?: string;
- color?: ohifTypes.RGB;
- opacity?: number;
- visibility?: boolean;
- isLocked?: boolean;
- active?: boolean;
- };
- } = {}
- ): void {
- if (config?.segmentIndex === 0) {
- throw new Error('Segment index 0 is reserved for "no label"');
- }
-
- const toolGroupId = config.toolGroupId ?? this._getApplicableToolGroupId();
-
- const { segmentationRepresentationUID, segmentation } = this._getSegmentationInfo(
+ public async addSegmentationRepresentation(
+ viewportId: string,
+ {
segmentationId,
- toolGroupId
- );
-
- let segmentIndex = config.segmentIndex;
- if (!segmentIndex) {
- // grab the next available segment index
- segmentIndex = segmentation.segments.length === 0 ? 1 : segmentation.segments.length;
+ type,
+ suppressEvents = false,
+ }: {
+ segmentationId: string;
+ type?: csToolsEnums.SegmentationRepresentations;
+ suppressEvents?: boolean;
}
-
- 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: config.properties?.label ?? `Segment ${segmentIndex}`,
- segmentIndex: segmentIndex,
- color: [rgbaColor[0], rgbaColor[1], rgbaColor[2]],
- opacity: rgbaColor[3],
- isVisible: true,
- isLocked: false,
- };
-
- segmentation.segmentCount++;
-
- // make the newly added segment the active segment
- this._setActiveSegment(segmentationId, segmentIndex);
-
- const suppressEvents = true;
- if (config.properties !== undefined) {
- const { color: newColor, opacity, isLocked, visibility, active } = config.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 === true) {
- 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 non-hydrated segmentations which might not be
- // persisted in the store
- this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
- segmentation,
- });
- }
-
- public removeSegment(segmentationId: string, segmentIndex: number): void {
+ ): Promise {
const segmentation = this.getSegmentation(segmentationId);
+ const csViewport = this.getAndValidateViewport(viewportId);
+ const colorLUTIndex = this._segmentationIdToColorLUTIndexMap.get(segmentationId);
- if (segmentation === undefined) {
- throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
- }
+ const defaultRepresentationType = csToolsEnums.SegmentationRepresentations.Labelmap;
+ let representationTypeToUse = type || defaultRepresentationType;
+ let isConverted = false;
- if (segmentIndex === 0) {
- throw new Error('Segment index 0 is reserved for "no label"');
- }
+ if (type === csToolsEnums.SegmentationRepresentations.Labelmap) {
+ const { isVolumeViewport, isVolumeSegmentation } = this.determineViewportAndSegmentationType(
+ csViewport,
+ segmentation
+ );
- 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 { dimensions } = labelmapVolume;
- const scalarData = labelmapVolume.getScalarData();
-
- // 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 setSegmentLocked(segmentationId: string, segmentIndex: number, isLocked: boolean): void {
- const suppressEvents = false;
- this._setSegmentLocked(segmentationId, segmentIndex, isLocked, suppressEvents);
- }
-
- /**
- * Toggles the locked state of a segment in a segmentation.
- * @param segmentationId - The ID of the segmentation.
- * @param segmentIndex - The index of the segment to toggle.
- */
- public toggleSegmentLocked(segmentationId: string, segmentIndex: number): void {
- const segmentation = this.getSegmentation(segmentationId);
- const segment = this._getSegmentInfo(segmentation, segmentIndex);
- const isLocked = !segment.isLocked;
- this._setSegmentLocked(segmentationId, segmentIndex, isLocked);
- }
-
- public setSegmentColor(
- segmentationId: string,
- segmentIndex: number,
- color: ohifTypes.RGB,
- 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._getApplicableToolGroupId();
-
- const suppressEvents = false;
- this._setActiveSegmentationForToolGroup(segmentationId, toolGroupId, suppressEvents);
- }
-
- public setActiveSegment(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]]);
- }
-
- public getActiveSegmentation(): Segmentation {
- const segmentations = this.getSegmentations();
-
- return segmentations.find(segmentation => segmentation.isActive);
- }
-
- public getActiveSegment() {
- const activeSegmentation = this.getActiveSegmentation();
- const { activeSegmentIndex, segments } = activeSegmentation;
-
- if (activeSegmentIndex === null) {
- return;
- }
-
- return segments[activeSegmentIndex];
- }
-
- /**
- * 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(
- segmentation: Segmentation,
- suppressEvents = false,
- notYetUpdatedAtSource = false
- ): string {
- const { id: segmentationId } = segmentation;
- let cachedSegmentation = this.segmentations[segmentationId];
- if (cachedSegmentation) {
- // Update the segmentation (mostly for assigning metadata/labels)
- Object.assign(cachedSegmentation, segmentation);
-
- this._updateCornerstoneSegmentations({
+ ({ representationTypeToUse, isConverted } = await this.handleViewportConversion(
+ isVolumeViewport,
+ isVolumeSegmentation,
+ csViewport,
+ segmentation,
+ viewportId,
segmentationId,
- notYetUpdatedAtSource,
- });
-
- if (!suppressEvents) {
- this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
- segmentation: cachedSegmentation,
- });
- }
-
- return segmentationId;
+ representationTypeToUse
+ ));
}
- const representationType = segmentation.type;
- const representationData = segmentation.representationData[representationType];
- cstSegmentation.addSegmentations([
- {
- segmentationId,
- representation: {
- type: representationType,
- data: {
- ...representationData,
- },
- },
- },
- ]);
-
- this.segmentations[segmentationId] = {
- ...segmentation,
- label: segmentation.label || '',
- segments: segmentation.segments || [null],
- activeSegmentIndex: segmentation.activeSegmentIndex ?? null,
- segmentCount: segmentation.segmentCount ?? 0,
- isActive: false,
- isVisible: true,
- };
-
- cachedSegmentation = this.segmentations[segmentationId];
-
- this._updateCornerstoneSegmentations({
+ await this._addSegmentationRepresentation(
+ viewportId,
segmentationId,
- notYetUpdatedAtSource: true,
- });
+ representationTypeToUse,
+ colorLUTIndex,
+ isConverted
+ );
if (!suppressEvents) {
- this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, {
- segmentation: cachedSegmentation,
- });
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, { segmentationId });
+ }
+ }
+
+ /**
+ * Creates an labelmap segmentation for a given display set
+ *
+ * @param displaySet - The display set to create the segmentation for.
+ * @param options - Optional parameters for creating the segmentation.
+ * @param options.segmentationId - Custom segmentation ID. If not provided, a UUID will be generated.
+ * @param options.FrameOfReferenceUID - Frame of reference UID for the segmentation.
+ * @param options.label - Label for the segmentation.
+ * @returns A promise that resolves to the created segmentation ID.
+ */
+ public async createLabelmapForDisplaySet(
+ displaySet: AppTypes.DisplaySet,
+ options?: {
+ segmentationId?: string;
+ segments?: { [segmentIndex: number]: Partial };
+ FrameOfReferenceUID?: string;
+ label?: string;
+ }
+ ): Promise {
+ // Todo: random does not makes sense, make this better, like
+ // labelmap 1, 2, 3 etc
+ const segmentationId = options?.segmentationId ?? `${csUtils.uuidv4()}`;
+
+ const isDynamicVolume = displaySet.isDynamicVolume;
+
+ let referenceImageIds = displaySet.imageIds;
+ if (isDynamicVolume) {
+ // get the middle timepoint for referenceImageIds
+ const timePoints = displaySet.dynamicVolumeInfo.timePoints;
+ const middleTimePoint = timePoints[Math.floor(timePoints.length / 2)];
+ referenceImageIds = middleTimePoint;
}
- return cachedSegmentation.id;
+ const derivedImages = await imageLoader.createAndCacheDerivedLabelmapImages(referenceImageIds);
+
+ const segs = this.getSegmentations();
+ const label = options.label || `Segmentation ${segs.length + 1}`;
+
+ const segImageIds = derivedImages.map(image => image.imageId);
+
+ const segmentationPublicInput: cstTypes.SegmentationPublicInput = {
+ segmentationId,
+ representation: {
+ type: LABELMAP,
+ data: {
+ imageIds: segImageIds,
+ referencedVolumeId: this._getVolumeIdForDisplaySet(displaySet),
+ referencedImageIds: referenceImageIds,
+ },
+ },
+ config: {
+ label,
+ segments:
+ options.segments && Object.keys(options.segments).length > 0
+ ? options.segments
+ : {
+ 1: {
+ label: 'Segment 1',
+ active: true,
+ },
+ },
+ cachedStats: {
+ info: `S${displaySet.SeriesNumber}: ${displaySet.SeriesDescription}`,
+ },
+ },
+ };
+
+ this.addOrUpdateSegmentation(segmentationPublicInput);
+ return segmentationId;
}
public async createSegmentationForSEGDisplaySet(
segDisplaySet,
- segmentationId?: string,
- suppressEvents = false
+ options: {
+ segmentationId?: string;
+ type: SegmentationRepresentations;
+ } = {
+ type: LABELMAP,
+ }
): Promise {
- // Todo: we only support creating labelmap for SEG displaySets for now
- const representationType = LABELMAP;
+ const { type } = options;
+ let { segmentationId } = options;
+ const { labelmapBufferArray } = segDisplaySet;
+
+ if (type !== LABELMAP) {
+ throw new Error('Only labelmap type is supported for SEG display sets right now');
+ }
+
+ if (!labelmapBufferArray) {
+ throw new Error('SEG reading failed');
+ }
segmentationId = segmentationId ?? segDisplaySet.displaySetInstanceUID;
+ const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
+ const referencedDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID(
+ referencedDisplaySetInstanceUID
+ );
- const defaultScheme = this._getDefaultSegmentationScheme();
+ const images = referencedDisplaySet.instances;
- const segmentation: Segmentation = {
- ...defaultScheme,
- id: segmentationId,
- displaySetInstanceUID: segDisplaySet.displaySetInstanceUID,
- type: representationType,
- label: segDisplaySet.SeriesDescription,
- representationData: {
- [LABELMAP]: {
- volumeId: segmentationId,
- referencedVolumeId: segDisplaySet.referencedVolumeId,
- },
- },
- };
-
- const labelmap = this.getLabelmapVolume(segmentationId);
- const cachedSegmentation = this.getSegmentation(segmentationId);
- if (labelmap && cachedSegmentation) {
- // if the labelmap 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(segmentation, cachedSegmentation),
- suppressEvents
- );
+ if (!images.length) {
+ throw new Error('No instances were provided for the referenced display set of the SEG');
}
- const { labelmapBufferArray, referencedVolumeId } = segDisplaySet;
+ const imageIds = images.map(image => image.imageId);
- if (!labelmapBufferArray || !referencedVolumeId) {
- throw new Error('No labelmapBufferArray or referencedVolumeId found for the SEG displaySet');
- }
+ const derivedSegmentationImages = await imageLoader.createAndCacheDerivedLabelmapImages(
+ imageIds as string[]
+ );
- // 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: window.SharedArrayBuffer,
- },
- });
- const derivedVolumeScalarData = derivedVolume.getScalarData();
+ segDisplaySet.images = derivedSegmentationImages;
const segmentsInfo = segDisplaySet.segMetadata.data;
- derivedVolumeScalarData.set(new Uint8Array(labelmapBufferArray[0]));
- segmentation.segments = segmentsInfo.map((segmentInfo, segmentIndex) => {
- if (segmentIndex === 0) {
+ const segments: { [segmentIndex: string]: cstTypes.Segment } = {};
+ const colorLUT = [];
+
+ segmentsInfo.forEach((segmentInfo, index) => {
+ if (index === 0) {
+ colorLUT.push([0, 0, 0, 0]);
return;
}
@@ -569,115 +431,147 @@ class SegmentationService extends PubSubService {
rgba,
} = segmentInfo;
- const { x, y, z } = segDisplaySet.centroids.get(segmentIndex) || { x: 0, y: 0, z: 0 };
- const centerWorld = derivedVolume.imageData.indexToWorld([x, y, z]);
+ colorLUT.push(rgba);
- segmentation.cachedStats = {
- ...segmentation.cachedStats,
- segmentCenter: {
- ...segmentation.cachedStats.segmentCenter,
- [segmentIndex]: {
- center: {
- image: [x, y, z],
- world: centerWorld,
- },
- modifiedTime: segDisplaySet.SeriesDate,
- },
- },
- };
+ const segmentIndex = Number(SegmentNumber);
- return {
+ const imageCentroidXYZ = segDisplaySet.centroids.get(index).image || { x: 0, y: 0, z: 0 };
+ const worldCentroidXYZ = segDisplaySet.centroids.get(index).world || { x: 0, y: 0, z: 0 };
+
+ segments[segmentIndex] = {
+ segmentIndex,
label: SegmentLabel || `Segment ${SegmentNumber}`,
- segmentIndex: Number(SegmentNumber),
- category: SegmentedPropertyCategoryCodeSequence
- ? SegmentedPropertyCategoryCodeSequence.CodeMeaning
- : '',
- type: SegmentedPropertyTypeCodeSequence
- ? SegmentedPropertyTypeCodeSequence.CodeMeaning
- : '',
- algorithmType: SegmentAlgorithmType,
- algorithmName: SegmentAlgorithmName,
- color: rgba,
- opacity: 255,
- isVisible: true,
- isLocked: false,
+ locked: false,
+ active: false,
+ cachedStats: {
+ center: {
+ image: [imageCentroidXYZ.x, imageCentroidXYZ.y, imageCentroidXYZ.z],
+ world: [worldCentroidXYZ.x, worldCentroidXYZ.y, worldCentroidXYZ.z],
+ },
+ modifiedTime: segDisplaySet.SeriesDate,
+ category: SegmentedPropertyCategoryCodeSequence
+ ? SegmentedPropertyCategoryCodeSequence.CodeMeaning
+ : '',
+ type: SegmentedPropertyTypeCodeSequence
+ ? SegmentedPropertyTypeCodeSequence.CodeMeaning
+ : '',
+ algorithmType: SegmentAlgorithmType,
+ algorithmName: SegmentAlgorithmName,
+ },
};
});
- segmentation.segmentCount = segmentsInfo.length - 1;
+ // get next color lut index
+ const colorLUTIndex = getNextColorLUTIndex();
+ addColorLUT(colorLUT, colorLUTIndex);
+ this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex);
- segDisplaySet.isLoaded = true;
+ // now we need to chop the volume array into chunks and set the scalar data for each derived segmentation image
+ const volumeScalarData = new Uint8Array(labelmapBufferArray[0]);
+
+ // We should parse the segmentation as separate slices to support overlapping segments.
+ // This parsing should occur in the CornerstoneJS library adapters.
+ // For now, we use the volume returned from the library and chop it here.
+ for (let i = 0; i < derivedSegmentationImages.length; i++) {
+ const voxelManager = derivedSegmentationImages[i]
+ .voxelManager as csTypes.IVoxelManager;
+ const scalarData = voxelManager.getScalarData();
+ scalarData.set(volumeScalarData.slice(i * scalarData.length, (i + 1) * scalarData.length));
+ voxelManager.setScalarData(scalarData);
+ }
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
segmentationId,
segDisplaySet,
});
- return this.addOrUpdateSegmentation(segmentation, suppressEvents);
+ const seg: cstTypes.SegmentationPublicInput = {
+ segmentationId,
+ representation: {
+ type: LABELMAP,
+ data: {
+ imageIds: derivedSegmentationImages.map(image => image.imageId),
+ referencedVolumeId: this._getVolumeIdForDisplaySet(referencedDisplaySet),
+ referencedImageIds: imageIds as string[],
+ },
+ },
+ config: {
+ label: segDisplaySet.SeriesDescription,
+ segments,
+ },
+ };
+
+ segDisplaySet.isLoaded = true;
+
+ this.addOrUpdateSegmentation(seg);
+
+ return segmentationId;
}
public async createSegmentationForRTDisplaySet(
rtDisplaySet,
- segmentationId?: string,
- suppressEvents = false
+ options: {
+ segmentationId?: string;
+ type: SegmentationRepresentations;
+ } = {
+ type: CONTOUR,
+ }
): Promise {
- // Todo: we currently only have support for contour representation for initial
- // RT display
- const representationType = CONTOUR;
+ const { type } = options;
+ let { segmentationId } = options;
+
+ // Currently, only contour representation is supported for RT display
+ if (type !== CONTOUR) {
+ throw new Error('Only contour type is supported for RT display sets right now');
+ }
+
+ // Assign segmentationId if not provided
segmentationId = segmentationId ?? rtDisplaySet.displaySetInstanceUID;
const { structureSet } = rtDisplaySet;
if (!structureSet) {
throw new Error(
- 'To create the contours from RT displaySet, the displaySet should be loaded first, you can perform rtDisplaySet.load() before calling this method.'
+ 'To create the contours from RT displaySet, the displaySet should be loaded first. You can perform rtDisplaySet.load() before calling this method.'
);
}
- const defaultScheme = this._getDefaultSegmentationScheme();
const rtDisplaySetUID = rtDisplaySet.displaySetInstanceUID;
+ // Map ROI contours to RT Struct Data
const allRTStructData = mapROIContoursToRTStructData(structureSet, rtDisplaySetUID);
- // sort by segmentIndex
+ // Sort by segmentIndex for consistency
allRTStructData.sort((a, b) => a.segmentIndex - b.segmentIndex);
const geometryIds = allRTStructData.map(({ geometryId }) => geometryId);
- const segmentation: Segmentation = {
- ...defaultScheme,
- id: segmentationId,
- displaySetInstanceUID: rtDisplaySetUID,
- type: representationType,
- label: rtDisplaySet.SeriesDescription,
- representationData: {
- [CONTOUR]: {
+ // Initialize SegmentationPublicInput similar to SEG function
+ const segmentation: cstTypes.SegmentationPublicInput = {
+ segmentationId,
+ representation: {
+ type: CONTOUR,
+ data: {
geometryIds,
},
},
+ config: {
+ label: rtDisplaySet.SeriesDescription,
+ },
};
- const cachedSegmentation = this.getSegmentation(segmentationId);
-
- if (cachedSegmentation) {
- // if the labelmap 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(segmentation, cachedSegmentation),
- suppressEvents
- );
- }
-
if (!structureSet.ROIContours?.length) {
throw new Error(
'The structureSet does not contain any ROIContours. Please ensure the structureSet is loaded first.'
);
}
- const segmentsCachedStats = {};
- const initializeContour = async rtStructData => {
+
+ const segments: { [segmentIndex: string]: cstTypes.Segment } = {};
+ let segmentsCachedStats = {};
+
+ // Process each segment similarly to the SEG function
+ for (const rtStructData of allRTStructData) {
const { data, id, color, segmentIndex, geometryId } = rtStructData;
- // catch error instead of failing to allow loading to continue
try {
const geometry = await geometryLoader.createAndCacheGeometry(geometryId, {
geometryData: {
@@ -690,228 +584,552 @@ class SegmentationService extends PubSubService {
type: csEnums.GeometryType.CONTOUR,
});
- const contourSet = geometry.data;
- const centroid = contourSet.getCentroid();
+ const contourSet = geometry.data as csTypes.IContourSet;
+ const centroid = contourSet.centroid;
- segmentsCachedStats[segmentIndex] = {
+ segmentsCachedStats = {
center: { world: centroid },
- modifiedTime: rtDisplaySet.SeriesDate, // we use the SeriesDate as the modifiedTime since this is the first time we are creating the segmentation
+ modifiedTime: rtDisplaySet.SeriesDate, // Using SeriesDate as modifiedTime
};
- segmentation.segments[segmentIndex] = {
+ segments[segmentIndex] = {
label: id,
segmentIndex,
- color,
- ...SEGMENT_CONSTANT,
+ cachedStats: segmentsCachedStats,
+ locked: false,
+ active: false,
};
+ // Broadcast segment loading progress
const numInitialized = Object.keys(segmentsCachedStats).length;
-
- // Calculate percentage completed
const percentComplete = Math.round((numInitialized / allRTStructData.length) * 100);
-
this._broadcastEvent(EVENTS.SEGMENT_LOADING_COMPLETE, {
percentComplete,
- // Note: this is not the geometryIds length since there might be
- // some missing ROINumbers
numSegments: allRTStructData.length,
});
} catch (e) {
- console.warn(e);
+ console.warn(`Error initializing contour for segment ${segmentIndex}:`, e);
+ continue; // Continue processing other segments even if one fails
}
- };
-
- const promiseArray = [];
-
- for (let i = 0; i < allRTStructData.length; i++) {
- const promise = new Promise((resolve, reject) => {
- setTimeout(() => {
- initializeContour(allRTStructData[i]).then(() => {
- resolve();
- });
- }, 0);
- });
-
- promiseArray.push(promise);
}
- await Promise.all(promiseArray);
-
- segmentation.segmentCount = allRTStructData.length;
- rtDisplaySet.isLoaded = true;
-
- segmentation.cachedStats = {
- ...segmentation.cachedStats,
- segmentCenter: {
- ...segmentation.cachedStats.segmentCenter,
- ...segmentsCachedStats,
- },
- };
+ // Assign processed segments to segmentation config
+ segmentation.config.segments = segments;
+ // Broadcast segmentation loading complete event
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
segmentationId,
rtDisplaySet,
});
- return this.addOrUpdateSegmentation(segmentation, suppressEvents);
+ // Mark the RT display set as loaded
+ rtDisplaySet.isLoaded = true;
+
+ // Add or update the segmentation in the state
+ this.addOrUpdateSegmentation(segmentation);
+
+ return segmentationId;
}
- // Todo: this should not run on the main thread
- public calculateCentroids = (
- segmentationId: string,
- segmentIndex?: number
- ): Map => {
- const segmentation = this.getSegmentation(segmentationId);
- const volume = this.getLabelmapVolume(segmentationId);
- const { dimensions, imageData } = volume;
- const scalarData = volume.getScalarData();
- const [dimX, dimY, numFrames] = dimensions;
- const frameLength = dimX * dimY;
+ /**
+ * Adds or updates a segmentation in the state
+ * @param segmentationId - The ID of the segmentation to add or update
+ * @param data - The data to add or update the segmentation with
+ *
+ * @remarks
+ * This method handles the addition or update of a segmentation in the state.
+ * If the segmentation already exists, it updates the existing segmentation.
+ * If the segmentation does not exist, it adds a new segmentation.
+ */
+ public addOrUpdateSegmentation(
+ data: cstTypes.SegmentationPublicInput | Partial
+ ) {
+ const segmentationId = data.segmentationId;
+ const existingSegmentation = cstSegmentation.state.getSegmentation(segmentationId);
- const segmentIndices = segmentIndex
- ? [segmentIndex]
- : segmentation.segments
- .filter(segment => segment?.segmentIndex)
- .map(segment => segment.segmentIndex);
+ if (existingSegmentation) {
+ // Update the existing segmentation
+ this.updateSegmentationInSource(segmentationId, data as Partial);
+ } else {
+ // Add a new segmentation
+ this.addSegmentationToSource(data as cstTypes.SegmentationPublicInput);
+ }
+ }
- const segmentIndicesSet = new Set(segmentIndices);
+ public setActiveSegmentation(viewportId: string, segmentationId: string): void {
+ cstSegmentation.activeSegmentation.setActiveSegmentation(viewportId, segmentationId);
+ }
- const centroids = new Map();
- for (const index of segmentIndicesSet) {
- centroids.set(index, { x: 0, y: 0, z: 0, count: 0 });
+ /**
+ * Gets the active segmentation for a viewport
+ * @param viewportId - The ID of the viewport to get the active segmentation for
+ * @returns The active segmentation object, or null if no segmentation is active
+ *
+ * @remarks
+ * This method retrieves the currently active segmentation for the specified viewport.
+ * The active segmentation is the one that is currently selected for editing operations.
+ * Returns null if no segmentation is active in the viewport.
+ */
+ public getActiveSegmentation(viewportId: string): cstTypes.Segmentation | null {
+ return cstSegmentation.activeSegmentation.getActiveSegmentation(viewportId);
+ }
+
+ /**
+ * Gets the active segment from the active segmentation in a viewport
+ * @param viewportId - The ID of the viewport to get the active segment from
+ * @returns The active segment object, or undefined if no segment is active
+ *
+ * @remarks
+ * This method retrieves the currently active segment from the active segmentation
+ * in the specified viewport. The active segment is the one that is currently
+ * selected for editing operations. Returns undefined if no segment is active or
+ * if there is no active segmentation.
+ */
+ public getActiveSegment(viewportId: string): cstTypes.Segment | undefined {
+ const activeSegmentation = this.getActiveSegmentation(viewportId);
+
+ if (!activeSegmentation) {
+ return;
}
- let voxelIndex = 0;
- for (let frame = 0; frame < numFrames; frame++) {
- for (let p = 0; p < frameLength; p++) {
- const segmentIndex = scalarData[voxelIndex++];
- if (segmentIndicesSet.has(segmentIndex)) {
- const centroid = centroids.get(segmentIndex);
- centroid.x += p % dimX;
- centroid.y += (p / dimX) | 0;
- centroid.z += frame;
- centroid.count++;
- }
+ const { segments } = activeSegmentation;
+
+ let activeSegment;
+ for (const segment of Object.values(segments)) {
+ if (segment.active) {
+ activeSegment = segment;
+ break;
}
}
- const result = new Map();
- for (const [index, centroid] of centroids) {
- const count = centroid.count;
- const normalizedCentroid = {
- x: centroid.x / count,
- y: centroid.y / count,
- z: centroid.z / count,
- };
- normalizedCentroid.world = imageData.indexToWorld([
- normalizedCentroid.x,
- normalizedCentroid.y,
- normalizedCentroid.z,
- ]);
- result.set(index, normalizedCentroid);
- }
+ return activeSegment;
+ }
- this.setCentroids(segmentationId, result);
- return result;
+ public hasCustomStyles(specifier: {
+ viewportId: string;
+ segmentationId: string;
+ type: SegmentationRepresentations;
+ }): boolean {
+ return cstSegmentation.config.style.hasCustomStyle(specifier);
+ }
+
+ public getStyle = (specifier: {
+ viewportId: string;
+ segmentationId: string;
+ type: SegmentationRepresentations;
+ segmentIndex?: number;
+ }) => {
+ const style = cstSegmentation.config.style.getStyle(specifier);
+
+ return style;
};
- private setCentroids = (
- segmentationId: string,
- centroids: Map
- ): void => {
- const segmentation = this.getSegmentation(segmentationId);
- const imageData = this.getLabelmapVolume(segmentationId).imageData; // Assuming this method returns imageData
+ public setStyle = (
+ specifier: {
+ type: SegmentationRepresentations;
+ viewportId?: string;
+ segmentationId?: string;
+ segmentIndex?: number;
+ },
+ style: LabelmapStyle | ContourStyle | SurfaceStyle
+ ) => {
+ cstSegmentation.config.style.setStyle(specifier, style);
+ };
- if (!segmentation.cachedStats) {
- segmentation.cachedStats = { segmentCenter: {} };
- } else if (!segmentation.cachedStats.segmentCenter) {
- segmentation.cachedStats.segmentCenter = {};
+ public resetToGlobalStyle = () => {
+ cstSegmentation.config.style.resetToGlobalStyle();
+ };
+
+ /**
+ * Adds a new segment to the specified segmentation.
+ * @param segmentationId - The ID of the segmentation to add the segment to.
+ * @param viewportId: The ID of the viewport to add the segment to, it is used to get the representation, if it is not
+ * provided, the first available representation for the segmentationId will be used.
+ * @param config - An object containing the configuration options for the new segment.
+ * - segmentIndex: (optional) The index of the segment to add. If not provided, the next available index will be used.
+ * - properties: (optional) An object containing the properties of the new segment.
+ * - label: (optional) The label of the new segment. If not provided, a default label will be used.
+ * - color: (optional) The color of the new segment in RGB format. If not provided, a default color will be used.
+ * - visibility: (optional) Whether the new segment should be visible. If not provided, the segment will be visible by default.
+ * - isLocked: (optional) Whether the new segment should be locked for editing. If not provided, the segment will not be locked by default.
+ * - active: (optional) Whether the new segment should be the active segment to be edited. If not provided, the segment will not be active by default.
+ */
+ public addSegment(
+ segmentationId: string,
+ config: {
+ segmentIndex?: number;
+ label?: string;
+ isLocked?: boolean;
+ active?: boolean;
+ color?: csTypes.Color; // Add color type
+ visibility?: boolean; // Add visibility option
+ } = {}
+ ): void {
+ if (config?.segmentIndex === 0) {
+ throw new Error('Segment index 0 is reserved for "no label"');
}
- for (const [segmentIndex, centroid] of centroids) {
- let world = centroid.world;
+ const csSegmentation = this.getCornerstoneSegmentation(segmentationId);
- // If world coordinates are not provided, calculate them
- if (!world || world.length === 0) {
- world = imageData.indexToWorld(centroid.image);
- }
+ let segmentIndex = config.segmentIndex;
+ if (!segmentIndex) {
+ // grab the next available segment index based on the object keys,
+ // so basically get the highest segment index value + 1
+ segmentIndex = Math.max(...Object.keys(csSegmentation.segments).map(Number)) + 1;
+ }
- segmentation.cachedStats.segmentCenter[segmentIndex] = {
- center: {
- image: centroid.image,
- world: world,
+ // update the segmentation
+ if (!config.label) {
+ config.label = `Segment ${segmentIndex}`;
+ }
+
+ const currentSegments = csSegmentation.segments;
+
+ cstSegmentation.updateSegmentations([
+ {
+ segmentationId,
+ payload: {
+ segments: {
+ ...currentSegments,
+ [segmentIndex]: {
+ ...currentSegments[segmentIndex],
+ segmentIndex,
+ cachedStats: {},
+ locked: false,
+ ...config,
+ },
+ },
},
- };
+ },
+ ]);
+
+ this.setActiveSegment(segmentationId, segmentIndex);
+
+ // Apply additional configurations
+ if (config.isLocked !== undefined) {
+ this._setSegmentLockedStatus(segmentationId, segmentIndex, config.isLocked);
}
- this.addOrUpdateSegmentation(segmentation, true, true);
+ // Get all viewports that have this segmentation
+ const viewportIds = this.getViewportIdsWithSegmentation(segmentationId);
+
+ viewportIds.forEach(viewportId => {
+ // Set color if provided
+ if (config.color !== undefined) {
+ this.setSegmentColor(viewportId, segmentationId, segmentIndex, config.color);
+ }
+
+ // Set visibility if provided
+ if (config.visibility !== undefined) {
+ this.setSegmentVisibility(viewportId, segmentationId, segmentIndex, config.visibility);
+ }
+ });
+ }
+
+ /**
+ * Removes a segment from a segmentation and updates the active segment index if necessary.
+ *
+ * @param segmentationId - The ID of the segmentation containing the segment to remove.
+ * @param segmentIndex - The index of the segment to remove.
+ *
+ * @remarks
+ * This method performs the following actions:
+ * 1. Clears the segment value in the Cornerstone segmentation.
+ * 2. Updates all related segmentation representations to remove the segment.
+ * 3. If the removed segment was the active segment, it updates the active segment index.
+ *
+ */
+ public removeSegment(segmentationId: string, segmentIndex: number): void {
+ cstSegmentation.removeSegment(segmentationId, segmentIndex);
+ }
+
+ public setSegmentVisibility(
+ viewportId: string,
+ segmentationId: string,
+ segmentIndex: number,
+ isVisible: boolean,
+ type?: SegmentationRepresentations
+ ): void {
+ this._setSegmentVisibility(viewportId, segmentationId, segmentIndex, isVisible, type);
+ }
+
+ /**
+ * Sets the locked status of a segment in a segmentation.
+ *
+ * @param segmentationId - The ID of the segmentation containing the segment.
+ * @param segmentIndex - The index of the segment to set the locked status for.
+ * @param isLocked - The new locked status of the segment.
+ *
+ * @remarks
+ * This method updates the locked status of a specific segment within a segmentation.
+ * A locked segment cannot be modified or edited.
+ */
+ public setSegmentLocked(segmentationId: string, segmentIndex: number, isLocked: boolean): void {
+ this._setSegmentLockedStatus(segmentationId, segmentIndex, isLocked);
+ }
+
+ /**
+ * Toggles the locked state of a segment in a segmentation.
+ * @param segmentationId - The ID of the segmentation.
+ * @param segmentIndex - The index of the segment to toggle.
+ */
+ public toggleSegmentLocked(segmentationId: string, segmentIndex: number): void {
+ const isLocked = cstSegmentation.segmentLocking.isSegmentIndexLocked(
+ segmentationId,
+ segmentIndex
+ );
+ this._setSegmentLockedStatus(segmentationId, segmentIndex, !isLocked);
+ }
+
+ public toggleSegmentVisibility(
+ viewportId: string,
+ segmentationId: string,
+ segmentIndex: number,
+ type: SegmentationRepresentations
+ ): void {
+ const isVisible = cstSegmentation.config.visibility.getSegmentIndexVisibility(
+ viewportId,
+ {
+ segmentationId,
+ type,
+ },
+ segmentIndex
+ );
+ this._setSegmentVisibility(viewportId, segmentationId, segmentIndex, !isVisible, type);
+ }
+
+ /**
+ * Sets the color of a specific segment in a segmentation.
+ *
+ * @param viewportId - The ID of the viewport containing the segmentation
+ * @param segmentationId - The ID of the segmentation containing the segment
+ * @param segmentIndex - The index of the segment to set the color for
+ * @param color - The new color to apply to the segment as an array of RGBA values
+ *
+ * @remarks
+ * This method updates the color of a specific segment within a segmentation.
+ * The color parameter should be an array of 4 numbers representing RGBA values.
+ */
+ public setSegmentColor(
+ viewportId: string,
+ segmentationId: string,
+ segmentIndex: number,
+ color: csTypes.Color
+ ): void {
+ cstSegmentation.config.color.setSegmentIndexColor(
+ viewportId,
+ segmentationId,
+ segmentIndex,
+ color
+ );
+ }
+
+ /**
+ * Gets the current color of a specific segment in a segmentation.
+ *
+ * @param viewportId - The ID of the viewport containing the segmentation
+ * @param segmentationId - The ID of the segmentation containing the segment
+ * @param segmentIndex - The index of the segment to get the color for
+ * @returns An array of 4 numbers representing the RGBA color values of the segment
+ *
+ * @remarks
+ * This method retrieves the current color of a specific segment within a segmentation.
+ * The returned color is an array of 4 numbers representing RGBA values.
+ */
+ public getSegmentColor(viewportId: string, segmentationId: string, segmentIndex: number) {
+ return cstSegmentation.config.color.getSegmentIndexColor(
+ viewportId,
+ segmentationId,
+ segmentIndex
+ );
+ }
+
+ /**
+ * Gets the labelmap volume for a segmentation
+ * @param segmentationId - The ID of the segmentation to get the labelmap volume for
+ * @returns The labelmap volume for the segmentation, or null if not found
+ *
+ * @remarks
+ * This method retrieves the labelmap volume data for a specific segmentation.
+ * The labelmap volume contains the actual segmentation data in the form of a 3D volume.
+ * Returns null if the segmentation does not have valid labelmap volume data.
+ */
+ public getLabelmapVolume(segmentationId: string) {
+ const csSegmentation = cstSegmentation.state.getSegmentation(segmentationId);
+ const labelmapData = csSegmentation.representationData[
+ SegmentationRepresentations.Labelmap
+ ] as cstTypes.LabelmapToolOperationDataVolume;
+
+ if (!labelmapData || !labelmapData.volumeId) {
+ return null;
+ }
+
+ const { volumeId } = labelmapData;
+ const labelmapVolume = cache.getVolume(volumeId);
+
+ return labelmapVolume;
+ }
+
+ /**
+ * Sets the label for a specific segment in a segmentation
+ * @param segmentationId - The ID of the segmentation containing the segment
+ * @param segmentIndex - The index of the segment to set the label for
+ * @param label - The new label to apply to the segment
+ *
+ * @remarks
+ * This method updates the text label of a specific segment within a segmentation.
+ * The label is used to identify and describe the segment in the UI.
+ */
+ public setSegmentLabel(segmentationId: string, segmentIndex: number, label: string) {
+ this._setSegmentLabel(segmentationId, segmentIndex, label);
+ }
+
+ /**
+ * Sets the active segment for a segmentation
+ * @param segmentationId - The ID of the segmentation containing the segment
+ * @param segmentIndex - The index of the segment to set as active
+ *
+ * @remarks
+ * This method updates which segment is considered "active" within a segmentation.
+ * The active segment is typically highlighted and available for editing operations.
+ */
+ public setActiveSegment(segmentationId: string, segmentIndex: number): void {
+ this._setActiveSegment(segmentationId, segmentIndex);
+ }
+
+ /**
+ * Controls whether inactive segmentations should be rendered in a viewport
+ * @param viewportId - The ID of the viewport to update
+ * @param renderInactive - Whether inactive segmentations should be rendered
+ *
+ * @remarks
+ * This method configures if segmentations that are not currently active
+ * should still be visible in the specified viewport. This can be useful
+ * for comparing or viewing multiple segmentations simultaneously.
+ */
+ public setRenderInactiveSegmentations(viewportId: string, renderInactive: boolean): void {
+ cstSegmentation.config.style.setRenderInactiveSegmentations(viewportId, renderInactive);
+ }
+
+ /**
+ * Gets whether inactive segmentations are being rendered for a viewport
+ * @param viewportId - The ID of the viewport to check
+ * @returns boolean indicating if inactive segmentations are rendered
+ *
+ * @remarks
+ * This method retrieves the current rendering state for inactive segmentations
+ * in the specified viewport. Returns true if inactive segmentations are visible.
+ */
+ public getRenderInactiveSegmentations(viewportId: string): boolean {
+ return cstSegmentation.config.style.getRenderInactiveSegmentations(viewportId);
+ }
+
+ /**
+ * 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 toggleSegmentationRepresentationVisibility = (
+ viewportId: string,
+ { segmentationId, type }: { segmentationId: string; type: SegmentationRepresentations }
+ ): void => {
+ this._toggleSegmentationRepresentationVisibility(viewportId, segmentationId, type);
};
+ public getViewportIdsWithSegmentation = (segmentationId: string): string[] => {
+ const viewportIds = cstSegmentation.state.getViewportIdsWithSegmentation(segmentationId);
+ return viewportIds;
+ };
+
+ /**
+ * Clears segmentation representations from the viewport.
+ * Unlike removeSegmentationRepresentations, this doesn't update
+ * removed display set and representation maps.
+ * We track removed segmentations manually to avoid re-adding them
+ * when the display set is added again.
+ * @param viewportId - The viewport ID to clear segmentation representations from.
+ */
+ public clearSegmentationRepresentations(viewportId: string): void {
+ this.removeSegmentationRepresentations(viewportId);
+ }
+
+ /**
+ * Completely removes a segmentation from the state
+ * @param segmentationId - The ID of the segmentation to remove.
+ */
+ public remove(segmentationId: string): void {
+ cstSegmentation.state.removeSegmentation(segmentationId);
+ }
+
+ public removeAllSegmentations(): void {
+ cstSegmentation.state.removeAllSegmentations();
+ }
+
+ /**
+ * It removes the segmentation representations from the viewport.
+ * @param viewportId - The viewport id to remove the segmentation representations from.
+ * @param specifier - The specifier to remove the segmentation representations.
+ *
+ * @remarks
+ * If no specifier is provided, all segmentation representations for the viewport are removed.
+ * If a segmentationId specifier is provided, only the segmentation representation with the specified segmentationId and type are removed.
+ * If a type specifier is provided, only the segmentation representation with the specified type are removed.
+ * If both a segmentationId and type specifier are provided, only the segmentation representation with the specified segmentationId and type are removed.
+ */
+ public removeSegmentationRepresentations(
+ viewportId: string,
+ specifier: {
+ segmentationId?: string;
+ type?: SegmentationRepresentations;
+ } = {}
+ ): void {
+ cstSegmentation.removeSegmentationRepresentations(viewportId, specifier);
+ }
+
public jumpToSegmentCenter(
segmentationId: string,
segmentIndex: number,
- toolGroupId?: string,
+ viewportId?: 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);
- if (!center?.world) {
+ if (!center) {
return;
}
- const { world } = center;
+ const { world } = center as { world: csTypes.Point3 };
- // todo: generalize
- toolGroupId = toolGroupId || this._getToolGroupIdsWithSegmentation(segmentationId);
+ // need to find which viewports are displaying the segmentation
+ const viewportIds = viewportId
+ ? [viewportId]
+ : this.getViewportIdsWithSegmentation(segmentationId);
- const toolGroups = [];
+ viewportIds.forEach(viewportId => {
+ const { viewport } = getEnabledElementByViewportId(viewportId);
+ viewport.jumpToWorld(world);
- 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);
- if (viewport instanceof StackViewport) {
- const { element } = viewport;
- const index = csUtils.getClosestStackImageIndexForPoint(world, viewport)
- cstUtils.viewport.jumpToSlice(element, { imageIndex: index })
- } else {
- cstUtils.viewport.jumpToWorld(viewport, world);
- }
- }
-
- if (highlightSegment) {
+ highlightSegment &&
this.highlightSegment(
segmentationId,
segmentIndex,
- toolGroup.id,
+ viewportId,
highlightAlpha,
animationLength,
- highlightHideOthers,
- highlightFunctionType
+ highlightHideOthers
);
- }
});
}
public highlightSegment(
segmentationId: string,
segmentIndex: number,
- toolGroupId?: string,
+ viewportId?: string,
alpha = 0.9,
animationLength = 750,
hideOthers = true,
@@ -921,245 +1139,381 @@ class SegmentationService extends PubSubService {
clearInterval(this.highlightIntervalId);
}
- const segmentation = this.getSegmentation(segmentationId);
- toolGroupId = toolGroupId ?? this._getApplicableToolGroupId();
+ const csSegmentation = this.getCornerstoneSegmentation(segmentationId);
- const segmentationRepresentation = this._getSegmentationRepresentation(
- segmentationId,
- toolGroupId
- );
+ const viewportIds = viewportId
+ ? [viewportId]
+ : this.getViewportIdsWithSegmentation(segmentationId);
- const { type } = segmentationRepresentation;
- const { segments } = segmentation;
+ viewportIds.forEach(viewportId => {
+ const segmentationRepresentation = this.getSegmentationRepresentations(viewportId, {
+ segmentationId,
+ });
- const highlightFn =
- type === LABELMAP ? this._highlightLabelmap.bind(this) : this._highlightContour.bind(this);
+ const representation = segmentationRepresentation[0];
+ const { type } = representation;
+ const segments = csSegmentation.segments;
- const adjustedAlpha = type === LABELMAP ? alpha : 1 - alpha;
+ const highlightFn =
+ type === LABELMAP ? this._highlightLabelmap.bind(this) : this._highlightContour.bind(this);
- highlightFn(
- segmentIndex,
- adjustedAlpha,
- hideOthers,
- segments,
- toolGroupId,
- animationLength,
- segmentationRepresentation
- );
+ const adjustedAlpha = type === LABELMAP ? alpha : 1 - alpha;
+
+ highlightFn(
+ segmentIndex,
+ adjustedAlpha,
+ hideOthers,
+ segments,
+ viewportId,
+ animationLength,
+ representation
+ );
+ });
}
- public createSegmentationForDisplaySet = async (
- displaySetInstanceUID: string,
- options?: {
- segmentationId: string;
- FrameOfReferenceUID: string;
- label: string;
+ private getAndValidateViewport(viewportId: string) {
+ const csViewport =
+ this.servicesManager.services.cornerstoneViewportService.getCornerstoneViewport(viewportId);
+ if (!csViewport) {
+ throw new Error(`Viewport with id ${viewportId} not found.`);
}
- ): Promise => {
- const { displaySetService } = this.servicesManager.services;
-
- const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
-
- // Todo: we currently only support labelmap for segmentation for a displaySet
- const representationType = LABELMAP;
-
- const volumeId = this._getVolumeIdForDisplaySet(displaySet);
-
- 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.createAndCacheDerivedSegmentationVolume(volumeId, {
- volumeId: segmentationId,
- targetBuffer: {
- type: 'Uint8Array',
- sharedArrayBuffer: window.SharedArrayBuffer,
- },
- });
-
- const defaultScheme = this._getDefaultSegmentationScheme();
-
- const segmentation: Segmentation = {
- ...defaultScheme,
- id: segmentationId,
- displaySetInstanceUID,
- label: options?.label,
- // We should set it as active by default, as it created for display
- isActive: true,
- type: representationType,
- FrameOfReferenceUID:
- options?.FrameOfReferenceUID || displaySet.instances?.[0]?.FrameOfReferenceUID,
- representationData: {
- LABELMAP: {
- volumeId: segmentationId,
- referencedVolumeId: volumeId, // Todo: this is so ugly
- },
- },
- description: `S${displaySet.SeriesNumber}: ${displaySet.SeriesDescription}`,
- };
-
- this.addOrUpdateSegmentation(segmentation);
-
- return segmentationId;
- };
+ return csViewport;
+ }
/**
- * 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
+ * Sets the visibility of a segmentation representation.
+ *
+ * @param viewportId - The ID of the viewport.
+ * @param segmentationId - The ID of the segmentation.
+ * @param isVisible - The new visibility state.
*/
- public toggleSegmentationVisibility = (segmentationId: string): void => {
- this._toggleSegmentationVisibility(segmentationId, false);
- };
-
- public addSegmentationRepresentationToToolGroup = async (
- toolGroupId: string,
+ private _setSegmentationRepresentationVisibility(
+ viewportId: string,
segmentationId: string,
- hydrateSegmentation = false,
- representationType = csToolsEnums.SegmentationRepresentations.Labelmap,
- suppressEvents = false
- ): Promise => {
- const segmentation = this.getSegmentation(segmentationId);
-
- toolGroupId = toolGroupId || this._getApplicableToolGroupId();
-
- 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;
- }
-
- // 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(
+ type: SegmentationRepresentations,
+ isVisible: boolean
+ ): void {
+ const representations = this.getSegmentationRepresentations(viewportId, {
segmentationId,
- toolGroupId,
- segmentationRepresentationUIDs[0]
- );
-
- // 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) {
- this._setSegmentLocked(segmentationId, segmentIndex, isLocked, suppressEvents);
- }
- }
-
- if (!suppressEvents) {
- this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
- segmentation,
- });
- }
- };
-
- public setSegmentRGBAColor = (
- 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,
+ type,
});
- };
+ const representation = representations[0];
- 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;
-
- // Not all segmentations have dipslaysets, some of them are derived in the client
- this._setDisplaySetIsHydrated(segmentationId, true);
-
- if (!suppressEvents) {
- this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
- segmentation,
- });
- }
- };
-
- private _setDisplaySetIsHydrated(displaySetUID: string, isHydrated: boolean): void {
- const { displaySetService } = this.servicesManager.services;
- const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
-
- if (!displaySet) {
+ if (!representation) {
+ console.debug(
+ 'No segmentation representation found for the given viewportId and segmentationId'
+ );
return;
}
- displaySet.isHydrated = isHydrated;
- displaySetService.setDisplaySetMetadataInvalidated(displaySetUID, false);
+ cstSegmentation.config.visibility.setSegmentationRepresentationVisibility(
+ viewportId,
+ {
+ segmentationId,
+ type,
+ },
+ isVisible
+ );
+ }
- this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
- segmentation: this.getSegmentation(displaySetUID),
+ private determineViewportAndSegmentationType(csViewport, segmentation) {
+ const isVolumeViewport =
+ csViewport.type === ViewportType.ORTHOGRAPHIC || csViewport.type === ViewportType.VOLUME_3D;
+ const isVolumeSegmentation = 'volumeId' in segmentation.representationData[LABELMAP];
+ return { isVolumeViewport, isVolumeSegmentation };
+ }
+
+ private async handleViewportConversion(
+ isVolumeViewport: boolean,
+ isVolumeSegmentation: boolean,
+ csViewport: csTypes.IViewport,
+ segmentation: cstTypes.Segmentation,
+ viewportId: string,
+ segmentationId: string,
+ representationType: csToolsEnums.SegmentationRepresentations
+ ) {
+ let representationTypeToUse = representationType;
+ let isConverted = false;
+
+ const handler = isVolumeViewport ? this.handleVolumeViewportCase : this.handleStackViewportCase;
+
+ ({ representationTypeToUse, isConverted } = await handler.apply(this, [
+ csViewport,
+ segmentation,
+ isVolumeSegmentation,
+ viewportId,
+ segmentationId,
+ ]));
+
+ return { representationTypeToUse, isConverted };
+ }
+
+ private async handleVolumeViewportCase(csViewport, segmentation, isVolumeSegmentation) {
+ if (csViewport.type === ViewportType.VOLUME_3D) {
+ return { representationTypeToUse: SegmentationRepresentations.Surface, isConverted: false };
+ } else {
+ await this.handleVolumeViewport(
+ csViewport as csTypes.IVolumeViewport,
+ segmentation,
+ isVolumeSegmentation
+ );
+ return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted: false };
+ }
+ }
+
+ private async handleStackViewportCase(
+ csViewport: csTypes.IViewport,
+ segmentation: cstTypes.Segmentation,
+ isVolumeSegmentation: boolean,
+ viewportId: string,
+ segmentationId: string
+ ): Promise<{ representationTypeToUse: SegmentationRepresentations; isConverted: boolean }> {
+ if (isVolumeSegmentation) {
+ const isConverted = await this.convertStackToVolumeViewport(csViewport);
+ return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted };
+ }
+
+ if (updateLabelmapSegmentationImageReferences(viewportId, segmentationId)) {
+ return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted: false };
+ }
+
+ const isConverted = await this.attemptStackToVolumeConversion(
+ csViewport as csTypes.IStackViewport,
+ segmentation,
+ viewportId,
+ segmentationId
+ );
+
+ return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted };
+ }
+
+ private async _addSegmentationRepresentation(
+ viewportId: string,
+ segmentationId: string,
+ representationType: csToolsEnums.SegmentationRepresentations,
+ colorLUTIndex: number,
+ isConverted: boolean
+ ): Promise {
+ const representation = {
+ type: representationType,
+ segmentationId,
+ config: { colorLUTOrIndex: colorLUTIndex },
+ };
+
+ const addRepresentation = () =>
+ cstSegmentation.addSegmentationRepresentations(viewportId, [representation]);
+
+ if (isConverted) {
+ const { viewportGridService } = this.servicesManager.services;
+ await new Promise(resolve => {
+ const { unsubscribe } = viewportGridService.subscribe(
+ viewportGridService.EVENTS.GRID_STATE_CHANGED,
+ () => {
+ addRepresentation();
+ unsubscribe();
+ resolve();
+ }
+ );
+ });
+ } else {
+ addRepresentation();
+ }
+ }
+ private async handleVolumeViewport(
+ viewport: csTypes.IVolumeViewport,
+ segmentation: SegmentationData,
+ isVolumeSegmentation: boolean
+ ): Promise {
+ if (isVolumeSegmentation) {
+ return; // Volume Labelmap on Volume Viewport is natively supported
+ }
+
+ const frameOfReferenceUID = viewport.getFrameOfReferenceUID();
+ const imageIds = getLabelmapImageIds(segmentation.segmentationId);
+ const segImage = cache.getImage(imageIds[0]);
+
+ if (segImage?.FrameOfReferenceUID === frameOfReferenceUID) {
+ await convertStackToVolumeLabelmap(segmentation);
+ }
+ }
+
+ private async convertStackToVolumeViewport(viewport: csTypes.IViewport): Promise {
+ const { viewportGridService, cornerstoneViewportService } = this.servicesManager.services;
+ const state = viewportGridService.getState();
+ const gridViewport = state.viewports.get(viewport.id);
+
+ const prevViewPresentation = viewport.getViewPresentation();
+ const prevViewReference = viewport.getViewReference();
+ const stackViewport = cornerstoneViewportService.getCornerstoneViewport(viewport.id);
+ const { element } = stackViewport;
+
+ const volumeViewportNewVolumeHandler = () => {
+ const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewport.id);
+ volumeViewport.setViewPresentation(prevViewPresentation);
+ volumeViewport.setViewReference(prevViewReference);
+ volumeViewport.render();
+
+ element.removeEventListener(
+ csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
+ volumeViewportNewVolumeHandler
+ );
+ };
+
+ element.addEventListener(
+ csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
+ volumeViewportNewVolumeHandler
+ );
+
+ viewportGridService.setDisplaySetsForViewport({
+ viewportId: viewport.id,
+ displaySetInstanceUIDs: gridViewport.displaySetInstanceUIDs,
+ viewportOptions: {
+ ...gridViewport.viewportOptions,
+ viewportType: ViewportType.ORTHOGRAPHIC,
+ },
});
+
+ return true;
+ }
+
+ private async attemptStackToVolumeConversion(
+ viewport: csTypes.IStackViewport,
+ segmentation: SegmentationData,
+ viewportId: string,
+ segmentationId: string
+ ): Promise {
+ const imageIds = getLabelmapImageIds(segmentation.segmentationId);
+ const frameOfReferenceUID = viewport.getFrameOfReferenceUID();
+ const segImage = cache.getImage(imageIds[0]);
+
+ if (segImage?.FrameOfReferenceUID === frameOfReferenceUID) {
+ const isConverted = await this.convertStackToVolumeViewport(viewport);
+ triggerSegmentationRepresentationModified(
+ viewportId,
+ segmentationId,
+ SegmentationRepresentations.Labelmap
+ );
+
+ return isConverted;
+ }
+ }
+
+ private addSegmentationToSource(segmentationPublicInput: cstTypes.SegmentationPublicInput) {
+ cstSegmentation.addSegmentations([segmentationPublicInput]);
+ }
+
+ private updateSegmentationInSource(
+ segmentationId: string,
+ payload: Partial
+ ) {
+ cstSegmentation.updateSegmentations([{ segmentationId, payload }]);
+ }
+
+ private _toOHIFSegmentationRepresentation(
+ viewportId: string,
+ csRepresentation: cstTypes.SegmentationRepresentation
+ ): SegmentationRepresentation {
+ const { segmentationId, type, active, visible } = csRepresentation;
+ const { colorLUTIndex } = csRepresentation;
+
+ const segmentsRepresentations: { [segmentIndex: number]: SegmentRepresentation } = {};
+
+ const segmentation = cstSegmentation.state.getSegmentation(segmentationId);
+
+ if (!segmentation) {
+ throw new Error(`Segmentation with ID ${segmentationId} not found.`);
+ }
+
+ const segmentIds = Object.keys(segmentation.segments);
+
+ for (const segmentId of segmentIds) {
+ const segmentIndex = parseInt(segmentId, 10);
+
+ const color = cstSegmentation.config.color.getSegmentIndexColor(
+ viewportId,
+ segmentationId,
+ segmentIndex
+ );
+
+ const isVisible = cstSegmentation.config.visibility.getSegmentIndexVisibility(
+ viewportId,
+ {
+ segmentationId,
+ type,
+ },
+ segmentIndex
+ );
+
+ segmentsRepresentations[segmentIndex] = {
+ color,
+ segmentIndex,
+ opacity: color[3],
+ visible: isVisible,
+ };
+ }
+
+ const styles = cstSegmentation.config.style.getStyle({
+ viewportId,
+ segmentationId,
+ type,
+ });
+
+ const id = `${segmentationId}-${type}-${viewportId}`;
+
+ return {
+ id: id,
+ segmentationId,
+ label: segmentation.label,
+ active,
+ type,
+ visible,
+ segments: segmentsRepresentations,
+ styles,
+ viewportId,
+ colorLUTIndex,
+ config: {},
+ };
+ }
+
+ private _initSegmentationService() {
+ eventTarget.addEventListener(
+ csToolsEnums.Events.SEGMENTATION_MODIFIED,
+ this._onSegmentationModifiedFromSource
+ );
+
+ eventTarget.addEventListener(
+ csToolsEnums.Events.SEGMENTATION_REMOVED,
+ this._onSegmentationModifiedFromSource
+ );
+
+ eventTarget.addEventListener(
+ csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED,
+ this._onSegmentationDataModifiedFromSource
+ );
+
+ eventTarget.addEventListener(
+ csToolsEnums.Events.SEGMENTATION_REPRESENTATION_MODIFIED,
+ this._onSegmentationRepresentationModifiedFromSource
+ );
+
+ eventTarget.addEventListener(
+ csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED,
+ this._onSegmentationRepresentationModifiedFromSource
+ );
+
+ eventTarget.addEventListener(
+ csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED,
+ this._onSegmentationRepresentationModifiedFromSource
+ );
+
+ eventTarget.addEventListener(
+ csToolsEnums.Events.SEGMENTATION_ADDED,
+ this._onSegmentationAddedFromSource
+ );
+ }
+
+ private getCornerstoneSegmentation(segmentationId: string) {
+ return cstSegmentation.state.getSegmentation(segmentationId);
}
private _highlightLabelmap(
@@ -1167,31 +1521,32 @@ class SegmentationService extends PubSubService {
alpha: number,
hideOthers: boolean,
segments: Segment[],
- toolGroupId: string,
+ viewportId: string,
animationLength: number,
- segmentationRepresentation: cstTypes.ToolGroupSpecificRepresentation
+ representation: cstTypes.SegmentationRepresentation
) {
+ const { segmentationId } = representation;
const newSegmentSpecificConfig = {
- [segmentIndex]: {
- LABELMAP: {
- fillAlpha: alpha,
- },
- },
+ fillAlpha: alpha,
};
if (hideOthers) {
+ throw new Error('hideOthers is not working right now');
for (let i = 0; i < segments.length; i++) {
if (i !== segmentIndex) {
newSegmentSpecificConfig[i] = {
- LABELMAP: {
- fillAlpha: 0,
- },
+ fillAlpha: 0,
};
}
}
}
- const { fillAlpha } = this.getConfiguration(toolGroupId);
+ const { fillAlpha } = this.getStyle({
+ viewportId,
+ segmentationId,
+ type: LABELMAP,
+ segmentIndex,
+ }) as cstTypes.LabelmapStyle;
let startTime: number = null;
const animation = (timestamp: number) => {
@@ -1202,24 +1557,26 @@ class SegmentationService extends PubSubService {
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / animationLength, 1);
- cstSegmentation.config.setSegmentSpecificConfig(
- toolGroupId,
- segmentationRepresentation.segmentationRepresentationUID,
+ cstSegmentation.config.style.setStyle(
{
- [segmentIndex]: {
- LABELMAP: {
- fillAlpha: easeInOutBell(progress, fillAlpha),
- },
- },
+ segmentationId,
+ segmentIndex,
+ type: LABELMAP,
+ },
+ {
+ fillAlpha: easeInOutBell(progress, fillAlpha),
}
);
if (progress < 1) {
requestAnimationFrame(animation);
} else {
- cstSegmentation.config.setSegmentSpecificConfig(
- toolGroupId,
- segmentationRepresentation.segmentationRepresentationUID,
+ cstSegmentation.config.style.setStyle(
+ {
+ segmentationId,
+ segmentIndex,
+ type: LABELMAP,
+ },
{}
);
}
@@ -1233,34 +1590,48 @@ class SegmentationService extends PubSubService {
alpha: number,
hideOthers: boolean,
segments: Segment[],
- toolGroupId: string,
+ viewportId: string,
animationLength: number,
- segmentationRepresentation: cstTypes.ToolGroupSpecificRepresentation
+ representation: cstTypes.SegmentationRepresentation
) {
+ const { segmentationId } = representation;
const startTime = performance.now();
+ const prevStyle = cstSegmentation.config.style.getStyle({
+ viewportId,
+ segmentationId,
+ type: CONTOUR,
+ segmentIndex,
+ }) as ContourStyle;
+
+ const prevOutlineWidth = prevStyle.outlineWidth;
+ // make this configurable
+ const baseline = Math.max(prevOutlineWidth * 3.5, 5);
+
const animate = (currentTime: number) => {
const progress = (currentTime - startTime) / animationLength;
if (progress >= 1) {
- cstSegmentation.config.setSegmentSpecificConfig(
- toolGroupId,
- segmentationRepresentation.segmentationRepresentationUID,
+ cstSegmentation.config.style.setStyle(
+ {
+ segmentationId,
+ segmentIndex,
+ type: CONTOUR,
+ },
{}
);
return;
}
- const reversedProgress = reverseEaseInOutBell(progress, 0.1);
- cstSegmentation.config.setSegmentSpecificConfig(
- toolGroupId,
- segmentationRepresentation.segmentationRepresentationUID,
+ const reversedProgress = reverseEaseInOutBell(progress, baseline);
+
+ cstSegmentation.config.style.setStyle(
{
- [segmentIndex]: {
- CONTOUR: {
- outlineOpacity: reversedProgress,
- fillAlpha: reversedProgress,
- },
- },
+ segmentationId,
+ segmentIndex,
+ type: CONTOUR,
+ },
+ {
+ outlineWidth: reversedProgress,
}
);
@@ -1270,337 +1641,33 @@ class SegmentationService extends PubSubService {
requestAnimationFrame(animate);
}
- public removeSegmentationRepresentationFromToolGroup(
- toolGroupId: string,
- segmentationRepresentationUIDsIds?: string[]
- ): void {
- const uids = segmentationRepresentationUIDsIds || [];
- if (!uids.length) {
- const representations = cstSegmentation.state.getSegmentationRepresentations(toolGroupId);
-
- if (!representations || !representations.length) {
- return;
- }
-
- uids.push(...representations.map(rep => rep.segmentationRepresentationUID));
- }
-
- cstSegmentation.removeSegmentationsFromToolGroup(toolGroupId, uids);
- }
-
- /**
- * 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;
- const { updatedToolGroupIds } = 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();
-
- const remainingHydratedSegmentations = remainingSegmentations.filter(
- segmentation => segmentation.hydrated
- );
-
- if (remainingHydratedSegmentations.length) {
- const { id } = remainingHydratedSegmentations[0];
-
- updatedToolGroupIds.forEach(toolGroupId => {
- this._setActiveSegmentationForToolGroup(id, toolGroupId, false);
- });
- }
- }
-
- this._setDisplaySetIsHydrated(segmentationId, false);
-
- this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, {
+ private _toggleSegmentationRepresentationVisibility = (
+ viewportId: string,
+ segmentationId: string,
+ type: SegmentationRepresentations
+ ): void => {
+ const representations = this.getSegmentationRepresentations(viewportId, {
segmentationId,
+ type,
});
- }
+ const representation = representations[0];
- public getConfiguration = (toolGroupId?: string): SegmentationConfig => {
- toolGroupId = toolGroupId ?? this._getApplicableToolGroupId();
-
- const brushSize = 1;
- // const brushSize = cstUtils.segmentation.getBrushSizeForToolGroup(
- // toolGroupId
- // );
-
- const brushThresholdGate = 1;
- // const brushThresholdGate = cstUtils.segmentation.getBrushThresholdForToolGroup(
- // toolGroupId
- // );
-
- const segmentationRepresentations =
- this.getSegmentationRepresentationsForToolGroup(toolGroupId);
-
- const typeToUse = segmentationRepresentations?.[0]?.type || LABELMAP;
-
- const config = cstSegmentation.config.getGlobalConfig();
- const { renderInactiveSegmentations } = config;
-
- const representation = config.representations[typeToUse];
-
- const {
- renderOutline,
- outlineWidthActive,
- renderFill,
- fillAlpha,
- fillAlphaInactive,
- outlineOpacity,
- outlineOpacityInactive,
- } = representation;
-
- return {
- brushSize,
- brushThresholdGate,
- fillAlpha,
- fillAlphaInactive,
- outlineWidthActive,
- renderFill,
- renderInactiveSegmentations,
- renderOutline,
- outlineOpacity,
- outlineOpacityInactive,
- };
- };
-
- public setConfiguration = (configuration: SegmentationConfig): void => {
- const {
- fillAlpha,
- fillAlphaInactive,
- outlineWidthActive,
- outlineOpacity,
- renderFill,
- renderInactiveSegmentations,
- renderOutline,
- } = configuration;
-
- const setConfigValueIfDefined = (key, value, transformFn = null) => {
- if (value !== undefined) {
- const transformedValue = transformFn ? transformFn(value) : value;
- this._setSegmentationConfig(key, transformedValue);
- }
- };
-
- setConfigValueIfDefined('renderOutline', renderOutline);
- setConfigValueIfDefined('outlineWidthActive', outlineWidthActive);
- setConfigValueIfDefined('outlineOpacity', outlineOpacity, v => v / 100);
- setConfigValueIfDefined('fillAlpha', fillAlpha, v => v / 100);
- setConfigValueIfDefined('renderFill', renderFill);
- setConfigValueIfDefined('fillAlphaInactive', fillAlphaInactive, v => v / 100);
- setConfigValueIfDefined('outlineOpacityInactive', fillAlphaInactive, v =>
- Math.max(0.75, v / 100)
- );
-
- if (renderInactiveSegmentations !== undefined) {
- const config = cstSegmentation.config.getGlobalConfig();
- config.renderInactiveSegmentations = renderInactiveSegmentations;
- cstSegmentation.config.setGlobalConfig(config);
- }
-
- // 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 setSegmentLabel(segmentationId: string, segmentIndex: number, label: string) {
- this._setSegmentLabel(segmentationId, segmentIndex, label);
- }
-
- private _setSegmentLabel(
- 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._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
- segmentation,
- });
- }
- }
-
- public shouldRenderSegmentation(viewportDisplaySetInstanceUIDs, segmentationFrameOfReferenceUID) {
- if (!viewportDisplaySetInstanceUIDs?.length) {
- return false;
- }
-
- const { displaySetService } = this.servicesManager.services;
-
- let shouldDisplaySeg = false;
-
- // check if the displaySet is sharing the same frameOfReferenceUID
- // with the new segmentation
- for (const displaySetInstanceUID of viewportDisplaySetInstanceUIDs) {
- 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 === segmentationFrameOfReferenceUID
- ) {
- shouldDisplaySeg = true;
- break;
- }
- }
-
- return shouldDisplaySeg;
- }
-
- private _getDefaultSegmentationScheme() {
- return {
- activeSegmentIndex: 1,
- cachedStats: {},
- label: '',
- segmentsLocked: [],
- displayText: [],
- hydrated: false, // by default we don't hydrate the segmentation for SEG displaySets
- segmentCount: 0,
- segments: [],
- isVisible: true,
- isActive: false,
- };
- }
-
- 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 segmentsHidden = cstSegmentation.config.visibility.getHiddenSegmentIndices(viewportId, {
+ segmentationId,
+ type: representation.type,
});
- const representation = this._getSegmentationRepresentation(segmentationId, toolGroupId);
-
- cstSegmentation.activeSegmentation.setActiveSegmentationRepresentation(
- toolGroupId,
- representation.segmentationRepresentationUID
+ const currentVisibility = segmentsHidden.size === 0;
+ this._setSegmentationRepresentationVisibility(
+ viewportId,
+ segmentationId,
+ representation.type,
+ !currentVisibility
);
-
- 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}`);
- }
-
+ private _setActiveSegment(segmentationId: string, segmentIndex: number) {
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 _getVolumeIdForDisplaySet(displaySet) {
@@ -1609,59 +1676,6 @@ class SegmentationService extends PubSubService {
return `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
}
- private _setSegmentColor = (
- segmentationId: string,
- segmentIndex: number,
- color: ohifTypes.RGB,
- 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._getApplicableToolGroupId();
-
- 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);
@@ -1669,463 +1683,90 @@ class SegmentationService extends PubSubService {
return;
}
- const { cachedStats } = segmentation;
+ const { segments } = segmentation;
+
+ const { cachedStats } = segments[segmentIndex];
if (!cachedStats) {
return;
}
- const { segmentCenter } = cachedStats;
-
- if (!segmentCenter) {
- return;
- }
-
- const { center } = segmentCenter[segmentIndex];
+ const { center } = cachedStats;
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;
-
+ private _setSegmentLockedStatus(segmentationId: string, segmentIndex: number, isLocked: boolean) {
cstSegmentation.segmentLocking.setSegmentIndexLocked(segmentationId, segmentIndex, isLocked);
-
- if (suppressEvents === false) {
- this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
- segmentation,
- });
- }
}
private _setSegmentVisibility(
+ viewportId: string,
segmentationId: string,
segmentIndex: number,
isVisible: boolean,
- toolGroupId?: string,
- suppressEvents = false
+ type?: SegmentationRepresentations
) {
- toolGroupId = toolGroupId ?? this._getApplicableToolGroupId();
-
- 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.setSegmentVisibility(
- toolGroupId,
- segmentationRepresentationUID,
+ cstSegmentation.config.visibility.setSegmentIndexVisibility(
+ viewportId,
+ {
+ segmentationId,
+ type,
+ },
segmentIndex,
isVisible
);
-
- // make sure to update the isVisible flag on the segmentation
- // if a segment becomes invisible then the segmentation should be invisible
- // in the status as well, and show correct icon
- segmentation.isVisible = segmentation.segments
- .filter(Boolean)
- .every(segment => segment.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);
+ private _setSegmentLabel(segmentationId: string, segmentIndex: number, segmentLabel: string) {
+ const segmentation = this.getCornerstoneSegmentation(segmentationId);
+ const { segments } = segmentation;
- if (segmentation === undefined) {
- throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
- }
+ segments[segmentIndex].label = segmentLabel;
- const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
-
- if (segmentInfo === undefined) {
- throw new Error(`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`);
- }
-
- toolGroupId = toolGroupId ?? this._getApplicableToolGroupId();
-
- 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) {
- 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 _setSegmentationConfig = (property, value) => {
- // Todo: currently we only support global config, and we get the type
- // from the first segmentation
- const typeToUse = this.getSegmentations()[0].type;
-
- const { cornerstoneViewportService } = this.servicesManager.services;
-
- const config = cstSegmentation.config.getGlobalConfig();
-
- config.representations[typeToUse][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._onSegmentationModifiedFromSource
- );
-
- 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 _onSegmentationModifiedFromSource = 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;
- }
-
- const { activeSegmentIndex, cachedStats, segmentsLocked, label, type } = segmentationState;
-
- if (![LABELMAP, CONTOUR].includes(type)) {
- throw new Error(
- `Unsupported segmentation type: ${type}. Only ${LABELMAP} and ${CONTOUR} are supported.`
- );
- }
-
- const representationData = segmentationState.representationData[type];
-
- // TODO: handle other representations when available in cornerstone3D
- const segmentationSchema = {
- ...segmentation,
- activeSegmentIndex,
- cachedStats,
- displayText: [],
- id: segmentationId,
- label,
- segmentsLocked,
- type,
- representationData: {
- [type]: {
- ...representationData,
+ cstSegmentation.updateSegmentations([
+ {
+ segmentationId,
+ payload: {
+ segments,
},
},
- };
+ ]);
+ }
- 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(
+ private _onSegmentationDataModifiedFromSource = evt => {
+ const { segmentationId } = evt.detail;
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_DATA_MODIFIED, {
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);
- const updatedToolGroupIds: Set = new Set();
-
- 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);
- updatedToolGroupIds.add(toolGroupId);
- }
- });
-
- // remove segmentation representations
- cstSegmentation.removeSegmentationsFromToolGroup(
- toolGroupId,
- UIDsToRemove,
- true // immediate
- );
- });
-
- // cleanup the segmentation state too
- segmentationState.removeSegmentation(segmentationId);
-
- if (removeFromCache && cache.getVolumeLoadObject(segmentationId)) {
- cache.removeVolumeLoadObject(segmentationId);
- }
-
- return { updatedToolGroupIds: Array.from(updatedToolGroupIds) };
- }
-
- 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 { segmentsHidden } = representation;
-
- const currentVisibility = segmentsHidden.size === 0 ? true : false;
- const newVisibility = !currentVisibility;
-
- cstSegmentation.config.visibility.setSegmentationVisibility(
- toolGroupId,
- representation.segmentationRepresentationUID,
- newVisibility
- );
-
- // update segments visibility
- const { segmentation } = this._getSegmentationInfo(segmentationId, toolGroupId);
-
- const segments = segmentation.segments.filter(Boolean);
-
- segments.forEach(segment => {
- segment.isVisible = newVisibility;
- });
});
};
- private _getToolGroupIdsWithSegmentation(segmentationId: string) {
- const segmentationState = cstSegmentation.state;
- const toolGroupIds = segmentationState.getToolGroupIdsWithSegmentation(segmentationId);
-
- return toolGroupIds;
- }
-
- private _getFrameOfReferenceUIDForSeg(displaySet) {
- const frameOfReferenceUID = displaySet.instance?.FrameOfReferenceUID;
-
- if (frameOfReferenceUID) {
- return frameOfReferenceUID;
- }
-
- // if not found we should try the ReferencedFrameOfReferenceSequence
- const referencedFrameOfReferenceSequence =
- displaySet.instance?.ReferencedFrameOfReferenceSequence;
-
- if (referencedFrameOfReferenceSequence) {
- return referencedFrameOfReferenceSequence.FrameOfReferenceUID;
- }
- }
-
- private _getApplicableToolGroupId = () => {
- const { toolGroupService, viewportGridService, cornerstoneViewportService } =
- this.servicesManager.services;
-
- const viewportInfo = cornerstoneViewportService.getViewportInfo(
- viewportGridService.getActiveViewportId()
- );
-
- if (!viewportInfo) {
- const toolGroupIds = toolGroupService.getToolGroupIds();
-
- return toolGroupIds[0];
- }
-
- return viewportInfo.getToolGroupId();
+ private _onSegmentationRepresentationModifiedFromSource = evt => {
+ const { segmentationId, viewportId } = evt.detail;
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, {
+ segmentationId,
+ viewportId,
+ });
};
- /**
- * Converts object of objects to array.
- *
- * @return {Array} Array of objects
- */
- private arrayOfObjects = obj => {
- return Object.entries(obj).map(e => ({ [e[0]]: e[1] }));
+ private _onSegmentationModifiedFromSource = (
+ evt: cstTypes.EventTypes.SegmentationModifiedEventType
+ ) => {
+ const { segmentationId } = evt.detail;
+
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_MODIFIED, {
+ segmentationId,
+ });
+ };
+
+ private _onSegmentationAddedFromSource = (
+ evt: cstTypes.EventTypes.SegmentationAddedEventType
+ ) => {
+ const { segmentationId } = evt.detail;
+
+ this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, {
+ segmentationId,
+ });
};
}
diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts
deleted file mode 100644
index 68c6540cb..000000000
--- a/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-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;
- // display texts
- displayText?: string[];
- // The name of algorithm used to generate the segment. (0062,0009)
- algorithmName?: string;
- // Type of algorithm used to generate the segment. (0062,0008)
- algorithmType?: string;
-};
-
-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;
- // displaySetInstanceUID
- displaySetInstanceUID: string;
- // 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 frame of reference UID of the segmentation
- FrameOfReferenceUID: string;
- // 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;
- // 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 type of the segmentation (e.g., Labelmap etc.)
- type: csToolsEnums.SegmentationRepresentations;
- // the segmentation representation data
- representationData: SegmentationRepresentationData;
-};
-
-type LabelmapSegmentationData = {
- volumeId: string;
- referencedVolumeId?: string;
-};
-
-type SegmentationRepresentationData = {
- LABELMAP?: LabelmapSegmentationData;
-};
-
-export type { SegmentationConfig, Segment, Segmentation };
diff --git a/extensions/cornerstone/src/services/SegmentationService/index.js b/extensions/cornerstone/src/services/SegmentationService/index.ts
similarity index 100%
rename from extensions/cornerstone/src/services/SegmentationService/index.js
rename to extensions/cornerstone/src/services/SegmentationService/index.ts
diff --git a/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts b/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts
index 030dd72d8..517231eb9 100644
--- a/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts
+++ b/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts
@@ -2,6 +2,7 @@ import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs
import { getRenderingEngines, utilities } from '@cornerstonejs/core';
import { pubSubServiceInterface, Types } from '@ohif/core';
+import createHydrateSegmentationSynchronizer from './createHydrateSegmentationSynchronizer';
const EVENTS = {
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
@@ -27,6 +28,7 @@ const VOI = 'voi';
const ZOOMPAN = 'zoompan';
const STACKIMAGE = 'stackimage';
const IMAGE_SLICE = 'imageslice';
+const HYDRATE_SEG = 'hydrateseg';
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
@@ -51,6 +53,7 @@ export default class SyncGroupService {
// handles both stack and volume viewports
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
[IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer,
+ [HYDRATE_SEG]: createHydrateSegmentationSynchronizer,
};
synchronizersByType: { [key: string]: Synchronizer[] } = {};
@@ -66,18 +69,18 @@ export default class SyncGroupService {
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
// Initialize if not already done
this.synchronizersByType[type] = this.synchronizersByType[type] || [];
-
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
if (syncCreator) {
- const synchronizer = syncCreator(id, options);
+ // Pass the servicesManager along with other parameters
+ const synchronizer = syncCreator(id, { ...options, servicesManager: this.servicesManager });
if (synchronizer) {
this.synchronizersByType[type].push(synchronizer);
return synchronizer;
}
} else {
- console.warn(`Unknown synchronizer type: ${type}, id: ${id}`);
+ console.debug(`Unknown synchronizer type: ${type}, id: ${id}`);
}
}
diff --git a/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts b/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts
new file mode 100644
index 000000000..561b95d57
--- /dev/null
+++ b/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts
@@ -0,0 +1,79 @@
+import { Types, getEnabledElementByViewportId } from '@cornerstonejs/core';
+import {
+ SynchronizerManager,
+ Synchronizer,
+ Enums,
+ Types as ToolsTypes,
+} from '@cornerstonejs/tools';
+
+const { createSynchronizer } = SynchronizerManager;
+const { SEGMENTATION_REPRESENTATION_ADDED } = Enums.Events;
+
+export default function createHydrateSegmentationSynchronizer(
+ synchronizerName: string,
+ { servicesManager, ...options }: { servicesManager: AppTypes.ServicesManager; options }
+): Synchronizer {
+ const stackImageSynchronizer = createSynchronizer(
+ synchronizerName,
+ SEGMENTATION_REPRESENTATION_ADDED,
+ (synchronizerInstance, sourceViewport, targetViewport, sourceEvent) =>
+ segmentationRepresentationModifiedCallback(
+ synchronizerInstance,
+ sourceViewport,
+ targetViewport,
+ sourceEvent,
+ { servicesManager, options }
+ ),
+ {
+ eventSource: 'eventTarget',
+ }
+ );
+
+ return stackImageSynchronizer;
+}
+
+const segmentationRepresentationModifiedCallback = async (
+ synchronizerInstance: Synchronizer,
+ sourceViewport: Types.IViewportId,
+ targetViewport: Types.IViewportId,
+ sourceEvent: Event,
+ { servicesManager, options }: { servicesManager: AppTypes.ServicesManager; options: unknown }
+) => {
+ const event = sourceEvent as ToolsTypes.EventTypes.SegmentationRepresentationModifiedEventType;
+
+ const { segmentationId, viewportId } = event.detail;
+ const { segmentationService, hangingProtocolService } = servicesManager.services;
+
+ const targetViewportId = targetViewport.viewportId;
+
+ const { viewport } = getEnabledElementByViewportId(targetViewportId);
+
+ const targetFrameOfReferenceUID = viewport.getFrameOfReferenceUID();
+
+ if (!targetFrameOfReferenceUID) {
+ console.debug('No frame of reference UID found for the target viewport');
+ return;
+ }
+
+ const targetViewportRepresentation = segmentationService.getSegmentationRepresentations(
+ targetViewportId,
+ { segmentationId }
+ );
+
+ if (targetViewportRepresentation.length > 0) {
+ return;
+ }
+
+ // whatever type the source viewport has, we need to add that to the target viewport
+ const sourceViewportRepresentation = segmentationService.getSegmentationRepresentations(
+ sourceViewport.viewportId,
+ { segmentationId }
+ );
+
+ const type = sourceViewportRepresentation[0].type;
+
+ await segmentationService.addSegmentationRepresentation(targetViewportId, {
+ segmentationId,
+ type,
+ });
+};
diff --git a/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts b/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts
index baf155764..7c677618b 100644
--- a/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts
+++ b/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts
@@ -7,7 +7,7 @@ export type ActionComponentInfo = {
id: string;
component: ReactNode;
location: ViewportActionCornersLocations;
- indexPriority: number;
+ indexPriority?: number;
};
class ViewportActionCornersService extends PubSubService {
@@ -33,21 +33,21 @@ class ViewportActionCornersService extends PubSubService {
public setServiceImplementation({
getState: getStateImplementation,
- setComponent: setComponentImplementation,
- setComponents: setComponentsImplementation,
- clear: clearImplementation,
+ addComponent: addComponentImplementation,
+ addComponents: addComponentsImplementation,
+ clear: clearComponentsImplementation,
}): void {
if (getStateImplementation) {
this.serviceImplementation._getState = getStateImplementation;
}
- if (setComponentImplementation) {
- this.serviceImplementation._setComponent = setComponentImplementation;
+ if (addComponentImplementation) {
+ this.serviceImplementation._addComponent = addComponentImplementation;
}
- if (setComponentsImplementation) {
- this.serviceImplementation._setComponents = setComponentsImplementation;
+ if (addComponentsImplementation) {
+ this.serviceImplementation._addComponents = addComponentsImplementation;
}
- if (clearImplementation) {
- this.serviceImplementation._clear = clearImplementation;
+ if (clearComponentsImplementation) {
+ this.serviceImplementation._clear = clearComponentsImplementation;
}
}
@@ -55,12 +55,12 @@ class ViewportActionCornersService extends PubSubService {
return this.serviceImplementation._getState();
}
- public setComponent(component: ActionComponentInfo) {
- this.serviceImplementation._setComponent(component);
+ public addComponent(component: ActionComponentInfo) {
+ this.serviceImplementation._addComponent(component);
}
- public setComponents(components: Array) {
- this.serviceImplementation._setComponents(components);
+ public addComponents(components: Array) {
+ this.serviceImplementation._addComponents(components);
}
public clear(viewportId: string) {
diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts
index 52607ec79..75fd18a83 100644
--- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts
+++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts
@@ -18,9 +18,20 @@ import { IViewportService } from './IViewportService';
import { RENDERING_ENGINE_ID } from './constants';
import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
-import { LutPresentation, PositionPresentation, Presentations } from '../../types/Presentation';
+import {
+ LutPresentation,
+ PositionPresentation,
+ Presentations,
+ SegmentationPresentation,
+ SegmentationPresentationItem,
+} from '../../types/Presentation';
import JumpPresets from '../../utils/JumpPresets';
+import { ViewportProperties } from '@cornerstonejs/core/types';
+import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
+import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
+import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
+import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
@@ -176,11 +187,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
* @param presentations - The presentations to apply to the viewport.
* @param viewportInfo - Contains a view reference for immediate application
*/
- public setPresentations(
- viewportId: string,
- presentations?: Presentations,
- viewportInfo?: ViewportInfo
- ): void {
+ public setPresentations(viewportId: string, presentations: Presentations): void {
const viewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport
| Types.IVolumeViewport;
@@ -189,83 +196,113 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return;
}
- const { lutPresentation, positionPresentation } = presentations;
- if (lutPresentation) {
- const { presentation } = lutPresentation;
- if (viewport instanceof BaseVolumeViewport) {
- if (presentation instanceof Map) {
- presentation.forEach((properties, volumeId) => {
- viewport.setProperties(properties, volumeId);
- });
- } else {
- viewport.setProperties(presentation);
- }
- } else {
- viewport.setProperties(presentation);
- }
+ const { lutPresentation, positionPresentation, segmentationPresentation } = presentations;
+
+ // Always set the segmentation presentation first, since there might be some
+ // lutpresentation states that need to be set on the segmentation
+ // Todo: i think we should even await this
+ this._setSegmentationPresentation(viewport, segmentationPresentation);
+
+ this._setLutPresentation(viewport, lutPresentation);
+ this._setPositionPresentation(viewport, { ...positionPresentation, viewportId });
+ }
+
+ /**
+ * Stores the presentation state for a given viewport inside the
+ * each store. This is used to persist the presentation state
+ * across different scenarios e.g., when the viewport is changing the
+ * display set, or when the viewport is moving to a different layout.
+ *
+ * @param viewportId The ID of the viewport.
+ */
+ public storePresentation({ viewportId }) {
+ const presentationIds = this.getPresentationIds(viewportId);
+ const { syncGroupService } = this.servicesManager.services;
+ const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
+
+ if (!presentationIds || Object.keys(presentationIds).length === 0) {
+ return null;
}
- const viewRef = viewportInfo?.getViewReference() || positionPresentation?.viewReference;
- if (viewRef) {
- viewport.setViewReference(viewRef);
+ const { lutPresentationId, positionPresentationId, segmentationPresentationId } =
+ presentationIds;
+
+ const positionPresentation = this._getPositionPresentation(viewportId);
+ const lutPresentation = this._getLutPresentation(viewportId);
+ const segmentationPresentation = this._getSegmentationPresentation(viewportId);
+
+ const { setLutPresentation } = useLutPresentationStore.getState();
+ const { setPositionPresentation } = usePositionPresentationStore.getState();
+ const { setSynchronizers } = useSynchronizersStore.getState();
+ const { setSegmentationPresentation } = useSegmentationPresentationStore.getState();
+
+ if (lutPresentationId) {
+ setLutPresentation(lutPresentationId, lutPresentation);
}
- if (positionPresentation?.position) {
- viewport.setViewPresentation(positionPresentation.position);
+
+ if (positionPresentationId) {
+ setPositionPresentation(positionPresentationId, positionPresentation);
+ }
+
+ if (segmentationPresentationId) {
+ setSegmentationPresentation(segmentationPresentationId, segmentationPresentation);
+ }
+
+ if (synchronizers?.length) {
+ setSynchronizers(
+ viewportId,
+ synchronizers.map(synchronizer => ({
+ id: synchronizer.id,
+ sourceViewports: [...synchronizer.getSourceViewports()],
+ targetViewports: [...synchronizer.getTargetViewports()],
+ }))
+ );
}
}
/**
- * Retrieves the position presentation information for a given viewport.
- * @param viewportId The ID of the viewport.
- * @returns The position presentation object containing various properties
- * such as ID, viewport type, initial image index, view plane normal, view up, zoom, and pan.
+ * Retrieves the presentations for a given viewport.
+ * @param viewportId - The ID of the viewport.
+ * @returns The presentations for the viewport.
*/
- public getPositionPresentation(viewportId: string): PositionPresentation {
+ public getPresentations(viewportId: string): Presentations {
+ const positionPresentation = this._getPositionPresentation(viewportId);
+ const lutPresentation = this._getLutPresentation(viewportId);
+ const segmentationPresentation = this._getSegmentationPresentation(viewportId);
+
+ return {
+ positionPresentation,
+ lutPresentation,
+ segmentationPresentation,
+ };
+ }
+
+ private getPresentationIds(viewportId: string): AppTypes.PresentationIds | null {
const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
- return;
+ return null;
}
- const presentationIds = viewportInfo.getPresentationIds();
-
- if (!presentationIds) {
- return;
- }
-
- const { positionPresentationId } = presentationIds;
+ return viewportInfo.getPresentationIds();
+ }
+ private _getPositionPresentation(viewportId: string): PositionPresentation {
const csViewport = this.getCornerstoneViewport(viewportId);
if (!csViewport) {
return;
}
+ const viewportInfo = this.viewportsById.get(viewportId);
+
return {
- id: positionPresentationId,
viewportType: viewportInfo.getViewportType(),
viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(),
- position: csViewport.getViewPresentation({ pan: true, zoom: true }),
+ viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }),
+ viewportId,
};
}
- /**
- * Retrieves the LUT (Lookup Table) presentation for a given viewport.
- * @param viewportId The ID of the viewport.
- * @returns The LUT presentation object, or undefined if the viewport does not exist.
- */
- public getLutPresentation(viewportId: string): LutPresentation {
- const viewportInfo = this.viewportsById.get(viewportId);
- if (!viewportInfo) {
- return;
- }
-
- const presentationIds = viewportInfo.getPresentationIds();
-
- if (!presentationIds) {
- return;
- }
-
- const { lutPresentationId } = presentationIds;
-
+ private _getLutPresentation(viewportId: string): LutPresentation {
const csViewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport
| Types.IVolumeViewport;
@@ -275,116 +312,39 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
const cleanProperties = properties => {
- if (properties.isComputedVOI) {
- delete properties.voiRange;
- delete properties.VOILUTFunction;
+ if (properties?.isComputedVOI) {
+ delete properties?.voiRange;
+ delete properties?.VOILUTFunction;
}
return properties;
};
- const presentation =
+ const properties =
csViewport instanceof BaseVolumeViewport
? new Map()
: cleanProperties(csViewport.getProperties());
- if (presentation instanceof Map) {
- csViewport.getActors().forEach(({ uid: volumeId }) => {
- const properties = cleanProperties(csViewport.getProperties(volumeId));
- presentation.set(volumeId, properties);
+ if (properties instanceof Map) {
+ const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds();
+ volumeIds?.forEach(volumeId => {
+ const csProps = cleanProperties(csViewport.getProperties(volumeId));
+ properties.set(volumeId, csProps);
});
}
- return {
- id: lutPresentationId,
- viewportType: viewportInfo.getViewportType(),
- presentation,
- };
- }
-
- /**
- * Retrieves the presentations for a given viewport.
- * @param viewportId - The ID of the viewport.
- * @returns The presentations for the viewport.
- */
- public getPresentations(viewportId: string): Presentations {
const viewportInfo = this.viewportsById.get(viewportId);
- if (!viewportInfo) {
- return;
- }
-
- const positionPresentation = this.getPositionPresentation(viewportId);
- const lutPresentation = this.getLutPresentation(viewportId);
return {
- positionPresentation,
- lutPresentation,
+ viewportType: viewportInfo.getViewportType(),
+ properties,
};
}
- /**
- * Stores the presentation state for a given viewport inside the
- * stateSyncService. This is used to persist the presentation state
- * across different scenarios e.g., when the viewport is changing the
- * display set, or when the viewport is moving to a different layout.
- *
- * @param viewportId The ID of the viewport.
- */
- public storePresentation({ viewportId }) {
- let presentations = null as Presentations;
- try {
- presentations = this.getPresentations(viewportId);
- if (!presentations?.positionPresentation && !presentations?.lutPresentation) {
- return;
- }
- } catch (error) {
- console.warn(error);
- return;
- }
+ private _getSegmentationPresentation(viewportId: string): SegmentationPresentation {
+ const { segmentationService } = this.servicesManager.services;
- const { stateSyncService, syncGroupService } = this.servicesManager.services;
-
- const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
-
- const { positionPresentationStore, synchronizersStore, lutPresentationStore } =
- stateSyncService.getState();
-
- const { lutPresentation, positionPresentation } = presentations;
- const { id: positionPresentationId } = positionPresentation;
- const { id: lutPresentationId } = lutPresentation;
-
- const updateStore = (store, id, value) => ({ ...store, [id]: value });
-
- const newState = {} as { [key: string]: any };
-
- if (lutPresentationId) {
- newState.lutPresentationStore = updateStore(
- lutPresentationStore,
- lutPresentationId,
- lutPresentation
- );
- }
-
- if (positionPresentationId) {
- newState.positionPresentationStore = updateStore(
- positionPresentationStore,
- positionPresentationId,
- positionPresentation
- );
- }
-
- if (synchronizers?.length) {
- newState.synchronizersStore = updateStore(
- synchronizersStore,
- viewportId,
- synchronizers.map(synchronizer => ({
- id: synchronizer.id,
- sourceViewports: [...synchronizer.getSourceViewports()],
- targetViewports: [...synchronizer.getTargetViewports()],
- }))
- );
- }
-
- stateSyncService.store(newState);
+ const presentation = segmentationService.getPresentation(viewportId);
+ return presentation;
}
/**
@@ -423,6 +383,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// and we would lose the presentation.
this.storePresentation({ viewportId: viewportInfo.getViewportId() });
+ // Todo: i don't like this here, move it
+ this.servicesManager.services.segmentationService.clearSegmentationRepresentations(
+ viewportInfo.getViewportId()
+ );
+
if (!viewportInfo) {
throw new Error('element is not enabled for the given viewportId');
}
@@ -431,8 +396,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// since those are the newly set ones, we set them here so that it handles defaults
const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
// Specify an over-ride for the viewport type, even though it is in the public
- // viewport options, because the one in the public viewport options is a suggestion
- // for initial view, whereas the one in viewportData is a requirement based on the
+ // viewport options, because the one in the viewportData is a requirement based on the
// type of data being displayed.
const viewportOptions = viewportInfo.setPublicViewportOptions(
publicViewportOptions,
@@ -647,13 +611,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
properties.colormap = colormap ?? properties.colormap;
}
- this._handleOverlays(viewport);
+ viewport.element.addEventListener(csEnums.Events.VIEWPORT_NEW_IMAGE_SET, evt => {
+ const { element } = evt.detail;
- if (!imageIds?.length) {
- return;
- }
+ if (element !== viewport.element) {
+ return;
+ }
- return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
+ csToolsUtils.stackContextPrefetch.enable(element);
+ });
+
+ let imageIdsToSet = imageIds;
+ const res = this._processExtraDisplaySetsForViewport(viewport);
+ imageIdsToSet = res?.imageIds ?? imageIdsToSet;
+
+ return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties });
this.setPresentations(viewport.id, presentations, viewportInfo);
if (displayArea) {
@@ -702,7 +674,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const lastSliceIndex = numberOfSlices - 1;
if (imageIndex !== undefined) {
- return csToolsUtils.clip(imageIndex, 0, lastSliceIndex);
+ return csUtils.clip(imageIndex, 0, lastSliceIndex);
}
if (preset === JumpPresets.First) {
@@ -764,7 +736,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const displaySetOptions = displaySetOptionsArray[index];
const { volumeId } = volume;
-
volumeInputArray.push({
imageIds,
volumeId,
@@ -775,8 +746,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
- const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus.loaded);
-
+ const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus?.loaded);
if (volumesNotLoaded.length) {
if (hangingProtocolService.getShouldPerformCustomImageLoad()) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
@@ -787,7 +757,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
volumesNotLoaded.forEach(volume => {
- if (!volume.loadStatus.loading) {
+ if (!volume.loadStatus?.loading && volume.load instanceof Function) {
volume.load();
}
});
@@ -798,8 +768,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
public async setVolumesForViewport(viewport, volumeInputArray, presentations) {
- const { displaySetService, toolGroupService, viewportGridService } =
- this.servicesManager.services;
+ const { displaySetService, viewportGridService } = this.servicesManager.services;
const viewportInfo = this.getViewportInfo(viewport.id);
const displaySetOptions = viewportInfo.getDisplaySetOptions();
@@ -811,7 +780,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { volumeId } = volumeInput;
const displaySetOption = displaySetOptions[index];
const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
- const properties = {};
+ const properties = {} as ViewportProperties;
if (voi && (voi.windowWidth || voi.windowCenter)) {
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
@@ -836,22 +805,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return { properties, volumeId };
});
+ // For SEG and RT viewports
+ this._processExtraDisplaySetsForViewport(viewport);
+
await viewport.setVolumes(volumeInputArray);
+
volumesProperties.forEach(({ properties, volumeId }) => {
viewport.setProperties(properties, volumeId);
});
this.setPresentations(viewport.id, presentations, viewportInfo);
- this._handleOverlays(viewport);
-
- const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
- csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
-
const imageIndex = this._getInitialImageIndexForViewport(viewportInfo);
if (imageIndex !== undefined) {
- csToolsUtils.jumpToSlice(viewport.element, {
+ csUtils.jumpToSlice(viewport.element, {
imageIndex,
});
}
@@ -863,7 +831,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
});
}
- private _handleOverlays(viewport: Types.IStackViewport | Types.IVolumeViewport) {
+ private _processExtraDisplaySetsForViewport(
+ viewport: Types.IStackViewport | Types.IVolumeViewport
+ ) {
const { displaySetService } = this.servicesManager.services;
// load any secondary displaySets
@@ -877,104 +847,41 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality)
);
- if (segOrRTSOverlayDisplaySet) {
- this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
- } else {
- // 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
- this._addSegmentationRepresentationToToolGroupIfNecessary(displaySetInstanceUIDs, viewport);
+ // if it is only the overlay displaySet, then we need to get the reference
+ // displaySet imageIds and set them as the imageIds for the viewport,
+ // here we can do some logic if the reference is missing
+ // then find the most similar match of displaySet instead
+ if (!segOrRTSOverlayDisplaySet) {
+ return;
}
+
+ const referenceDisplaySet = displaySetService.getDisplaySetByUID(
+ segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID
+ );
+ const imageIds = referenceDisplaySet.images.map(image => image.imageId);
+ this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
+ return { imageIds };
}
- private _addSegmentationRepresentationToToolGroupIfNecessary(
- displaySetInstanceUIDs: string[],
- viewport: any
+ private addOverlayRepresentationForDisplaySet(
+ displaySet: OhifTypes.DisplaySet,
+ viewport: Types.IViewport
) {
- const { segmentationService, toolGroupService } = this.servicesManager.services;
-
- const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
-
- // this only returns hydrated segmentations
- const segmentations = segmentationService.getSegmentations();
-
- for (const segmentation of segmentations) {
- const toolGroupSegmentationRepresentations =
- segmentationService.getSegmentationRepresentationsForToolGroup(toolGroup.id) || [];
-
- // if there is already a segmentation representation for this segmentation
- // for this toolGroup, don't bother at all
- const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find(
- representation => representation.segmentationId === segmentation.id
- );
-
- if (isSegmentationInToolGroup) {
- continue;
- }
-
- // otherwise, check if the hydrated segmentations are in the same FrameOfReferenceUID
- // as the primary displaySet, if so add the representation (since it was not there)
- const { id: segDisplaySetInstanceUID } = segmentation;
- let segFrameOfReferenceUID = this._getFrameOfReferenceUID(segDisplaySetInstanceUID);
-
- if (!segFrameOfReferenceUID) {
- // if the segmentation displaySet does not have a FrameOfReferenceUID, we might check the
- // segmentation itself maybe it has a FrameOfReferenceUID
- const { FrameOfReferenceUID } = segmentation;
- if (FrameOfReferenceUID) {
- segFrameOfReferenceUID = FrameOfReferenceUID;
- }
- }
-
- if (!segFrameOfReferenceUID) {
- return;
- }
-
- let shouldDisplaySeg = false;
-
- for (const displaySetInstanceUID of displaySetInstanceUIDs) {
- const primaryFrameOfReferenceUID = this._getFrameOfReferenceUID(displaySetInstanceUID);
-
- if (segFrameOfReferenceUID === primaryFrameOfReferenceUID) {
- shouldDisplaySeg = true;
- break;
- }
- }
-
- if (!shouldDisplaySeg) {
- return;
- }
-
- segmentationService.addSegmentationRepresentationToToolGroup(
- toolGroup.id,
- segmentation.id,
- false, // already hydrated,
- segmentation.type
- );
- }
- }
-
- private addOverlayRepresentationForDisplaySet(displaySet: any, viewport: any) {
- const { segmentationService, toolGroupService } = this.servicesManager.services;
-
- const { referencedVolumeId } = displaySet;
+ const { segmentationService } = this.servicesManager.services;
const segmentationId = displaySet.displaySetInstanceUID;
- const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
-
const representationType =
- referencedVolumeId && cache.getVolume(referencedVolumeId) !== undefined
+ displaySet.Modality === 'SEG'
? csToolsEnums.SegmentationRepresentations.Labelmap
: csToolsEnums.SegmentationRepresentations.Contour;
- segmentationService.addSegmentationRepresentationToToolGroup(
- toolGroup.id,
+ segmentationService.addSegmentationRepresentation(viewport.id, {
segmentationId,
- false,
- representationType
- );
+ type: representationType,
+ });
+
+ // store the segmentation presentation id in the viewport info
+ this.storePresentation({ viewportId: viewport.id });
}
// Todo: keepCamera is an interim solution until we have a better solution for
@@ -1032,7 +939,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
return this._setOtherViewport(
- viewport as Types.IViewport,
+ viewport,
viewportData as StackViewportData,
viewportInfo,
presentations
@@ -1063,11 +970,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// calculate the slab thickness based on the volume dimensions
const imageVolume = cache.getVolume(volumeId);
- const { dimensions } = imageVolume;
+ const { dimensions, spacing } = imageVolume;
const slabThickness = Math.sqrt(
- dimensions[0] * dimensions[0] +
- dimensions[1] * dimensions[1] +
- dimensions[2] * dimensions[2]
+ Math.pow(dimensions[0] * spacing[0], 2) +
+ Math.pow(dimensions[1] * spacing[1], 2) +
+ Math.pow(dimensions[2] * spacing[2], 2)
);
return slabThickness;
@@ -1128,9 +1035,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const viewports = this.getRenderingEngine().getViewports();
// Store the current position presentations for each viewport.
- viewports.forEach(({ id }) => {
- const presentation = this.getPositionPresentation(id);
- this.beforeResizePositionPresentations.set(id, presentation);
+ viewports.forEach(({ id: viewportId }) => {
+ const presentation = this._getPositionPresentation(viewportId);
+ this.beforeResizePositionPresentations.set(viewportId, presentation);
});
// Resize the rendering engine and render.
@@ -1159,6 +1066,65 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.gridResizeTimeOut = null;
}, this.gridResizeDelay);
}
+
+ private _setLutPresentation(
+ viewport: Types.IStackViewport | Types.IVolumeViewport,
+ lutPresentation: LutPresentation
+ ): void {
+ if (!lutPresentation) {
+ return;
+ }
+
+ const { properties } = lutPresentation;
+ if (viewport instanceof BaseVolumeViewport) {
+ if (properties instanceof Map) {
+ properties.forEach((propertiesEntry, volumeId) => {
+ viewport.setProperties(propertiesEntry, volumeId);
+ });
+ } else {
+ viewport.setProperties(properties);
+ }
+ } else {
+ viewport.setProperties(properties);
+ }
+ }
+
+ private _setPositionPresentation(
+ viewport: Types.IStackViewport | Types.IVolumeViewport,
+ positionPresentation: PositionPresentation
+ ): void {
+ const viewRef = positionPresentation?.viewReference;
+ if (viewRef) {
+ viewport.setViewReference(viewRef);
+ }
+
+ const viewPresentation = positionPresentation?.viewPresentation;
+ if (viewPresentation) {
+ viewport.setViewPresentation(viewPresentation);
+ }
+ }
+
+ private _setSegmentationPresentation(
+ viewport: Types.IStackViewport | Types.IVolumeViewport,
+ segmentationPresentation: SegmentationPresentation
+ ): void {
+ if (!segmentationPresentation) {
+ return;
+ }
+
+ const { segmentationService } = this.servicesManager.services;
+
+ segmentationPresentation.forEach((presentation: SegmentationPresentationItem) => {
+ const { segmentationId, type, hydrated } = presentation;
+
+ if (hydrated) {
+ segmentationService.addSegmentationRepresentation(viewport.id, {
+ segmentationId,
+ type,
+ });
+ }
+ });
+ }
}
export default CornerstoneViewportService;
diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts
index 9281ccf5c..091c7f79d 100644
--- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts
+++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts
@@ -5,7 +5,6 @@ import {
VolumeViewport,
utilities,
} from '@cornerstonejs/core';
-import { Types as CoreTypes } from '@ohif/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
@@ -25,7 +24,7 @@ export type ViewportOptions = {
toolGroupId: string;
viewportId: string;
// Presentation ID to store/load presentation state from
- presentationIds?: CoreTypes.PresentationIds;
+ presentationIds?: AppTypes.PresentationIds;
orientation?: Enums.OrientationAxis;
background?: Types.Point3;
displayArea?: Types.DisplayArea;
@@ -46,7 +45,7 @@ export type PublicViewportOptions = {
id?: string;
viewportType?: string;
toolGroupId?: string;
- presentationIds?: CoreTypes.PresentationIds;
+ presentationIds?: string[];
viewportId?: string;
orientation?: Enums.OrientationAxis;
background?: Types.Point3;
@@ -283,7 +282,7 @@ class ViewportInfo {
return this.viewportOptions;
}
- public getPresentationIds(): CoreTypes.PresentationIds {
+ public getPresentationIds(): AppTypes.PresentationIds | null {
const { presentationIds } = this.viewportOptions;
return presentationIds;
}
diff --git a/extensions/cornerstone/src/stores/index.ts b/extensions/cornerstone/src/stores/index.ts
new file mode 100644
index 000000000..f834c825e
--- /dev/null
+++ b/extensions/cornerstone/src/stores/index.ts
@@ -0,0 +1,4 @@
+export { useLutPresentationStore } from './useLutPresentationStore';
+export { usePositionPresentationStore } from './usePositionPresentationStore';
+export { useSegmentationPresentationStore } from './useSegmentationPresentationStore';
+export { useSynchronizersStore } from './useSynchronizersStore';
diff --git a/extensions/cornerstone/src/stores/presentationUtils.ts b/extensions/cornerstone/src/stores/presentationUtils.ts
new file mode 100644
index 000000000..bdc4fdf98
--- /dev/null
+++ b/extensions/cornerstone/src/stores/presentationUtils.ts
@@ -0,0 +1,42 @@
+const JOIN_STR = '&';
+
+// The default lut presentation id if none defined
+const DEFAULT_STR = 'default';
+
+// This code finds the first unique index to add to the presentation id so that
+// two viewports containing the same display set in the same type of viewport
+// can have different presentation information. This allows comparison of
+// a single display set in two or more viewports, when the user has simply
+// dragged and dropped the view in twice. For example, it allows displaying
+// bone, brain and soft tissue views of a single display set, and to still
+// remember the specific changes to each viewport.
+const addUniqueIndex = (
+ arr,
+ key,
+ viewports: AppTypes.ViewportGrid.Viewports,
+ isUpdatingSameViewport
+) => {
+ arr.push(0);
+
+ // If we are updating the viewport, we should not increment the index
+ if (isUpdatingSameViewport) {
+ return;
+ }
+
+ // The 128 is just a value that is larger than how many viewports we
+ // display at once, used as an upper bound on how many unique presentation
+ // ID's might exist for a single display set at once.
+ for (let displayInstance = 0; displayInstance < 128; displayInstance++) {
+ arr[arr.length - 1] = displayInstance;
+ const testId = arr.join(JOIN_STR);
+ if (
+ !Array.from(viewports.values()).find(
+ viewport => viewport.viewportOptions?.presentationIds?.[key] === testId
+ )
+ ) {
+ break;
+ }
+ }
+};
+
+export { addUniqueIndex, DEFAULT_STR, JOIN_STR };
diff --git a/extensions/cornerstone/src/stores/useLutPresentationStore.ts b/extensions/cornerstone/src/stores/useLutPresentationStore.ts
new file mode 100644
index 000000000..0f0e29b0e
--- /dev/null
+++ b/extensions/cornerstone/src/stores/useLutPresentationStore.ts
@@ -0,0 +1,166 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { LutPresentation } from '../types/Presentation';
+import { addUniqueIndex, DEFAULT_STR, JOIN_STR } from './presentationUtils';
+
+/**
+ * Identifier for the LUT Presentation store type.
+ */
+const PRESENTATION_TYPE_ID = 'lutPresentationId';
+
+/**
+ * Flag to enable or disable debug mode for the store.
+ * Set to `true` to enable zustand devtools.
+ */
+const DEBUG_STORE = false;
+
+/**
+ * Represents the state and actions for managing LUT presentations.
+ */
+type LutPresentationState = {
+ /**
+ * Type identifier for the store.
+ */
+ type: string;
+
+ /**
+ * Stores LUT presentations indexed by their presentation ID.
+ */
+ lutPresentationStore: Record;
+
+ /**
+ * Sets the LUT presentation for a given key.
+ *
+ * @param key - The key identifying the LUT presentation.
+ * @param value - The `LutPresentation` to associate with the key.
+ */
+ setLutPresentation: (key: string, value: LutPresentation) => void;
+
+ /**
+ * Clears all LUT presentations from the store.
+ */
+ clearLutPresentationStore: () => void;
+
+ /**
+ * Retrieves the presentation ID based on the provided parameters.
+ *
+ * @param id - The presentation ID to check.
+ * @param options - Configuration options.
+ * @param options.viewport - The current viewport in grid
+ * @param options.viewports - All available viewports in grid
+ * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
+ * @returns The presentation ID or undefined.
+ */
+ getPresentationId: (
+ id: string,
+ options: {
+ viewport: AppTypes.ViewportGrid.Viewport;
+ viewports: AppTypes.ViewportGrid.Viewports;
+ isUpdatingSameViewport: boolean;
+ }
+ ) => string | undefined;
+};
+
+/**
+ * Generates a presentation ID for LUT based on the viewport configuration.
+ *
+ * @param id - The ID to check.
+ * @param options - Configuration options.
+ * @param options.viewport - The current viewport.
+ * @param options.viewports - All available viewports.
+ * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
+ * @returns The LUT presentation ID or undefined.
+ */
+const getLutPresentationId = (
+ id: string,
+ {
+ viewport,
+ viewports,
+ isUpdatingSameViewport,
+ }: {
+ viewport: AppTypes.ViewportGrid.Viewport;
+ viewports: AppTypes.ViewportGrid.Viewports;
+ isUpdatingSameViewport: boolean;
+ }
+): string | undefined => {
+ if (id !== PRESENTATION_TYPE_ID) {
+ return;
+ }
+
+ const getLutId = (ds): string => {
+ if (!ds || !ds.options) {
+ return DEFAULT_STR;
+ }
+ if (ds.options.id) {
+ return ds.options.id;
+ }
+ const arr = Object.entries(ds.options).map(([key, val]) => `${key}=${val}`);
+ if (!arr.length) {
+ return DEFAULT_STR;
+ }
+ return arr.join(JOIN_STR);
+ };
+
+ if (!viewport || !viewport.viewportOptions || !viewport.displaySetInstanceUIDs?.length) {
+ return;
+ }
+
+ const { displaySetOptions, displaySetInstanceUIDs } = viewport;
+ const lutId = getLutId(displaySetOptions[0]);
+ const lutPresentationArr = [lutId];
+
+ for (const uid of displaySetInstanceUIDs) {
+ lutPresentationArr.push(uid);
+ }
+
+ addUniqueIndex(lutPresentationArr, PRESENTATION_TYPE_ID, viewports, isUpdatingSameViewport);
+
+ return lutPresentationArr.join(JOIN_STR);
+};
+
+/**
+ * Creates the LUT Presentation store.
+ *
+ * @param set - The zustand set function.
+ * @returns The LUT Presentation store state and actions.
+ */
+const createLutPresentationStore = (set): LutPresentationState => ({
+ type: PRESENTATION_TYPE_ID,
+ lutPresentationStore: {},
+
+ /**
+ * Sets the LUT presentation for a given key.
+ */
+ setLutPresentation: (key, value) =>
+ set(
+ state => ({
+ lutPresentationStore: {
+ ...state.lutPresentationStore,
+ [key]: value,
+ },
+ }),
+ false,
+ 'setLutPresentation'
+ ),
+
+ /**
+ * Clears all LUT presentations from the store.
+ */
+ clearLutPresentationStore: () =>
+ set({ lutPresentationStore: {} }, false, 'clearLutPresentationStore'),
+
+ /**
+ * Retrieves the presentation ID based on the provided parameters.
+ */
+ getPresentationId: getLutPresentationId,
+});
+
+/**
+ * Zustand store for managing LUT presentations.
+ * Applies devtools middleware when DEBUG_STORE is enabled.
+ */
+export const useLutPresentationStore = create()(
+ DEBUG_STORE
+ ? devtools(createLutPresentationStore, { name: 'LutPresentationStore' })
+ : createLutPresentationStore
+);
diff --git a/extensions/cornerstone/src/stores/usePositionPresentationStore.ts b/extensions/cornerstone/src/stores/usePositionPresentationStore.ts
new file mode 100644
index 000000000..1b06bd8e5
--- /dev/null
+++ b/extensions/cornerstone/src/stores/usePositionPresentationStore.ts
@@ -0,0 +1,172 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { PositionPresentation } from '../types/Presentation';
+import { addUniqueIndex, JOIN_STR } from './presentationUtils';
+
+const PRESENTATION_TYPE_ID = 'positionPresentationId';
+const DEBUG_STORE = true;
+
+/**
+ * Represents the state and actions for managing position presentations.
+ */
+type PositionPresentationState = {
+ /**
+ * Type identifier for the store.
+ */
+ type: string;
+
+ /**
+ * Stores position presentations indexed by their presentation ID.
+ */
+ positionPresentationStore: Record;
+
+ /**
+ * Sets the position presentation for a given key.
+ *
+ * @param key - The key identifying the position presentation.
+ * @param value - The `PositionPresentation` to associate with the key.
+ */
+ setPositionPresentation: (key: string, value: PositionPresentation) => void;
+
+ /**
+ * Clears all position presentations from the store.
+ */
+ clearPositionPresentationStore: () => void;
+
+ /**
+ * Retrieves the presentation ID based on the provided parameters.
+ *
+ * @param id - The ID to check.
+ * @param options - Configuration options.
+ * @param options.viewport - The current viewport.
+ * @param options.viewports - All available viewports.
+ * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
+ * @returns The position presentation ID or undefined.
+ */
+ getPresentationId: (
+ id: string,
+ options: {
+ viewport: any;
+ viewports: any;
+ isUpdatingSameViewport: boolean;
+ }
+ ) => string | undefined;
+
+ getPositionPresentationId: (
+ viewport: any,
+ viewports?: any,
+ isUpdatingSameViewport?: boolean
+ ) => string | undefined;
+};
+
+/**
+ * Generates a position presentation ID based on the viewport configuration.
+ *
+ * @param id - The ID to check.
+ * @param options - Configuration options.
+ * @param options.viewport - The current viewport.
+ * @param options.viewports - All available viewports.
+ * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
+ * @returns The position presentation ID or undefined.
+ */
+const getPresentationId = (
+ id: string,
+ {
+ viewport,
+ viewports,
+ isUpdatingSameViewport,
+ }: {
+ viewport: any;
+ viewports: any;
+ isUpdatingSameViewport: boolean;
+ }
+): string | undefined => {
+ if (id !== PRESENTATION_TYPE_ID) {
+ return;
+ }
+
+ if (!viewport?.viewportOptions || !viewport.displaySetInstanceUIDs?.length) {
+ return;
+ }
+
+ return getPositionPresentationId(viewport, viewports, isUpdatingSameViewport);
+};
+
+function getPositionPresentationId(viewport, viewports, isUpdatingSameViewport) {
+ const { viewportOptions = {}, displaySetInstanceUIDs = [], displaySetOptions = [] } = viewport;
+ const { id: viewportOptionId, orientation } = viewportOptions;
+
+ const positionPresentationArr = [orientation || 'acquisition'];
+ if (viewportOptionId) {
+ positionPresentationArr.push(viewportOptionId);
+ }
+
+ if (displaySetOptions?.some(ds => ds.options?.blendMode || ds.options?.displayPreset)) {
+ positionPresentationArr.push(`custom`);
+ }
+
+ for (const uid of displaySetInstanceUIDs) {
+ positionPresentationArr.push(uid);
+ }
+
+ if (viewports && viewports.length && isUpdatingSameViewport !== undefined) {
+ addUniqueIndex(
+ positionPresentationArr,
+ PRESENTATION_TYPE_ID,
+ viewports,
+ isUpdatingSameViewport
+ );
+ } else {
+ positionPresentationArr.push(0);
+ }
+
+ return positionPresentationArr.join(JOIN_STR);
+}
+
+/**
+ * Creates the Position Presentation store.
+ *
+ * @param set - The zustand set function.
+ * @returns The Position Presentation store state and actions.
+ */
+const createPositionPresentationStore = set => ({
+ type: PRESENTATION_TYPE_ID,
+ positionPresentationStore: {},
+
+ /**
+ * Sets the position presentation for a given key.
+ */
+ setPositionPresentation: (key, value) =>
+ set(
+ state => ({
+ positionPresentationStore: {
+ ...state.positionPresentationStore,
+ [key]: value,
+ },
+ }),
+ false,
+ 'setPositionPresentation'
+ ),
+
+ /**
+ * Clears all position presentations from the store.
+ */
+ clearPositionPresentationStore: () =>
+ set({ positionPresentationStore: {} }, false, 'clearPositionPresentationStore'),
+
+ /**
+ * Retrieves the presentation ID based on the provided parameters.
+ */
+ getPresentationId,
+ getPositionPresentationId: getPositionPresentationId,
+});
+
+/**
+ * Zustand store for managing position presentations.
+ * Applies devtools middleware when DEBUG_STORE is enabled.
+ */
+export const usePositionPresentationStore = create()(
+ DEBUG_STORE
+ ? devtools(createPositionPresentationStore, { name: 'PositionPresentationStore' })
+ : createPositionPresentationStore
+);
diff --git a/extensions/cornerstone/src/stores/useSegmentationPresentationStore.ts b/extensions/cornerstone/src/stores/useSegmentationPresentationStore.ts
new file mode 100644
index 000000000..60cbb8fe0
--- /dev/null
+++ b/extensions/cornerstone/src/stores/useSegmentationPresentationStore.ts
@@ -0,0 +1,238 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { SegmentationPresentation } from '../types/Presentation';
+import { JOIN_STR } from './presentationUtils';
+import { getViewportOrientationFromImageOrientationPatient } from '../utils/getViewportOrientationFromImageOrientationPatient';
+
+const PRESENTATION_TYPE_ID = 'segmentationPresentationId';
+const DEBUG_STORE = false;
+
+/**
+ * The keys are the presentationId.
+ */
+type SegmentationPresentationStore = {
+ /**
+ * Type identifier for the store.
+ */
+ type: string;
+
+ /**
+ * Stores segmentation presentations indexed by their presentation ID.
+ */
+ segmentationPresentationStore: Record;
+
+ /**
+ * Sets the segmentation presentation for a given segmentation ID.
+ *
+ * @param presentationId - The presentation ID.
+ * @param value - The `SegmentationPresentation` to associate with the ID.
+ */
+ setSegmentationPresentation: (presentationId: string, value: SegmentationPresentation) => void;
+
+ /**
+ * Clears all segmentation presentations from the store.
+ */
+ clearSegmentationPresentationStore: () => void;
+
+ /**
+ * Retrieves the presentation ID based on the provided parameters.
+ *
+ * @param id - The ID to check.
+ * @param options - Configuration options.
+ * @param options.viewport - The current viewport.
+ * @param options.viewports - All available viewports.
+ * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
+ * @param options.servicesManager - The services manager instance.
+ * @returns The segmentation presentation ID or undefined.
+ */
+ getPresentationId: (
+ id: string,
+ options: {
+ viewport: AppTypes.ViewportGrid.Viewport;
+ viewports: AppTypes.ViewportGrid.Viewports;
+ isUpdatingSameViewport: boolean;
+ servicesManager: AppTypes.ServicesManager;
+ }
+ ) => string | undefined;
+
+ /**
+ * Adds a new segmentation presentation state.
+ *
+ * @param presentationId - The presentation ID.
+ * @param segmentationPresentation - The `SegmentationPresentation` to add.
+ * @param servicesManager - The services manager instance.
+ */
+ addSegmentationPresentation: (
+ presentationId: string,
+ segmentationPresentation: SegmentationPresentation,
+ { servicesManager }: { servicesManager: AppTypes.ServicesManager }
+ ) => void;
+
+ /**
+ * Gets the current segmentation presentation ID.
+ *
+ * @param params - Parameters for retrieving the segmentation presentation ID.
+ * @param params.viewport - The current viewport.
+ * @param params.servicesManager - The services manager instance.
+ * @returns The current segmentation presentation ID.
+ */
+ getSegmentationPresentationId: ({
+ viewport,
+ servicesManager,
+ }: {
+ viewport: AppTypes.ViewportGrid.Viewport;
+ servicesManager: AppTypes.ServicesManager;
+ }) => string;
+};
+
+/**
+ * Generates a segmentation presentation ID based on the viewport configuration.
+ *
+ * @param id - The ID to check.
+ * @param options - Configuration options.
+ * @param options.viewport - The current viewport.
+ * @param options.viewports - All available viewports.
+ * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
+ * @param options.servicesManager - The services manager instance.
+ * @returns The segmentation presentation ID or undefined.
+ */
+const getPresentationId = (
+ id: string,
+ {
+ viewport,
+ viewports,
+ isUpdatingSameViewport,
+ servicesManager,
+ }: {
+ viewport: AppTypes.ViewportGrid.Viewport;
+ viewports: AppTypes.ViewportGrid.Viewports;
+ isUpdatingSameViewport: boolean;
+ servicesManager: AppTypes.ServicesManager;
+ }
+): string | undefined => {
+ if (id !== PRESENTATION_TYPE_ID) {
+ return;
+ }
+
+ return _getSegmentationPresentationId({ viewport, servicesManager });
+};
+
+/**
+ * Helper function to generate the segmentation presentation ID.
+ *
+ * @param params - Parameters for generating the segmentation presentation ID.
+ * @param params.viewport - The current viewport.
+ * @param params.servicesManager - The services manager instance.
+ * @returns The segmentation presentation ID or undefined.
+ */
+const _getSegmentationPresentationId = ({
+ viewport,
+ servicesManager,
+}: {
+ viewport: AppTypes.ViewportGrid.Viewport;
+ servicesManager: AppTypes.ServicesManager;
+}) => {
+ if (!viewport?.viewportOptions || !viewport.displaySetInstanceUIDs?.length) {
+ return;
+ }
+
+ const { displaySetInstanceUIDs, viewportOptions } = viewport;
+
+ let orientation = viewportOptions.orientation;
+
+ if (!orientation) {
+ // Calculate orientation from the viewport sample image
+ const displaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
+ displaySetInstanceUIDs[0]
+ );
+ const sampleImage = displaySet.images?.[0];
+ const imageOrientationPatient = sampleImage?.ImageOrientationPatient;
+
+ orientation = getViewportOrientationFromImageOrientationPatient(imageOrientationPatient);
+ }
+
+ const segmentationPresentationArr = [];
+
+ segmentationPresentationArr.push(...displaySetInstanceUIDs);
+
+ // Uncomment if unique indexing is needed
+ // addUniqueIndex(
+ // segmentationPresentationArr,
+ // 'segmentationPresentationId',
+ // viewports,
+ // isUpdatingSameViewport
+ // );
+
+ return segmentationPresentationArr.join(JOIN_STR);
+};
+
+/**
+ * Creates the Segmentation Presentation store.
+ *
+ * @param set - The zustand set function.
+ * @returns The Segmentation Presentation store state and actions.
+ */
+const createSegmentationPresentationStore = set => ({
+ type: PRESENTATION_TYPE_ID,
+ segmentationPresentationStore: {},
+
+ /**
+ * Clears all segmentation presentations from the store.
+ */
+ clearSegmentationPresentationStore: () =>
+ set({ segmentationPresentationStore: {} }, false, 'clearSegmentationPresentationStore'),
+
+ /**
+ * Adds a new segmentation presentation to the store.
+ */
+ addSegmentationPresentation: (
+ presentationId: string,
+ segmentationPresentation: SegmentationPresentation,
+ { servicesManager }: { servicesManager: AppTypes.ServicesManager }
+ ) =>
+ set(
+ state => ({
+ segmentationPresentationStore: {
+ ...state.segmentationPresentationStore,
+ [presentationId]: segmentationPresentation,
+ },
+ }),
+ false,
+ 'addSegmentationPresentation'
+ ),
+
+ /**
+ * Sets the segmentation presentation for a given presentation ID.
+ */
+ setSegmentationPresentation: (presentationId: string, value: SegmentationPresentation) =>
+ set(
+ state => ({
+ segmentationPresentationStore: {
+ ...state.segmentationPresentationStore,
+ [presentationId]: value,
+ },
+ }),
+ false,
+ 'setSegmentationPresentation'
+ ),
+
+ /**
+ * Retrieves the presentation ID based on the provided parameters.
+ */
+ getPresentationId,
+
+ /**
+ * Retrieves the current segmentation presentation ID.
+ */
+ getSegmentationPresentationId: _getSegmentationPresentationId,
+});
+
+/**
+ * Zustand store for managing segmentation presentations.
+ * Applies devtools middleware when DEBUG_STORE is enabled.
+ */
+export const useSegmentationPresentationStore = create()(
+ DEBUG_STORE
+ ? devtools(createSegmentationPresentationStore, { name: 'Segmentation Presentation Store' })
+ : createSegmentationPresentationStore
+);
diff --git a/extensions/cornerstone/src/stores/useSynchronizersStore.ts b/extensions/cornerstone/src/stores/useSynchronizersStore.ts
new file mode 100644
index 000000000..612a0c9dc
--- /dev/null
+++ b/extensions/cornerstone/src/stores/useSynchronizersStore.ts
@@ -0,0 +1,84 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+
+/**
+ * Identifier for the synchronizers store type.
+ */
+const PRESENTATION_TYPE_ID = 'synchronizersStoreId';
+
+/**
+ * Flag to enable or disable debug mode for the store.
+ * Set to `true` to enable zustand devtools.
+ */
+const DEBUG_STORE = false;
+
+/**
+ * Information about a single synchronizer.
+ */
+type SynchronizerInfo = {
+ id: string;
+ type: string;
+ sourceViewports: Array<{ viewportId: string; renderingEngineId: string }>;
+ targetViewports: Array<{ viewportId: string; renderingEngineId: string }>;
+};
+
+/**
+ * State shape for the Synchronizers store.
+ */
+type SynchronizersState = {
+ /**
+ * Stores synchronizer information indexed by a unique key.
+ */
+ synchronizersStore: Record;
+
+ /**
+ * Sets the synchronizers for a specific viewport.
+ *
+ * @param viewportId - The ID of the viewport.
+ * @param synchronizers - An array of SynchronizerInfo.
+ */
+ setSynchronizers: (viewportId: string, synchronizers: SynchronizerInfo[]) => void;
+
+ /**
+ * Clears the entire synchronizers store.
+ */
+ clearSynchronizersStore: () => void;
+};
+
+/**
+ * Creates the Synchronizers store.
+ *
+ * @param set - The zustand set function.
+ * @returns The synchronizers store state and actions.
+ */
+const createSynchronizersStore = (set): SynchronizersState => ({
+ synchronizersStore: {},
+ type: PRESENTATION_TYPE_ID,
+
+ setSynchronizers: (viewportId: string, synchronizers: SynchronizerInfo[]) => {
+ set(
+ state => ({
+ synchronizersStore: {
+ ...state.synchronizersStore,
+ [viewportId]: synchronizers,
+ },
+ }),
+ false,
+ 'setSynchronizers'
+ );
+ },
+
+ clearSynchronizersStore: () => {
+ set({ synchronizersStore: {} }, false, 'clearSynchronizersStore');
+ },
+});
+
+/**
+ * Zustand store for managing synchronizers.
+ * Applies devtools middleware when DEBUG_STORE is enabled.
+ */
+export const useSynchronizersStore = create()(
+ DEBUG_STORE
+ ? devtools(createSynchronizersStore, { name: 'SynchronizersStore' })
+ : createSynchronizersStore
+);
diff --git a/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts b/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts
index 8f125d1b6..324265660 100644
--- a/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts
+++ b/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts
@@ -1,5 +1,5 @@
-import { SynchronizerManager, Synchronizer, utilities } from '@cornerstonejs/tools';
-import { EVENTS, getRenderingEngine, Types } from '@cornerstonejs/core';
+import { SynchronizerManager, Synchronizer } from '@cornerstonejs/tools';
+import { EVENTS, getRenderingEngine, type Types, utilities } from '@cornerstonejs/core';
const frameViewSyncCallback = (
synchronizerInstance: Synchronizer,
diff --git a/extensions/cornerstone/src/tools/CalibrationLineTool.ts b/extensions/cornerstone/src/tools/CalibrationLineTool.ts
index 6c77fbe79..2c4f46d27 100644
--- a/extensions/cornerstone/src/tools/CalibrationLineTool.ts
+++ b/extensions/cornerstone/src/tools/CalibrationLineTool.ts
@@ -1,5 +1,5 @@
import { LengthTool, utilities } from '@cornerstonejs/tools';
-import callInputDialog from '../utils/callInputDialog';
+import { callInputDialog } from '@ohif/extension-default';
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
const { calibrateImageSpacing } = utilities;
diff --git a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx
index c2ba56cbd..f5d036e6f 100644
--- a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx
+++ b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx
@@ -1,5 +1,5 @@
import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core';
-import { IStackViewport, IVolumeViewport, Point3 } from '@cornerstonejs/core/dist/esm/types';
+import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/types';
import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools';
import { guid, b64toBlob } from '@ohif/core/src/utils';
import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider';
diff --git a/extensions/cornerstone/src/types/AppTypes.ts b/extensions/cornerstone/src/types/AppTypes.ts
index 90997eb27..36cb848b2 100644
--- a/extensions/cornerstone/src/types/AppTypes.ts
+++ b/extensions/cornerstone/src/types/AppTypes.ts
@@ -9,6 +9,13 @@ import ColorbarServiceType from '../services/ColorbarService';
import * as cornerstone from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools';
+import type {
+ SegmentRepresentation as SegmentRep,
+ SegmentationData as SegData,
+ SegmentationRepresentation as SegRep,
+ SegmentationInfo as SegInfo,
+} from '../services/SegmentationService/SegmentationService';
+
declare global {
namespace AppTypes {
export type CornerstoneCacheService = CornerstoneCacheServiceType;
@@ -18,6 +25,7 @@ declare global {
export type ToolGroupService = ToolGroupServiceType;
export type ViewportActionCornersService = ViewportActionCornersServiceType;
export type ColorbarService = ColorbarServiceType;
+
export interface Services {
cornerstoneViewportService?: CornerstoneViewportServiceType;
toolGroupService?: ToolGroupServiceType;
@@ -28,6 +36,19 @@ declare global {
colorbarService?: ColorbarServiceType;
}
+ export namespace Segmentation {
+ export type SegmentRepresentation = SegmentRep;
+ export type SegmentationData = SegData;
+ export type SegmentationRepresentation = SegRep;
+ export type SegmentationInfo = SegInfo;
+ }
+
+ export interface PresentationIds {
+ lutPresentationId: string;
+ positionPresentationId: string;
+ segmentationPresentationId: string;
+ }
+
export interface Test {
services?: Services;
cornerstone?: typeof cornerstone;
diff --git a/extensions/cornerstone/src/types/Presentation.ts b/extensions/cornerstone/src/types/Presentation.ts
index 1fd332f82..1d3d0a9da 100644
--- a/extensions/cornerstone/src/types/Presentation.ts
+++ b/extensions/cornerstone/src/types/Presentation.ts
@@ -1,16 +1,24 @@
import type { Types } from '@cornerstonejs/core';
+import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
/**
* Represents a position presentation in a viewport. This is basically
* viewport specific camera position and zoom, and not the display set
*/
export type PositionPresentation = {
- id: string;
viewportType: string;
// The view reference has the basic information as to what image orientation/slice is shown
viewReference: Types.ViewReference;
// The position information has the zoom/pan and possibly other related information, but not LUT
- position: Types.ViewPresentation;
+ viewPresentation: Types.ViewPresentation;
+ /**
+ * Optionals
+ */
+ initialImageIndex?: number;
+ // viewportId helps when hydrating SR or SEG - we can use it to filter
+ // presentations to get the one prior to the current viewport and reuse it
+ // for viewReference and viewPresentation
+ viewportId?: string;
};
/**
@@ -20,15 +28,40 @@ export type PositionPresentation = {
* itself
*/
export interface LutPresentation {
- id: string;
viewportType: string;
- presentation: Record | Types.ViewportProperties;
+ // either a single object with the properties itself or a map of properties with volumeId keys
+ properties: Record | Types.ViewportProperties;
}
+/**
+ * Represents a LUT presentation in a viewport, and is really related
+ * to displaySets and not the viewport itself. So that is why it can
+ * be an object with volumeId keys, or a single object with the properties
+ * itself
+ *
+ * each presentation has a segmentationId and a type and a value for
+ * hydrated and config.
+ *
+ * The hydrated property can be a boolean or null. It's null if the segmentation
+ * representation hasn't been created yet. It's true if the representation is
+ * currently in the viewport. It's false if the representation was in the viewport
+ * but has been removed.
+ *
+ * Config is the segmentation config, Todo: add stuff here
+ */
+export type SegmentationPresentationItem = {
+ segmentationId: string;
+ type: SegmentationRepresentations;
+ hydrated: boolean | null;
+ config?: unknown;
+};
+
+export type SegmentationPresentation = SegmentationPresentationItem[];
+
/**
* Presentation can be a PositionPresentation or a LutPresentation.
*/
-type Presentation = PositionPresentation | LutPresentation;
+type Presentation = PositionPresentation | LutPresentation | SegmentationPresentation;
/**
* Viewport presentations object that can contain a positionPresentation
@@ -37,6 +70,7 @@ type Presentation = PositionPresentation | LutPresentation;
export type Presentations = {
positionPresentation?: PositionPresentation;
lutPresentation?: LutPresentation;
+ segmentationPresentation?: SegmentationPresentation;
};
export default Presentation;
diff --git a/extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx b/extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx
new file mode 100644
index 000000000..3f1d4699e
--- /dev/null
+++ b/extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx
@@ -0,0 +1,78 @@
+import { useEffect, useState, memo } from 'react';
+
+const MODALITIES_REQUIRING_CINE_AUTO_MOUNT = ['OT', 'US'];
+
+const ActiveViewportBehavior = memo(
+ ({ servicesManager, viewportId }: withAppTypes<{ viewportId: string }>) => {
+ const { displaySetService, cineService, viewportGridService, customizationService } =
+ servicesManager.services;
+
+ const [activeViewportId, setActiveViewportId] = useState(viewportId);
+
+ useEffect(() => {
+ const subscription = viewportGridService.subscribe(
+ viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
+ ({ viewportId }) => setActiveViewportId(viewportId)
+ );
+
+ return () => subscription.unsubscribe();
+ }, [viewportId, viewportGridService]);
+
+ useEffect(() => {
+ if (cineService.isViewportCineClosed(activeViewportId)) {
+ return;
+ }
+
+ const displaySetInstanceUIDs =
+ viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
+
+ if (!displaySetInstanceUIDs) {
+ return;
+ }
+
+ const displaySets = displaySetInstanceUIDs.map(uid =>
+ displaySetService.getDisplaySetByUID(uid)
+ );
+
+ if (!displaySets.length) {
+ return;
+ }
+
+ const modalities = displaySets.map(displaySet => displaySet?.Modality);
+
+ const { modalities: sourceModalities } = customizationService.getModeCustomization(
+ 'autoCineModalities',
+ {
+ id: 'autoCineModalities',
+ modalities: MODALITIES_REQUIRING_CINE_AUTO_MOUNT,
+ }
+ );
+
+ const requiresCine = modalities.some(modality => sourceModalities.includes(modality));
+
+ if (requiresCine && !cineService.getState().isCineEnabled) {
+ cineService.setIsCineEnabled(true);
+ }
+ }, [
+ activeViewportId,
+ cineService,
+ viewportGridService,
+ displaySetService,
+ customizationService,
+ ]);
+
+ return null;
+ },
+ arePropsEqual
+);
+
+ActiveViewportBehavior.displayName = 'ActiveViewportBehavior';
+
+function arePropsEqual(prevProps, nextProps) {
+ return (
+ prevProps.viewportId === nextProps.viewportId &&
+ prevProps.servicesManager === nextProps.servicesManager
+ );
+}
+
+export default ActiveViewportBehavior;
diff --git a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx
index 0d512dfe8..5e72d37ab 100644
--- a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx
+++ b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx
@@ -8,7 +8,6 @@ import {
BaseVolumeViewport,
} from '@cornerstonejs/core';
import { ToolGroupManager } from '@cornerstonejs/tools';
-import PropTypes from 'prop-types';
import { ViewportDownloadForm } from '@ohif/ui';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
@@ -22,7 +21,7 @@ const CornerstoneViewportDownloadForm = ({
onClose,
activeViewportId: activeViewportIdProp,
cornerstoneViewportService,
-}) => {
+}: withAppTypes) => {
const enabledElement = OHIFgetEnabledElement(activeViewportIdProp);
const activeViewportElement = enabledElement?.element;
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
@@ -246,9 +245,4 @@ const CornerstoneViewportDownloadForm = ({
);
};
-CornerstoneViewportDownloadForm.propTypes = {
- onClose: PropTypes.func,
- activeViewportId: PropTypes.string.isRequired,
-};
-
export default CornerstoneViewportDownloadForm;
diff --git a/extensions/cornerstone/src/utils/dicomLoaderService.js b/extensions/cornerstone/src/utils/dicomLoaderService.js
index b9d5ea6dd..ba43213fd 100644
--- a/extensions/cornerstone/src/utils/dicomLoaderService.js
+++ b/extensions/cornerstone/src/utils/dicomLoaderService.js
@@ -161,7 +161,7 @@ class DicomLoaderService {
getDicomDataMethod = fetchIt.bind(this, imageId);
break;
default:
- throw new Error(`Unsupported image type: ${loaderType} for imageId: ${imageId}`);
+ return;
}
return getDicomDataMethod();
diff --git a/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts b/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts
index e63e85f79..c8f4cf300 100644
--- a/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts
+++ b/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts
@@ -1,5 +1,5 @@
import { getEnabledElement } from '@cornerstonejs/core';
-import { IEnabledElement } from '@cornerstonejs/core/dist/esm/types';
+import { IEnabledElement } from '@cornerstonejs/core/types';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
diff --git a/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts b/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts
index 2d44dd983..d70410188 100644
--- a/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts
+++ b/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts
@@ -22,7 +22,7 @@ export default function getCornerstoneViewportType(
return Enums.ViewportType.VIDEO;
}
if (lowerViewportType === WHOLESLIDE) {
- return Enums.ViewportType.WholeSlide;
+ return Enums.ViewportType.WHOLE_SLIDE;
}
if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) {
diff --git a/extensions/cornerstone/src/utils/getViewportOrientationFromImageOrientationPatient.ts b/extensions/cornerstone/src/utils/getViewportOrientationFromImageOrientationPatient.ts
new file mode 100644
index 000000000..583b972e6
--- /dev/null
+++ b/extensions/cornerstone/src/utils/getViewportOrientationFromImageOrientationPatient.ts
@@ -0,0 +1,56 @@
+import { CONSTANTS, utilities } from '@cornerstonejs/core';
+
+const { MPR_CAMERA_VALUES } = CONSTANTS;
+
+/**
+ * Determines the viewport orientation (axial, sagittal, or coronal) based on the image orientation patient values.
+ * This is done by comparing the view vectors with predefined MPR camera values.
+ *
+ * @param imageOrientationPatient - Array of 6 numbers representing the image orientation patient values.
+ * The first 3 numbers represent the direction cosines of the first row and the second 3 numbers
+ * represent the direction cosines of the first column.
+ *
+ * @returns The viewport orientation as a string ('axial', 'sagittal', 'coronal') or undefined if
+ * the orientation cannot be determined or if the input is invalid.
+ *
+ * @example
+ * ```typescript
+ * const orientation = getViewportOrientationFromImageOrientationPatient([1,0,0,0,1,0]);
+ * console.debug(orientation); // 'axial'
+ * ```
+ */
+export const getViewportOrientationFromImageOrientationPatient = (
+ imageOrientationPatient: number[]
+): string | undefined => {
+ if (!imageOrientationPatient || imageOrientationPatient.length !== 6) {
+ return undefined;
+ }
+
+ const viewRight = imageOrientationPatient.slice(0, 3);
+ const viewDown = imageOrientationPatient.slice(3, 6);
+ const viewUp = [-viewDown[0], -viewDown[1], -viewDown[2]];
+
+ // Compare vectors with MPR camera values using utilities.isEqual
+ if (
+ utilities.isEqual(viewRight, MPR_CAMERA_VALUES.axial.viewRight) &&
+ utilities.isEqual(viewUp, MPR_CAMERA_VALUES.axial.viewUp)
+ ) {
+ return 'axial';
+ }
+
+ if (
+ utilities.isEqual(viewRight, MPR_CAMERA_VALUES.sagittal.viewRight) &&
+ utilities.isEqual(viewUp, MPR_CAMERA_VALUES.sagittal.viewUp)
+ ) {
+ return 'sagittal';
+ }
+
+ if (
+ utilities.isEqual(viewRight, MPR_CAMERA_VALUES.coronal.viewRight) &&
+ utilities.isEqual(viewUp, MPR_CAMERA_VALUES.coronal.viewUp)
+ ) {
+ return 'coronal';
+ }
+
+ return undefined;
+};
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts
index 022b70cab..a21756c4e 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts
@@ -1,5 +1,7 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import { getDisplayUnit } from './utils';
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
@@ -19,9 +21,11 @@ const Angle = {
getValueTypeFromToolType,
customizationService
) => {
- const { annotation, viewportId } = csToolsEventDetail;
+ const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -55,7 +59,7 @@ const Angle = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
- const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
+ const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
@@ -65,6 +69,8 @@ const Angle = {
FrameOfReferenceUID,
points,
textBox,
+ isLocked,
+ isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -158,12 +164,15 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
};
}
-function getDisplayText(mappedAnnotations, displaySet, customizationService) {
- if (!mappedAnnotations || !mappedAnnotations.length) {
- return '';
- }
+function getDisplayText(mappedAnnotations, displaySet) {
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
- const displayText = [];
+ if (!mappedAnnotations || !mappedAnnotations.length) {
+ return displayText;
+ }
// Area is the same for all series
const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
@@ -181,9 +190,9 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
return displayText;
}
const roundedAngle = utils.roundNumber(angle, 2);
- displayText.push(
- `${roundedAngle} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})`
- );
+
+ displayText.primary.push(`${roundedAngle} ${getDisplayUnit(unit)}`);
+ displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts
index c91239144..bffba4a2a 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts
@@ -1,6 +1,7 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
+import { getIsLocked } from './utils/getIsLocked';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
-
+import { getIsVisible } from './utils/getIsVisible';
const Length = {
toAnnotation: measurement => {},
@@ -17,9 +18,11 @@ const Length = {
getValueTypeFromToolType,
customizationService
) => {
- const { annotation, viewportId } = csToolsEventDetail;
+ const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -53,7 +56,7 @@ const Length = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
- const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
+ const displayText = getDisplayText(mappedAnnotations, displaySet);
return {
uid: annotationUID,
@@ -61,6 +64,8 @@ const Length = {
FrameOfReferenceUID,
points,
textBox,
+ isLocked,
+ isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -107,15 +112,17 @@ function getMappedAnnotations(annotation, displaySetService) {
return annotations;
}
-function getDisplayText(mappedAnnotations, displaySet, customizationService) {
- if (!mappedAnnotations) {
- return '';
+function getDisplayText(mappedAnnotations, displaySet) {
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
+
+ if (!mappedAnnotations || !mappedAnnotations.length) {
+ return displayText;
}
- const displayText = [];
-
- // Area is the same for all series
- const { SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
+ const { SeriesNumber, SOPInstanceUID, frameNumber, text } = mappedAnnotations[0];
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
@@ -127,7 +134,13 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
- displayText.push(`(S: ${SeriesNumber}${instanceText}${frameText})`);
+ // Add the annotation text to the primary array
+ if (text) {
+ displayText.primary.push(text);
+ }
+
+ // Add the series information to the secondary array
+ displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts
index fceed7e48..241848b43 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts
@@ -1,9 +1,9 @@
-import { annotation } from '@cornerstonejs/tools';
-
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getDisplayUnit } from './utils';
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
const Bidirectional = {
toAnnotation: measurement => {},
@@ -14,9 +14,12 @@ const Bidirectional = {
getValueTypeFromToolType,
customizationService
) => {
- const { annotation, viewportId } = csToolsEventDetail;
+ const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
+
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -50,7 +53,7 @@ const Bidirectional = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
- const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
+ const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
@@ -60,6 +63,8 @@ const Bidirectional = {
FrameOfReferenceUID,
points,
textBox,
+ isLocked,
+ isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -153,12 +158,15 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
};
}
-function getDisplayText(mappedAnnotations, displaySet, customizationService) {
- if (!mappedAnnotations || !mappedAnnotations.length) {
- return '';
- }
+function getDisplayText(mappedAnnotations, displaySet) {
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
- const displayText = [];
+ if (!mappedAnnotations || !mappedAnnotations.length) {
+ return displayText;
+ }
// Area is the same for all series
const { length, width, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
@@ -175,10 +183,9 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
- displayText.push(
- `L: ${roundedLength} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})`
- );
- displayText.push(`W: ${roundedWidth} ${getDisplayUnit(unit)}`);
+ displayText.primary.push(`L: ${roundedLength} ${getDisplayUnit(unit)}`);
+ displayText.primary.push(`W: ${roundedWidth} ${getDisplayUnit(unit)}`);
+ displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts
index d6dba8762..ff529602f 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts
@@ -3,6 +3,8 @@ import { getDisplayUnit } from './utils';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getStatisticDisplayString } from './utils/getValueDisplayString';
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
const CircleROI = {
toAnnotation: measurement => {},
@@ -13,9 +15,12 @@ const CircleROI = {
getValueTypeFromToolType,
customizationService
) => {
- const { annotation, viewportId } = csToolsEventDetail;
+ const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
+
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -49,7 +54,7 @@ const CircleROI = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
- const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
+ const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
@@ -59,6 +64,8 @@ const CircleROI = {
FrameOfReferenceUID,
points,
textBox,
+ isLocked,
+ isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -161,12 +168,15 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
};
}
-function getDisplayText(mappedAnnotations, displaySet, customizationService) {
- if (!mappedAnnotations || !mappedAnnotations.length) {
- return '';
- }
+function getDisplayText(mappedAnnotations, displaySet) {
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
- const displayText = [];
+ if (!mappedAnnotations || !mappedAnnotations.length) {
+ return displayText;
+ }
// Area is the same for all series
const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0];
@@ -183,7 +193,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
// Area sometimes becomes undefined if `preventHandleOutsideImage` is off.
const roundedArea = utils.roundNumber(area || 0, 2);
- displayText.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
+ displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
// Todo: we need a better UI for displaying all these information
mappedAnnotations.forEach(mappedAnnotation => {
@@ -191,10 +201,8 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const maxStr = getStatisticDisplayString(max, unit, 'max');
- const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
- if (!displayText.includes(str)) {
- displayText.push(str);
- }
+ displayText.primary.push(maxStr);
+ displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
});
return displayText;
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts
index 717fcaf0a..e4f3449d8 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts
@@ -1,5 +1,7 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import { getDisplayUnit } from './utils';
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
@@ -19,9 +21,12 @@ const CobbAngle = {
getValueTypeFromToolType,
customizationService
) => {
- const { annotation, viewportId } = csToolsEventDetail;
+ const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
+
if (!metadata || !data) {
console.warn('Cobb Angle tool: Missing metadata or data');
return null;
@@ -55,7 +60,7 @@ const CobbAngle = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
- const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
+ const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
@@ -65,6 +70,8 @@ const CobbAngle = {
FrameOfReferenceUID,
points,
textBox,
+ isLocked,
+ isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -158,14 +165,17 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
};
}
-function getDisplayText(mappedAnnotations, displaySet, customizationService) {
+function getDisplayText(mappedAnnotations, displaySet) {
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
+
if (!mappedAnnotations || !mappedAnnotations.length) {
- return '';
+ return displayText;
}
- const displayText = [];
-
- // Area is the same for all series
+ // Angle is the same for all series
const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
@@ -181,9 +191,9 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
return displayText;
}
const roundedAngle = utils.roundNumber(angle, 2);
- displayText.push(
- `${roundedAngle} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})`
- );
+
+ displayText.primary.push(`${roundedAngle} ${getDisplayUnit(unit)}`);
+ displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts
index d9ccd35e5..7cdbfceb1 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts
@@ -1,5 +1,7 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import { getDisplayUnit } from './utils';
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getStatisticDisplayString } from './utils/getValueDisplayString';
@@ -13,9 +15,12 @@ const EllipticalROI = {
getValueTypeFromToolType,
customizationService
) => {
- const { annotation, viewportId } = csToolsEventDetail;
+ const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
+
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -60,6 +65,8 @@ const EllipticalROI = {
points,
textBox,
metadata,
+ isLocked,
+ isVisible,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
referencedImageId,
@@ -162,11 +169,14 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
}
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
- if (!mappedAnnotations || !mappedAnnotations.length) {
- return '';
- }
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
- const displayText = [];
+ if (!mappedAnnotations || !mappedAnnotations.length) {
+ return displayText;
+ }
// Area is the same for all series
const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0];
@@ -182,17 +192,15 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
const roundedArea = utils.roundNumber(area, 2);
- displayText.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
+ displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
// Todo: we need a better UI for displaying all these information
mappedAnnotations.forEach(mappedAnnotation => {
const { unit, max, SeriesNumber } = mappedAnnotation;
const maxStr = getStatisticDisplayString(max, unit, 'max');
- const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
- if (!displayText.includes(str)) {
- displayText.push(str);
- }
+ displayText.primary.push(maxStr);
+ displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
});
return displayText;
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts
index 71abc6148..0b0c1c6e8 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts
@@ -1,6 +1,9 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
+import { config } from '@cornerstonejs/tools/annotation';
const Length = {
toAnnotation: measurement => {},
@@ -18,9 +21,17 @@ const Length = {
getValueTypeFromToolType,
customizationService
) => {
- const { annotation, viewportId } = csToolsEventDetail;
+ const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
+ const colorString = config.style.getStyleProperty('color', { annotationUID });
+
+ // color string is like 'rgb(255, 255, 255)' we need them to be in RGBA array [255, 255, 255, 255]
+ // Todo: this should be in a utility
+ // const color = colorString.replace('rgb(', '').replace(')', '').split(',').map(Number);
+
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -54,7 +65,7 @@ const Length = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
- const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
+ const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
@@ -64,7 +75,10 @@ const Length = {
FrameOfReferenceUID,
points,
textBox,
+ isLocked,
+ isVisible,
metadata,
+ // color,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
referencedImageId,
@@ -158,14 +172,17 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
};
}
-function getDisplayText(mappedAnnotations, displaySet, customizationService) {
+function getDisplayText(mappedAnnotations, displaySet) {
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
+
if (!mappedAnnotations || !mappedAnnotations.length) {
- return '';
+ return displayText;
}
- const displayText = [];
-
- // Area is the same for all series
+ // Length is the same for all series
const { length, SeriesNumber, SOPInstanceUID, frameNumber, unit } = mappedAnnotations[0];
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
@@ -182,7 +199,8 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
return displayText;
}
const roundedLength = utils.roundNumber(length, 2);
- displayText.push(`${roundedLength} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`);
+ displayText.primary.push(`${roundedLength} ${unit}`);
+ displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts
index ff6809a7a..a9a3d03b3 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts
@@ -2,7 +2,8 @@ import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { getDisplayUnit } from './utils';
import { utils } from '@ohif/core';
-
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
/**
* Represents a mapping utility for Livewire measurements.
*/
@@ -28,6 +29,8 @@ const LivewireContour = {
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Livewire tool: Missing metadata or data');
return null;
@@ -65,7 +68,9 @@ const LivewireContour = {
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
- displayText: getDisplayText(annotation, displaySet, customizationService),
+ isLocked,
+ isVisible,
+ displayText: getDisplayText(annotation, displaySet),
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport: () => getColumnValueReport(annotation, customizationService),
@@ -119,7 +124,7 @@ function getColumnValueReport(annotation, customizationService) {
* @param {Object} displaySet - The display set object.
* @returns {string[]} - An array of display text.
*/
-function getDisplayText(annotation, displaySet, customizationService) {
+function getDisplayText(annotation, displaySet) {
const { metadata, data } = annotation;
if (!data.cachedStats || !data.cachedStats[`imageId:${metadata.referencedImageId}`]) {
@@ -142,19 +147,26 @@ function getDisplayText(annotation, displaySet, customizationService) {
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
const { SeriesNumber } = displaySet;
+ let seriesText = null;
if (SeriesNumber !== undefined) {
- displayText.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
+ seriesText = `S: ${SeriesNumber}${instanceText}${frameText}`;
}
+ const texts = [];
if (area) {
- /**
- * Add Area
- * Area sometimes becomes undefined if `preventHandleOutsideImage` is off
- */
const roundedArea = utils.roundNumber(area || 0, 2);
- displayText.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
+ texts.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
}
+ if (seriesText) {
+ texts.push(seriesText);
+ }
+
+ displayText.push({
+ text: texts,
+ series: seriesText,
+ });
+
return displayText;
}
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts
index 252e72792..b3309e723 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts
@@ -1,7 +1,10 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
-
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
+import { getDisplayUnit } from './utils';
+import { getStatisticDisplayString } from './utils/getValueDisplayString';
/**
* Represents a mapping utility for Planar Freehand ROI measurements.
*/
@@ -12,10 +15,11 @@ const PlanarFreehandROI = {
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} csToolsEventDetail Cornerstone event data
- * @param {DisplaySetService} DisplaySetService Service for managing display sets
+ * @param {DisplaySetService} displaySetService Service for managing display sets
* @param {CornerstoneViewportService} CornerstoneViewportService Service for managing viewports
* @param {Function} getValueTypeFromToolType Function to get value type from tool type
- * @returns {Measurement} Measurement instance
+ * @param {CustomizationService} customizationService Service for customization
+ * @returns {Measurement | null} Measurement instance or null if invalid
*/
toMeasurement: (
csToolsEventDetail,
@@ -27,8 +31,10 @@ const PlanarFreehandROI = {
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
- console.warn('PlanarFreehandROI tool: Missing metadata or data');
+ console.debug('PlanarFreehandROI tool: Missing metadata or data');
return null;
}
@@ -51,6 +57,9 @@ const PlanarFreehandROI = {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
}
+ const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
+ const displayText = getDisplayText(mappedAnnotations, displaySet);
+
return {
uid: annotationUID,
SOPInstanceUID,
@@ -65,21 +74,72 @@ const PlanarFreehandROI = {
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
- displayText: getDisplayText(annotation, displaySet, customizationService, displaySetService),
+ displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport: () => getColumnValueReport(annotation, customizationService),
+ isLocked,
+ isVisible,
};
},
};
/**
- * This function is used to convert the measurement data to a
- * format that is suitable for report generation (e.g. for the csv report).
- * The report returns a list of columns and corresponding values.
+ * Maps annotations to a structured format with relevant attributes.
*
- * @param {object} annotation
- * @returns {object} Report's content from this tool
+ * @param {Object} annotation The annotation object.
+ * @param {DisplaySetService} displaySetService Service for managing display sets.
+ * @returns {Array} Mapped annotations.
+ */
+function getMappedAnnotations(annotation, displaySetService) {
+ const { metadata, data } = annotation;
+ const { cachedStats } = data;
+ const { referencedImageId } = metadata;
+ const targets = Object.keys(cachedStats);
+
+ if (!targets.length) {
+ return [];
+ }
+
+ const annotations = [];
+ Object.keys(cachedStats).forEach(targetId => {
+ const targetStats = cachedStats[targetId];
+
+ const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes(
+ referencedImageId,
+ displaySetService,
+ annotation
+ );
+
+ const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
+
+ const { SeriesNumber } = displaySet;
+ const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats;
+
+ annotations.push({
+ SeriesInstanceUID,
+ SOPInstanceUID,
+ SeriesNumber,
+ frameNumber,
+ Modality,
+ unit: modalityUnit,
+ mean,
+ stdDev,
+ max,
+ area,
+ areaUnit,
+ });
+ });
+
+ return annotations;
+}
+
+/**
+ * Converts the measurement data to a format suitable for report generation.
+ *
+ * @param {object} annotation The annotation object.
+ * @param {CustomizationService} customizationService Service for customization.
+ * @returns {object} Report's content.
*/
function getColumnValueReport(annotation, customizationService) {
const { PlanarFreehandROI } = customizationService.get('cornerstone.measurements');
@@ -118,28 +178,25 @@ function getColumnValueReport(annotation, customizationService) {
/**
* Retrieves the display text for an annotation in a display set.
*
- * @param {Object} annotation - The annotation object.
- * @param {Object} displaySet - The display set object.
- * @returns {string[]} - An array of display text.
+ * @param {Array} mappedAnnotations The mapped annotations.
+ * @param {Object} displaySet The display set object.
+ * @returns {Object} Display text with primary and secondary information.
*/
-function getDisplayText(annotation, displaySet, customizationService, displaySetService) {
- const { PlanarFreehandROI } = customizationService.get('cornerstone.measurements');
- const { displayText: displayTextClosed, displayTextOpen } = PlanarFreehandROI;
+function getDisplayText(mappedAnnotations, displaySet) {
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
- const { metadata, data } = annotation;
+ if (!mappedAnnotations || !mappedAnnotations.length) {
+ return displayText;
+ }
- const isClosed = data.contour?.closed;
- const displayText = isClosed ? displayTextClosed : displayTextOpen;
-
- const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes(
- metadata.referencedImageId,
- displaySetService,
- annotation
- );
-
- const displayTextArray = [];
+ // Area is the same for all series
+ const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0];
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
+
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
@@ -148,47 +205,19 @@ function getDisplayText(annotation, displaySet, customizationService, displaySet
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
- const { SeriesNumber } = displaySet;
- if (SeriesNumber !== undefined) {
- displayTextArray.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
- }
+ const roundedArea = utils.roundNumber(area || 0, 2);
+ displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
- const stats =
- data.cachedStats[`imageId:${metadata.referencedImageId}`] ||
- Array.from(Object.values(data.cachedStats))[0];
+ mappedAnnotations.forEach(mappedAnnotation => {
+ const { unit, max, SeriesNumber } = mappedAnnotation;
- if (!stats) {
- return displayTextArray;
- }
+ const maxStr = getStatisticDisplayString(max, unit, 'max');
- const roundValues = values => {
- if (Array.isArray(values)) {
- return values.map(value => {
- if (isNaN(value)) {
- return value;
- }
- return utils.roundNumber(value, 2);
- });
- }
- return isNaN(values) ? [values] : [utils.roundNumber(values, 2)];
- };
-
- const findUnitForValue = (displayTextItems, value) =>
- displayTextItems.find(({ type, for: filter }) => type === 'unit' && filter.includes(value))
- ?.value;
-
- const formatDisplayText = (displayName, result, unit) =>
- `${displayName}: ${roundValues(result).join(', ')} ${unit}`;
-
- displayText.forEach(({ displayName, value, type }) => {
- if (type === 'value') {
- const result = stats[value];
- const unit = stats[findUnitForValue(displayText, value)] || '';
- displayTextArray.push(formatDisplayText(displayName, result, unit));
- }
+ displayText.primary.push(maxStr);
+ displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
});
- return displayTextArray;
+ return displayText;
}
export default PlanarFreehandROI;
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts
index e6b401aff..8ad3e70e6 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts
@@ -2,7 +2,8 @@ import SUPPORTED_TOOLS from './constants/supportedTools';
import { getDisplayUnit } from './utils';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
-
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
const Probe = {
toAnnotation: measurement => {},
@@ -21,7 +22,8 @@ const Probe = {
) => {
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
-
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Probe tool: Missing metadata or data');
return null;
@@ -65,6 +67,8 @@ const Probe = {
FrameOfReferenceUID,
points,
metadata,
+ isLocked,
+ isVisible,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
referencedImageId,
@@ -155,11 +159,14 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
}
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
- if (!mappedAnnotations || !mappedAnnotations.length) {
- return '';
- }
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
- const displayText = [];
+ if (!mappedAnnotations || !mappedAnnotations.length) {
+ return displayText;
+ }
const { value, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
@@ -172,13 +179,12 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
- if (value === undefined) {
- return displayText;
+
+ if (value !== undefined) {
+ const roundedValue = utils.roundNumber(value, 2);
+ displayText.primary.push(`${roundedValue} ${getDisplayUnit(unit)}`);
+ displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
}
- const roundedValue = utils.roundNumber(value, 2);
- displayText.push(
- `${roundedValue} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})`
- );
return displayText;
}
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts
index ade974f91..c35636b10 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts
@@ -3,6 +3,8 @@ import { getDisplayUnit } from './utils';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getStatisticDisplayString } from './utils/getValueDisplayString';
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
const RectangleROI = {
toAnnotation: measurement => {},
@@ -13,8 +15,10 @@ const RectangleROI = {
getValueTypeFromToolType,
customizationService
) => {
- const { annotation, viewportId } = csToolsEventDetail;
+ const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Rectangle ROI tool: Missing metadata or data');
@@ -71,6 +75,8 @@ const RectangleROI = {
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
+ isLocked,
+ isVisible,
};
},
};
@@ -163,11 +169,14 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
}
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
- if (!mappedAnnotations || !mappedAnnotations.length) {
- return '';
- }
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
- const displayText = [];
+ if (!mappedAnnotations || !mappedAnnotations.length) {
+ return displayText;
+ }
// Area is the same for all series
const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0];
@@ -184,7 +193,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
// Area sometimes becomes undefined if `preventHandleOutsideImage` is off.
const roundedArea = utils.roundNumber(area || 0, 2);
- displayText.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
+ displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
// Todo: we need a better UI for displaying all these information
mappedAnnotations.forEach(mappedAnnotation => {
@@ -192,10 +201,8 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const maxStr = getStatisticDisplayString(max, unit, 'max');
- const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
- if (!displayText.includes(str)) {
- displayText.push(str);
- }
+ displayText.primary.push(maxStr);
+ displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
});
return displayText;
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts
index ca01ce456..8ced413e8 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts
@@ -1,25 +1,32 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
+import { getDisplayUnit } from './utils';
+import { getStatisticDisplayString } from './utils/getValueDisplayString';
/**
* Represents a mapping utility for Spline ROI measurements.
*/
const SplineROI = {
- toAnnotation: measurement => {},
+ toAnnotation: measurement => {
+ // Implementation for converting measurement to annotation
+ },
/**
* Maps cornerstone annotation event data to measurement service format.
*
- * @param {Object} csToolsEventDetail Cornerstone event data
- * @param {DisplaySetService} DisplaySetService Service for managing display sets
- * @param {CornerstoneViewportService} CornerstoneViewportService Service for managing viewports
- * @param {Function} getValueTypeFromToolType Function to get value type from tool type
- * @returns {Measurement} Measurement instance
+ * @param {Object} csToolsEventDetail - Cornerstone event data
+ * @param {DisplaySetService} displaySetService - Service for managing display sets
+ * @param {CornerstoneViewportService} CornerstoneViewportService - Service for managing viewports
+ * @param {Function} getValueTypeFromToolType - Function to get value type from tool type
+ * @param {CustomizationService} customizationService - Service for customization
+ * @returns {Measurement | null} Measurement instance or null if invalid
*/
toMeasurement: (
csToolsEventDetail,
- DisplaySetService,
+ displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType,
customizationService
@@ -27,6 +34,8 @@ const SplineROI = {
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('SplineROI tool: Missing metadata or data');
return null;
@@ -39,18 +48,21 @@ const SplineROI = {
}
const { SOPInstanceUID, SeriesInstanceUID, frameNumber, StudyInstanceUID } =
- getSOPInstanceAttributes(referencedImageId);
+ getSOPInstanceAttributes(referencedImageId, displaySetService, annotation);
let displaySet;
if (SOPInstanceUID) {
- displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
+ displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
- displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
+ displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
}
+ const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
+ const displayText = getDisplayText(mappedAnnotations, displaySet);
+
return {
uid: annotationUID,
SOPInstanceUID,
@@ -61,24 +73,76 @@ const SplineROI = {
frameNumber,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
+ referencedImageId,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
- displayText: getDisplayText(annotation, displaySet, customizationService),
+ displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport: () => getColumnValueReport(annotation, customizationService),
+ isLocked,
+ isVisible,
};
},
};
/**
- * This function is used to convert the measurement data to a
- * format that is suitable for report generation (e.g. for the csv report).
- * The report returns a list of columns and corresponding values.
+ * Maps annotations to a structured format with relevant attributes.
*
- * @param {object} annotation
- * @returns {object} Report's content from this tool
+ * @param {Object} annotation - The annotation object.
+ * @param {DisplaySetService} displaySetService - Service for managing display sets.
+ * @returns {Array} Mapped annotations.
+ */
+function getMappedAnnotations(annotation, displaySetService) {
+ const { metadata, data } = annotation;
+ const { cachedStats } = data;
+ const { referencedImageId } = metadata;
+ const targets = Object.keys(cachedStats);
+
+ if (!targets.length) {
+ return [];
+ }
+
+ const annotations = [];
+ Object.keys(cachedStats).forEach(targetId => {
+ const targetStats = cachedStats[targetId];
+
+ const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes(
+ referencedImageId,
+ displaySetService,
+ annotation
+ );
+
+ const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
+
+ const { SeriesNumber } = displaySet;
+ const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats;
+
+ annotations.push({
+ SeriesInstanceUID,
+ SOPInstanceUID,
+ SeriesNumber,
+ frameNumber,
+ Modality,
+ unit: modalityUnit,
+ mean,
+ stdDev,
+ max,
+ area,
+ areaUnit,
+ });
+ });
+
+ return annotations;
+}
+
+/**
+ * Converts the measurement data to a format suitable for report generation.
+ *
+ * @param {object} annotation - The annotation object.
+ * @param {CustomizationService} customizationService - Service for customization.
+ * @returns {object} Report's content.
*/
function getColumnValueReport(annotation, customizationService) {
const { SplineROI } = customizationService.get('cornerstone.measurements');
@@ -107,11 +171,6 @@ function getColumnValueReport(annotation, customizationService) {
/** Add points */
if (data.contour.polyline) {
- /**
- * Points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
- * convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
- * so that it can be used in the CSV report
- */
columns.push('points');
values.push(data.contour.polyline.map(p => p.join(' ')).join(';'));
}
@@ -122,23 +181,25 @@ function getColumnValueReport(annotation, customizationService) {
/**
* Retrieves the display text for an annotation in a display set.
*
- * @param {Object} annotation - The annotation object.
+ * @param {Array} mappedAnnotations - The mapped annotations.
* @param {Object} displaySet - The display set object.
- * @returns {string[]} - An array of display text.
+ * @returns {Object} Display text with primary and secondary information.
*/
-function getDisplayText(annotation, displaySet, customizationService) {
- const { SplineROI } = customizationService.get('cornerstone.measurements');
- const { displayText } = SplineROI;
- const { metadata, data } = annotation;
+function getDisplayText(mappedAnnotations, displaySet) {
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
- if (!data.cachedStats || !data.cachedStats[`imageId:${metadata.referencedImageId}`]) {
- return [];
+ if (!mappedAnnotations || !mappedAnnotations.length) {
+ return displayText;
}
- const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes(metadata.referencedImageId);
- const displayTextArray = [];
+ // Area is the same for all series
+ const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0];
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
+
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
@@ -147,41 +208,20 @@ function getDisplayText(annotation, displaySet, customizationService) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
- const { SeriesNumber } = displaySet;
- if (SeriesNumber !== undefined) {
- displayTextArray.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
- }
+ const roundedArea = utils.roundNumber(area || 0, 2);
+ displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
- const stats = data.cachedStats[`imageId:${metadata.referencedImageId}`];
+ // we don't have max yet for splines rois
+ // mappedAnnotations.forEach(mappedAnnotation => {
+ // const { unit, max, SeriesNumber } = mappedAnnotation;
- const roundValues = values => {
- if (Array.isArray(values)) {
- return values.map(value => {
- if (isNaN(value)) {
- return value;
- }
- return utils.roundNumber(value);
- });
- }
- return isNaN(values) ? values : utils.roundNumber(values);
- };
+ // const maxStr = getStatisticDisplayString(max, unit, 'max');
- const findUnitForValue = (displayTextItems, value) =>
- displayTextItems.find(({ type, for: filter }) => type === 'unit' && filter.includes(value))
- ?.value;
+ // displayText.primary.push(maxStr);
+ // displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
+ // });
- const formatDisplayText = (displayName, result, unit) =>
- `${displayName}: ${Array.isArray(result) ? roundValues(result).join(', ') : roundValues(result)} ${unit}`;
-
- displayText.forEach(({ displayName, value, type }) => {
- if (type === 'value') {
- const result = stats[value];
- const unit = stats[findUnitForValue(displayText, value)] || '';
- displayTextArray.push(formatDisplayText(displayName, result, unit));
- }
- });
-
- return displayTextArray;
+ return displayText;
}
export default SplineROI;
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts
index b6997f3f5..e11f69870 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts
@@ -1,8 +1,8 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
-import { getDisplayUnit } from './utils';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
-
+import { getIsLocked } from './utils/getIsLocked';
+import { getIsVisible } from './utils/getIsVisible';
const UltrasoundDirectional = {
toAnnotation: measurement => {},
@@ -19,9 +19,10 @@ const UltrasoundDirectional = {
getValueTypeFromToolType,
customizationService
) => {
- const { annotation, viewportId } = csToolsEventDetail;
+ const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
-
+ const isLocked = getIsLocked(annotationUID);
+ const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -72,6 +73,8 @@ const UltrasoundDirectional = {
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
+ isLocked,
+ isVisible,
};
},
};
@@ -167,11 +170,14 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
}
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
- if (!mappedAnnotations || !mappedAnnotations.length) {
- return '';
- }
+ const displayText = {
+ primary: [],
+ secondary: [],
+ };
- const displayText = [];
+ if (!mappedAnnotations || !mappedAnnotations.length) {
+ return displayText;
+ }
const { xValues, yValues, units, isUnitless, SeriesNumber, SOPInstanceUID, frameNumber } =
mappedAnnotations[0];
@@ -185,21 +191,23 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
- const seriesText = `(S: ${SeriesNumber}${instanceText}${frameText})`;
+ const seriesText = `S: ${SeriesNumber}${instanceText}${frameText}`;
if (xValues === undefined || yValues === undefined) {
return displayText;
}
if (isUnitless) {
- displayText.push(`${utils.roundNumber(xValues[0], 2)} ${units[0]} ${seriesText}`);
+ displayText.primary.push(`${utils.roundNumber(xValues[0], 2)} ${units[0]}`);
} else {
const dist1 = Math.abs(xValues[1] - xValues[0]);
const dist2 = Math.abs(yValues[1] - yValues[0]);
- displayText.push(`${utils.roundNumber(dist1)} ${units[0]} ${seriesText}`);
- displayText.push(`${utils.roundNumber(dist2)} ${units[1]} ${seriesText}`);
+ displayText.primary.push(`${utils.roundNumber(dist1)} ${units[0]}`);
+ displayText.primary.push(`${utils.roundNumber(dist2)} ${units[1]}`);
}
+ displayText.secondary.push(seriesText);
+
return displayText;
}
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsLocked.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsLocked.ts
new file mode 100644
index 000000000..8a6ef2726
--- /dev/null
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsLocked.ts
@@ -0,0 +1,5 @@
+import { locking } from '@cornerstonejs/tools/annotation';
+
+export const getIsLocked = annotationUID => {
+ return locking.isAnnotationLocked(annotationUID);
+};
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsVisible.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsVisible.ts
new file mode 100644
index 000000000..fb310a2fc
--- /dev/null
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsVisible.ts
@@ -0,0 +1,6 @@
+import { visibility } from '@cornerstonejs/tools/annotation';
+
+export const getIsVisible = annotationUID => {
+ const isVisible = visibility.isAnnotationVisible(annotationUID);
+ return isVisible;
+};
diff --git a/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts b/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts
new file mode 100644
index 000000000..9fcf0f40b
--- /dev/null
+++ b/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts
@@ -0,0 +1,32 @@
+import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
+import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
+import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
+
+export function getViewportPresentations(
+ viewportId: string,
+ viewportOptions: AppTypes.ViewportGrid.GridViewportOptions
+) {
+ const { lutPresentationStore } = useLutPresentationStore.getState();
+ const { positionPresentationStore } = usePositionPresentationStore.getState();
+ const { segmentationPresentationStore } = useSegmentationPresentationStore.getState();
+
+ // NOTE: this is the new viewport state, we should not get the presentationIds from the cornerstoneViewportService
+ // since that has the old viewport state
+ const { presentationIds } = viewportOptions;
+
+ if (!presentationIds) {
+ return {
+ positionPresentation: null,
+ lutPresentation: null,
+ segmentationPresentation: null,
+ };
+ }
+
+ const { lutPresentationId, positionPresentationId, segmentationPresentationId } = presentationIds;
+
+ return {
+ positionPresentation: positionPresentationStore[positionPresentationId],
+ lutPresentation: lutPresentationStore[lutPresentationId],
+ segmentationPresentation: segmentationPresentationStore[segmentationPresentationId],
+ };
+}
diff --git a/extensions/cornerstone/src/utils/removeToolGroupSegmentationRepresentations.ts b/extensions/cornerstone/src/utils/removeViewportSegmentationRepresentations.ts
similarity index 67%
rename from extensions/cornerstone/src/utils/removeToolGroupSegmentationRepresentations.ts
rename to extensions/cornerstone/src/utils/removeViewportSegmentationRepresentations.ts
index b0f3e5796..edc7fc6f6 100644
--- a/extensions/cornerstone/src/utils/removeToolGroupSegmentationRepresentations.ts
+++ b/extensions/cornerstone/src/utils/removeViewportSegmentationRepresentations.ts
@@ -1,7 +1,7 @@
import { segmentation } from '@cornerstonejs/tools';
-function removeToolGroupSegmentationRepresentations(toolGroupId) {
- const representations = segmentation.state.getSegmentationRepresentations(toolGroupId);
+function removeViewportSegmentationRepresentations(viewportId) {
+ const representations = segmentation.state.getSegmentationRepresentations(viewportId);
if (!representations || !representations.length) {
return;
@@ -9,10 +9,9 @@ function removeToolGroupSegmentationRepresentations(toolGroupId) {
representations.forEach(representation => {
segmentation.state.removeSegmentationRepresentation(
- toolGroupId,
representation.segmentationRepresentationUID
);
});
}
-export default removeToolGroupSegmentationRepresentations;
+export default removeViewportSegmentationRepresentations;
diff --git a/extensions/default/src/Components/SidePanelWithServices.tsx b/extensions/default/src/Components/SidePanelWithServices.tsx
index e3ad5c6c2..26bed59ef 100644
--- a/extensions/default/src/Components/SidePanelWithServices.tsx
+++ b/extensions/default/src/Components/SidePanelWithServices.tsx
@@ -5,7 +5,7 @@ import { Types } from '@ohif/core';
export type SidePanelWithServicesProps = {
servicesManager: AppTypes.ServicesManager;
side: 'left' | 'right';
- className: string;
+ className?: string;
activeTabIndex: number;
tabs: any;
expandedWidth?: number;
diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js
index a7628e80a..42c1c6033 100644
--- a/extensions/default/src/DicomLocalDataSource/index.js
+++ b/extensions/default/src/DicomLocalDataSource/index.js
@@ -209,6 +209,10 @@ function createDicomLocalApi(dicomLocalConfig) {
return imageIds;
},
getImageIdsForInstance({ instance, frame }) {
+ if (instance.imageId) {
+ return instance.imageId;
+ }
+
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
const storedInstance = DicomMetadataStore.getInstance(
StudyInstanceUID,
diff --git a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx
index 22eea9bc8..ad292a268 100644
--- a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx
+++ b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx
@@ -2,8 +2,9 @@ import dcmjs from 'dcmjs';
import moment from 'moment';
import React, { useState, useMemo, useEffect } from 'react';
import { classes } from '@ohif/core';
-import { InputRange, Select, Typography, InputFilterText } from '@ohif/ui';
+import { InputFilterText } from '@ohif/ui';
import debounce from 'lodash.debounce';
+import { Select, SelectTrigger, SelectContent, SelectItem, Slider } from '@ohif/ui-next';
import DicomTagTable from './DicomTagTable';
import './DicomTagBrowser.css';
@@ -57,7 +58,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
return {
value: displaySetInstanceUID,
- label: `${SeriesNumber} (${Modality}): ${SeriesDescription}`,
+ label: `${SeriesNumber} (${Modality}): ${SeriesDescription}`,
description: displayDate,
};
});
@@ -107,62 +108,62 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
}, []);
return (
-
-
-
-
- Series
-
-
+
+
+
+
+ Series
-
-
- {showInstanceList && (
- onSelectChange({ value })}
>
- Instance Number
-
- )}
+
+ {displaySetList.find(ds => ds.value === selectedDisplaySetInstanceUID)?.label ||
+ 'Select Series'}
+
+
+ {displaySetList.map(item => {
+ return (
+
+ {item.label}
+ {item.description}
+
+ );
+ })}
+
+
+
{showInstanceList && (
-
-
{
- setInstanceNumber(parseInt(value));
+
+
+ Instance Number ({instanceNumber} of {activeDisplaySet.images.length})
+
+ {
+ setInstanceNumber(value);
}}
- minValue={1}
- maxValue={activeDisplaySet.images.length}
+ min={1}
+ max={activeDisplaySet.images.length}
step={1}
- inputClassName="w-full"
- labelPosition="left"
- trackColor={'#3a3f99'}
+ className="pt-4"
/>
)}
+
+
+ Search metadata
+
+
+
-
-
-
-
);
diff --git a/extensions/default/src/DicomWebDataSource/qido.js b/extensions/default/src/DicomWebDataSource/qido.js
index 4bea9a074..ee13fa8c4 100644
--- a/extensions/default/src/DicomWebDataSource/qido.js
+++ b/extensions/default/src/DicomWebDataSource/qido.js
@@ -154,9 +154,11 @@ function mapParams(params, options = {}) {
// Add more fields here if you want them in the result
].join(',');
- const { supportsWildcard } = options;
+ const useWildcard =
+ params?.disableWildcard !== undefined ? !params.disableWildcard : options.supportsWildcard;
+
const withWildcard = value => {
- return supportsWildcard && value ? `*${value}*` : value;
+ return useWildcard && value ? `*${value}*` : value;
};
const parameters = {
diff --git a/extensions/default/src/DicomWebDataSource/utils/getImageId.js b/extensions/default/src/DicomWebDataSource/utils/getImageId.js
index 29877bd43..b3dcef097 100644
--- a/extensions/default/src/DicomWebDataSource/utils/getImageId.js
+++ b/extensions/default/src/DicomWebDataSource/utils/getImageId.js
@@ -29,6 +29,10 @@ export default function getImageId({ instance, frame, config, thumbnail = false
return;
}
+ if (instance.imageId && frame === undefined) {
+ return instance.imageId;
+ }
+
if (instance.url) {
return instance.url;
}
diff --git a/extensions/default/src/Panels/PanelMeasurementTable.tsx b/extensions/default/src/Panels/PanelMeasurementTable.tsx
deleted file mode 100644
index 3df4c5cbe..000000000
--- a/extensions/default/src/Panels/PanelMeasurementTable.tsx
+++ /dev/null
@@ -1,305 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
-import { utils } from '@ohif/core';
-import {
- MeasurementTable,
- Dialog,
- Input,
- useViewportGrid,
- ButtonEnums,
- ActionButtons,
-} from '@ohif/ui';
-import debounce from 'lodash.debounce';
-
-import createReportDialogPrompt, {
- CREATE_REPORT_DIALOG_RESPONSE,
-} from './createReportDialogPrompt';
-import createReportAsync from '../Actions/createReportAsync';
-import findSRWithSameSeriesDescription from '../utils/findSRWithSameSeriesDescription';
-import { Separator } from '@ohif/ui-next';
-
-const { downloadCSVReport } = utils;
-
-export default function PanelMeasurementTable({
- servicesManager,
- commandsManager,
- extensionManager,
-}: withAppTypes): React.FunctionComponent {
- const { t } = useTranslation('MeasurementTable');
-
- const [viewportGrid, viewportGridService] = useViewportGrid();
- const { activeViewportId, viewports } = viewportGrid;
- const { measurementService, uiDialogService, uiNotificationService, displaySetService } =
- servicesManager.services;
- const [displayMeasurements, setDisplayMeasurements] = useState([]);
-
- useEffect(() => {
- const debouncedSetDisplayMeasurements = debounce(setDisplayMeasurements, 100);
- // ~~ Initial
- setDisplayMeasurements(_getMappedMeasurements(measurementService));
-
- // ~~ Subscription
- const added = measurementService.EVENTS.MEASUREMENT_ADDED;
- const addedRaw = measurementService.EVENTS.RAW_MEASUREMENT_ADDED;
- const updated = measurementService.EVENTS.MEASUREMENT_UPDATED;
- const removed = measurementService.EVENTS.MEASUREMENT_REMOVED;
- const cleared = measurementService.EVENTS.MEASUREMENTS_CLEARED;
- const subscriptions = [];
-
- [added, addedRaw, updated, removed, cleared].forEach(evt => {
- subscriptions.push(
- measurementService.subscribe(evt, () => {
- debouncedSetDisplayMeasurements(_getMappedMeasurements(measurementService));
- }).unsubscribe
- );
- });
-
- return () => {
- subscriptions.forEach(unsub => {
- unsub();
- });
- debouncedSetDisplayMeasurements.cancel();
- };
- }, []);
-
- async function exportReport() {
- const measurements = measurementService.getMeasurements();
-
- downloadCSVReport(measurements, measurementService);
- }
-
- async function clearMeasurements() {
- measurementService.clearMeasurements();
- }
-
- async function createReport(): Promise
{
- // filter measurements that are added to the active study
- const activeViewport = viewports.get(activeViewportId);
- const measurements = measurementService.getMeasurements();
- const displaySet = displaySetService.getDisplaySetByUID(
- activeViewport.displaySetInstanceUIDs[0]
- );
- const trackedMeasurements = measurements.filter(
- m => displaySet.StudyInstanceUID === m.referenceStudyUID
- );
-
- if (trackedMeasurements.length <= 0) {
- uiNotificationService.show({
- title: 'No Measurements',
- message: 'No Measurements are added to the current Study.',
- type: 'info',
- duration: 3000,
- });
- return;
- }
-
- const promptResult = await createReportDialogPrompt(uiDialogService, {
- extensionManager,
- });
-
- if (promptResult.action === CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT) {
- const dataSources = extensionManager.getDataSources(promptResult.dataSourceName);
- const dataSource = dataSources[0];
-
- const SeriesDescription =
- // isUndefinedOrEmpty
- promptResult.value === undefined || promptResult.value === ''
- ? 'Research Derived Series' // default
- : promptResult.value; // provided value
-
- // Reuse an existing series having the same series description to avoid
- // creating too many series instances.
- const options = findSRWithSameSeriesDescription(SeriesDescription, displaySetService);
-
- const getReport = async () => {
- return commandsManager.runCommand(
- 'storeMeasurements',
- {
- measurementData: trackedMeasurements,
- dataSource,
- additionalFindingTypes: ['ArrowAnnotate'],
- options,
- },
- 'CORNERSTONE_STRUCTURED_REPORT'
- );
- };
-
- return createReportAsync({ servicesManager, getReport });
- }
- }
-
- const jumpToImage = ({ uid, isActive }) => {
- measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid);
-
- onMeasurementItemClickHandler({ uid, isActive });
- };
-
- const onMeasurementItemEditHandler = ({ uid, isActive }) => {
- const measurement = measurementService.getMeasurement(uid);
- //Todo: why we are jumping to image?
- // jumpToImage({ id, isActive });
-
- const onSubmitHandler = ({ action, value }) => {
- switch (action.id) {
- case 'save': {
- measurementService.update(
- uid,
- {
- ...measurement,
- ...value,
- },
- true
- );
- }
- }
- uiDialogService.dismiss({ id: 'enter-annotation' });
- };
-
- uiDialogService.create({
- id: 'enter-annotation',
- centralize: true,
- isDraggable: false,
- showOverlay: true,
- content: Dialog,
- contentProps: {
- title: 'Annotation',
- noCloseButton: true,
- value: { label: measurement.label || '' },
- body: ({ value, setValue }) => {
- const onChangeHandler = event => {
- event.persist();
- setValue(value => ({ ...value, label: event.target.value }));
- };
-
- const onKeyPressHandler = event => {
- if (event.key === 'Enter') {
- onSubmitHandler({ value, action: { id: 'save' } });
- }
- };
- return (
-
- );
- },
- actions: [
- { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
- { id: 'save', text: 'Save', type: ButtonEnums.type.primary },
- ],
- onSubmit: onSubmitHandler,
- },
- });
- };
-
- const onMeasurementItemClickHandler = ({ uid, isActive }) => {
- if (!isActive) {
- const measurements = [...displayMeasurements];
- const measurement = measurements.find(m => m.uid === uid);
-
- measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true));
- measurement.isActive = true;
- setDisplayMeasurements(measurements);
- }
- };
-
- return (
- <>
-
-
-
-
- >
- );
-}
-
-PanelMeasurementTable.propTypes = {
- servicesManager: PropTypes.object.isRequired,
-};
-
-function _getMappedMeasurements(measurementService) {
- const measurements = measurementService.getMeasurements();
-
- const mappedMeasurements = measurements.map((m, index) =>
- _mapMeasurementToDisplay(m, index, measurementService.VALUE_TYPES)
- );
-
- return mappedMeasurements;
-}
-
-/**
- * Map the measurements to the display text.
- * Adds finding and site information to the displayText and/or label,
- * and provides as 'displayText' and 'label', while providing the original
- * values as baseDisplayText and baseLabel
- */
-function _mapMeasurementToDisplay(measurement, index, types) {
- const {
- displayText: baseDisplayText,
- uid,
- label: baseLabel,
- type,
- selected,
- findingSites,
- finding,
- } = measurement;
-
- const firstSite = findingSites?.[0];
- const label = baseLabel || finding?.text || firstSite?.text || '(empty)';
- let displayText = baseDisplayText || [];
- if (findingSites) {
- const siteText = [];
- findingSites.forEach(site => {
- if (site?.text !== label) {
- siteText.push(site.text);
- }
- });
- displayText = [...siteText, ...displayText];
- }
- if (finding && finding?.text !== label) {
- displayText = [finding.text, ...displayText];
- }
-
- return {
- uid,
- label,
- baseLabel,
- measurementType: type,
- displayText,
- baseDisplayText,
- isActive: selected,
- finding,
- findingSites,
- };
-}
diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx
index 9fa050b48..4dd7225b2 100644
--- a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx
+++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx
@@ -1,5 +1,4 @@
-import React, { useState, useEffect, useRef } from 'react';
-import PropTypes from 'prop-types';
+import React, { useState, useEffect } from 'react';
import { useImageViewer, useViewportGrid } from '@ohif/ui';
import { StudyBrowser } from '@ohif/ui-next';
import { utils } from '@ohif/core';
@@ -80,7 +79,7 @@ function PanelStudyBrowser({
uiNotificationService.show({
title: 'Thumbnail Double Click',
message: 'The selected display sets could not be added to the viewport.',
- type: 'info',
+ type: 'error',
duration: 3000,
});
}
@@ -198,7 +197,7 @@ function PanelStudyBrowser({
// if (!hasLoadedViewports) {
// return;
// }
- const { displaySetsAdded, options } = data;
+ const { displaySetsAdded } = data;
displaySetsAdded.forEach(async dSet => {
const newImageSrcEntry = {};
const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID);
@@ -319,16 +318,6 @@ function PanelStudyBrowser({
);
}
-PanelStudyBrowser.propTypes = {
- servicesManager: PropTypes.object.isRequired,
- dataSource: PropTypes.shape({
- getImageIdsForDisplaySet: PropTypes.func.isRequired,
- }).isRequired,
- getImageSrc: PropTypes.func.isRequired,
- getStudiesForPatientByMRN: PropTypes.func.isRequired,
- requestDisplaySetCreationForStudy: PropTypes.func.isRequired,
-};
-
export default PanelStudyBrowser;
/**
diff --git a/extensions/default/src/Panels/createReportDialogPrompt.tsx b/extensions/default/src/Panels/createReportDialogPrompt.tsx
index 55c921502..4e8dfa304 100644
--- a/extensions/default/src/Panels/createReportDialogPrompt.tsx
+++ b/extensions/default/src/Panels/createReportDialogPrompt.tsx
@@ -1,11 +1,7 @@
import React from 'react';
import { ButtonEnums, Dialog, Input, Select } from '@ohif/ui';
-
-export const CREATE_REPORT_DIALOG_RESPONSE = {
- CANCEL: 0,
- CREATE_REPORT: 1,
-};
+import PROMPT_RESPONSES from '../utils/_shared/PROMPT_RESPONSES';
export default function CreateReportDialogPrompt(uiDialogService, { extensionManager }) {
return new Promise(function (resolve, reject) {
@@ -16,7 +12,7 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan
uiDialogService.dismiss({ id: dialogId });
// Notify of cancel action
resolve({
- action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL,
+ action: PROMPT_RESPONSES.CANCEL,
value: undefined,
dataSourceName: undefined,
});
@@ -32,14 +28,14 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan
switch (action.id) {
case 'save':
resolve({
- action: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT,
+ action: PROMPT_RESPONSES.CREATE_REPORT,
value: value.label,
dataSourceName: value.dataSourceName,
});
break;
case 'cancel':
resolve({
- action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL,
+ action: PROMPT_RESPONSES.CANCEL,
value: undefined,
dataSourceName: undefined,
});
@@ -90,7 +86,7 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan
if (event.key === 'Enter') {
uiDialogService.dismiss({ id: dialogId });
resolve({
- action: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT,
+ action: PROMPT_RESPONSES.CREATE_REPORT,
value: value.label,
});
}
@@ -102,7 +98,7 @@ export default function CreateReportDialogPrompt(uiDialogService, { extensionMan
diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts
index 9037ff051..adcc3a86e 100644
--- a/extensions/default/src/commandsModule.ts
+++ b/extensions/default/src/commandsModule.ts
@@ -1,4 +1,4 @@
-import { utils, Types } from '@ohif/core';
+import { Types } from '@ohif/core';
import { ContextMenuController, defaultContextMenu } from './CustomizableContextMenu';
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
@@ -10,8 +10,13 @@ import findViewportsByPosition, {
import { ContextMenuProps } from './CustomizableContextMenu/types';
import { NavigateHistory } from './types/commandModuleTypes';
import { history } from '@ohif/app';
+import { useViewportGridStore } from './stores/useViewportGridStore';
+import { useDisplaySetSelectorStore } from './stores/useDisplaySetSelectorStore';
+import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolStageIndexStore';
+import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
+import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
+import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
-const { subscribeToNextViewportGridChange } = utils;
export type HangingProtocolParams = {
protocolId?: string;
stageIndex?: number;
@@ -36,7 +41,6 @@ const commandsModule = ({
uiNotificationService,
viewportGridService,
displaySetService,
- stateSyncService,
} = servicesManager.services;
// Define a context menu controller for use with any context menus
@@ -134,9 +138,9 @@ const commandsModule = ({
// the activeViewportId
const state = viewportGridService.getState();
const hpInfo = hangingProtocolService.getState();
- const stateSyncReduce = reuseCachedLayouts(state, hangingProtocolService, stateSyncService);
- const { hangingProtocolStageIndexMap, viewportGridStore, displaySetSelectorMap } =
- stateSyncReduce;
+ reuseCachedLayouts(state, hangingProtocolService);
+ const { hangingProtocolStageIndexMap } = useHangingProtocolStageIndexStore.getState();
+ const { displaySetSelectorMap } = useDisplaySetSelectorStore.getState();
if (!protocolId) {
// Reuse the previous protocol id, and optionally stage
@@ -165,7 +169,8 @@ const commandsModule = ({
useStageIdx || 0
}`;
- const restoreProtocol = !reset && viewportGridStore[storedHanging];
+ const { viewportGridState } = useViewportGridStore.getState();
+ const restoreProtocol = !reset && viewportGridState[storedHanging];
if (
protocolId === hpInfo.protocolId &&
@@ -185,17 +190,15 @@ const commandsModule = ({
restoreProtocol,
});
if (restoreProtocol) {
- viewportGridService.set(viewportGridStore[storedHanging]);
+ viewportGridService.set(viewportGridState[storedHanging]);
}
}
// Do this after successfully applying the update
- // Note, don't store the active display set - it is only needed while
- // changing display sets. This causes jump to measurement to fail on
- // multi-study display.
- delete displaySetSelectorMap[
- `${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`
- ];
- stateSyncService.store(stateSyncReduce);
+ const { setDisplaySetSelector } = useDisplaySetSelectorStore.getState();
+ setDisplaySetSelector(
+ `${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`,
+ null
+ );
return true;
} catch (e) {
console.error(e);
@@ -215,7 +218,8 @@ const commandsModule = ({
stageIndex: desiredStageIndex,
activeStudy,
} = hangingProtocolService.getActiveProtocol();
- const { toggleHangingProtocol } = stateSyncService.getState();
+ const { toggleHangingProtocol, setToggleHangingProtocol } =
+ useToggleHangingProtocolStore.getState();
const storedHanging = `${activeStudy.StudyInstanceUID}:${protocolId}:${stageIndex | 0}`;
if (
protocol.id === protocolId &&
@@ -227,14 +231,9 @@ const commandsModule = ({
};
return actions.setHangingProtocol(previousState);
} else {
- stateSyncService.store({
- toggleHangingProtocol: {
- ...toggleHangingProtocol,
- [storedHanging]: {
- protocolId: protocol.id,
- stageIndex: desiredStageIndex,
- },
- },
+ setToggleHangingProtocol(storedHanging, {
+ protocolId: protocol.id,
+ stageIndex: desiredStageIndex,
});
return actions.setHangingProtocol({
protocolId,
@@ -281,12 +280,15 @@ const commandsModule = ({
const completeLayout = () => {
const state = viewportGridService.getState();
- const stateReduce = findViewportsByPosition(state, { numRows, numCols }, stateSyncService);
+ findViewportsByPosition(state, { numRows, numCols });
+
+ const { viewportsByPosition, initialInDisplay } = useViewportsByPositionStore.getState();
+
const findOrCreateViewport = layoutFindOrCreate.bind(
null,
hangingProtocolService,
isHangingProtocolLayout,
- stateReduce.viewportsByPosition
+ { ...viewportsByPosition, initialInDisplay }
);
viewportGridService.setLayout({
@@ -295,7 +297,6 @@ const commandsModule = ({
findOrCreateViewport,
isHangingProtocolLayout,
});
- stateSyncService.store(stateReduce);
};
// Need to finish any work in the callback
window.setTimeout(completeLayout, 0);
@@ -309,9 +310,9 @@ const commandsModule = ({
if (layout.numCols === 1 && layout.numRows === 1) {
// The viewer is in one-up. Check if there is a state to restore/toggle back to.
- const { toggleOneUpViewportGridStore } = stateSyncService.getState();
+ const { toggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState();
- if (!toggleOneUpViewportGridStore.layout) {
+ if (!toggleOneUpViewportGridStore) {
return;
}
// There is a state to toggle back to. The viewport that was
@@ -374,11 +375,9 @@ const commandsModule = ({
// We are not in one-up, so toggle to one up.
// Store the current viewport grid state so we can toggle it back later.
- stateSyncService.store({
- toggleOneUpViewportGridStore: viewportGridState,
- });
+ const { setToggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState();
+ setToggleOneUpViewportGridStore(viewportGridState);
- // This findOrCreateViewport only return one viewport - the active
// one being toggled to one up.
const findOrCreateViewport = () => {
return {
@@ -395,19 +394,6 @@ const commandsModule = ({
findOrCreateViewport,
isHangingProtocolLayout: true,
});
-
- // Subscribe to ANY (i.e. manual and hanging protocol) layout changes so that
- // any grid layout state to toggle to from one up is cleared. This is performed on
- // a timeout to avoid clearing the state for the actual to one up change.
- // Whenever the next layout change event is fired, the subscriptions are unsubscribed.
- const clearToggleOneUpViewportGridStore = () => {
- const toggleOneUpViewportGridStore = {};
- stateSyncService.store({
- toggleOneUpViewportGridStore,
- });
- };
-
- subscribeToNextViewportGridChange(viewportGridService, clearToggleOneUpViewportGridStore);
}
},
diff --git a/extensions/default/src/findViewportsByPosition.ts b/extensions/default/src/findViewportsByPosition.ts
index 7d0d10f83..87ff955f4 100644
--- a/extensions/default/src/findViewportsByPosition.ts
+++ b/extensions/default/src/findViewportsByPosition.ts
@@ -1,4 +1,4 @@
-import { StateSyncService } from '@ohif/core';
+import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
/**
* This find or create viewport is paired with the reduce results from
@@ -69,16 +69,12 @@ export const findOrCreateViewport = (
* @returns Set of states that can be applied to the state sync to remember
* the current view state.
*/
-const findViewportsByPosition = (
- state,
- { numRows, numCols },
- syncService: StateSyncService
-): Record
> => {
+const findViewportsByPosition = (state, { numRows, numCols }) => {
const { viewports } = state;
- const syncState = syncService.getState();
- const viewportsByPosition = { ...syncState.viewportsByPosition };
+ const { setViewportsByPosition, addInitialInDisplay } = useViewportsByPositionStore.getState();
const initialInDisplay = [];
+ const viewportsByPosition = {};
viewports.forEach(viewport => {
if (viewport.positionId) {
const storedViewport = {
@@ -86,6 +82,7 @@ const findViewportsByPosition = (
viewportOptions: { ...viewport.viewportOptions },
};
viewportsByPosition[viewport.positionId] = storedViewport;
+ setViewportsByPosition(viewport.positionId, storedViewport);
}
});
@@ -99,10 +96,7 @@ const findViewportsByPosition = (
}
}
- // Store the initially displayed elements
- viewportsByPosition.initialInDisplay = initialInDisplay;
-
- return { viewportsByPosition };
+ initialInDisplay.forEach(displaySetInstanceUID => addInitialInDisplay(displaySetInstanceUID));
};
export default findViewportsByPosition;
diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js
index 6673bbc4e..ec0da3579 100644
--- a/extensions/default/src/getHangingProtocolModule.js
+++ b/extensions/default/src/getHangingProtocolModule.js
@@ -27,6 +27,17 @@ const defaultProtocol = {
viewportType: 'stack',
toolGroupId: 'default',
allowUnmatchedView: true,
+ syncGroups: [
+ {
+ type: 'hydrateseg',
+ id: 'sameFORId',
+ source: true,
+ target: true,
+ options: {
+ matchingRules: ['sameFOR'],
+ },
+ },
+ ],
},
displaySets: [
{
@@ -58,8 +69,6 @@ const defaultProtocol = {
},
},
],
- // Can be used to select matching studies
- // studyMatchingRules: [],
},
},
stages: [
@@ -88,6 +97,17 @@ const defaultProtocol = {
// index: 180,
// preset: 'middle', // 'first', 'last', 'middle'
// },
+ syncGroups: [
+ {
+ type: 'hydrateseg',
+ id: 'sameFORId',
+ source: true,
+ target: true,
+ // options: {
+ // matchingRules: ['sameFOR'],
+ // },
+ },
+ ],
},
displaySets: [
{
diff --git a/extensions/default/src/getPanelModule.tsx b/extensions/default/src/getPanelModule.tsx
index 24c1cb3d8..22d77e8aa 100644
--- a/extensions/default/src/getPanelModule.tsx
+++ b/extensions/default/src/getPanelModule.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { WrappedPanelStudyBrowser, PanelMeasurementTable } from './Panels';
+import { WrappedPanelStudyBrowser } from './Panels';
import i18n from 'i18next';
// TODO:
@@ -8,17 +8,6 @@ import i18n from 'i18next';
// - show errors in UI for thumbnails if promise fails
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
- const wrappedMeasurementPanel = ({ tab }) => {
- return (
-
- );
- };
-
return [
{
name: 'seriesList',
@@ -34,14 +23,6 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
/>
),
},
- {
- name: 'measurements',
- iconName: 'tab-linear',
- iconLabel: 'Measure',
- label: i18n.t('SidePanel:Measurements'),
- secondaryLabel: i18n.t('SidePanel:Measurements'),
- component: wrappedMeasurementPanel,
- },
];
}
diff --git a/extensions/default/src/hangingprotocols/hpMNGrid.ts b/extensions/default/src/hangingprotocols/hpMNGrid.ts
index b65cc0b70..1f07242c8 100644
--- a/extensions/default/src/hangingprotocols/hpMNGrid.ts
+++ b/extensions/default/src/hangingprotocols/hpMNGrid.ts
@@ -1,5 +1,20 @@
import { Types } from '@ohif/core';
+/**
+ * Sync group configuration for hydrating segmentations across viewports
+ * that share the same frame of reference
+ * @type {Types.HangingProtocol.SyncGroup}
+ */
+export const HYDRATE_SEG_SYNC_GROUP = {
+ type: 'hydrateseg',
+ id: 'sameFORId',
+ source: true,
+ target: true,
+ options: {
+ matchingRules: ['sameFOR'],
+ },
+} as const;
+
/**
* This hanging protocol can be activated on the primary mode by directly
* referencing it in a URL or by directly including it within a mode, e.g.:
@@ -48,6 +63,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
viewportType: 'stack',
toolGroupId: 'default',
allowUnmatchedView: true,
+ syncGroups: [HYDRATE_SEG_SYNC_GROUP],
},
displaySets: [
{
@@ -59,6 +75,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
stages: [
{
id: '2x2',
+ name: '2x2',
stageActivation: {
enabled: {
minViewportsMatched: 4,
@@ -76,6 +93,17 @@ const hpMN: Types.HangingProtocol.Protocol = {
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
+ syncGroups: [
+ {
+ type: 'hydrateseg',
+ id: 'sameFORId',
+ source: true,
+ target: true,
+ options: {
+ matchingRules: ['sameFOR'],
+ },
+ },
+ ],
},
displaySets: [
{
@@ -99,6 +127,17 @@ const hpMN: Types.HangingProtocol.Protocol = {
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
+ syncGroups: [
+ {
+ type: 'hydrateseg',
+ id: 'sameFORId',
+ source: true,
+ target: true,
+ // options: {
+ // matchingRules: ['sameFOR'],
+ // },
+ },
+ ],
},
displaySets: [
{
@@ -111,6 +150,17 @@ const hpMN: Types.HangingProtocol.Protocol = {
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
+ syncGroups: [
+ {
+ type: 'hydrateseg',
+ id: 'sameFORId',
+ source: true,
+ target: true,
+ // options: {
+ // matchingRules: ['sameFOR'],
+ // },
+ },
+ ],
},
displaySets: [
{
diff --git a/extensions/default/src/hooks/usePatientInfo.tsx b/extensions/default/src/hooks/usePatientInfo.tsx
new file mode 100644
index 000000000..ef8773a0d
--- /dev/null
+++ b/extensions/default/src/hooks/usePatientInfo.tsx
@@ -0,0 +1,63 @@
+import { useState, useEffect } from 'react';
+import { utils } from '@ohif/core';
+
+const { formatPN, formatDate } = utils;
+
+function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
+ const { displaySetService } = servicesManager.services;
+
+ const [patientInfo, setPatientInfo] = useState({
+ PatientName: '',
+ PatientID: '',
+ PatientSex: '',
+ PatientDOB: '',
+ });
+ const [isMixedPatients, setIsMixedPatients] = useState(false);
+ const displaySets = displaySetService.getActiveDisplaySets();
+
+ const checkMixedPatients = PatientID => {
+ const displaySets = displaySetService.getActiveDisplaySets();
+ let isMixedPatients = false;
+ displaySets.forEach(displaySet => {
+ const instance = displaySet?.instances?.[0] || displaySet?.instance;
+ if (!instance) {
+ return;
+ }
+ if (instance.PatientID !== PatientID) {
+ isMixedPatients = true;
+ }
+ });
+ setIsMixedPatients(isMixedPatients);
+ };
+
+ const updatePatientInfo = () => {
+ const displaySet = displaySets[0];
+ const instance = displaySet?.instances?.[0] || displaySet?.instance;
+ if (!instance) {
+ return;
+ }
+ setPatientInfo({
+ PatientID: instance.PatientID || null,
+ PatientName: instance.PatientName ? formatPN(instance.PatientName.Alphabetic) : null,
+ PatientSex: instance.PatientSex || null,
+ PatientDOB: formatDate(instance.PatientBirthDate) || null,
+ });
+ checkMixedPatients(instance.PatientID || null);
+ };
+
+ useEffect(() => {
+ const subscription = displaySetService.subscribe(
+ displaySetService.EVENTS.DISPLAY_SETS_ADDED,
+ () => updatePatientInfo()
+ );
+ return () => subscription.unsubscribe();
+ }, []);
+
+ useEffect(() => {
+ updatePatientInfo();
+ }, [displaySets]);
+
+ return { patientInfo, isMixedPatients };
+}
+
+export default usePatientInfo;
diff --git a/extensions/default/src/index.ts b/extensions/default/src/index.ts
index 2c2524181..72f4ecad7 100644
--- a/extensions/default/src/index.ts
+++ b/extensions/default/src/index.ts
@@ -18,6 +18,23 @@ import { createReportDialogPrompt } from './Panels';
import createReportAsync from './Actions/createReportAsync';
import StaticWadoClient from './DicomWebDataSource/utils/StaticWadoClient';
import { cleanDenaturalizedDataset } from './DicomWebDataSource/utils';
+import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
+import { useViewportGridStore } from './stores/useViewportGridStore';
+import { useUIStateStore } from './stores/useUIStateStore';
+import { useDisplaySetSelectorStore } from './stores/useDisplaySetSelectorStore';
+import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolStageIndexStore';
+import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
+import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
+import {
+ callLabelAutocompleteDialog,
+ showLabelAnnotationPopup,
+ callInputDialog,
+} from './utils/callInputDialog';
+import colorPickerDialog from './utils/colorPickerDialog';
+
+import promptSaveReport from './utils/promptSaveReport';
+import promptLabelAnnotation from './utils/promptLabelAnnotation';
+import usePatientInfo from './hooks/usePatientInfo';
const defaultExtension: Types.Extensions.Extension = {
/**
@@ -25,6 +42,14 @@ const defaultExtension: Types.Extensions.Extension = {
*/
id,
preRegistration,
+ onModeExit() {
+ useViewportGridStore.getState().clearViewportGridState();
+ useUIStateStore.getState().clearUIState();
+ useDisplaySetSelectorStore.getState().clearDisplaySetSelectorMap();
+ useHangingProtocolStageIndexStore.getState().clearHangingProtocolStageIndexMap();
+ useToggleHangingProtocolStore.getState().clearToggleHangingProtocol();
+ useViewportsByPositionStore.getState().clearViewportsByPosition();
+ },
getDataSourcesModule,
getViewportModule,
getLayoutTemplateModule,
@@ -58,4 +83,19 @@ export {
createReportAsync,
StaticWadoClient,
cleanDenaturalizedDataset,
+ // Export all stores
+ useDisplaySetSelectorStore,
+ useHangingProtocolStageIndexStore,
+ useToggleHangingProtocolStore,
+ useToggleOneUpViewportGridStore,
+ useUIStateStore,
+ useViewportGridStore,
+ useViewportsByPositionStore,
+ showLabelAnnotationPopup,
+ callLabelAutocompleteDialog,
+ callInputDialog,
+ promptSaveReport,
+ promptLabelAnnotation,
+ colorPickerDialog,
+ usePatientInfo,
};
diff --git a/extensions/default/src/init.ts b/extensions/default/src/init.ts
index 2e5e831c8..ee5eee7ef 100644
--- a/extensions/default/src/init.ts
+++ b/extensions/default/src/init.ts
@@ -16,8 +16,7 @@ export default function init({
configuration = {},
commandsManager,
}: withAppTypes): void {
- const { stateSyncService, toolbarService, cineService, viewportGridService } =
- servicesManager.services;
+ const { toolbarService, cineService, viewportGridService } = servicesManager.services;
toolbarService.registerEventForToolbarUpdate(cineService, [
cineService.EVENTS.CINE_STATE_CHANGED,
@@ -29,38 +28,6 @@ export default function init({
// we need to recalculate the SUV Scaling Factors
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handlePETImageMetadata);
- // viewportGridStore is a sync state which stores the entire
- // ViewportGridService getState, by the keys `::`
- // Used to recover manual changes to the layout of a stage.
- stateSyncService.register('viewportGridStore', { clearOnModeExit: true });
-
- // uiStateStore is a sync state which stores the relevant
- // UI state for the viewer
- stateSyncService.register('uiStateStore', { clearOnModeExit: true });
-
- // displaySetSelectorMap stores a map from
- // `::` to
- // a displaySetInstanceUID, used to display named display sets in
- // specific spots within a hanging protocol and be able to remember what the
- // user did with those named spots between stages and protocols.
- stateSyncService.register('displaySetSelectorMap', { clearOnModeExit: true });
-
- // Stores a map from `:${protocolId}` to the getHPInfo results
- // in order to recover the correct stage when returning to a Hanging Protocol.
- stateSyncService.register('hangingProtocolStageIndexMap', {
- clearOnModeExit: true,
- });
-
- // Stores a map from the to be applied hanging protocols `:`
- // to the previously applied hanging protocolStageIndexMap key, in order to toggle
- // off the applied protocol and remember the old state.
- stateSyncService.register('toggleHangingProtocol', { clearOnModeExit: true });
-
- // Stores the viewports by `rows-cols` position so that when the layout
- // changes numRows and numCols, the viewports can be remembers and then replaced
- // afterwards.
- stateSyncService.register('viewportsByPosition', { clearOnModeExit: true });
-
// Adds extra custom attributes for use by hanging protocols
registerHangingProtocolAttributes({ servicesManager });
@@ -119,7 +86,6 @@ const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
const imageIds = instances.map(instance => instance.imageId);
const instanceMetadataArray = [];
-
// try except block to prevent errors when the metadata is not correct
try {
imageIds.forEach(imageId => {
diff --git a/extensions/default/src/stores/index.ts b/extensions/default/src/stores/index.ts
new file mode 100644
index 000000000..992132405
--- /dev/null
+++ b/extensions/default/src/stores/index.ts
@@ -0,0 +1,7 @@
+export { useDisplaySetSelectorStore } from './useDisplaySetSelectorStore';
+export { useHangingProtocolStageIndexStore } from './useHangingProtocolStageIndexStore';
+export { useToggleHangingProtocolStore } from './useToggleHangingProtocolStore';
+export { useToggleOneUpViewportGridStore } from './useToggleOneUpViewportGridStore';
+export { useUIStateStore } from './useUIStateStore';
+export { useViewportGridStore } from './useViewportGridStore';
+export { useViewportsByPositionStore } from './useViewportsByPositionStore';
diff --git a/extensions/default/src/stores/useDisplaySetSelectorStore.ts b/extensions/default/src/stores/useDisplaySetSelectorStore.ts
new file mode 100644
index 000000000..e5973ad0e
--- /dev/null
+++ b/extensions/default/src/stores/useDisplaySetSelectorStore.ts
@@ -0,0 +1,83 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+
+/**
+ * Identifier for the display set selector store type.
+ */
+const PRESENTATION_TYPE_ID = 'displaySetSelectorId';
+
+/**
+ * Flag to enable or disable debug mode for the store.
+ * Set to `true` to enable zustand devtools.
+ */
+const DEBUG_STORE = false;
+
+/**
+ * State shape for the Display Set Selector store.
+ */
+type DisplaySetSelectorState = {
+ /**
+ * Type identifier for the store.
+ */
+ type: string;
+
+ /**
+ * Stores a mapping from `::` to `displaySetInstanceUID`.
+ */
+ displaySetSelectorMap: Record;
+
+ /**
+ * Sets the display set selector for a given key.
+ *
+ * @param key - The key.
+ * @param value - The `displaySetInstanceUID` to associate with the key.
+ */
+ setDisplaySetSelector: (key: string, value: string) => void;
+
+ /**
+ * Clears the entire display set selector map.
+ */
+ clearDisplaySetSelectorMap: () => void;
+};
+
+/**
+ * Creates the Display Set Selector store.
+ *
+ * @param set - The zustand set function.
+ * @returns The display set selector store state and actions.
+ */
+const createDisplaySetSelectorStore = (set): DisplaySetSelectorState => ({
+ type: PRESENTATION_TYPE_ID,
+ displaySetSelectorMap: {},
+
+ /**
+ * Sets the display set selector for a given key.
+ */
+ setDisplaySetSelector: (key: string, value: string) =>
+ set(
+ state => ({
+ displaySetSelectorMap: {
+ ...state.displaySetSelectorMap,
+ [key]: value,
+ },
+ }),
+ false,
+ 'setDisplaySetSelector'
+ ),
+
+ /**
+ * Clears the entire display set selector map.
+ */
+ clearDisplaySetSelectorMap: () =>
+ set({ displaySetSelectorMap: {} }, false, 'clearDisplaySetSelectorMap'),
+});
+
+/**
+ * Zustand store for managing display set selectors.
+ * Applies devtools middleware when DEBUG_STORE is enabled.
+ */
+export const useDisplaySetSelectorStore = create()(
+ DEBUG_STORE
+ ? devtools(createDisplaySetSelectorStore, { name: 'DisplaySetSelectorStore' })
+ : createDisplaySetSelectorStore
+);
diff --git a/extensions/default/src/stores/useHangingProtocolStageIndexStore.ts b/extensions/default/src/stores/useHangingProtocolStageIndexStore.ts
new file mode 100644
index 000000000..4a2b0b727
--- /dev/null
+++ b/extensions/default/src/stores/useHangingProtocolStageIndexStore.ts
@@ -0,0 +1,76 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { Types } from '@ohif/core';
+
+const PRESENTATION_TYPE_ID = 'hangingProtocolStageIndexId';
+const DEBUG_STORE = false;
+
+/**
+ * Represents the state and actions for managing hanging protocol stage indexes.
+ */
+type HangingProtocolStageIndexState = {
+ /**
+ * Stores a mapping from key to `HPInfo`.
+ */
+ hangingProtocolStageIndexMap: Record;
+
+ /**
+ * Sets the hanging protocol stage index for a given key.
+ *
+ * @param key - The key.
+ * @param value - The `HPInfo` to associate with the key.
+ */
+ setHangingProtocolStageIndex: (key: string, value: Types.HangingProtocol.HPInfo) => void;
+
+ /**
+ * Clears all hanging protocol stage indexes.
+ */
+ clearHangingProtocolStageIndexMap: () => void;
+
+ /**
+ * Type identifier for the store.
+ */
+ type: string;
+};
+
+/**
+ * Creates the Hanging Protocol Stage Index store.
+ *
+ * @param set - The zustand set function.
+ * @returns The hanging protocol stage index store state and actions.
+ */
+const createHangingProtocolStageIndexStore = (set): HangingProtocolStageIndexState => ({
+ hangingProtocolStageIndexMap: {},
+ type: PRESENTATION_TYPE_ID,
+
+ /**
+ * Sets the hanging protocol stage index for a given key.
+ */
+ setHangingProtocolStageIndex: (key, value) =>
+ set(
+ state => ({
+ hangingProtocolStageIndexMap: {
+ ...state.hangingProtocolStageIndexMap,
+ [key]: value,
+ },
+ }),
+ false,
+ 'setHangingProtocolStageIndex'
+ ),
+
+ /**
+ * Clears all hanging protocol stage indexes.
+ */
+ clearHangingProtocolStageIndexMap: () =>
+ set({ hangingProtocolStageIndexMap: {} }, false, 'clearHangingProtocolStageIndexMap'),
+});
+
+/**
+ * Zustand store for managing hanging protocol stage indexes.
+ * Applies devtools middleware when DEBUG_STORE is enabled.
+ */
+export const useHangingProtocolStageIndexStore = create()(
+ DEBUG_STORE
+ ? devtools(createHangingProtocolStageIndexStore, { name: 'HangingProtocolStageIndexStore' })
+ : createHangingProtocolStageIndexStore
+);
diff --git a/extensions/default/src/stores/useToggleHangingProtocolStore.ts b/extensions/default/src/stores/useToggleHangingProtocolStore.ts
new file mode 100644
index 000000000..e2863ffa7
--- /dev/null
+++ b/extensions/default/src/stores/useToggleHangingProtocolStore.ts
@@ -0,0 +1,76 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { Types } from '@ohif/core';
+
+const PRESENTATION_TYPE_ID = 'toggleHangingProtocolId';
+const DEBUG_STORE = false;
+
+/**
+ * Represents the state and actions for managing toggle hanging protocols.
+ */
+type ToggleHangingProtocolState = {
+ /**
+ * Stores a mapping from key to `HPInfo`.
+ */
+ toggleHangingProtocol: Record;
+
+ /**
+ * Sets the toggle hanging protocol for a given key.
+ *
+ * @param key - The key .
+ * @param value - The `HPInfo` to associate with the key.
+ */
+ setToggleHangingProtocol: (key: string, value: Types.HangingProtocol.HPInfo) => void;
+
+ /**
+ * Clears all toggle hanging protocols.
+ */
+ clearToggleHangingProtocol: () => void;
+
+ /**
+ * Type identifier for the store.
+ */
+ type: string;
+};
+
+/**
+ * Creates the Toggle Hanging Protocol store.
+ *
+ * @param set - The zustand set function.
+ * @returns The toggle hanging protocol store state and actions.
+ */
+const createToggleHangingProtocolStore = (set): ToggleHangingProtocolState => ({
+ toggleHangingProtocol: {},
+ type: PRESENTATION_TYPE_ID,
+
+ /**
+ * Sets the toggle hanging protocol for a given key.
+ */
+ setToggleHangingProtocol: (key, value) =>
+ set(
+ state => ({
+ toggleHangingProtocol: {
+ ...state.toggleHangingProtocol,
+ [key]: value,
+ },
+ }),
+ false,
+ 'setToggleHangingProtocol'
+ ),
+
+ /**
+ * Clears all toggle hanging protocols.
+ */
+ clearToggleHangingProtocol: () =>
+ set({ toggleHangingProtocol: {} }, false, 'clearToggleHangingProtocol'),
+});
+
+/**
+ * Zustand store for managing toggle hanging protocols.
+ * Applies devtools middleware when DEBUG_STORE is enabled.
+ */
+export const useToggleHangingProtocolStore = create()(
+ DEBUG_STORE
+ ? devtools(createToggleHangingProtocolStore, { name: 'ToggleHangingProtocolStore' })
+ : createToggleHangingProtocolStore
+);
diff --git a/extensions/default/src/stores/useToggleOneUpViewportGridStore.ts b/extensions/default/src/stores/useToggleOneUpViewportGridStore.ts
new file mode 100644
index 000000000..d67c08de8
--- /dev/null
+++ b/extensions/default/src/stores/useToggleOneUpViewportGridStore.ts
@@ -0,0 +1,19 @@
+import { create } from 'zustand';
+
+const PRESENTATION_TYPE_ID = 'toggleOneUpViewportGridId';
+
+type ToggleOneUpViewportGridState = {
+ toggleOneUpViewportGridStore: any | null;
+ setToggleOneUpViewportGridStore: (state: any) => void;
+ clearToggleOneUpViewportGridStore: () => void;
+ type: string;
+};
+
+// Stores the entire ViewportGridService getState when toggling to one up
+// (e.g. via a double click) so that it can be restored when toggling back.
+export const useToggleOneUpViewportGridStore = create(set => ({
+ toggleOneUpViewportGridStore: null,
+ type: PRESENTATION_TYPE_ID,
+ setToggleOneUpViewportGridStore: state => set({ toggleOneUpViewportGridStore: state }),
+ clearToggleOneUpViewportGridStore: () => set({ toggleOneUpViewportGridStore: null }),
+}));
diff --git a/extensions/default/src/stores/useUIStateStore.ts b/extensions/default/src/stores/useUIStateStore.ts
new file mode 100644
index 000000000..708e7a737
--- /dev/null
+++ b/extensions/default/src/stores/useUIStateStore.ts
@@ -0,0 +1,87 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+
+/**
+ * Identifier for the UI State store type.
+ */
+const PRESENTATION_TYPE_ID = 'uiStateId';
+
+/**
+ * Flag to enable or disable debug mode for the store.
+ * Set to `true` to enable zustand devtools.
+ */
+const DEBUG_STORE = false;
+
+/**
+ * Represents the UI state.
+ */
+type UIState = {
+ [key: string]: unknown;
+};
+
+/**
+ * State shape for the UI State store.
+ */
+type UIStateStore = {
+ /**
+ * Type identifier for the store.
+ */
+ type: string;
+
+ /**
+ * Stores the UI state as a key-value mapping.
+ */
+ uiState: UIState;
+
+ /**
+ * Sets the UI state for a given key.
+ *
+ * @param key - The key to set in the UI state.
+ * @param value - The value to associate with the key.
+ */
+ setUIState: (key: string, value: unknown) => void;
+
+ /**
+ * Clears all UI state.
+ */
+ clearUIState: () => void;
+};
+
+/**
+ * Creates the UI State store.
+ *
+ * @param set - The zustand set function.
+ * @returns The UI State store state and actions.
+ */
+const createUIStateStore = (set): UIStateStore => ({
+ type: PRESENTATION_TYPE_ID,
+ uiState: {},
+
+ /**
+ * Sets the UI state for a given key.
+ */
+ setUIState: (key, value) =>
+ set(
+ state => ({
+ uiState: {
+ ...state.uiState,
+ [key]: value,
+ },
+ }),
+ false,
+ 'setUIState'
+ ),
+
+ /**
+ * Clears all UI state.
+ */
+ clearUIState: () => set({ uiState: {} }, false, 'clearUIState'),
+});
+
+/**
+ * Zustand store for managing UI state.
+ * Applies devtools middleware when DEBUG_STORE is enabled.
+ */
+export const useUIStateStore = create()(
+ DEBUG_STORE ? devtools(createUIStateStore, { name: 'UIStateStore' }) : createUIStateStore
+);
diff --git a/extensions/default/src/stores/useViewportGridStore.ts b/extensions/default/src/stores/useViewportGridStore.ts
new file mode 100644
index 000000000..77655c974
--- /dev/null
+++ b/extensions/default/src/stores/useViewportGridStore.ts
@@ -0,0 +1,89 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+
+/**
+ * Identifier for the viewport grid store type.
+ */
+const PRESENTATION_TYPE_ID = 'viewportGridId';
+
+/**
+ * Flag to enable or disable debug mode for the store.
+ * Set to `true` to enable zustand devtools.
+ */
+const DEBUG_STORE = false;
+
+/**
+ * Represents the state of the viewport grid.
+ */
+type ViewportGridState = {
+ [key: string]: unknown;
+};
+
+/**
+ * State shape for the Viewport Grid store.
+ */
+type ViewportGridStore = {
+ /**
+ * Type identifier for the store.
+ */
+ type: string;
+
+ /**
+ * Stores the viewport grid state as a key-value mapping.
+ */
+ viewportGridState: ViewportGridState;
+
+ /**
+ * Sets the viewport grid state for a given key.
+ *
+ * @param key - The key to set in the viewport grid state.
+ * @param value - The value to associate with the key.
+ */
+ setViewportGridState: (key: string, value: unknown) => void;
+
+ /**
+ * Clears the entire viewport grid state.
+ */
+ clearViewportGridState: () => void;
+};
+
+/**
+ * Creates the Viewport Grid store.
+ *
+ * @param set - The zustand set function.
+ * @returns The Viewport Grid store state and actions.
+ */
+const createViewportGridStore = (set): ViewportGridStore => ({
+ type: PRESENTATION_TYPE_ID,
+ viewportGridState: {},
+
+ /**
+ * Sets the viewport grid state for a given key.
+ */
+ setViewportGridState: (key, value) =>
+ set(
+ state => ({
+ viewportGridState: {
+ ...state.viewportGridState,
+ [key]: value,
+ },
+ }),
+ false,
+ 'setViewportGridState'
+ ),
+
+ /**
+ * Clears the entire viewport grid state.
+ */
+ clearViewportGridState: () => set({ viewportGridState: {} }, false, 'clearViewportGridState'),
+});
+
+/**
+ * Zustand store for managing viewport grid state.
+ * Applies devtools middleware when DEBUG_STORE is enabled.
+ */
+export const useViewportGridStore = create()(
+ DEBUG_STORE
+ ? devtools(createViewportGridStore, { name: 'ViewportGridStore' })
+ : createViewportGridStore
+);
diff --git a/extensions/default/src/stores/useViewportsByPositionStore.ts b/extensions/default/src/stores/useViewportsByPositionStore.ts
new file mode 100644
index 000000000..6466caf7f
--- /dev/null
+++ b/extensions/default/src/stores/useViewportsByPositionStore.ts
@@ -0,0 +1,100 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+
+const PRESENTATION_TYPE_ID = 'viewportsByPositionId';
+const DEBUG_STORE = true;
+
+/**
+ * Represents the state and actions for managing viewports by position.
+ */
+type ViewportsByPositionState = {
+ /**
+ * Type identifier for the store.
+ */
+ type: string;
+
+ /**
+ * Stores viewports indexed by their position.
+ */
+ viewportsByPosition: Record;
+
+ /**
+ * Stores initial display viewports as an array of strings.
+ */
+ initialInDisplay: string[];
+
+ /**
+ * Sets the viewport for a given key.
+ *
+ * @param key - The key identifying the viewport position.
+ * @param value - The viewport data to associate with the key.
+ */
+ setViewportsByPosition: (key: string, value: unknown) => void;
+
+ /**
+ * Clears all viewports by position.
+ */
+ clearViewportsByPosition: () => void;
+
+ /**
+ * Adds an initial display viewport.
+ *
+ * @param value - The viewport identifier to add.
+ */
+ addInitialInDisplay: (value: string) => void;
+};
+
+/**
+ * Creates the Viewports By Position store.
+ *
+ * @param set - The zustand set function.
+ * @returns The Viewports By Position store state and actions.
+ */
+const createViewportsByPositionStore = (set): ViewportsByPositionState => ({
+ type: PRESENTATION_TYPE_ID,
+ viewportsByPosition: {},
+ initialInDisplay: [],
+
+ /**
+ * Sets the viewport for a given key.
+ */
+ setViewportsByPosition: (key, value) =>
+ set(
+ state => ({
+ viewportsByPosition: {
+ ...state.viewportsByPosition,
+ [key]: value,
+ },
+ }),
+ false,
+ 'setViewportsByPosition'
+ ),
+
+ /**
+ * Clears all viewports by position.
+ */
+ clearViewportsByPosition: () =>
+ set({ viewportsByPosition: {} }, false, 'clearViewportsByPosition'),
+
+ /**
+ * Adds an initial display viewport.
+ */
+ addInitialInDisplay: value =>
+ set(
+ state => ({
+ initialInDisplay: [...state.initialInDisplay, value],
+ }),
+ false,
+ 'addInitialInDisplay'
+ ),
+});
+
+/**
+ * Zustand store for managing viewports by position.
+ * Applies devtools middleware when DEBUG_STORE is enabled.
+ */
+export const useViewportsByPositionStore = create()(
+ DEBUG_STORE
+ ? devtools(createViewportsByPositionStore, { name: 'ViewportsByPositionStore' })
+ : createViewportsByPositionStore
+);
diff --git a/extensions/measurement-tracking/src/_shared/PROMPT_RESPONSES.js b/extensions/default/src/utils/_shared/PROMPT_RESPONSES.ts
similarity index 100%
rename from extensions/measurement-tracking/src/_shared/PROMPT_RESPONSES.js
rename to extensions/default/src/utils/_shared/PROMPT_RESPONSES.ts
diff --git a/extensions/measurement-tracking/src/_shared/getNextSRSeriesNumber.js b/extensions/default/src/utils/_shared/getNextSRSeriesNumber.ts
similarity index 100%
rename from extensions/measurement-tracking/src/_shared/getNextSRSeriesNumber.js
rename to extensions/default/src/utils/_shared/getNextSRSeriesNumber.ts
diff --git a/extensions/cornerstone/src/utils/callInputDialog.tsx b/extensions/default/src/utils/callInputDialog.tsx
similarity index 99%
rename from extensions/cornerstone/src/utils/callInputDialog.tsx
rename to extensions/default/src/utils/callInputDialog.tsx
index 05946b87f..321b308a8 100644
--- a/extensions/cornerstone/src/utils/callInputDialog.tsx
+++ b/extensions/default/src/utils/callInputDialog.tsx
@@ -14,7 +14,7 @@ import { Input, Dialog, ButtonEnums, LabellingFlow } from '@ohif/ui';
* @param {string?} dialogConfig.inputLabel - show label above the input
*/
-function callInputDialog(
+export function callInputDialog(
uiDialogService,
data,
callback,
diff --git a/extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.css b/extensions/default/src/utils/colorPickerDialog.css
similarity index 100%
rename from extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.css
rename to extensions/default/src/utils/colorPickerDialog.css
diff --git a/extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.tsx b/extensions/default/src/utils/colorPickerDialog.tsx
similarity index 92%
rename from extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.tsx
rename to extensions/default/src/utils/colorPickerDialog.tsx
index 38e85efb2..9de59eeed 100644
--- a/extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.tsx
+++ b/extensions/default/src/utils/colorPickerDialog.tsx
@@ -4,7 +4,7 @@ import { ChromePicker } from 'react-color';
import './colorPickerDialog.css';
-function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
+function colorPickerDialog(uiDialogService, rgbaColor, callback) {
const dialogId = 'pick-color';
const onSubmitHandler = ({ action, value }) => {
@@ -55,4 +55,4 @@ function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
}
}
-export default callColorPickerDialog;
+export default colorPickerDialog;
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptLabelAnnotation.js b/extensions/default/src/utils/promptLabelAnnotation.js
similarity index 73%
rename from extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptLabelAnnotation.js
rename to extensions/default/src/utils/promptLabelAnnotation.js
index b244ccead..f0da02508 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptLabelAnnotation.js
+++ b/extensions/default/src/utils/promptLabelAnnotation.js
@@ -1,10 +1,8 @@
-function promptLabelAnnotation({ servicesManager, extensionManager }, ctx, evt) {
+import { showLabelAnnotationPopup } from './callInputDialog';
+
+function promptLabelAnnotation({ servicesManager }, ctx, evt) {
const { measurementService, customizationService } = servicesManager.services;
const { viewportId, StudyInstanceUID, SeriesInstanceUID, measurementId } = evt;
- const utilityModule = extensionManager.getModuleEntry(
- '@ohif/extension-cornerstone.utilityModule.common'
- );
- const { showLabelAnnotationPopup } = utilityModule.exports;
return new Promise(async function (resolve) {
const labelConfig = customizationService.get('measurementLabels');
const measurement = measurementService.getMeasurement(measurementId);
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptSaveReport.js b/extensions/default/src/utils/promptSaveReport.js
similarity index 88%
rename from extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptSaveReport.js
rename to extensions/default/src/utils/promptSaveReport.js
index 3e98bcb2b..ae5c53be6 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptSaveReport.js
+++ b/extensions/default/src/utils/promptSaveReport.js
@@ -1,6 +1,7 @@
-import { createReportAsync, createReportDialogPrompt } from '@ohif/extension-default';
-import getNextSRSeriesNumber from '../../_shared/getNextSRSeriesNumber';
-import RESPONSE from '../../_shared/PROMPT_RESPONSES';
+import createReportAsync from '../Actions/createReportAsync';
+import { createReportDialogPrompt } from '../Panels';
+import getNextSRSeriesNumber from './getNextSRSeriesNumber';
+import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
async function promptSaveReport({ servicesManager, commandsManager, extensionManager }, ctx, evt) {
const { uiDialogService, measurementService, displaySetService } = servicesManager.services;
@@ -17,7 +18,7 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
extensionManager,
});
- if (promptResult.action === RESPONSE.CREATE_REPORT) {
+ if (promptResult.action === PROMPT_RESPONSES.CREATE_REPORT) {
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const measurements = measurementService.getMeasurements();
diff --git a/extensions/default/src/utils/reuseCachedLayouts.ts b/extensions/default/src/utils/reuseCachedLayouts.ts
index d321082a2..3e6f72850 100644
--- a/extensions/default/src/utils/reuseCachedLayouts.ts
+++ b/extensions/default/src/utils/reuseCachedLayouts.ts
@@ -1,4 +1,7 @@
-import { HangingProtocolService, StateSyncService, Types } from '@ohif/core';
+import { HangingProtocolService, Types } from '@ohif/core';
+import { useViewportGridStore } from '../stores/useViewportGridStore';
+import { useDisplaySetSelectorStore } from '../stores/useDisplaySetSelectorStore';
+import { useHangingProtocolStageIndexStore } from '../stores/useHangingProtocolStageIndexStore';
export type ReturnType = {
hangingProtocolStageIndexMap: Record;
@@ -14,11 +17,7 @@ export type ReturnType = {
* @returns Set of states that can be applied to the state sync to remember
* the current view state.
*/
-const reuseCachedLayout = (
- state,
- hangingProtocolService: HangingProtocolService,
- syncService: StateSyncService
-): ReturnType => {
+const reuseCachedLayout = (state, hangingProtocolService: HangingProtocolService): ReturnType => {
const { activeViewportId } = state;
const { protocol } = hangingProtocolService.getActiveProtocol();
@@ -29,16 +28,14 @@ const reuseCachedLayout = (
const hpInfo = hangingProtocolService.getState();
const { protocolId, stageIndex, activeStudyUID } = hpInfo;
- const syncState = syncService.getState();
- const viewportGridStore = { ...syncState.viewportGridStore };
- const displaySetSelectorMap = { ...syncState.displaySetSelectorMap };
+ const { viewportGridState, setViewportGridState } = useViewportGridStore.getState();
+ const { displaySetSelectorMap, setDisplaySetSelector } = useDisplaySetSelectorStore.getState();
+ const { hangingProtocolStageIndexMap, setHangingProtocolStageIndex } =
+ useHangingProtocolStageIndexStore.getState();
const stage = protocol.stages[stageIndex];
const storeId = `${activeStudyUID}:${protocolId}:${stageIndex}`;
const cacheId = `${activeStudyUID}:${protocolId}`;
- const hangingProtocolStageIndexMap = {
- ...syncState.hangingProtocolStageIndexMap,
- };
const { rows, columns } = stage.viewportStructure.properties;
const custom =
stage.viewports.length !== state.viewports.size ||
@@ -48,7 +45,7 @@ const reuseCachedLayout = (
hangingProtocolStageIndexMap[cacheId] = hpInfo;
if (storeId && custom) {
- viewportGridStore[storeId] = { ...state };
+ setViewportGridState(storeId, { ...state });
}
state.viewports.forEach((viewport, viewportId) => {
@@ -62,21 +59,24 @@ const reuseCachedLayout = (
continue;
}
if (viewportId === activeViewportId && i === 0) {
- displaySetSelectorMap[`${activeStudyUID}:activeDisplaySet:0`] = displaySetUID;
+ setDisplaySetSelector(`${activeStudyUID}:activeDisplaySet:0`, displaySetUID);
}
if (displaySetOptions[i]?.id) {
- displaySetSelectorMap[
+ setDisplaySetSelector(
`${activeStudyUID}:${displaySetOptions[i].id}:${
displaySetOptions[i].matchedDisplaySetsIndex || 0
- }`
- ] = displaySetUID;
+ }`,
+ displaySetUID
+ );
}
}
});
+ setHangingProtocolStageIndex(cacheId, hpInfo);
+
return {
hangingProtocolStageIndexMap,
- viewportGridStore,
+ viewportGridStore: viewportGridState,
displaySetSelectorMap,
};
};
diff --git a/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx b/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx
index b1a30f48e..b3200d8b0 100644
--- a/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx
+++ b/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx
@@ -5,7 +5,7 @@ import { MeasurementTable } from '@ohif/ui';
import { withTranslation, WithTranslation } from 'react-i18next';
import { EVENTS as MicroscopyEvents } from '../../services/MicroscopyService';
import dcmjs from 'dcmjs';
-import callInputDialog from '../../utils/callInputDialog';
+import { callInputDialog } from '@ohif/extension-default';
import constructSR from '../../utils/constructSR';
import { saveByteArray } from '../../utils/saveByteArray';
import { Separator } from '@ohif/ui-next';
diff --git a/extensions/dicom-microscopy/src/getCommandsModule.ts b/extensions/dicom-microscopy/src/getCommandsModule.ts
index 28750f917..5ed91a13a 100644
--- a/extensions/dicom-microscopy/src/getCommandsModule.ts
+++ b/extensions/dicom-microscopy/src/getCommandsModule.ts
@@ -1,6 +1,6 @@
import { CommandsManager, ExtensionManager } from '@ohif/core';
+import { callInputDialog } from '@ohif/extension-default';
import styles from './utils/styles';
-import callInputDialog from './utils/callInputDialog';
export default function getCommandsModule({
servicesManager,
@@ -26,7 +26,6 @@ export default function getCommandsModule({
setLabel: ({ uid }) => {
const roiAnnotation = microscopyService.getAnnotation(uid);
-
callInputDialog({
uiDialogService,
defaultValue: '',
diff --git a/extensions/dicom-microscopy/src/utils/callInputDialog.tsx b/extensions/dicom-microscopy/src/utils/callInputDialog.tsx
deleted file mode 100644
index d36d83657..000000000
--- a/extensions/dicom-microscopy/src/utils/callInputDialog.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React from 'react';
-import { Input, Dialog, ButtonEnums } from '@ohif/ui';
-
-/**
- *
- * @param {*} data
- * @param {*} data.text
- * @param {*} data.label
- * @param {*} event
- * @param {func} callback
- * @param {*} isArrowAnnotateInputDialog
- */
-export default function callInputDialog({
- uiDialogService,
- title = 'Annotation',
- defaultValue = '',
- callback = (value: string, action: string) => {},
-}) {
- const dialogId = 'microscopy-input-dialog';
-
- const onSubmitHandler = ({ action, value }) => {
- switch (action.id) {
- case 'save':
- callback(value.value, action.id);
- break;
- case 'cancel':
- callback('', action.id);
- break;
- }
- uiDialogService.dismiss({ id: dialogId });
- };
-
- if (uiDialogService) {
- uiDialogService.create({
- id: dialogId,
- centralize: true,
- isDraggable: false,
- showOverlay: true,
- content: Dialog,
- contentProps: {
- title: title,
- value: { value: defaultValue },
- noCloseButton: true,
- onClose: () => uiDialogService.dismiss({ id: dialogId }),
- actions: [
- { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
- { id: 'save', text: 'Save', type: ButtonEnums.type.primary },
- ],
- onSubmit: onSubmitHandler,
- body: ({ value, setValue }) => {
- return (
- {
- event.persist();
- setValue(value => ({ ...value, value: event.target.value }));
- }}
- onKeyPress={event => {
- if (event.key === 'Enter') {
- onSubmitHandler({ value, action: { id: 'save' } });
- }
- }}
- />
- );
- },
- },
- });
- }
-}
diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json
index 9035eace9..d71d8cfe6 100644
--- a/extensions/measurement-tracking/package.json
+++ b/extensions/measurement-tracking/package.json
@@ -32,10 +32,11 @@
"start": "yarn run dev"
},
"peerDependencies": {
- "@cornerstonejs/core": "^1.86.0",
- "@cornerstonejs/tools": "^1.86.0",
+ "@cornerstonejs/core": "^2.1.13",
+ "@cornerstonejs/tools": "^2.1.13",
"@ohif/core": "3.9.0-beta.105",
"@ohif/extension-cornerstone-dicom-sr": "3.9.0-beta.105",
+ "@ohif/extension-default": "3.9.0-beta.104",
"@ohif/ui": "3.9.0-beta.105",
"classnames": "^2.3.2",
"dcmjs": "*",
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx
index 7e89712ee..21b9efe9b 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx
@@ -1,17 +1,16 @@
-import React, { useContext, useEffect } from 'react';
+import React, { useContext, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import { Machine } from 'xstate';
import { useMachine } from '@xstate/react';
import { useViewportGrid } from '@ohif/ui';
+import { promptLabelAnnotation, promptSaveReport } from '@ohif/extension-default';
import { machineConfiguration, defaultOptions, RESPONSE } from './measurementTrackingMachine';
import promptBeginTracking from './promptBeginTracking';
import promptTrackNewSeries from './promptTrackNewSeries';
import promptTrackNewStudy from './promptTrackNewStudy';
-import promptSaveReport from './promptSaveReport';
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
import hydrateStructuredReport from './hydrateStructuredReport';
import { useAppConfig } from '@state';
-import promptLabelAnnotation from './promptLabelAnnotation';
const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
@@ -96,17 +95,18 @@ function TrackedMeasurementsContextProvider(
const trackedMeasurement = trackedMeasurements[0];
const referencedDisplaySetUID = trackedMeasurement.displaySetInstanceUID;
- const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
- const imageIndex = viewport.getCurrentImageIdIndex();
+
+ // update the previously stored positionPresentation with the new viewportId
+ // presentation so that when we put the referencedDisplaySet back in the viewport
+ // it will be in the correct position zoom and pan
+ commandsManager.runCommand('updateStoredPositionPresentation', {
+ viewportId: activeViewportId,
+ displaySetInstanceUID: referencedDisplaySetUID,
+ });
viewportGridService.setDisplaySetsForViewport({
viewportId: activeViewportId,
displaySetInstanceUIDs: [referencedDisplaySetUID],
- viewportOptions: {
- initialImageOptions: {
- index: imageIndex,
- },
- },
});
},
showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => {
@@ -196,7 +196,9 @@ function TrackedMeasurementsContextProvider(
// - Fix viewport border resize
// - created/destroyed hooks for extensions (cornerstone measurement subscriptions in it's `init`)
- const measurementTrackingMachine = Machine(machineConfiguration, machineOptions);
+ const measurementTrackingMachine = useMemo(() => {
+ return Machine(machineConfiguration, machineOptions);
+ }, []); // Empty dependency array ensures this is only created once
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useMachine(
measurementTrackingMachine
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js
index d53408e3c..e23de0c62 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js
@@ -1,4 +1,3 @@
-import { hydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr';
import { assign } from 'xstate';
const RESPONSE = {
diff --git a/extensions/measurement-tracking/src/getPanelModule.tsx b/extensions/measurement-tracking/src/getPanelModule.tsx
index ef09f59ad..f2bc44cf9 100644
--- a/extensions/measurement-tracking/src/getPanelModule.tsx
+++ b/extensions/measurement-tracking/src/getPanelModule.tsx
@@ -24,7 +24,6 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }):
/>
),
},
-
{
name: 'trackedMeasurements',
iconName: 'tab-linear',
diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx
new file mode 100644
index 000000000..7ac0ebcc1
--- /dev/null
+++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx
@@ -0,0 +1,147 @@
+import React, { useEffect, useState } from 'react';
+import { PanelMeasurement } from '@ohif/extension-cornerstone';
+import { useViewportGrid } from '@ohif/ui';
+import { StudySummary } from '@ohif/ui-next';
+import { Button, Icons } from '@ohif/ui-next';
+import { DicomMetadataStore, utils } from '@ohif/core';
+import { useTrackedMeasurements } from '../getContextModule';
+import { useTranslation } from 'react-i18next';
+
+const { downloadCSVReport, formatDate } = utils;
+
+const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
+ key: undefined, //
+ date: '', // '07-Sep-2010',
+ modality: '', // 'CT',
+ description: '', // 'CHEST/ABD/PELVIS W CONTRAST',
+};
+
+function PanelMeasurementTableTracking({
+ servicesManager,
+ extensionManager,
+ commandsManager,
+}: withAppTypes) {
+ const [viewportGrid] = useViewportGrid();
+ const { t } = useTranslation('MeasurementTable');
+ const { measurementService, customizationService } = servicesManager.services;
+ const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
+ const { trackedStudy, trackedSeries } = trackedMeasurements.context;
+ const [displayStudySummary, setDisplayStudySummary] = useState(
+ DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
+ );
+
+ useEffect(() => {
+ const updateDisplayStudySummary = async () => {
+ if (trackedMeasurements.matches('tracking') && trackedStudy) {
+ const studyMeta = DicomMetadataStore.getStudy(trackedStudy);
+ if (!studyMeta || !studyMeta.series || studyMeta.series.length === 0) {
+ console.debug('Study metadata not available');
+ return;
+ }
+
+ const instanceMeta = studyMeta.series[0].instances[0];
+ const { StudyDate, StudyDescription } = instanceMeta;
+
+ const modalities = new Set();
+ studyMeta.series.forEach(series => {
+ if (trackedSeries.includes(series.SeriesInstanceUID)) {
+ modalities.add(series.instances[0].Modality);
+ }
+ });
+ const modality = Array.from(modalities).join('/');
+
+ setDisplayStudySummary(prevSummary => {
+ if (prevSummary.key !== trackedStudy) {
+ return {
+ key: trackedStudy,
+ date: StudyDate,
+ modality,
+ description: StudyDescription,
+ };
+ }
+ return prevSummary;
+ });
+ } else if (!trackedStudy) {
+ setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE);
+ }
+ };
+
+ updateDisplayStudySummary();
+ }, [trackedMeasurements, trackedStudy, trackedSeries]);
+
+ const { disableEditing } = customizationService.getCustomization(
+ 'PanelMeasurement.disableEditing',
+ {
+ id: 'default.disableEditing',
+ disableEditing: false,
+ }
+ );
+
+ return (
+ <>
+ {displayStudySummary.key && (
+
+ )}
+
+ trackedStudy === measurement.referenceStudyUID &&
+ trackedSeries.includes(measurement.referenceSeriesUID)
+ }
+ customHeader={({ additionalFindings, measurements }) => {
+ const disabled = additionalFindings.length === 0 && measurements.length === 0;
+
+ if (disableEditing || disabled) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+ }}
+ >
+ >
+ );
+}
+
+export default PanelMeasurementTableTracking;
diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx
deleted file mode 100644
index 6efa62ac2..000000000
--- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx
+++ /dev/null
@@ -1,314 +0,0 @@
-import React, { useEffect, useState, useRef } from 'react';
-import PropTypes from 'prop-types';
-import { StudySummary, MeasurementTable, useViewportGrid, ActionButtons } from '@ohif/ui';
-import { DicomMetadataStore, utils } from '@ohif/core';
-import { useDebounce } from '@hooks';
-import { useAppConfig } from '@state';
-import debounce from 'lodash.debounce';
-import { useTranslation } from 'react-i18next';
-import { Separator } from '@ohif/ui-next';
-
-import { useTrackedMeasurements } from '../../getContextModule';
-
-const { downloadCSVReport } = utils;
-const { formatDate } = utils;
-
-const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
- key: undefined, //
- date: '', // '07-Sep-2010',
- modality: '', // 'CT',
- description: '', // 'CHEST/ABD/PELVIS W CONTRAST',
-};
-
-function PanelMeasurementTableTracking({ servicesManager, extensionManager }: withAppTypes) {
- const [viewportGrid] = useViewportGrid();
- const { t } = useTranslation('MeasurementTable');
- const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString());
- const debouncedMeasurementChangeTimestamp = useDebounce(measurementChangeTimestamp, 200);
- const { measurementService, uiDialogService, displaySetService, customizationService } =
- servicesManager.services;
- const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
- const { trackedStudy, trackedSeries } = trackedMeasurements.context;
- const [displayStudySummary, setDisplayStudySummary] = useState(
- DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
- );
- const [displayMeasurements, setDisplayMeasurements] = useState([]);
- const measurementsPanelRef = useRef(null);
- const [appConfig] = useAppConfig();
-
- useEffect(() => {
- const measurements = measurementService.getMeasurements();
- const mappedMeasurements = measurements.map(m =>
- _mapMeasurementToDisplay(m, displaySetService)
- );
- setDisplayMeasurements(mappedMeasurements);
- // eslint-ignore-next-line
- }, [
- measurementService,
- displaySetService,
- trackedStudy,
- trackedSeries,
- debouncedMeasurementChangeTimestamp,
- ]);
-
- // ~~ DisplayStudySummary
- useEffect(() => {
- const updateDisplayStudySummary = async () => {
- if (trackedMeasurements.matches('tracking')) {
- const StudyInstanceUID = trackedStudy;
- const studyMeta = DicomMetadataStore.getStudy(StudyInstanceUID);
- const instanceMeta = studyMeta.series[0].instances[0];
- const { StudyDate, StudyDescription } = instanceMeta;
-
- const modalities = new Set();
- studyMeta.series.forEach(series => {
- if (trackedSeries.includes(series.SeriesInstanceUID)) {
- modalities.add(series.instances[0].Modality);
- }
- });
- const modality = Array.from(modalities).join('/');
-
- if (displayStudySummary.key !== StudyInstanceUID) {
- setDisplayStudySummary({
- key: StudyInstanceUID,
- date: StudyDate, // TODO: Format: '07-Sep-2010'
- modality,
- description: StudyDescription,
- });
- }
- } else if (trackedStudy === '' || trackedStudy === undefined) {
- setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE);
- }
- };
- updateDisplayStudySummary();
- }, [displayStudySummary.key, trackedSeries, trackedMeasurements, trackedStudy]);
-
- // TODO: Better way to consolidated, debounce, check on change?
- // Are we exposing the right API for measurementService?
- // This watches for ALL measurementService changes. It updates a timestamp,
- // which is debounced. After a brief period of inactivity, this triggers
- // a re-render where we grab up-to-date measurements
- useEffect(() => {
- const added = measurementService.EVENTS.MEASUREMENT_ADDED;
- const addedRaw = measurementService.EVENTS.RAW_MEASUREMENT_ADDED;
- const updated = measurementService.EVENTS.MEASUREMENT_UPDATED;
- const removed = measurementService.EVENTS.MEASUREMENT_REMOVED;
- const cleared = measurementService.EVENTS.MEASUREMENTS_CLEARED;
- const subscriptions = [];
-
- [added, addedRaw, updated, removed, cleared].forEach(evt => {
- subscriptions.push(
- measurementService.subscribe(evt, () => {
- setMeasurementsUpdated(Date.now().toString());
- if (evt === added) {
- debounce(() => {
- measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight;
- }, 300)();
- }
- }).unsubscribe
- );
- });
-
- return () => {
- subscriptions.forEach(unsub => {
- unsub();
- });
- };
- }, [measurementService, sendTrackedMeasurementsEvent]);
-
- async function exportReport() {
- const measurements = measurementService.getMeasurements();
- const trackedMeasurements = measurements.filter(
- m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
- );
- downloadCSVReport(trackedMeasurements);
- }
-
- const jumpToImage = ({ uid, isActive }) => {
- measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid);
- onMeasurementItemClickHandler({ uid, isActive });
- };
-
- const onMeasurementItemEditHandler = ({ uid, isActive }) => {
- jumpToImage({ uid, isActive });
- const labelConfig = customizationService.get('measurementLabels');
- const measurement = measurementService.getMeasurement(uid);
- const utilityModule = extensionManager.getModuleEntry(
- '@ohif/extension-cornerstone.utilityModule.common'
- );
- const { showLabelAnnotationPopup } = utilityModule.exports;
- showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(
- (val: Map) => {
- measurementService.update(
- uid,
- {
- ...val,
- },
- true
- );
- }
- );
- };
-
- const onMeasurementItemClickHandler = ({ uid, isActive }) => {
- if (!isActive) {
- const measurements = [...displayMeasurements];
- const measurement = measurements.find(m => m.uid === uid);
- measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true));
- measurement.isActive = true;
- setDisplayMeasurements(measurements);
- }
- };
-
- const displayMeasurementsWithoutFindings = displayMeasurements.filter(
- dm =>
- dm.measurementType !== measurementService.VALUE_TYPES.POINT &&
- dm.referencedImageId &&
- trackedStudy === dm.referenceStudyUID &&
- trackedSeries.includes(dm.referenceSeriesUID)
- );
- const additionalFindings = displayMeasurements.filter(
- dm =>
- dm.measurementType === measurementService.VALUE_TYPES.POINT &&
- dm.referencedImageId &&
- trackedStudy === dm.referenceStudyUID &&
- trackedSeries.includes(dm.referenceSeriesUID)
- );
- const nonAcquisitionMeasurements = displayMeasurements.filter(
- dm =>
- dm.referencedImageId == null ||
- trackedStudy !== dm.referenceStudyUID ||
- trackedSeries.includes(dm.referenceSeriesUID)
- );
- const disabled =
- additionalFindings.length === 0 &&
- displayMeasurementsWithoutFindings.length === 0 &&
- nonAcquisitionMeasurements.length === 0;
-
- return (
- <>
-
- {displayStudySummary.key && (
-
- )}
-
- {additionalFindings.length !== 0 && (
-
- )}
- {nonAcquisitionMeasurements.length !== 0 && (
-
- )}
-
- {!appConfig?.disableEditing && (
-
-
{
- sendTrackedMeasurementsEvent('SAVE_REPORT', {
- viewportId: viewportGrid.activeViewportId,
- isBackupSave: true,
- });
- },
- },
- ]}
- disabled={disabled}
- />
-
- )}
- >
- );
-}
-
-PanelMeasurementTableTracking.propTypes = {
- servicesManager: PropTypes.shape({
- services: PropTypes.shape({
- measurementService: PropTypes.shape({
- getMeasurements: PropTypes.func.isRequired,
- VALUE_TYPES: PropTypes.object.isRequired,
- }).isRequired,
- }).isRequired,
- }).isRequired,
-};
-
-// TODO: This could be a measurementService mapper
-function _mapMeasurementToDisplay(measurement, displaySetService) {
- const { referenceSeriesUID } = measurement;
-
- // TODO: We don't deal with multiframe well yet, would need to update
- // This in OHIF-312 when we add FrameIndex to measurements
-
- const {
- displayText: baseDisplayText,
- uid,
- label: baseLabel,
- type,
- selected,
- findingSites,
- finding,
- referencedImageId,
- } = measurement;
-
- const firstSite = findingSites?.[0];
- const label = baseLabel || finding?.text || firstSite?.text || '(empty)';
- let displayText = baseDisplayText || [];
- if (findingSites) {
- const siteText = [];
- findingSites.forEach(site => {
- if (site?.text !== label) {
- siteText.push(site.text);
- }
- });
- displayText = [...siteText, ...displayText];
- }
- if (finding && finding?.text !== label) {
- displayText = [finding.text, ...displayText];
- }
-
- return {
- uid,
- label,
- baseLabel,
- measurementType: type,
- displayText,
- baseDisplayText,
- isActive: selected,
- finding,
- findingSites,
- referencedImageId,
- };
-}
-
-export default PanelMeasurementTableTracking;
diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx
index 97b9ee9aa..16de940d7 100644
--- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx
+++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx
@@ -12,7 +12,17 @@ import { PanelStudyBrowserTrackingHeader } from './PanelStudyBrowserTrackingHead
import { defaultActionIcons, defaultViewPresets } from './constants';
const { formatDate, createStudyBrowserTabs } = utils;
-
+const thumbnailNoImageModalities = [
+ 'SR',
+ 'SEG',
+ 'SM',
+ 'RTSTRUCT',
+ 'RTPLAN',
+ 'RTDOSE',
+ 'DOC',
+ 'OT',
+ 'PMAP',
+];
/**
*
* @param {*} param0
@@ -94,7 +104,7 @@ function PanelStudyBrowserTracking({
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',
+ type: 'error',
duration: 3000,
});
}
@@ -171,7 +181,11 @@ function PanelStudyBrowserTracking({
return;
}
- const currentDisplaySets = displaySetService.activeDisplaySets;
+ let currentDisplaySets = displaySetService.activeDisplaySets;
+ // filter non based on the list of modalities that are supported by cornerstone
+ currentDisplaySets = currentDisplaySets.filter(
+ ds => !thumbnailNoImageModalities.includes(ds.Modality)
+ );
if (!currentDisplaySets.length) {
return;
@@ -677,18 +691,6 @@ function _mapDisplaySets(
return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets];
}
-const thumbnailNoImageModalities = [
- 'SR',
- 'SEG',
- 'SM',
- 'RTSTRUCT',
- 'RTPLAN',
- 'RTDOSE',
- 'DOC',
- 'OT',
- 'PMAP',
-];
-
function _getComponentType(ds) {
if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) {
return 'thumbnailNoImage';
diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx
index f0c7cdac0..be40e23b5 100644
--- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx
+++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx
@@ -8,7 +8,9 @@ import { useTrackedMeasurements } from './../getContextModule';
import { BaseVolumeViewport, Enums } from '@cornerstonejs/core';
import { useTranslation } from 'react-i18next';
-function TrackedCornerstoneViewport(props: withAppTypes) {
+function TrackedCornerstoneViewport(
+ props: withAppTypes<{ viewportId: string; displaySets: AppTypes.DisplaySet[] }>
+) {
const { displaySets, viewportId, servicesManager, extensionManager } = props;
const {
@@ -193,7 +195,7 @@ function TrackedCornerstoneViewport(props: withAppTypes) {
viewportId === activeViewportId
);
- viewportActionCornersService.setComponents([
+ viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -305,7 +307,7 @@ const _getArrowsComponent = (isTracked, switchMeasurement, isActiveViewport) =>
return (
switchMeasurement(direction)}
- className={isActiveViewport ? 'visible' : 'invisible group-hover:visible'}
+ className={isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'}
>
);
};
diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx
index 1e429c1f8..3f3f77349 100644
--- a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx
+++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx
@@ -1,171 +1,76 @@
-import React, { useEffect, useState } from 'react';
-import PropTypes from 'prop-types';
-import { Icon, ActionButtons } from '@ohif/ui';
-import { useTranslation } from 'react-i18next';
-import { eventTarget } from '@cornerstonejs/core';
-import { Enums } from '@cornerstonejs/tools';
+import React, { useEffect } from 'react';
+import { useActiveViewportSegmentationRepresentations } from '@ohif/extension-cornerstone';
import { handleROIThresholding } from '../../utils/handleROIThresholding';
+import { debounce } from '@ohif/core/src/utils';
export default function PanelRoiThresholdSegmentation({
servicesManager,
commandsManager,
}: withAppTypes) {
- const { segmentationService, uiNotificationService } = servicesManager.services;
- const { t } = useTranslation('PanelSUVExport');
+ const { segmentationService } = servicesManager.services;
+ const { segmentationsWithRepresentations: segmentationsInfo } =
+ useActiveViewportSegmentationRepresentations({ servicesManager });
- const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
- const [activeSegmentation, setActiveSegmentation] = useState(null);
-
- /**
- * Update UI based on segmentation changes (added, removed, updated)
- */
useEffect(() => {
- // ~~ Subscription
- const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
- const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
- const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
- const subscriptions = [];
+ const segmentationIds = segmentationsInfo.map(
+ segmentationInfo => segmentationInfo.segmentation.segmentationId
+ );
- [added, updated, removed].forEach(evt => {
- const { unsubscribe } = segmentationService.subscribe(evt, () => {
- const segmentations = segmentationService.getSegmentations();
- setSegmentations(segmentations);
-
- const activeSegmentation = segmentations.filter(seg => seg.isActive);
- setActiveSegmentation(activeSegmentation[0]);
- });
-
- subscriptions.push(unsubscribe);
- });
-
- return () => {
- subscriptions.forEach(unsub => {
- unsub();
- });
+ const initialRun = async () => {
+ for (const segmentationId of segmentationIds) {
+ await handleROIThresholding({
+ segmentationId,
+ commandsManager,
+ segmentationService,
+ });
+ }
};
+
+ initialRun();
}, []);
useEffect(() => {
- const callback = async evt => {
- const { detail } = evt;
- const { segmentationId } = detail;
-
- if (!segmentationId) {
- return;
- }
-
+ const debouncedHandleROIThresholding = debounce(async eventDetail => {
+ const { segmentationId } = eventDetail;
await handleROIThresholding({
segmentationId,
commandsManager,
segmentationService,
});
+ }, 100);
- const segmentation = segmentationService.getSegmentation(segmentationId);
-
- const { cachedStats } = segmentation;
- if (!cachedStats) {
- return;
- }
-
- // segment 1
- const suvPeak = cachedStats?.['1']?.suvPeak?.suvPeak;
-
- if (Number.isNaN(suvPeak)) {
- uiNotificationService.show({
- title: 'SUV Peak',
- message: 'Segmented volume does not allow SUV Peak calculation',
- type: 'warning',
- });
- }
+ const dataModifiedCallback = eventDetail => {
+ debouncedHandleROIThresholding(eventDetail);
};
- eventTarget.addEventListenerDebounced(Enums.Events.SEGMENTATION_DATA_MODIFIED, callback, 250);
+ const dataModifiedSubscription = segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
+ dataModifiedCallback
+ );
return () => {
- eventTarget.removeEventListenerDebounced(Enums.Events.SEGMENTATION_DATA_MODIFIED, callback);
+ dataModifiedSubscription.unsubscribe();
};
- }, []);
+ }, [commandsManager, segmentationService]);
- if (!activeSegmentation) {
- return null;
- }
-
- const tmtvValue = activeSegmentation.cachedStats?.tmtv?.value || null;
- const config = activeSegmentation.cachedStats?.tmtv?.config || {};
-
- const actions = [
- {
- label: 'Export CSV',
- onClick: () => {
- commandsManager.runCommand('exportTMTVReportCSV', {
- segmentations,
- tmtv: tmtvValue,
- config,
- });
- },
- disabled: tmtvValue === null,
- },
- {
- label: 'Export RT Report',
- onClick: () => {
- commandsManager.runCommand('createTMTVRTReport');
- },
- disabled: tmtvValue === null,
- },
- ];
+ // Find the first segmentation with a TMTV value since all of them have the same value
+ const tmtvSegmentation = segmentationsInfo.find(
+ info => info.segmentation.cachedStats?.tmtv !== undefined
+ );
+ const tmtvValue = tmtvSegmentation?.segmentation.cachedStats?.tmtv;
return (
- <>
-
-
- {tmtvValue !== null ? (
-
-
- {'TMTV:'}
-
-
{`${tmtvValue} mL`}
-
- ) : null}
-
-
+
+
+ {tmtvValue !== null && tmtvValue !== undefined ? (
+
+
+ {'TMTV:'}
+
+
{`${tmtvValue.toFixed(3)} mL`}
-
+ ) : null}
-
{
- // navigate to a url in a new tab
- window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
- }}
- >
-
- {'User Guide'}
-
- >
+
);
}
-
-PanelRoiThresholdSegmentation.propTypes = {
- commandsManager: PropTypes.shape({
- runCommand: PropTypes.func.isRequired,
- }),
- servicesManager: PropTypes.shape({
- services: PropTypes.shape({
- segmentationService: PropTypes.shape({
- getSegmentation: PropTypes.func.isRequired,
- getSegmentations: PropTypes.func.isRequired,
- toggleSegmentationVisibility: PropTypes.func.isRequired,
- subscribe: PropTypes.func.isRequired,
- EVENTS: PropTypes.object.isRequired,
- }).isRequired,
- }).isRequired,
- }).isRequired,
-};
diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx
index 1ec15652e..605d17bbb 100644
--- a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx
+++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx
@@ -9,14 +9,10 @@ function segmentationItemEditHandler({ id, servicesManager }: withAppTypes) {
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save': {
- segmentationService.addOrUpdateSegmentation(
- {
- ...segmentation,
- ...value,
- },
- false, // don't suppress event
- true // it should update cornerstone
- );
+ segmentationService.addOrUpdateSegmentation({
+ ...segmentation,
+ ...value,
+ });
}
}
uiDialogService.dismiss({ id: 'enter-annotation' });
diff --git a/extensions/tmtv/src/Panels/PanelTMTV.tsx b/extensions/tmtv/src/Panels/PanelTMTV.tsx
new file mode 100644
index 000000000..753a8ccc9
--- /dev/null
+++ b/extensions/tmtv/src/Panels/PanelTMTV.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import {
+ PanelSegmentation,
+ useActiveViewportSegmentationRepresentations,
+} from '@ohif/extension-cornerstone';
+import { Button, Icons } from '@ohif/ui-next';
+
+export default function PanelTMTV({
+ servicesManager,
+ commandsManager,
+ extensionManager,
+ configuration,
+}: withAppTypes) {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+const ExportCSV = ({ servicesManager, commandsManager }: withAppTypes) => {
+ const { segmentationsWithRepresentations: representations } =
+ useActiveViewportSegmentationRepresentations({ servicesManager });
+
+ const tmtv = representations[0]?.segmentation.cachedStats?.tmtv;
+
+ const segmentations = representations.map(representation => representation.segmentation);
+
+ if (!segmentations.length) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/extensions/tmtv/src/Panels/RectangleROIOptions.tsx b/extensions/tmtv/src/Panels/RectangleROIOptions.tsx
index 85d13821a..19101ba06 100644
--- a/extensions/tmtv/src/Panels/RectangleROIOptions.tsx
+++ b/extensions/tmtv/src/Panels/RectangleROIOptions.tsx
@@ -75,7 +75,7 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes)
}, [selectedSegmentationId, config]);
useEffect(() => {
- const segmentations = segmentationService.getSegmentations();
+ const segmentations = segmentationService.getSegmentationRepresentations();
if (!segmentations.length) {
return;
@@ -90,13 +90,12 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes)
*/
useEffect(() => {
// ~~ Subscription
- const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
- const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
+ const updated = segmentationService.EVENTS.SEGMENTATION_MODIFIED;
const subscriptions = [];
- [added, updated].forEach(evt => {
+ [updated].forEach(evt => {
const { unsubscribe } = segmentationService.subscribe(evt, () => {
- const segmentations = segmentationService.getSegmentations();
+ const segmentations = segmentationService.getSegmentationRepresentations();
if (!segmentations.length) {
return;
diff --git a/extensions/tmtv/src/commandsModule.ts b/extensions/tmtv/src/commandsModule.ts
index 8ea6ebd1e..35e6f67ef 100644
--- a/extensions/tmtv/src/commandsModule.ts
+++ b/extensions/tmtv/src/commandsModule.ts
@@ -1,23 +1,23 @@
-import { vec3 } from 'gl-matrix';
import OHIF from '@ohif/core';
import * as cs from '@cornerstonejs/core';
import * as csTools from '@cornerstonejs/tools';
import { classes } from '@ohif/core';
import getThresholdValues from './utils/getThresholdValue';
-import calculateTMTV from './utils/calculateTMTV';
import createAndDownloadTMTVReport from './utils/createAndDownloadTMTVReport';
import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructureSet';
import { getWebWorkerManager } from '@cornerstonejs/core';
+import { Enums } from '@cornerstonejs/tools';
+
+const { SegmentationRepresentations } = Enums;
const metadataProvider = classes.MetadataProvider;
const ROI_THRESHOLD_MANUAL_TOOL_IDS = [
'RectangleROIStartEndThreshold',
'RectangleROIThreshold',
- 'CircleROIStartEndThreshold'
+ 'CircleROIStartEndThreshold',
];
-const LABELMAP = csTools.Enums.SegmentationRepresentations.Labelmap;
const workerManager = getWebWorkerManager();
@@ -36,6 +36,28 @@ const workerFn = () => {
});
};
+function getVolumesFromSegmentation(segmentationId) {
+ const csSegmentation = csTools.segmentation.state.getSegmentation(segmentationId);
+ const labelmapData = csSegmentation.representationData[
+ SegmentationRepresentations.Labelmap
+ ] as csTools.Types.LabelmapToolOperationDataVolume;
+
+ const { volumeId, referencedVolumeId } = labelmapData;
+ const labelmapVolume = cs.cache.getVolume(volumeId);
+ const referencedVolume = cs.cache.getVolume(referencedVolumeId);
+
+ return { labelmapVolume, referencedVolume };
+}
+
+function getLabelmapVolumeFromSegmentation(segmentation) {
+ const { representationData } = segmentation;
+ const { volumeId } = representationData[
+ SegmentationRepresentations.Labelmap
+ ] as csTools.Types.LabelmapToolOperationDataVolume;
+
+ return cs.cache.getVolume(volumeId);
+}
+
const commandsModule = ({ servicesManager, commandsManager, extensionManager }: withAppTypes) => {
const {
viewportGridService,
@@ -60,20 +82,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
return enabledElement;
}
- function _getMatchedViewportsToolGroupIds() {
- const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
- const toolGroupIds = [];
- viewportMatchDetails.forEach(viewport => {
- const { viewportOptions } = viewport;
- const { toolGroupId } = viewportOptions;
- if (toolGroupIds.indexOf(toolGroupId) === -1) {
- toolGroupIds.push(toolGroupId);
- }
- });
-
- return toolGroupIds;
- }
-
function _getAnnotationsSelectedByToolNames(toolNames) {
return toolNames.reduce((allAnnotationUIDs, toolName) => {
const annotationUIDs =
@@ -102,7 +110,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
}
ptDisplaySet = displaySets.find(displaySet => displaySet.Modality === 'PT');
-
if (ptDisplaySet) {
break;
}
@@ -142,55 +149,48 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
createNewLabelmapFromPT: async ({ label }) => {
// Create a segmentation of the same resolution as the source data
// using volumeLoader.createAndCacheDerivedVolume.
+
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
const ptDisplaySet = actions.getMatchingPTDisplaySet({
viewportMatchDetails,
});
+ let withPTViewportId = null;
+
+ for (const [viewportId, { displaySetsInfo }] of viewportMatchDetails.entries()) {
+ const isPT = displaySetsInfo.some(
+ ({ displaySetInstanceUID }) =>
+ displaySetInstanceUID === ptDisplaySet.displaySetInstanceUID
+ );
+
+ if (isPT) {
+ withPTViewportId = viewportId;
+ break;
+ }
+ }
+
if (!ptDisplaySet) {
uiNotificationService.error('No matching PT display set found');
return;
}
- const currentSegmentations = segmentationService.getSegmentations();
+ const currentSegmentations =
+ segmentationService.getSegmentationRepresentations(withPTViewportId);
- const segmentationId = await segmentationService.createSegmentationForDisplaySet(
- ptDisplaySet.displaySetInstanceUID,
- { label: `Segmentation ${currentSegmentations.length + 1}` }
- );
+ const displaySet = displaySetService.getDisplaySetByUID(ptDisplaySet.displaySetInstanceUID);
- // Add Segmentation to all toolGroupIds in the viewer
- const toolGroupIds = _getMatchedViewportsToolGroupIds();
- const representationType = LABELMAP;
-
- for (const toolGroupId of toolGroupIds) {
- const hydrateSegmentation = true;
- await segmentationService.addSegmentationRepresentationToToolGroup(
- toolGroupId,
- segmentationId,
- hydrateSegmentation,
- representationType
- );
-
- segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
- }
-
- segmentationService.addSegment(segmentationId, {
- segmentIndex: 1,
- properties: {
- label: 'Segment 1',
- },
+ const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
+ label: `Segmentation ${currentSegmentations.length + 1}`,
+ segments: { 1: { label: 'Segment 1', active: true } },
});
+
+ segmentationService.addSegmentationRepresentation(withPTViewportId, {
+ segmentationId,
+ });
+
return segmentationId;
},
- setSegmentationActiveForToolGroups: ({ segmentationId }) => {
- const toolGroupIds = _getMatchedViewportsToolGroupIds();
-
- toolGroupIds.forEach(toolGroupId => {
- segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
- });
- },
thresholdSegmentationByRectangleROITool: ({ segmentationId, config, segmentIndex }) => {
const segmentation = csTools.segmentation.state.getSegmentation(segmentationId);
@@ -201,12 +201,12 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
const ctDisplaySet = matchDetails.get('ctDisplaySet');
const ctVolumeId = `${volumeLoaderScheme}:${ctDisplaySet.displaySetInstanceUID}`; // VolumeId with loader id + volume id
- const { volumeId: segVolumeId } = representationData[LABELMAP];
+ const { volumeId: segVolumeId } = representationData[
+ SegmentationRepresentations.Labelmap
+ ] as csTools.Types.LabelmapToolOperationDataVolume;
const { referencedVolumeId } = cs.cache.getVolume(segVolumeId);
- const annotationUIDs = _getAnnotationsSelectedByToolNames(
- ROI_THRESHOLD_MANUAL_TOOL_IDS
- );
+ const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS);
if (annotationUIDs.length === 0) {
uiNotificationService.show({
@@ -284,16 +284,21 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
{ overwrite: true, segmentIndex }
);
},
- calculateSuvPeak: async ({ labelmap, segmentIndex }) => {
+ calculateSuvPeak: async ({ segmentationId, segmentIndex }) => {
+ const segmentation = segmentationService.getSegmentation(segmentationId);
+
+ const { representationData } = segmentation;
+ const { volumeId, referencedVolumeId } = representationData[
+ SegmentationRepresentations.Labelmap
+ ] as csTools.Types.LabelmapToolOperationDataVolume;
+
+ const labelmap = cs.cache.getVolume(volumeId);
+ const referencedVolume = cs.cache.getVolume(referencedVolumeId);
+
// if we put it in the top, it will appear in other modes
workerManager.registerWorker('suv-peak-worker', workerFn, options);
- const { referencedVolumeId } = labelmap;
- const referencedVolume = cs.cache.getVolume(referencedVolumeId);
-
- const annotationUIDs = _getAnnotationsSelectedByToolNames(
- ROI_THRESHOLD_MANUAL_TOOL_IDS
- );
+ const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS);
const annotations = annotationUIDs.map(annotationUID =>
csTools.annotation.state.getAnnotation(annotationUID)
@@ -304,8 +309,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
origin: labelmap.origin,
direction: labelmap.direction,
spacing: labelmap.spacing,
- scalarData: labelmap.scalarData,
metadata: labelmap.metadata,
+ scalarData: labelmap.voxelManager.getCompleteScalarDataArray(),
};
const referenceVolumeProps = {
@@ -313,8 +318,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
origin: referencedVolume.origin,
direction: referencedVolume.direction,
spacing: referencedVolume.spacing,
- scalarData: referencedVolume.scalarData,
metadata: referencedVolume.metadata,
+ scalarData: referencedVolume.voxelManager.getCompleteScalarDataArray(),
};
// metadata in annotations has enabledElement which is not serializable
@@ -335,12 +340,13 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
};
});
- const suvPeak = await workerManager.executeTask('suv-peak-worker', 'calculateSuvPeak', {
- labelmapProps,
- referenceVolumeProps,
- annotations: annotationsToSend,
- segmentIndex,
- });
+ const suvPeak =
+ (await workerManager.executeTask('suv-peak-worker', 'calculateSuvPeak', {
+ labelmapProps,
+ referenceVolumeProps,
+ annotations: annotationsToSend,
+ segmentIndex,
+ })) || {};
return {
suvPeak: suvPeak.mean,
@@ -349,38 +355,40 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
suvMaxLPS: suvPeak.maxLPS,
};
},
- getLesionStats: ({ labelmap, segmentIndex = 1 }) => {
- const { scalarData, spacing } = labelmap;
- const referencedScalarData = cs.cache.getVolume(labelmap.referencedVolumeId).getScalarData();
+ getLesionStats: ({ segmentationId, segmentIndex = 1 }) => {
+ const { labelmapVolume, referencedVolume } = getVolumesFromSegmentation(segmentationId);
+ const { voxelManager: segVoxelManager, imageData, spacing } = labelmapVolume;
+ const { voxelManager: refVoxelManager } = referencedVolume;
let segmentationMax = -Infinity;
let segmentationMin = Infinity;
const segmentationValues = [];
-
let voxelCount = 0;
- for (let i = 0; i < scalarData.length; i++) {
- if (scalarData[i] === segmentIndex) {
- const value = referencedScalarData[i];
- segmentationValues.push(value);
- if (value > segmentationMax) {
- segmentationMax = value;
+
+ const callback = ({ value, index }) => {
+ if (value === segmentIndex) {
+ const refValue = refVoxelManager.getAtIndex(index) as number;
+ segmentationValues.push(refValue);
+ if (refValue > segmentationMax) {
+ segmentationMax = refValue;
}
- if (value < segmentationMin) {
- segmentationMin = value;
+ if (refValue < segmentationMin) {
+ segmentationMin = refValue;
}
voxelCount++;
}
- }
- const mean = segmentationValues.reduce((a, b) => a + b, 0) / voxelCount;
+ };
+ segVoxelManager.forEach(callback, { imageData });
+ const mean = segmentationValues.reduce((a, b) => a + b, 0) / voxelCount;
const stats = {
minValue: segmentationMin,
maxValue: segmentationMax,
meanValue: mean,
stdValue: Math.sqrt(
- segmentationValues
- .map((k) => (k - mean) ** 2)
- .reduce((acc, curr) => acc + curr, 0) / voxelCount),
+ segmentationValues.map(k => (k - mean) ** 2).reduce((acc, curr) => acc + curr, 0) /
+ voxelCount
+ ),
volume: voxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3,
};
@@ -393,21 +401,30 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
lesionGlyoclysisStats: volume * meanValue,
};
},
- calculateTMTV: ({ segmentations }) => {
- const labelmaps = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
+ calculateTMTV: async ({ segmentations }) => {
+ const labelmapProps = segmentations.map(segmentation => {
+ const labelmap = getLabelmapVolumeFromSegmentation(segmentation);
+ return {
+ dimensions: labelmap.dimensions,
+ spacing: labelmap.spacing,
+ scalarData: labelmap.voxelManager.getCompleteScalarDataArray(),
+ origin: labelmap.origin,
+ direction: labelmap.direction,
+ };
+ });
- if (!labelmaps.length) {
+ if (!labelmapProps.length) {
return;
}
- return calculateTMTV(labelmaps);
+ return await workerManager.executeTask('suv-peak-worker', 'calculateTMTV', labelmapProps);
},
- exportTMTVReportCSV: ({ segmentations, tmtv, config, options }) => {
+ exportTMTVReportCSV: async ({ segmentations, tmtv, config, options }) => {
const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
segmentations,
});
- const tlg = actions.getTotalLesionGlycolysis({ segmentations });
+ const tlg = await actions.getTotalLesionGlycolysis({ segmentations });
const additionalReportRows = [
{ key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } },
{ key: 'Threshold Configuration', value: { ...config } },
@@ -422,51 +439,34 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
createAndDownloadTMTVReport(segReport, additionalReportRows, options);
},
- getTotalLesionGlycolysis: ({ segmentations }) => {
- const labelmapVolumes = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
+ getTotalLesionGlycolysis: async ({ segmentations }) => {
+ const labelmapProps = segmentations.map(segmentation => {
+ const labelmap = getLabelmapVolumeFromSegmentation(segmentation);
+ return {
+ dimensions: labelmap.dimensions,
+ spacing: labelmap.spacing,
+ scalarData: labelmap.voxelManager.getCompleteScalarDataArray(),
+ origin: labelmap.origin,
+ direction: labelmap.direction,
+ };
+ });
- let mergedLabelmap;
- // merge labelmap will through an error if labels maps are not the same size
- // or same direction or ....
- try {
- mergedLabelmap =
- csTools.utilities.segmentation.createMergedLabelmapForIndex(labelmapVolumes);
- } catch (e) {
- console.error('commandsModule::getTotalLesionGlycolysis', e);
- return;
- }
+ const { referencedVolume: ptVolume } = getVolumesFromSegmentation(
+ segmentations[0].segmentationId
+ );
- // grabbing the first labelmap referenceVolume since it will be the same for all
- const { referencedVolumeId, spacing } = labelmapVolumes[0];
+ const ptVolumeProps = {
+ dimensions: ptVolume.dimensions,
+ spacing: ptVolume.spacing,
+ scalarData: ptVolume.voxelManager.getCompleteScalarDataArray(),
+ origin: ptVolume.origin,
+ direction: ptVolume.direction,
+ };
- if (!referencedVolumeId) {
- console.error('commandsModule::getTotalLesionGlycolysis:No referencedVolumeId found');
- }
-
- const ptVolume = cs.cache.getVolume(referencedVolumeId);
- const mergedLabelData = mergedLabelmap.getScalarData();
-
- if (mergedLabelData.length !== ptVolume.getScalarData().length) {
- console.error(
- 'commandsModule::getTotalLesionGlycolysis:Labelmap and ptVolume are not the same size'
- );
- }
-
- let suv = 0;
- let totalLesionVoxelCount = 0;
- for (let i = 0; i < mergedLabelData.length; i++) {
- // if not background
- if (mergedLabelData[i] !== 0) {
- suv += ptVolume.getScalarData()[i];
- totalLesionVoxelCount += 1;
- }
- }
-
- // Average SUV for the merged labelmap
- const averageSuv = suv / totalLesionVoxelCount;
-
- // total Lesion Glycolysis [suv * ml]
- return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3;
+ return await workerManager.executeTask('suv-peak-worker', 'getTotalLesionGlycolysis', {
+ labelmapProps,
+ referenceVolumeProps: ptVolumeProps,
+ });
},
setStartSliceForROIThresholdTool: () => {
const { viewport } = _getActiveViewportsEnabledElement();
@@ -480,7 +480,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
const annotation = csTools.annotation.state.getAnnotation(annotationUID);
- // set the current focalpoint
+ // set the current focal point
annotation.data.startCoordinate = focalPoint;
// IMPORTANT: invalidate the toolData for the cached stat to get updated
// and re-calculate the projection points
@@ -498,7 +498,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
const annotation = csTools.annotation.state.getAnnotation(annotationUID);
- // get the current focalpoint
+ // get the current focal point
const focalPointToEnd = viewport.getCamera().focalPoint;
annotation.data.endCoordinate = focalPointToEnd;
@@ -536,27 +536,34 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
const report = {};
for (const segmentation of segmentations) {
- const { id, label, cachedStats: data } = segmentation;
+ const { label, segmentationId, representationData } =
+ segmentation as csTools.Types.Segmentation;
+ const id = segmentationId;
const segReport = { id, label };
- if (!data) {
+ if (!representationData) {
report[id] = segReport;
continue;
}
- Object.keys(data).forEach(key => {
- if (typeof data[key] !== 'object') {
- segReport[key] = data[key];
- } else {
- Object.keys(data[key]).forEach(subKey => {
- const newKey = `${key}_${subKey}`;
- segReport[newKey] = data[key][subKey];
- });
- }
- });
+ const { cachedStats } = segmentation.segments[1] || {}; // Assuming we want stats from the first segment
- const labelmapVolume = segmentationService.getLabelmapVolume(id);
+ if (cachedStats) {
+ Object.entries(cachedStats).forEach(([key, value]) => {
+ if (typeof value !== 'object') {
+ segReport[key] = value;
+ } else {
+ Object.entries(value).forEach(([subKey, subValue]) => {
+ const newKey = `${key}_${subKey}`;
+ segReport[newKey] = subValue;
+ });
+ }
+ });
+ }
+
+ const labelmapVolume =
+ segmentation.representationData[SegmentationRepresentations.Labelmap];
if (!labelmapVolume) {
report[id] = segReport;
@@ -564,9 +571,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
}
const referencedVolumeId = labelmapVolume.referencedVolumeId;
- segReport.referencedVolumeId = referencedVolumeId;
- const referencedVolume = segmentationService.getLabelmapVolume(referencedVolumeId);
+ const referencedVolume = cs.cache.getVolume(referencedVolumeId);
if (!referencedVolume) {
report[id] = segReport;
@@ -603,6 +609,11 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
},
setFusionPTColormap: ({ toolGroupId, colormap }) => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
+
+ if (!toolGroup) {
+ return;
+ }
+
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
const ptDisplaySet = actions.getMatchingPTDisplaySet({
@@ -650,9 +661,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
createNewLabelmapFromPT: {
commandFn: actions.createNewLabelmapFromPT,
},
- setSegmentationActiveForToolGroups: {
- commandFn: actions.setSegmentationActiveForToolGroups,
- },
thresholdSegmentationByRectangleROITool: {
commandFn: actions.thresholdSegmentationByRectangleROITool,
},
diff --git a/extensions/tmtv/src/getHangingProtocolModule.js b/extensions/tmtv/src/getHangingProtocolModule.ts
similarity index 91%
rename from extensions/tmtv/src/getHangingProtocolModule.js
rename to extensions/tmtv/src/getHangingProtocolModule.ts
index cfe5d65fe..d4f8d9bf6 100644
--- a/extensions/tmtv/src/getHangingProtocolModule.js
+++ b/extensions/tmtv/src/getHangingProtocolModule.ts
@@ -18,7 +18,7 @@ import {
* image, covering all three rows. It has synchronizers for windowLevel for all CT and PT images, and
* also camera synchronizer for each orientation
*/
-const stage1 = {
+const stage1: AppTypes.HangingProtocol.ProtocolStage = {
name: 'default',
id: 'default',
viewportStructure: {
@@ -131,7 +131,7 @@ const stage2 = {
* The layout follows a simple grid pattern with 2 rows and 3 columns.
* It includes synchronizers as well.
*/
-const stage3 = {
+const stage3: AppTypes.HangingProtocol.ProtocolStage = {
name: '2x3-layout',
id: '2x3-layout',
viewportStructure: {
@@ -153,7 +153,7 @@ const stage3 = {
* from the top row spans the full height of both rows.
* It includes synchronizers as well.
*/
-const stage4 = {
+const stage4: AppTypes.HangingProtocol.ProtocolStage = {
name: '2x4-layout',
id: '2x4-layout',
viewportStructure: {
@@ -218,7 +218,23 @@ const stage4 = {
],
};
-const ptCT = {
+/**
+ * This layout displays three fusion viewports: axial, sagittal, and coronal.
+ * It follows a simple grid pattern with 1 row and 3 columns.
+ */
+// const stage0: AppTypes.HangingProtocol.ProtocolStage = {
+// name: 'Fusion 1x3',
+// viewportStructure: {
+// layoutType: 'grid',
+// properties: {
+// rows: 1,
+// columns: 3,
+// },
+// },
+// viewports: [fusionAXIAL, fusionSAGITTAL, fusionCORONAL],
+// };
+
+const ptCT: AppTypes.HangingProtocol.Protocol = {
id: '@ohif/extension-tmtv.hangingProtocolModule.ptCT',
locked: true,
name: 'Default',
@@ -318,7 +334,6 @@ const ptCT = {
],
},
},
-
stages: [stage1, stage2, stage3, stage4],
numberOfPriorsReferenced: -1,
};
diff --git a/extensions/tmtv/src/getPanelModule.tsx b/extensions/tmtv/src/getPanelModule.tsx
index 98c7b3f79..fb9cade98 100644
--- a/extensions/tmtv/src/getPanelModule.tsx
+++ b/extensions/tmtv/src/getPanelModule.tsx
@@ -1,14 +1,10 @@
import React from 'react';
import { PanelPetSUV, PanelROIThresholdExport } from './Panels';
import { Toolbox } from '@ohif/ui-next';
-
-// TODO:
-// - No loading UI exists yet
-// - cancel promises when component is destroyed
-// - show errors in UI for thumbnails if promise fails
+import PanelTMTV from './Panels/PanelTMTV';
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
- const wrappedPanelPetSuv = ({}) => {
+ const wrappedPanelPetSuv = () => {
return (
{
+ const wrappedROIThresholdToolbox = () => {
+ return (
+
+ );
+ };
+
+ const wrappedROIThresholdExport = () => {
+ return (
+
+ );
+ };
+
+ const wrappedPanelTMTV = () => {
return (
<>
- >
- );
- };
-
- const wrappedROIThresholdExport = () => {
- return (
- <>
+
{
+const createVolume = ({ dimensions, origin, direction, spacing, metadata, scalarData }) => {
const imageData = vtkImageData.newInstance();
imageData.setDimensions(dimensions);
imageData.setOrigin(origin);
@@ -21,12 +22,21 @@ const createVolume = ({ dimensions, origin, direction, spacing, scalarData, meta
imageData.modified();
+ const voxelManager = utilities.VoxelManager.createScalarVolumeVoxelManager({
+ scalarData,
+ dimensions,
+ numberOfComponents: 1,
+ });
return {
imageData,
+ spacing,
+ origin,
+ direction,
metadata,
- getScalarData: () => scalarData,
+ voxelManager,
};
};
+
/**
* This method calculates the SUV peak on a segmented ROI from a reference PET
* volume. If a rectangle annotation is provided, the peak is calculated within that
@@ -40,22 +50,15 @@ const createVolume = ({ dimensions, origin, direction, spacing, scalarData, meta
* @returns
*/
function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, segmentIndex = 1 }) {
- const labelmap = createVolume(labelmapProps);
- const referenceVolume = createVolume(referenceVolumeProps);
+ const labelmapInfo = createVolume(labelmapProps);
+ const referenceInfo = createVolume(referenceVolumeProps);
- if (referenceVolume.metadata.Modality !== 'PT') {
+ if (referenceInfo.metadata.Modality !== 'PT') {
return;
}
- const labelmapData = labelmap.getScalarData();
- const referenceVolumeData = referenceVolume.getScalarData();
-
- if (labelmapData.length !== referenceVolumeData.length) {
- throw new Error('labelmap and referenceVolume must have the same number of pixels');
- }
-
- const { dimensions, imageData: labelmapImageData } = labelmap;
- const { imageData: referenceVolumeImageData } = referenceVolume;
+ const { dimensions, imageData: labelmapImageData } = labelmapInfo;
+ const { imageData: referenceVolumeImageData } = referenceInfo;
let boundsIJK;
// Todo: using the first annotation for now
@@ -69,7 +72,7 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se
return ijk;
});
- boundsIJK = utilities.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions);
+ boundsIJK = cstUtils.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions);
}
let max = 0;
@@ -77,14 +80,13 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se
let maxLPS = [0, 0, 0];
const callback = ({ pointIJK, pointLPS }) => {
- const offset = referenceVolumeImageData.computeOffsetIndex(pointIJK);
- const value = labelmapData[offset];
+ const value = labelmapInfo.voxelManager.getAtIJKPoint(pointIJK);
if (value !== segmentIndex) {
return;
}
- const referenceValue = referenceVolumeData[offset];
+ const referenceValue = referenceInfo.voxelManager.getAtIJKPoint(pointIJK);
if (referenceValue > max) {
max = referenceValue;
@@ -93,7 +95,12 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se
}
};
- utilities.pointInShapeCallback(labelmapImageData, () => true, callback, boundsIJK);
+ labelmapInfo.voxelManager.forEach(callback, {
+ boundsIJK,
+ imageData: labelmapImageData,
+ isInObject: () => true,
+ returnPoints: true,
+ });
const direction = labelmapImageData.getDirection().slice(0, 3);
@@ -123,7 +130,7 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se
count += 1;
};
- utilities.pointInSurroundingSphereCallback(
+ cstUtils.pointInSurroundingSphereCallback(
referenceVolumeImageData,
suvPeakCirclePoints,
suvPeakMeanCallback
@@ -139,8 +146,64 @@ function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, se
};
}
+function calculateTMTV(labelmapProps, segmentIndex = 1) {
+ const labelmaps = labelmapProps.map(props => createVolume(props));
+
+ const mergedLabelmap =
+ labelmaps.length === 1
+ ? labelmaps[0]
+ : cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps);
+
+ const { imageData, spacing } = mergedLabelmap;
+ const values = imageData.getPointData().getScalars().getData();
+
+ // count non-zero values inside the outputData, this would
+ // consider the overlapping regions to be only counted once
+ const numVoxels = values.reduce((acc, curr) => {
+ if (curr > 0) {
+ return acc + 1;
+ }
+ return acc;
+ }, 0);
+
+ return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
+}
+
+function getTotalLesionGlycolysis({ labelmapProps, referenceVolumeProps }) {
+ const labelmaps = labelmapProps.map(props => createVolume(props));
+
+ const mergedLabelmap =
+ labelmaps.length === 1
+ ? labelmaps[0]
+ : cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps);
+
+ // grabbing the first labelmap referenceVolume since it will be the same for all
+ const { spacing } = labelmaps[0];
+
+ const ptVolume = createVolume(referenceVolumeProps);
+
+ let suv = 0;
+ let totalLesionVoxelCount = 0;
+ const scalarDataLength = mergedLabelmap.voxelManager.getScalarDataLength();
+ for (let i = 0; i < scalarDataLength; i++) {
+ // if not background
+ if (mergedLabelmap.voxelManager.getAtIndex(i) !== 0) {
+ suv += ptVolume.voxelManager.getAtIndex(i);
+ totalLesionVoxelCount += 1;
+ }
+ }
+
+ // Average SUV for the merged labelmap
+ const averageSuv = suv / totalLesionVoxelCount;
+
+ // total Lesion Glycolysis [suv * ml]
+ return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3;
+}
+
const obj = {
calculateSuvPeak,
+ calculateTMTV,
+ getTotalLesionGlycolysis,
};
expose(obj);
diff --git a/extensions/tmtv/src/utils/calculateTMTV.ts b/extensions/tmtv/src/utils/calculateTMTV.ts
index 8d9f5f3e5..0fcc680ae 100644
--- a/extensions/tmtv/src/utils/calculateTMTV.ts
+++ b/extensions/tmtv/src/utils/calculateTMTV.ts
@@ -20,17 +20,21 @@ function calculateTMTV(labelmaps: Array, segmentIndex = 1):
volumeId
);
- const { imageData, spacing } = mergedLabelmap;
- const values = imageData.getPointData().getScalars().getData();
+ const { imageData, spacing, voxelManager } = mergedLabelmap;
// count non-zero values inside the outputData, this would
// consider the overlapping regions to be only counted once
- const numVoxels = values.reduce((acc, curr) => {
- if (curr > 0) {
- return acc + 1;
+ let numVoxels = 0;
+ const callback = ({ value }) => {
+ if (value > 0) {
+ numVoxels += 1;
}
- return acc;
- }, 0);
+ };
+
+ voxelManager.forEach(callback, {
+ imageData,
+ isInObject: () => true,
+ });
return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
}
diff --git a/extensions/tmtv/src/utils/handleROIThresholding.ts b/extensions/tmtv/src/utils/handleROIThresholding.ts
index ab47fa032..bb5ae0c55 100644
--- a/extensions/tmtv/src/utils/handleROIThresholding.ts
+++ b/extensions/tmtv/src/utils/handleROIThresholding.ts
@@ -1,64 +1,97 @@
-import { cache } from '@cornerstonejs/core';
+import { Segment, Segmentation } from '@cornerstonejs/tools/types';
+import { triggerEvent, eventTarget, Enums } from '@cornerstonejs/core';
export const handleROIThresholding = async ({
segmentationId,
commandsManager,
segmentationService,
- config = {},
-}) => {
+}: withAppTypes<{
+ segmentationId: string;
+}>) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
+ triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, {
+ progress: 0,
+ type: 'Calculate Lesion Stats',
+ id: segmentationId,
+ });
+
// re-calculating the cached stats for the active segmentation
const updatedPerSegmentCachedStats = {};
- segmentation.segments = await Promise.all(
- segmentation.segments.map(async segment => {
- if (!segment || !segment.segmentIndex) {
- return segment;
- }
+ for (const [segmentIndex, segment] of Object.entries(segmentation.segments)) {
+ if (!segment) {
+ continue;
+ }
- const labelmap = cache.getVolume(segmentationId);
+ const numericSegmentIndex = Number(segmentIndex);
- const segmentIndex = segment.segmentIndex;
+ const lesionStats = await commandsManager.run('getLesionStats', {
+ segmentationId,
+ segmentIndex: numericSegmentIndex,
+ });
- const lesionStats = commandsManager.run('getLesionStats', { labelmap, segmentIndex });
- const suvPeak = await commandsManager.run('calculateSuvPeak', { labelmap, segmentIndex });
- const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
+ const suvPeak = await commandsManager.run('calculateSuvPeak', {
+ segmentationId,
+ segmentIndex: numericSegmentIndex,
+ });
- // update segDetails with the suv peak for the active segmentation
- const cachedStats = {
- lesionStats,
- suvPeak,
- lesionGlyoclysisStats,
- };
+ const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
- segment.cachedStats = cachedStats;
- segment.displayText = [
- `SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`,
- `Volume: ${lesionStats.volume.toFixed(2)} mm3`,
- ];
- updatedPerSegmentCachedStats[segmentIndex] = cachedStats;
+ // update segDetails with the suv peak for the active segmentation
+ const cachedStats = {
+ lesionStats,
+ suvPeak,
+ lesionGlyoclysisStats,
+ };
- return segment;
- })
- );
+ const updatedSegment: Segment = {
+ ...segment,
+ cachedStats: {
+ ...segment.cachedStats,
+ ...cachedStats,
+ },
+ };
- const notYetUpdatedAtSource = true;
+ updatedPerSegmentCachedStats[numericSegmentIndex] = cachedStats;
+ segmentation.segments[segmentIndex] = updatedSegment;
+ }
+
+ // all available segmentations
const segmentations = segmentationService.getSegmentations();
- const tmtv = commandsManager.run('calculateTMTV', { segmentations });
+ const tmtv = await commandsManager.run('calculateTMTV', { segmentations });
- segmentation.cachedStats = Object.assign(segmentation.cachedStats, updatedPerSegmentCachedStats, {
- tmtv: {
- value: tmtv.toFixed(3),
- config: { ...config },
- },
+ triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, {
+ progress: 100,
+ type: 'Calculate Lesion Stats',
+ id: segmentationId,
});
- segmentationService.addOrUpdateSegmentation(
- {
+ // add the tmtv to all the segment cachedStats, although it is a global
+ // value but we don't have any other way to display it for now
+ // Update all segmentations with the calculated TMTV
+ segmentations.forEach(segmentation => {
+ segmentation.cachedStats = {
+ ...segmentation.cachedStats,
+ tmtv,
+ };
+
+ // Update each segment within the segmentation
+ Object.keys(segmentation.segments).forEach(segmentIndex => {
+ segmentation.segments[segmentIndex].cachedStats = {
+ ...segmentation.segments[segmentIndex].cachedStats,
+ tmtv,
+ };
+ });
+
+ // Update the segmentation object
+ const updatedSegmentation: Segmentation = {
...segmentation,
- },
- false, // don't suppress events
- notYetUpdatedAtSource
- );
+ segments: {
+ ...segmentation.segments,
+ },
+ };
+
+ segmentationService.addOrUpdateSegmentation(updatedSegmentation);
+ });
};
diff --git a/extensions/tmtv/src/utils/hpViewports.ts b/extensions/tmtv/src/utils/hpViewports.ts
index cce8454cb..c206c5c52 100644
--- a/extensions/tmtv/src/utils/hpViewports.ts
+++ b/extensions/tmtv/src/utils/hpViewports.ts
@@ -1,4 +1,22 @@
-const ctAXIAL = {
+// Common sync group configurations
+const cameraPositionSync = (id: string) => ({
+ type: 'cameraPosition',
+ id,
+ source: true,
+ target: true,
+});
+
+const hydrateSegSync = {
+ type: 'hydrateseg',
+ id: 'sameFORId',
+ source: true,
+ target: true,
+ options: {
+ matchingRules: ['sameFOR'],
+ },
+};
+
+const ctAXIAL: AppTypes.HangingProtocol.Viewport = {
viewportOptions: {
viewportId: 'ctAXIAL',
viewportType: 'volume',
@@ -9,12 +27,7 @@ const ctAXIAL = {
preset: 'first', // 'first', 'last', 'middle'
},
syncGroups: [
- {
- type: 'cameraPosition',
- id: 'axialSync',
- source: true,
- target: true,
- },
+ cameraPositionSync('axialSync'),
{
type: 'voi',
id: 'ctWLSync',
@@ -24,6 +37,7 @@ const ctAXIAL = {
syncColormap: true,
},
},
+ hydrateSegSync,
],
},
displaySets: [
@@ -33,49 +47,14 @@ const ctAXIAL = {
],
};
-const ctSAGITTAL = {
+const ctSAGITTAL: AppTypes.HangingProtocol.Viewport = {
viewportOptions: {
viewportId: 'ctSAGITTAL',
viewportType: 'volume',
orientation: 'sagittal',
toolGroupId: 'ctToolGroup',
syncGroups: [
- {
- type: 'cameraPosition',
- id: 'sagittalSync',
- source: true,
- target: true,
- },
- {
- type: 'voi',
- id: 'ctWLSync',
- source: true,
- target: true,
- options: {
- syncColormap: true,
- },
- },
- ],
- },
- displaySets: [
- {
- id: 'ctDisplaySet',
- },
- ],
-};
-const ctCORONAL = {
- viewportOptions: {
- viewportId: 'ctCORONAL',
- viewportType: 'volume',
- orientation: 'coronal',
- toolGroupId: 'ctToolGroup',
- syncGroups: [
- {
- type: 'cameraPosition',
- id: 'coronalSync',
- source: true,
- target: true,
- },
+ cameraPositionSync('sagittalSync'),
{
type: 'voi',
id: 'ctWLSync',
@@ -85,6 +64,7 @@ const ctCORONAL = {
syncColormap: true,
},
},
+ hydrateSegSync,
],
},
displaySets: [
@@ -94,7 +74,34 @@ const ctCORONAL = {
],
};
-const ptAXIAL = {
+const ctCORONAL: AppTypes.HangingProtocol.Viewport = {
+ viewportOptions: {
+ viewportId: 'ctCORONAL',
+ viewportType: 'volume',
+ orientation: 'coronal',
+ toolGroupId: 'ctToolGroup',
+ syncGroups: [
+ cameraPositionSync('coronalSync'),
+ {
+ type: 'voi',
+ id: 'ctWLSync',
+ source: true,
+ target: true,
+ options: {
+ syncColormap: true,
+ },
+ },
+ hydrateSegSync,
+ ],
+ },
+ displaySets: [
+ {
+ id: 'ctDisplaySet',
+ },
+ ],
+};
+
+const ptAXIAL: AppTypes.HangingProtocol.Viewport = {
viewportOptions: {
viewportId: 'ptAXIAL',
viewportType: 'volume',
@@ -106,12 +113,7 @@ const ptAXIAL = {
preset: 'first', // 'first', 'last', 'middle'
},
syncGroups: [
- {
- type: 'cameraPosition',
- id: 'axialSync',
- source: true,
- target: true,
- },
+ cameraPositionSync('axialSync'),
{
type: 'voi',
id: 'ptWLSync',
@@ -131,6 +133,7 @@ const ptAXIAL = {
syncInvertState: false,
},
},
+ hydrateSegSync,
],
},
displaySets: [
@@ -146,7 +149,7 @@ const ptAXIAL = {
],
};
-const ptSAGITTAL = {
+const ptSAGITTAL: AppTypes.HangingProtocol.Viewport = {
viewportOptions: {
viewportId: 'ptSAGITTAL',
viewportType: 'volume',
@@ -154,12 +157,7 @@ const ptSAGITTAL = {
background: [1, 1, 1],
toolGroupId: 'ptToolGroup',
syncGroups: [
- {
- type: 'cameraPosition',
- id: 'sagittalSync',
- source: true,
- target: true,
- },
+ cameraPositionSync('sagittalSync'),
{
type: 'voi',
id: 'ptWLSync',
@@ -179,6 +177,7 @@ const ptSAGITTAL = {
syncInvertState: false,
},
},
+ hydrateSegSync,
],
},
displaySets: [
@@ -194,7 +193,7 @@ const ptSAGITTAL = {
],
};
-const ptCORONAL = {
+const ptCORONAL: AppTypes.HangingProtocol.Viewport = {
viewportOptions: {
viewportId: 'ptCORONAL',
viewportType: 'volume',
@@ -202,12 +201,7 @@ const ptCORONAL = {
background: [1, 1, 1],
toolGroupId: 'ptToolGroup',
syncGroups: [
- {
- type: 'cameraPosition',
- id: 'coronalSync',
- source: true,
- target: true,
- },
+ cameraPositionSync('coronalSync'),
{
type: 'voi',
id: 'ptWLSync',
@@ -227,6 +221,7 @@ const ptCORONAL = {
syncInvertState: false,
},
},
+ hydrateSegSync,
],
},
displaySets: [
@@ -242,7 +237,7 @@ const ptCORONAL = {
],
};
-const fusionAXIAL = {
+const fusionAXIAL: AppTypes.HangingProtocol.Viewport = {
viewportOptions: {
viewportId: 'fusionAXIAL',
viewportType: 'volume',
@@ -253,12 +248,7 @@ const fusionAXIAL = {
preset: 'first', // 'first', 'last', 'middle'
},
syncGroups: [
- {
- type: 'cameraPosition',
- id: 'axialSync',
- source: true,
- target: true,
- },
+ cameraPositionSync('axialSync'),
{
type: 'voi',
id: 'ctWLSync',
@@ -284,6 +274,7 @@ const fusionAXIAL = {
syncInvertState: false,
},
},
+ hydrateSegSync,
],
},
displaySets: [
@@ -297,8 +288,8 @@ const fusionAXIAL = {
name: 'hsv',
opacity: [
{ value: 0, opacity: 0 },
- { value: 0.1, opacity: 0.9 },
- { value: 1, opacity: 0.95 },
+ { value: 0.1, opacity: 0.8 },
+ { value: 1, opacity: 0.9 },
],
},
voi: {
@@ -320,12 +311,7 @@ const fusionSAGITTAL = {
// preset: 'middle', // 'first', 'last', 'middle'
// },
syncGroups: [
- {
- type: 'cameraPosition',
- id: 'sagittalSync',
- source: true,
- target: true,
- },
+ cameraPositionSync('sagittalSync'),
{
type: 'voi',
id: 'ctWLSync',
@@ -351,6 +337,7 @@ const fusionSAGITTAL = {
syncInvertState: false,
},
},
+ hydrateSegSync,
],
},
displaySets: [
@@ -364,8 +351,8 @@ const fusionSAGITTAL = {
name: 'hsv',
opacity: [
{ value: 0, opacity: 0 },
- { value: 0.1, opacity: 0.9 },
- { value: 1, opacity: 0.95 },
+ { value: 0.1, opacity: 0.8 },
+ { value: 1, opacity: 0.9 },
],
},
voi: {
@@ -387,12 +374,7 @@ const fusionCORONAL = {
// preset: 'middle', // 'first', 'last', 'middle'
// },
syncGroups: [
- {
- type: 'cameraPosition',
- id: 'coronalSync',
- source: true,
- target: true,
- },
+ cameraPositionSync('coronalSync'),
{
type: 'voi',
id: 'ctWLSync',
@@ -418,6 +400,7 @@ const fusionCORONAL = {
syncInvertState: false,
},
},
+ hydrateSegSync,
],
},
displaySets: [
@@ -431,8 +414,8 @@ const fusionCORONAL = {
name: 'hsv',
opacity: [
{ value: 0, opacity: 0 },
- { value: 0.1, opacity: 0.9 },
- { value: 1, opacity: 0.95 },
+ { value: 0.1, opacity: 0.8 },
+ { value: 1, opacity: 0.9 },
],
},
voi: {
@@ -443,7 +426,7 @@ const fusionCORONAL = {
],
};
-const mipSAGITTAL = {
+const mipSAGITTAL: AppTypes.HangingProtocol.Viewport = {
viewportOptions: {
viewportId: 'mipSagittal',
viewportType: 'volume',
@@ -470,6 +453,7 @@ const mipSAGITTAL = {
syncInvertState: false,
},
},
+ hydrateSegSync,
],
// Custom props can be used to set custom properties which extensions
diff --git a/increaseEventEmitterLimit.mjs b/increaseEventEmitterLimit.mjs
deleted file mode 100644
index 22ff97d37..000000000
--- a/increaseEventEmitterLimit.mjs
+++ /dev/null
@@ -1,7 +0,0 @@
-// increase the event emitter limit
-import { EventEmitter } from 'events';
-
-EventEmitter.defaultMaxListeners = 1000;
-
-// process
-process.setMaxListeners(1000);
diff --git a/modes/basic-dev-mode/src/index.ts b/modes/basic-dev-mode/src/index.ts
index 649dd81cd..48378982d 100644
--- a/modes/basic-dev-mode/src/index.ts
+++ b/modes/basic-dev-mode/src/index.ts
@@ -72,7 +72,10 @@ function modeFactory({ modeConfiguration }) {
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
- { toolName: toolNames.StackScrollMouseWheel, bindings: [] },
+ {
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
+ },
],
passive: [
{ toolName: toolNames.Length },
diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts
index 350a51b1f..740ff9079 100644
--- a/modes/basic-test-mode/src/index.ts
+++ b/modes/basic-test-mode/src/index.ts
@@ -42,7 +42,10 @@ const dicompdf = {
const dicomSeg = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg',
- panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
+};
+
+const cornerstone = {
+ panel: '@ohif/extension-cornerstone.panelModule.panelSegmentation',
};
const dicomPmap = {
@@ -147,7 +150,7 @@ function modeFactory() {
// leftPanels: [ohif.thumbnailList],
// rightPanels: [dicomSeg.panel, ohif.measurements],
leftPanels: [tracked.thumbnailList],
- rightPanels: [dicomSeg.panel, tracked.measurements],
+ rightPanels: [cornerstone.panel, tracked.measurements],
// rightPanelClosed: true, // optional prop to start with collapse panels
viewports: [
{
diff --git a/modes/basic-test-mode/src/initToolGroups.ts b/modes/basic-test-mode/src/initToolGroups.ts
index b313d8e0b..ef56f9cd6 100644
--- a/modes/basic-test-mode/src/initToolGroups.ts
+++ b/modes/basic-test-mode/src/initToolGroups.ts
@@ -31,7 +31,10 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
- { toolName: toolNames.StackScrollMouseWheel, bindings: [] },
+ {
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
+ },
],
passive: [
{ toolName: toolNames.Length },
@@ -62,7 +65,6 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage
{ toolName: toolNames.Angle },
{ toolName: toolNames.CobbAngle },
{ toolName: toolNames.Magnify },
- { toolName: toolNames.SegmentationDisplay },
{ toolName: toolNames.WindowLevelRegion },
{ toolName: toolNames.UltrasoundDirectional },
{ toolName: toolNames.PlanarFreehandROI },
@@ -116,8 +118,8 @@ function initSRToolGroup(extensionManager, toolGroupService, commandsManager) {
],
},
{
- toolName: toolNames.StackScrollMouseWheel,
- bindings: [],
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
],
passive: [
@@ -165,7 +167,10 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
- { toolName: toolNames.StackScrollMouseWheel, bindings: [] },
+ {
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
+ },
],
passive: [
{ toolName: toolNames.Length },
@@ -194,7 +199,6 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
{ toolName: toolNames.RectangleROI },
{ toolName: toolNames.StackScroll },
{ toolName: toolNames.Angle },
- { toolName: toolNames.SegmentationDisplay },
{ toolName: toolNames.WindowLevelRegion },
{ toolName: toolNames.PlanarFreehandROI },
{ toolName: toolNames.SplineROI },
diff --git a/modes/basic-test-mode/src/moreTools.ts b/modes/basic-test-mode/src/moreTools.ts
index 204bac680..91634d0f0 100644
--- a/modes/basic-test-mode/src/moreTools.ts
+++ b/modes/basic-test-mode/src/moreTools.ts
@@ -68,7 +68,7 @@ const moreTools = [
},
},
listeners: {
- [EVENTS.STACK_VIEWPORT_NEW_STACK]: {
+ [EVENTS.VIEWPORT_NEW_IMAGE_SET]: {
commandName: 'toggleImageSliceSync',
commandOptions: { toggledState: true },
},
@@ -188,7 +188,13 @@ const moreTools = [
label: 'Ultrasound Directional',
tooltip: 'Ultrasound Directional',
commands: setToolActiveToolbar,
- evaluate: ['evaluate.cornerstoneTool', 'evaluate.isUS'],
+ evaluate: [
+ 'evaluate.cornerstoneTool',
+ {
+ name: 'evaluate.modality.supported',
+ supportedModalities: ['US'],
+ },
+ ],
}),
createButton({
id: 'WindowLevelRegion',
diff --git a/modes/longitudinal/src/customizations.tsx b/modes/longitudinal/src/customizations.tsx
new file mode 100644
index 000000000..649a8bd46
--- /dev/null
+++ b/modes/longitudinal/src/customizations.tsx
@@ -0,0 +1,26 @@
+export const performCustomizations = customizationService => {
+ // Set the custom SegmentationTable
+ customizationService.addModeCustomizations([
+ // To disable editing in the SegmentationTable
+ {
+ id: 'PanelSegmentation.disableEditing',
+ disableEditing: true,
+ },
+ // To disable editing in the MeasurementTable
+ // {
+ // id: 'PanelMeasurement.disableEditing',
+ // disableEditing: true,
+ // },
+ // {
+ // id: 'measurementLabels',
+ // labelOnMeasure: true,
+ // exclusive: true,
+ // items: [
+ // { value: 'Head', label: 'Head' },
+ // { value: 'Shoulder', label: 'Shoulder' },
+ // { value: 'Knee', label: 'Knee' },
+ // { value: 'Toe', label: 'Toe' },
+ // ],
+ // },
+ ]);
+};
diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts
index fa4b90e9f..7ce082c77 100644
--- a/modes/longitudinal/src/index.ts
+++ b/modes/longitudinal/src/index.ts
@@ -4,6 +4,7 @@ import { id } from './id';
import initToolGroups from './initToolGroups';
import toolbarButtons from './toolbarButtons';
import moreTools from './moreTools';
+import { performCustomizations } from './customizations';
// Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it.
@@ -15,7 +16,11 @@ const ohif = {
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
wsiSopClassHandler:
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
- measurements: '@ohif/extension-default.panelModule.measurements',
+};
+
+const cornerstone = {
+ measurements: '@ohif/extension-cornerstone.panelModule.measurements',
+ segmentation: '@ohif/extension-cornerstone.panelModule.panelSegmentation',
};
const tracked = {
@@ -43,7 +48,6 @@ const dicompdf = {
const dicomSeg = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg',
- panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
};
const dicomPmap = {
@@ -86,19 +90,7 @@ function modeFactory({ modeConfiguration }) {
measurementService.clearMeasurements();
- // customizationService.addModeCustomizations([
- // {
- // id: 'measurementLabels',
- // labelOnMeasure: true,
- // exclusive: true,
- // items: [
- // { value: 'Head', label: 'Head' },
- // { value: 'Shoulder', label: 'Shoulder' },
- // { value: 'Knee', label: 'Knee' },
- // { value: 'Toe', label: 'Toe' },
- // ],
- // },
- // ]);
+ performCustomizations(customizationService);
// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager, this.labelConfig);
@@ -116,13 +108,6 @@ function modeFactory({ modeConfiguration }) {
'MoreTools',
]);
- customizationService.addModeCustomizations([
- {
- id: 'segmentation.panel',
- disableEditing: true,
- },
- ]);
-
// // ActivatePanel event trigger for when a segmentation or measurement is added.
// // Do not force activation so as to respect the state the user may have left the UI in.
// _activatePanelTriggersSubscriptions = [
@@ -192,7 +177,7 @@ function modeFactory({ modeConfiguration }) {
id: ohif.layout,
props: {
leftPanels: [tracked.thumbnailList],
- rightPanels: [dicomSeg.panel, tracked.measurements],
+ rightPanels: [cornerstone.segmentation, tracked.measurements],
rightPanelClosed: true,
viewports: [
{
diff --git a/modes/longitudinal/src/initToolGroups.js b/modes/longitudinal/src/initToolGroups.js
index 20d1cd622..6743602d0 100644
--- a/modes/longitudinal/src/initToolGroups.js
+++ b/modes/longitudinal/src/initToolGroups.js
@@ -39,7 +39,10 @@ function initDefaultToolGroup(
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
- { toolName: toolNames.StackScrollMouseWheel, bindings: [] },
+ {
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
+ },
],
passive: [
{ toolName: toolNames.Length },
@@ -77,7 +80,6 @@ function initDefaultToolGroup(
{ toolName: toolNames.Angle },
{ toolName: toolNames.CobbAngle },
{ toolName: toolNames.Magnify },
- { toolName: toolNames.SegmentationDisplay },
{ toolName: toolNames.CalibrationLine },
{
toolName: toolNames.PlanarFreehandContourSegmentation,
@@ -150,8 +152,8 @@ function initSRToolGroup(extensionManager, toolGroupService) {
],
},
{
- toolName: toolNames.StackScrollMouseWheel,
- bindings: [],
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
],
passive: [
@@ -163,12 +165,6 @@ function initSRToolGroup(extensionManager, toolGroupService) {
{ toolName: SRToolNames.SRPlanarFreehandROI },
{ toolName: SRToolNames.SRRectangleROI },
{ toolName: toolNames.WindowLevelRegion },
- {
- toolName: SRToolNames.SRPlanarFreehandContourSegmentation,
- configuration: {
- displayOnePointAsCrosshairs: true,
- },
- },
],
enabled: [
{
@@ -206,7 +202,10 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager, m
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
- { toolName: toolNames.StackScrollMouseWheel, bindings: [] },
+ {
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
+ },
],
passive: [
{ toolName: toolNames.Length },
@@ -244,7 +243,6 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager, m
{ toolName: toolNames.Angle },
{ toolName: toolNames.CobbAngle },
{ toolName: toolNames.PlanarFreehandROI },
- { toolName: toolNames.SegmentationDisplay },
{ toolName: toolNames.WindowLevelRegion },
{
toolName: toolNames.PlanarFreehandContourSegmentation,
diff --git a/modes/longitudinal/src/moreTools.ts b/modes/longitudinal/src/moreTools.ts
index 3c4ca9310..483cc0aa5 100644
--- a/modes/longitudinal/src/moreTools.ts
+++ b/modes/longitudinal/src/moreTools.ts
@@ -46,7 +46,13 @@ const moreTools = [
label: 'Rotate Right',
tooltip: 'Rotate +90',
commands: 'rotateViewportCW',
- evaluate: 'evaluate.action',
+ evaluate: [
+ 'evaluate.action',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video'],
+ },
+ ],
}),
createButton({
id: 'flipHorizontal',
@@ -54,7 +60,13 @@ const moreTools = [
label: 'Flip Horizontal',
tooltip: 'Flip Horizontally',
commands: 'flipViewportHorizontal',
- evaluate: ['evaluate.viewportProperties.toggle', 'evaluate.not3D'],
+ evaluate: [
+ 'evaluate.viewportProperties.toggle',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video', 'volume3d'],
+ },
+ ],
}),
createButton({
id: 'ImageSliceSync',
@@ -68,12 +80,18 @@ const moreTools = [
},
},
listeners: {
- [EVENTS.STACK_VIEWPORT_NEW_STACK]: {
+ [EVENTS.VIEWPORT_NEW_IMAGE_SET]: {
commandName: 'toggleImageSliceSync',
commandOptions: { toggledState: true },
},
},
- evaluate: ['evaluate.cornerstone.synchronizer', 'evaluate.not3D'],
+ evaluate: [
+ 'evaluate.cornerstone.synchronizer',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video', 'volume3d'],
+ },
+ ],
}),
createButton({
id: 'ReferenceLines',
@@ -85,7 +103,13 @@ const moreTools = [
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
},
- evaluate: 'evaluate.cornerstoneTool.toggle',
+ evaluate: [
+ 'evaluate.cornerstoneTool.toggle',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video'],
+ },
+ ],
}),
createButton({
id: 'ImageOverlayViewer',
@@ -93,7 +117,13 @@ const moreTools = [
label: 'Image Overlay',
tooltip: 'Toggle Image Overlay',
commands: 'toggleEnabledDisabledToolbar',
- evaluate: 'evaluate.cornerstoneTool.toggle',
+ evaluate: [
+ 'evaluate.cornerstoneTool.toggle',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video'],
+ },
+ ],
}),
createButton({
id: 'StackScroll',
@@ -109,7 +139,13 @@ const moreTools = [
label: 'Invert',
tooltip: 'Invert Colors',
commands: 'invertViewport',
- evaluate: 'evaluate.viewportProperties.toggle',
+ evaluate: [
+ 'evaluate.viewportProperties.toggle',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video'],
+ },
+ ],
}),
createButton({
id: 'Probe',
@@ -125,7 +161,13 @@ const moreTools = [
label: 'Cine',
tooltip: 'Cine',
commands: 'toggleCine',
- evaluate: ['evaluate.cine', 'evaluate.not3D'],
+ evaluate: [
+ 'evaluate.cine',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['volume3d'],
+ },
+ ],
}),
createButton({
id: 'Angle',
@@ -149,7 +191,13 @@ const moreTools = [
label: 'Zoom-in',
tooltip: 'Zoom-in',
commands: setToolActiveToolbar,
- evaluate: 'evaluate.cornerstoneTool',
+ evaluate: [
+ 'evaluate.cornerstoneTool',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video'],
+ },
+ ],
}),
createButton({
id: 'CalibrationLine',
@@ -157,7 +205,13 @@ const moreTools = [
label: 'Calibration',
tooltip: 'Calibration Line',
commands: setToolActiveToolbar,
- evaluate: 'evaluate.cornerstoneTool',
+ evaluate: [
+ 'evaluate.cornerstoneTool',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video'],
+ },
+ ],
}),
createButton({
id: 'TagBrowser',
@@ -172,7 +226,13 @@ const moreTools = [
label: 'Magnify Probe',
tooltip: 'Magnify Probe',
commands: 'toggleActiveDisabledToolbar',
- evaluate: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
+ evaluate: [
+ 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video'],
+ },
+ ],
}),
createButton({
id: 'UltrasoundDirectionalTool',
@@ -180,7 +240,13 @@ const moreTools = [
label: 'Ultrasound Directional',
tooltip: 'Ultrasound Directional',
commands: setToolActiveToolbar,
- evaluate: ['evaluate.cornerstoneTool', 'evaluate.isUS'],
+ evaluate: [
+ 'evaluate.cornerstoneTool',
+ {
+ name: 'evaluate.modality.supported',
+ supportedModalities: ['US'],
+ },
+ ],
}),
createButton({
id: 'WindowLevelRegion',
@@ -188,7 +254,13 @@ const moreTools = [
label: 'Window Level Region',
tooltip: 'Window Level Region',
commands: setToolActiveToolbar,
- evaluate: 'evaluate.cornerstoneTool',
+ evaluate: [
+ 'evaluate.cornerstoneTool',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video'],
+ },
+ ],
}),
],
},
diff --git a/modes/longitudinal/src/toolbarButtons.ts b/modes/longitudinal/src/toolbarButtons.ts
index 9b98db3fa..145e98439 100644
--- a/modes/longitudinal/src/toolbarButtons.ts
+++ b/modes/longitudinal/src/toolbarButtons.ts
@@ -126,7 +126,13 @@ const toolbarButtons: Button[] = [
icon: 'tool-window-level',
label: 'Window Level',
commands: setToolActiveToolbar,
- evaluate: 'evaluate.cornerstoneTool',
+ evaluate: [
+ 'evaluate.cornerstoneTool',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['wholeSlide'],
+ },
+ ],
},
},
// Pan...
@@ -162,7 +168,13 @@ const toolbarButtons: Button[] = [
icon: 'tool-capture',
label: 'Capture',
commands: 'showDownloadViewportModal',
- evaluate: 'evaluate.action',
+ evaluate: [
+ 'evaluate.action',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video'],
+ },
+ ],
},
},
{
diff --git a/modes/preclinical-4d/src/getWorkflowSettings.ts b/modes/preclinical-4d/src/getWorkflowSettings.ts
index efaab9b1f..624175ff5 100644
--- a/modes/preclinical-4d/src/getWorkflowSettings.ts
+++ b/modes/preclinical-4d/src/getWorkflowSettings.ts
@@ -2,12 +2,11 @@ const dynamicVolume = {
sopClassHandler:
'@ohif/extension-cornerstone-dynamic-volume.sopClassHandlerModule.dynamic-volume',
leftPanel: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-volume',
- toolBox: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-toolbox',
- export: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-export',
+ segmentation: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-segmentation',
};
const cornerstone = {
- segmentation: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
+ segmentation: '@ohif/extension-cornerstone.panelModule.panelSegmentationNoHeader',
activeViewportWindowLevel: '@ohif/extension-cornerstone.panelModule.activeViewportWindowLevel',
};
@@ -60,7 +59,7 @@ function getWorkflowSettings({ servicesManager }) {
layout: {
panels: {
left: defaultLeftPanel,
- right: [[dynamicVolume.toolBox, cornerstone.segmentation, dynamicVolume.export]],
+ right: [[dynamicVolume.segmentation]],
},
options: {
leftPanelClosed: false,
diff --git a/modes/preclinical-4d/src/index.tsx b/modes/preclinical-4d/src/index.tsx
index 209e30fc0..23ac99294 100644
--- a/modes/preclinical-4d/src/index.tsx
+++ b/modes/preclinical-4d/src/index.tsx
@@ -36,7 +36,7 @@ function modeFactory({ modeConfiguration }) {
return {
id,
routeName: 'dynamic-volume',
- displayName: '4D PT/CT',
+ displayName: 'Preclinical 4D',
onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) {
const {
measurementService,
@@ -64,11 +64,31 @@ function modeFactory({ modeConfiguration }) {
// specific to the step
customizationService.addModeCustomizations([
{
- id: 'segmentation.panel',
- segmentationPanelMode: 'expanded',
- addSegment: false,
+ id: 'PanelSegmentation.tableMode',
+ mode: 'expanded',
+ },
+ {
+ id: 'PanelSegmentation.onSegmentationAdd',
onSegmentationAdd: () => {
- commandsManager.run('createNewLabelmapFromPT');
+ commandsManager.run('createNewLabelMapForDynamicVolume');
+ },
+ },
+ {
+ id: 'PanelSegmentation.showAddSegment',
+ showAddSegment: false,
+ },
+ {
+ id: 'PanelSegmentation.readableText',
+ // remove following if you are not interested in that stats
+ readableText: {
+ lesionStats: 'Lesion Statistics',
+ minValue: 'Minimum Value',
+ maxValue: 'Maximum Value',
+ meanValue: 'Mean Value',
+ volume: 'Volume',
+ suvPeak: 'SUV Peak',
+ suvMax: 'Maximum SUV',
+ suvMaxIJK: 'SUV Max IJK',
},
},
]);
diff --git a/modes/preclinical-4d/src/initToolGroups.tsx b/modes/preclinical-4d/src/initToolGroups.tsx
index e477c7feb..6f8dd0ef8 100644
--- a/modes/preclinical-4d/src/initToolGroups.tsx
+++ b/modes/preclinical-4d/src/initToolGroups.tsx
@@ -33,7 +33,10 @@ function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, se
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
- { toolName: toolNames.StackScrollMouseWheel, bindings: [] },
+ {
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
+ },
],
passive: [
{ toolName: toolNames.Length },
@@ -100,9 +103,8 @@ function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, se
{ toolName: toolNames.SphereScissors },
{ toolName: toolNames.StackScroll },
{ toolName: toolNames.Magnify },
- { toolName: toolNames.SegmentationDisplay },
],
- enabled: [{ toolName: toolNames.SegmentationDisplay }],
+ enabled: [],
disabled: [
{
toolName: toolNames.Crosshairs,
diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx
index a37e61763..2a53e5998 100644
--- a/modes/segmentation/src/index.tsx
+++ b/modes/segmentation/src/index.tsx
@@ -4,9 +4,6 @@ import toolbarButtons from './toolbarButtons';
import segmentationButtons from './segmentationButtons';
import initToolGroups from './initToolGroups';
-const DEFAULT_TOOL_GROUP_ID = 'default';
-const VOLUME3D_TOOL_GROUP_ID = 'volume3d';
-
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
@@ -17,11 +14,10 @@ const ohif = {
const cornerstone = {
viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone',
+ panelTool: '@ohif/extension-cornerstone.panelModule.panelSegmentationWithTools',
};
const segmentation = {
- panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
- panelTool: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentationWithTools',
sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg',
};
@@ -80,7 +76,6 @@ function modeFactory({ modeConfiguration }) {
const {
toolGroupService,
syncGroupService,
- toolbarService,
segmentationService,
cornerstoneViewportService,
uiDialogService,
@@ -111,10 +106,10 @@ function modeFactory({ modeConfiguration }) {
return {
valid:
modalitiesArray.length === 1
- ? !['SM', 'US', 'MG', 'OT', 'DOC', 'CR'].includes(modalitiesArray[0])
+ ? !['SM', 'ECG', 'OT', 'DOC'].includes(modalitiesArray[0])
: true,
description:
- 'The mode does not support studies that ONLY include the following modalities: SM, US, MG, OT, DOC, CR',
+ 'The mode does not support studies that ONLY include the following modalities: SM, OT, DOC',
};
},
/**
@@ -137,7 +132,8 @@ function modeFactory({ modeConfiguration }) {
id: ohif.layout,
props: {
leftPanels: [ohif.leftPanel],
- rightPanels: [segmentation.panelTool],
+ rightPanels: [cornerstone.panelTool],
+ leftPanelClosed: true,
viewports: [
{
namespace: cornerstone.viewport,
diff --git a/modes/segmentation/src/initToolGroups.ts b/modes/segmentation/src/initToolGroups.ts
index babfe4a11..387a4b0da 100644
--- a/modes/segmentation/src/initToolGroups.ts
+++ b/modes/segmentation/src/initToolGroups.ts
@@ -17,7 +17,7 @@ function createTools(utilityModule) {
{ toolName: toolNames.WindowLevel, bindings: [{ mouseButton: Enums.MouseBindings.Primary }] },
{ toolName: toolNames.Pan, bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }] },
{ toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }] },
- { toolName: toolNames.StackScrollMouseWheel, bindings: [] },
+ { toolName: toolNames.StackScroll, bindings: [{ mouseButton: Enums.MouseBindings.Wheel }] },
],
passive: [
{
@@ -86,7 +86,6 @@ function createTools(utilityModule) {
{ toolName: toolNames.SphereScissors },
{ toolName: toolNames.StackScroll },
{ toolName: toolNames.Magnify },
- { toolName: toolNames.SegmentationDisplay },
{ toolName: toolNames.WindowLevelRegion },
{ toolName: toolNames.UltrasoundDirectional },
diff --git a/modes/segmentation/src/toolbarButtons.ts b/modes/segmentation/src/toolbarButtons.ts
index 89b4092c6..91fc11e20 100644
--- a/modes/segmentation/src/toolbarButtons.ts
+++ b/modes/segmentation/src/toolbarButtons.ts
@@ -142,7 +142,13 @@ const toolbarButtons: Button[] = [
label: 'Flip Horizontal',
tooltip: 'Flip Horizontally',
commands: 'flipViewportHorizontal',
- evaluate: ['evaluate.viewportProperties.toggle', 'evaluate.not3D'],
+ evaluate: [
+ 'evaluate.viewportProperties.toggle',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['volume3d'],
+ },
+ ],
}),
createButton({
id: 'ReferenceLines',
@@ -194,7 +200,13 @@ const toolbarButtons: Button[] = [
label: 'Cine',
tooltip: 'Cine',
commands: 'toggleCine',
- evaluate: ['evaluate.cine', 'evaluate.not3D'],
+ evaluate: [
+ 'evaluate.cine',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['volume3d'],
+ },
+ ],
}),
createButton({
id: 'Angle',
@@ -249,7 +261,13 @@ const toolbarButtons: Button[] = [
label: 'Ultrasound Directional',
tooltip: 'Ultrasound Directional',
commands: setToolActiveToolbar,
- evaluate: ['evaluate.cornerstoneTool', 'evaluate.isUS'],
+ evaluate: [
+ 'evaluate.cornerstoneTool',
+ {
+ name: 'evaluate.modality.supported',
+ supportedModalities: ['US'],
+ },
+ ],
}),
createButton({
id: 'WindowLevelRegion',
diff --git a/modes/tmtv/src/index.ts b/modes/tmtv/src/index.ts
index 1ef04ca73..ef383e65d 100644
--- a/modes/tmtv/src/index.ts
+++ b/modes/tmtv/src/index.ts
@@ -11,20 +11,19 @@ const { MetadataProvider } = classes;
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
- measurements: '@ohif/extension-default.panelModule.measure',
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
};
const cs3d = {
viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone',
- segPanel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
+ segPanel: '@ohif/extension-cornerstone.panelModule.panelSegmentationNoHeader',
+ measurements: '@ohif/extension-cornerstone.panelModule.measurements',
};
const tmtv = {
hangingProtocol: '@ohif/extension-tmtv.hangingProtocolModule.ptCT',
petSUV: '@ohif/extension-tmtv.panelModule.petSUV',
- toolbox: '@ohif/extension-tmtv.panelModule.tmtvBox',
- export: '@ohif/extension-tmtv.panelModule.tmtvExport',
+ tmtv: '@ohif/extension-tmtv.panelModule.tmtv',
};
const extensionDependencies = {
@@ -104,13 +103,30 @@ function modeFactory({ modeConfiguration }) {
customizationService.addModeCustomizations([
{
- id: 'segmentation.panel',
- segmentationPanelMode: 'expanded',
- addSegment: false,
+ id: 'PanelSegmentation.tableMode',
+ mode: 'expanded',
+ },
+ {
+ id: 'PanelSegmentation.onSegmentationAdd',
onSegmentationAdd: () => {
commandsManager.run('createNewLabelmapFromPT');
},
},
+ {
+ id: 'PanelSegmentation.readableText',
+ // remove following if you are not interested in that stats
+ readableText: {
+ lesionStats: 'Lesion Statistics',
+ minValue: 'Minimum Value',
+ maxValue: 'Maximum Value',
+ meanValue: 'Mean Value',
+ volume: 'Volume',
+ suvPeak: 'SUV Peak',
+ suvMax: 'Maximum SUV',
+ suvMaxIJK: 'SUV Max IJK',
+ lesionGlyoclysisStats: 'Lesion Glycolysis',
+ },
+ },
]);
// For the hanging protocol we need to decide on the window level
@@ -200,7 +216,7 @@ function modeFactory({ modeConfiguration }) {
props: {
leftPanels: [ohif.thumbnailList],
leftPanelClosed: true,
- rightPanels: [[tmtv.toolbox, cs3d.segPanel, tmtv.export], tmtv.petSUV],
+ rightPanels: [tmtv.tmtv, tmtv.petSUV],
viewports: [
{
namespace: cs3d.viewport,
diff --git a/modes/tmtv/src/initToolGroups.js b/modes/tmtv/src/initToolGroups.js
index 899bcc32d..73f66eb62 100644
--- a/modes/tmtv/src/initToolGroups.js
+++ b/modes/tmtv/src/initToolGroups.js
@@ -6,14 +6,7 @@ export const toolGroupIds = {
default: 'default',
};
-function _initToolGroups(
- toolNames,
- Enums,
- toolGroupService,
- commandsManager,
- modeLabelConfig,
- servicesManager
-) {
+function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, modeLabelConfig) {
const tools = {
active: [
{
@@ -28,7 +21,10 @@ function _initToolGroups(
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
- { toolName: toolNames.StackScrollMouseWheel, bindings: [] },
+ {
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
+ },
],
passive: [
{ toolName: toolNames.Length },
@@ -127,7 +123,7 @@ function _initToolGroups(
},
},
],
- enabled: [{ toolName: toolNames.SegmentationDisplay }],
+ enabled: [],
disabled: [
{
toolName: toolNames.Crosshairs,
@@ -155,7 +151,8 @@ function _initToolGroups(
const mipTools = {
active: [
{
- toolName: toolNames.VolumeRotateMouseWheel,
+ toolName: toolNames.VolumeRotate,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
configuration: {
rotateIncrementDegrees: 5,
},
@@ -169,7 +166,6 @@ function _initToolGroups(
},
],
enabled: [
- { toolName: toolNames.SegmentationDisplay },
{
toolName: toolNames.OrientationMarker,
configuration: {
@@ -184,22 +180,8 @@ function _initToolGroups(
toolGroupService.createToolGroupAndAddTools(toolGroupIds.MIP, mipTools);
}
-function initToolGroups(
- toolNames,
- Enums,
- toolGroupService,
- commandsManager,
- modeLabelConfig,
- servicesManager
-) {
- _initToolGroups(
- toolNames,
- Enums,
- toolGroupService,
- commandsManager,
- modeLabelConfig,
- servicesManager
- );
+function initToolGroups(toolNames, Enums, toolGroupService, commandsManager, modeLabelConfig) {
+ _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, modeLabelConfig);
}
export default initToolGroups;
diff --git a/modes/tmtv/src/utils/setFusionActiveVolume.js b/modes/tmtv/src/utils/setFusionActiveVolume.js
index 30d42455c..75f4538dc 100644
--- a/modes/tmtv/src/utils/setFusionActiveVolume.js
+++ b/modes/tmtv/src/utils/setFusionActiveVolume.js
@@ -7,6 +7,7 @@ export default function setFusionActiveVolume(
displaySetService
) {
const matchDetails = matches.get('ptDisplaySet');
+ const matchDetails2 = matches.get('ctDisplaySet');
if (!matchDetails) {
return;
@@ -32,10 +33,13 @@ export default function setFusionActiveVolume(
// Todo: this should not take into account the loader id
const volumeId = `cornerstoneStreamingImageVolume:${displaySets[0].displaySetInstanceUID}`;
+ const { SeriesInstanceUID: SeriesInstanceUID2 } = matchDetails2;
+ const ctDisplaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID2);
+ const ctVolumeId = `cornerstoneStreamingImageVolume:${ctDisplaySets[0].displaySetInstanceUID}`;
const windowLevelConfig = {
...wlToolConfig,
- volumeId,
+ volumeId: ctVolumeId,
};
const ellipticalROIConfig = {
diff --git a/package.json b/package.json
index 497cb13bb..3f54ce3c9 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
"dev:orthanc:no:cache": "lerna run dev:orthanc:no:cache --stream",
"dev:dcm4chee": "lerna run dev:dcm4chee --stream",
"dev:static": "lerna run dev:static --stream",
- "orthanc:up": "docker-compose -f platform/app/.recipes/Nginx-Orthanc/docker-compose.yml up",
+ "orthanc:up": "docker compose -f platform/app/.recipes/Nginx-Orthanc/docker-compose.yml up",
"install:dev": "cp -f yarn.lock addOns/yarn.lock && cd addOns && yarn install --modules-folder ../node_modules",
"preinstall": "node preinstall.js",
"start": "yarn run dev",
@@ -86,8 +86,8 @@
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0",
- "prettier": "^3.3.2",
- "prettier-plugin-tailwindcss": "^0.6.5"
+ "prettier": "^3.3.3",
+ "prettier-plugin-tailwindcss": "0.6.8"
},
"husky": {
"hooks": {
@@ -110,7 +110,7 @@
"glob-parent": "^6.0.2",
"trim": "^1.0.0",
"package-json": "^8.1.0",
- "typescript": "4.6.4",
+ "typescript": "5.5.4",
"sharp": "^0.32.6",
"ip": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
"**/express/path-to-regexp": "0.1.10",
diff --git a/platform/app/.gitignore b/platform/app/.gitignore
index 82f6c1b36..9062b1c28 100644
--- a/platform/app/.gitignore
+++ b/platform/app/.gitignore
@@ -1,2 +1,3 @@
.vercel
test-results
+store.json
diff --git a/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc-Keycloak/dockerfile b/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc-Keycloak/dockerfile
index fa066291e..6c5e0b873 100644
--- a/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc-Keycloak/dockerfile
+++ b/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc-Keycloak/dockerfile
@@ -1,7 +1,7 @@
# docker-compose
# --------------
# This dockerfile is used by the `docker-compose.yml` adjacent file. When
-# running `docker-compose build`, this dockerfile helps build the "webapp" image.
+# running `docker compose build`, this dockerfile helps build the "webapp" image.
# All paths are relative to the `context`, which is the project root directory.
#
# docker build
diff --git a/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc/dockerfile b/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc/dockerfile
index 643c52a57..53a0087ff 100644
--- a/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc/dockerfile
+++ b/platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc/dockerfile
@@ -1,7 +1,7 @@
# docker-compose
# --------------
# This dockerfile is used by the `docker-compose.yml` adjacent file. When
-# running `docker-compose build`, this dockerfile helps build the "webapp" image.
+# running `docker compose build`, this dockerfile helps build the "webapp" image.
# All paths are relative to the `context`, which is the project root directory.
#
# docker build
diff --git a/platform/app/.webpack/webpack.pwa.js b/platform/app/.webpack/webpack.pwa.js
index 6a90e73b4..cd6487589 100644
--- a/platform/app/.webpack/webpack.pwa.js
+++ b/platform/app/.webpack/webpack.pwa.js
@@ -18,12 +18,18 @@ const PUBLIC_DIR = path.join(__dirname, '../public');
const HTML_TEMPLATE = process.env.HTML_TEMPLATE || 'index.html';
const PUBLIC_URL = process.env.PUBLIC_URL || '/';
const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js';
+
+// proxy settings
const PROXY_TARGET = process.env.PROXY_TARGET;
const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
+const PROXY_PATH_REWRITE_FROM = process.env.PROXY_PATH_REWRITE_FROM;
+const PROXY_PATH_REWRITE_TO = process.env.PROXY_PATH_REWRITE_TO;
+
const OHIF_PORT = Number(process.env.OHIF_PORT || 3000);
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
const Dotenv = require('dotenv-webpack');
const writePluginImportFile = require('./writePluginImportsFile.js');
+// const MillionLint = require('@million/lint');
const copyPluginFromExtensions = writePluginImportFile(SRC_DIR, DIST_DIR);
@@ -75,6 +81,8 @@ module.exports = (env, argv) => {
],
},
plugins: [
+ // For debugging re-renders
+ // MillionLint.webpack(),
new Dotenv(),
// Clean output.path
new CleanWebpackPlugin(),
@@ -104,21 +112,12 @@ module.exports = (env, argv) => {
to: `${DIST_DIR}/app-config.js`,
},
// Copy Dicom Microscopy Viewer build files
- // This is in pluginCOnfig.json now
- // {
- // from: '../../../node_modules/dicom-microscopy-viewer/dist/dynamic-import',
- // to: DIST_DIR,
- // globOptions: {
- // ignore: ['**/*.min.js.map'],
- // },
- // // The dicom-microscopy-viewer is optional, so if it doeesn't get
- // // installed, it shouldn't cause issues.
- // noErrorOnMissing: true,
- // },
- // Copy dicom-image-loader build files
{
- from: '../../../node_modules/@cornerstonejs/dicom-image-loader/dist/dynamic-import',
+ from: '../../../node_modules/dicom-microscopy-viewer/dist/dynamic-import',
to: DIST_DIR,
+ globOptions: {
+ ignore: ['**/*.min.js.map'],
+ },
},
],
}),
@@ -179,12 +178,23 @@ module.exports = (env, argv) => {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
+ devMiddleware: {
+ writeToDisk: true,
+ },
},
});
if (hasProxy) {
mergedConfig.devServer.proxy = mergedConfig.devServer.proxy || {};
- mergedConfig.devServer.proxy[PROXY_TARGET] = PROXY_DOMAIN;
+ mergedConfig.devServer.proxy = {
+ [PROXY_TARGET]: {
+ target: PROXY_DOMAIN,
+ changeOrigin: true,
+ pathRewrite: {
+ [`^${PROXY_PATH_REWRITE_FROM}`]: PROXY_PATH_REWRITE_TO,
+ },
+ },
+ };
}
if (isProdBuild) {
diff --git a/platform/app/cypress.config.ts b/platform/app/cypress.config.ts
index debb68a6f..eecb13a79 100644
--- a/platform/app/cypress.config.ts
+++ b/platform/app/cypress.config.ts
@@ -13,10 +13,6 @@ export default defineConfig({
console.log(launchOptions.args); // print all current args
console.log('***', browser.family, browser.name, '***');
- if (browser.family === 'chromium' && browser.name !== 'electron') {
- // auto open devtools
- launchOptions.args.push('--enable-features=SharedArrayBuffer');
- }
// whatever you return here becomes the launchOptions
return launchOptions;
diff --git a/platform/app/cypress/integration/customization/HangingProtocol.spec.js b/platform/app/cypress/integration/customization/HangingProtocol.spec.js
index 743caecba..795db727a 100644
--- a/platform/app/cypress/integration/customization/HangingProtocol.spec.js
+++ b/platform/app/cypress/integration/customization/HangingProtocol.spec.js
@@ -11,7 +11,7 @@ describe('OHIF HP', () => {
});
it('Should display 3 up', () => {
- cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 3);
+ cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 4);
});
it('Should navigate next/previous stage', () => {
diff --git a/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js b/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js
index 431ff8b4e..20a3a6cd1 100644
--- a/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js
+++ b/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js
@@ -10,11 +10,11 @@ describe('OHIF Double Click', () => {
});
it('Should double click each viewport to one up and back', () => {
- const numExpectedViewports = 3;
+ const numExpectedViewports = 4;
cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', numExpectedViewports);
- for (let i = 0; i < numExpectedViewports; i += 1) {
- cy.wait(2000);
+ for (let i = 0; i < 3; i += 1) {
+ cy.wait(1000);
// For whatever reason, with Cypress tests, we have to activate the
// viewport we are double clicking first.
diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js
index 7e6806413..d307b5139 100644
--- a/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js
+++ b/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js
@@ -12,7 +12,7 @@ describe('OHIF Context Menu', function () {
// Add length measurement
cy.addLengthMeasurement();
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click();
- cy.get('[data-cy="measurement-item"]').as('measurementItem').click();
+ cy.get('[data-cy="data-row"]').as('measurementItem').click();
const [x1, y1] = [150, 100];
cy.get('@viewport')
@@ -30,6 +30,6 @@ describe('OHIF Context Menu', function () {
// Click "Finding" subMenu
cy.get('[data-cy="context-menu-item"]').as('item').contains('Aortic insufficiency').click();
- cy.get('[data-cy="measurement-item"]').as('measure-item').contains('Aortic insufficiency');
+ cy.get('[data-cy="data-row"]').as('measure-item').contains('Aortic insufficiency');
});
});
diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js
index 444ed5a09..bf551565d 100644
--- a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js
+++ b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js
@@ -85,7 +85,7 @@ describe('OHIF Cornerstone Toolbar', () => {
// The exact text is slightly dependent on the viewport resolution, so leave a range
cy.get('@viewportInfoBottomLeft').should($txt => {
const text = $txt.text();
- expect(text).to.include('W:118').include('L:479');
+ expect(text).to.include('L:479');
});
});
@@ -117,7 +117,7 @@ describe('OHIF Cornerstone Toolbar', () => {
//Verify the measurement exists in the table
cy.get('@measurementsPanel').should('be.visible');
- cy.get('[data-cy="measurement-item"]').as('measure').its('length').should('be.at.least', 1);
+ cy.get('[data-cy="data-row"]').as('measure').its('length').should('be.at.least', 1);
});
/*it('checks if angle annotation can be added on viewport without causing any errors', () => {
@@ -351,7 +351,7 @@ describe('OHIF Cornerstone Toolbar', () => {
//Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
- cy.get('[data-cy="measurement-item"]')
+ cy.get('[data-cy="data-row"]')
.its('length')
.should('be.at.least', 2);
diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js
index bb5c1716e..1cd9fa334 100644
--- a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js
+++ b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js
@@ -29,16 +29,17 @@ describe('OHIF Measurement Panel', function () {
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click();
- cy.get('[data-cy="measurement-item"]').as('measurementItem').click();
+ cy.get('[data-cy="data-row"]').as('measurementItem').click();
- cy.get('[data-cy="measurement-item"]').find('svg').eq(0).as('measurementItemSvg').click();
+ cy.get('[data-cy="data-row"]').find('svg').eq(0).as('measurementItemSvg').click();
// enter Bone label
- cy.get('[data-cy="input-annotation"]').should('exist');
- cy.get('[data-cy="input-annotation"]').should('be.visible');
- cy.get('[data-cy="input-annotation"]').type('Bone{enter}');
+ // Todo: move it to the new annotation input with drop down
+ // cy.get('[data-cy="input-annotation"]').should('exist');
+ // cy.get('[data-cy="input-annotation"]').should('be.visible');
+ // cy.get('[data-cy="input-annotation"]').type('Bone{enter}');
- cy.get('[data-cy="measurement-item"]').as('measurementItem').should('contain.text', 'Bone');
+ // cy.get('[data-cy="data-row"]').as('measurementItem').should('contain.text', 'Bone');
});
it('checks if image would jump when clicked on a measurement item', function () {
@@ -58,7 +59,7 @@ describe('OHIF Measurement Panel', function () {
cy.get('@viewportInfoBottomRight').should('contains.text', '(14/');
// Click on first measurement item
- cy.get('[data-cy="measurement-item"]').eq(0).click();
+ cy.get('[data-cy="data-row"]').eq(0).click();
cy.get('@viewportInfoBottomRight').should('contains.text', '(1/');
cy.get('@viewportInfoBottomRight').should('not.contains.text', '(14/');
diff --git a/platform/app/netlify.toml b/platform/app/netlify.toml
index bc07d914e..9e4a6e5ee 100644
--- a/platform/app/netlify.toml
+++ b/platform/app/netlify.toml
@@ -39,9 +39,3 @@
no-cache,
no-store,
must-revalidate'''
-
- # COMMENT: For sharedArrayBuffer, see https://developer.chrome.com/blog/enabling-shared-array-buffer/
- Cross-Origin-Embedder-Policy = "require-corp"
- Cross-Origin-Opener-Policy = "same-origin"
- # set CORP to cross-origin for anyone who wants to use the viewer in an iframe
- Cross-Origin-Resource-Policy = "cross-origin"
diff --git a/platform/app/package.json b/platform/app/package.json
index 289278331..aa31dbecf 100644
--- a/platform/app/package.json
+++ b/platform/app/package.json
@@ -29,8 +29,8 @@
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack serve --config .webpack/webpack.pwa.js",
"dev:no:cache": "cross-env NODE_ENV=development webpack serve --no-cache --config .webpack/webpack.pwa.js",
- "dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker-nginx-orthanc.js webpack serve --config .webpack/webpack.pwa.js",
- "dev:orthanc:no:cache": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker-nginx-orthanc.js webpack serve --no-cache --config .webpack/webpack.pwa.js",
+ "dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=http://localhost:3000/pacs/dicom-web PROXY_DOMAIN=http://localhost:8042 PROXY_PATH_REWRITE_FROM=/pacs/dicom-web PROXY_PATH_REWRITE_TO=/dicom-web APP_CONFIG=config/docker-nginx-orthanc.js webpack serve --config .webpack/webpack.pwa.js",
+ "dev:orthanc:no:cache": "cross-env NODE_ENV=development PROXY_TARGET=http://localhost:3000/pacs/dicom-web PROXY_DOMAIN=http://localhost:8042 PROXY_PATH_REWRITE_FROM=/pacs/dicom-web PROXY_PATH_REWRITE_TO=/dicom-web APP_CONFIG=config/docker-nginx-orthanc.js webpack serve --no-cache --config .webpack/webpack.pwa.js",
"dev:dcm4chee": "cross-env NODE_ENV=development APP_CONFIG=config/local_dcm4chee.js webpack serve --config .webpack/webpack.pwa.js",
"dev:static": "cross-env NODE_ENV=development APP_CONFIG=config/local_static.js webpack serve --config .webpack/webpack.pwa.js",
"dev:viewer": "yarn run dev",
@@ -53,7 +53,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
- "@cornerstonejs/dicom-image-loader": "^1.86.0",
+ "@cornerstonejs/dicom-image-loader": "^2.1.13",
"@emotion/serialize": "^1.1.3",
"@ohif/core": "3.9.0-beta.105",
"@ohif/extension-cornerstone": "3.9.0-beta.105",
@@ -100,7 +100,8 @@
"react-router-dom": "^6.8.1",
"react-shepherd": "6.1.1",
"shepherd.js": "13.0.3",
- "url-loader": "^4.1.1"
+ "url-loader": "^4.1.1",
+ "zustand": "4.5.5"
},
"devDependencies": {
"@babel/plugin-proposal-private-methods": "^7.18.6",
diff --git a/platform/app/public/_headers b/platform/app/public/_headers
index dfe145ba7..e69de29bb 100644
--- a/platform/app/public/_headers
+++ b/platform/app/public/_headers
@@ -1,6 +0,0 @@
-/*
- # Ensure the examples are cross-origin isolated so that
- # we can use SharedArrayBuffer
- # See https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated
- Cross-Origin-Embedder-Policy: require-corp
- Cross-Origin-Opener-Policy: same-origin
diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js
index 61923e5da..0e5443099 100644
--- a/platform/app/public/config/default.js
+++ b/platform/app/public/config/default.js
@@ -427,14 +427,14 @@ window.config = {
title: 'Jumping to Measurements in the Panel',
text: 'Click the measurement in the measurement panel to jump to it.',
attachTo: {
- element: '[data-cy="measurement-item"]',
+ element: '[data-cy="data-row"]',
on: 'left-start',
},
advanceOn: {
- selector: '[data-cy="measurement-item"]',
+ selector: '[data-cy="data-row"]',
event: 'click',
},
- beforeShowPromise: () => waitForElement('[data-cy="measurement-item"]'),
+ beforeShowPromise: () => waitForElement('[data-cy="data-row"]'),
},
{
id: 'changeLayout',
diff --git a/platform/app/public/config/default_16bit.js b/platform/app/public/config/default_16bit.js
index 4c11977cc..d1b6558ca 100644
--- a/platform/app/public/config/default_16bit.js
+++ b/platform/app/public/config/default_16bit.js
@@ -17,7 +17,6 @@ window.config = {
showCPUFallbackMessage: true,
showLoadingIndicator: true,
useNorm16Texture: true,
- useSharedArrayBuffer: 'AUTO',
maxNumRequests: {
interaction: 100,
thumbnail: 75,
diff --git a/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js b/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js
index 163703b7a..b5ea6c45b 100644
--- a/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js
+++ b/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js
@@ -2,10 +2,6 @@
window.config = {
routerBasename: '/ohif-viewer/',
showStudyList: true,
- customizationService: {
- dicomUploadComponent:
- '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
- },
extensions: [],
modes: [],
// below flag is for performance reasons, but it might not work for all servers
diff --git a/platform/app/public/config/docker-nginx-dcm4chee.js b/platform/app/public/config/docker-nginx-dcm4chee.js
index 6d7e9fc47..b44f35788 100644
--- a/platform/app/public/config/docker-nginx-dcm4chee.js
+++ b/platform/app/public/config/docker-nginx-dcm4chee.js
@@ -2,10 +2,6 @@
window.config = {
routerBasename: '/',
showStudyList: true,
- customizationService: {
- dicomUploadComponent:
- '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
- },
extensions: [],
modes: [],
// below flag is for performance reasons, but it might not work for all servers
diff --git a/platform/app/public/config/docker-nginx-orthanc.js b/platform/app/public/config/docker-nginx-orthanc.js
index 7a480ea89..f4be9f944 100644
--- a/platform/app/public/config/docker-nginx-orthanc.js
+++ b/platform/app/public/config/docker-nginx-orthanc.js
@@ -2,10 +2,6 @@
window.config = {
routerBasename: '/',
showStudyList: true,
- customizationService: {
- dicomUploadComponent:
- '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
- },
extensions: [],
modes: [],
// below flag is for performance reasons, but it might not work for all servers
diff --git a/platform/app/public/config/google.js b/platform/app/public/config/google.js
index e416a9e81..08799ff84 100644
--- a/platform/app/public/config/google.js
+++ b/platform/app/public/config/google.js
@@ -1,10 +1,6 @@
/** @type {AppTypes.Config} */
window.config = {
routerBasename: '/',
- customizationService: {
- dicomUploadComponent:
- '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
- },
enableGoogleCloudAdapter: false,
// below flag is for performance reasons, but it might not work for all servers
showWarningMessageForCrossOrigin: true,
diff --git a/platform/app/public/config/local_dcm4chee.js b/platform/app/public/config/local_dcm4chee.js
index 5148bf4a4..1e223a4b9 100644
--- a/platform/app/public/config/local_dcm4chee.js
+++ b/platform/app/public/config/local_dcm4chee.js
@@ -1,10 +1,6 @@
/** @type {AppTypes.Config} */
window.config = {
routerBasename: '/',
- customizationService: {
- dicomUploadComponent:
- '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
- },
showStudyList: true,
extensions: [],
modes: [],
diff --git a/platform/app/public/config/local_orthanc.js b/platform/app/public/config/local_orthanc.js
index 9fa52fe6b..b5c287f5f 100644
--- a/platform/app/public/config/local_orthanc.js
+++ b/platform/app/public/config/local_orthanc.js
@@ -3,10 +3,6 @@ window.config = {
routerBasename: '/',
extensions: [],
modes: [],
- customizationService: {
- dicomUploadComponent:
- '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
- },
showStudyList: true,
maxNumberOfWebWorkers: 3,
showLoadingIndicator: true,
diff --git a/platform/app/public/config/netlify.js b/platform/app/public/config/netlify.js
index 65857e0da..678b88a6c 100644
--- a/platform/app/public/config/netlify.js
+++ b/platform/app/public/config/netlify.js
@@ -383,14 +383,14 @@ window.config = {
title: 'Jumping to Measurements in the Panel',
text: 'Click the measurement in the measurement panel to jump to it.',
attachTo: {
- element: '[data-cy="measurement-item"]',
+ element: '[data-cy="data-row"]',
on: 'left-start',
},
advanceOn: {
- selector: '[data-cy="measurement-item"]',
+ selector: '[data-cy="data-row"]',
event: 'click',
},
- beforeShowPromise: () => waitForElement('[data-cy="measurement-item"]'),
+ beforeShowPromise: () => waitForElement('[data-cy="data-row"]'),
},
{
id: 'changeLayout',
diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx
index 23690d612..253080bd7 100644
--- a/platform/app/src/App.tsx
+++ b/platform/app/src/App.tsx
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import i18n from '@ohif/i18n';
import { I18nextProvider } from 'react-i18next';
import { BrowserRouter } from 'react-router-dom';
+
import Compose from './routes/Mode/Compose';
import {
ExtensionManager,
@@ -16,7 +17,6 @@ import {
DialogProvider,
Modal,
ModalProvider,
- SnackbarProvider,
ThemeWrapper,
ViewportDialogProvider,
ViewportGridProvider,
@@ -24,7 +24,11 @@ import {
UserAuthenticationProvider,
ToolboxProvider,
} from '@ohif/ui';
-import { ThemeWrapper as ThemeWrapperNext } from '@ohif/ui-next';
+import {
+ ThemeWrapper as ThemeWrapperNext,
+ NotificationProvider,
+ TooltipProvider,
+} from '@ohif/ui-next';
// Viewer Project
// TODO: Should this influence study list?
import { AppConfigProvider } from '@state';
@@ -88,17 +92,19 @@ function App({
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2');
- const max3DTextureSize = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE);
- appConfigState.max3DTextureSize = max3DTextureSize;
+ if (gl) {
+ const max3DTextureSize = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE);
+ appConfigState.max3DTextureSize = max3DTextureSize;
+ }
const {
uiDialogService,
uiModalService,
- uiNotificationService,
uiViewportDialogService,
viewportGridService,
cineService,
userAuthenticationService,
+ uiNotificationService,
customizationService,
} = servicesManager.services;
@@ -112,8 +118,8 @@ function App({
[ViewportGridProvider, { service: viewportGridService }],
[ViewportDialogProvider, { service: uiViewportDialogService }],
[CineProvider, { service: cineService }],
- // [NotificationProvider, { service: uiNotificationService }],
- [SnackbarProvider, { service: uiNotificationService }],
+ [NotificationProvider, { service: uiNotificationService }],
+ [TooltipProvider],
[DialogProvider, { service: uiDialogService }],
[ModalProvider, { service: uiModalService, modal: Modal }],
[ShepherdJourneyProvider],
diff --git a/platform/app/src/appInit.js b/platform/app/src/appInit.js
index 60f5ee4db..ce864b054 100644
--- a/platform/app/src/appInit.js
+++ b/platform/app/src/appInit.js
@@ -9,7 +9,6 @@ import {
UIDialogService,
UIViewportDialogService,
MeasurementService,
- StateSyncService,
DisplaySetService,
ToolbarService,
ViewportGridService,
@@ -73,7 +72,6 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
UserAuthenticationService.REGISTRATION,
PanelService.REGISTRATION,
WorkflowStepsService.REGISTRATION,
- StateSyncService.REGISTRATION,
[StudyPrefetcherService.REGISTRATION, appConfig.studyPrefetcher],
]);
diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx
index f520e6cd7..0d41892e3 100644
--- a/platform/app/src/components/ViewportGrid.tsx
+++ b/platform/app/src/components/ViewportGrid.tsx
@@ -23,13 +23,8 @@ function ViewerViewportGrid(props: withAppTypes) {
});
const layoutHash = useRef(null);
- const {
- displaySetService,
- measurementService,
- hangingProtocolService,
- cornerstoneViewportService,
- uiNotificationService,
- } = servicesManager.services;
+ const { displaySetService, measurementService, hangingProtocolService, uiNotificationService } =
+ servicesManager.services;
const generateLayoutHash = () => `${numCols}-${numRows}`;
@@ -129,7 +124,7 @@ function ViewerViewportGrid(props: withAppTypes) {
title: 'Drag and Drop',
message:
'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.',
- type: 'info',
+ type: 'error',
duration: 3000,
});
}
diff --git a/platform/app/src/routes/Debug.tsx b/platform/app/src/routes/Debug.tsx
index 0bebe7f60..67ad96544 100644
--- a/platform/app/src/routes/Debug.tsx
+++ b/platform/app/src/routes/Debug.tsx
@@ -6,7 +6,7 @@ import { Icon } from '@ohif/ui';
function Debug() {
return (
-
+
![]()
- {!window.crossOriginIsolated && (
-
- We use SharedArrayBuffer to render volume data (e.g., MPR). If you are seeing
- this message, it means that your browser has not enabled COOP/COEP. Please see
- the following link for more information:{' '}
-
- Learn More
-
-
- )}
diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx
index 785810ca3..49621e88a 100644
--- a/platform/app/src/routes/WorkList/WorkList.tsx
+++ b/platform/app/src/routes/WorkList/WorkList.tsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useMemo } from 'react';
import classnames from 'classnames';
-import PropTypes, { object } from 'prop-types';
+import PropTypes from 'prop-types';
import { Link, useNavigate } from 'react-router-dom';
import moment from 'moment';
import qs from 'query-string';
@@ -20,7 +20,6 @@ import {
StudyListPagination,
StudyListFilter,
TooltipClipboard,
- Header,
useModal,
AboutModal,
UserPreferences,
@@ -31,10 +30,12 @@ import {
ButtonEnums,
} from '@ohif/ui';
+import { Header } from '@ohif/ui-next';
+
import { Types } from '@ohif/ui';
import i18n from '@ohif/i18n';
-import { Onboarding } from '@ohif/ui-next';
+import { Onboarding, ScrollArea } from '@ohif/ui-next';
const PatientInfoVisibility = Types.PatientInfoVisibility;
@@ -497,29 +498,32 @@ function WorkList({
}
const { customizationService } = servicesManager.services;
- const { component: dicomUploadComponent } =
- customizationService.get('dicomUploadComponent') ?? {};
+ const { component: DicomUploadComponent } =
+ customizationService.getCustomization('dicomUploadComponent') || {};
+
const uploadProps =
- dicomUploadComponent && dataSource.getConfig()?.dicomUploadEnabled
+ DicomUploadComponent && dataSource.getConfig()?.dicomUploadEnabled
? {
title: 'Upload files',
closeButton: true,
shouldCloseOnEsc: false,
shouldCloseOnOverlayClick: false,
- content: dicomUploadComponent.bind(null, {
- dataSource,
- onComplete: () => {
- hide();
- onRefresh();
- },
- onStarted: () => {
- show({
- ...uploadProps,
- // when upload starts, hide the default close button as closing the dialogue must be handled by the upload dialogue itself
- closeButton: false,
- });
- },
- }),
+ content: () => (
+
{
+ hide();
+ onRefresh();
+ }}
+ onStarted={() => {
+ show({
+ ...uploadProps,
+ // when upload starts, hide the default close button as closing the dialogue must be handled by the upload dialogue itself
+ closeButton: false,
+ });
+ }}
+ />
+ ),
}
: undefined;
@@ -537,45 +541,51 @@ function WorkList({
/>
-
-
100 ? 101 : numOfStudies}
- filtersMeta={filtersMeta}
- filterValues={{ ...filterValues, ...defaultSortValues }}
- onChange={setFilterValues}
- clearFilters={() => setFilterValues(defaultFilterValues)}
- isFiltering={isFiltering(filterValues, defaultFilterValues)}
- onUploadClick={uploadProps ? () => show(uploadProps) : undefined}
- getDataSourceConfigurationComponent={
- dataSourceConfigurationComponent ? () => dataSourceConfigurationComponent() : undefined
- }
- />
- {hasStudies ? (
+
+
-
100 ? 101 : numOfStudies}
filtersMeta={filtersMeta}
+ filterValues={{ ...filterValues, ...defaultSortValues }}
+ onChange={setFilterValues}
+ clearFilters={() => setFilterValues(defaultFilterValues)}
+ isFiltering={isFiltering(filterValues, defaultFilterValues)}
+ onUploadClick={uploadProps ? () => show(uploadProps) : undefined}
+ getDataSourceConfigurationComponent={
+ dataSourceConfigurationComponent
+ ? () => dataSourceConfigurationComponent()
+ : undefined
+ }
/>
-
- ) : (
-
- {appConfig.showLoadingIndicator && isLoadingData ? (
-
- ) : (
-
- )}
-
- )}
+ ) : (
+
+ {appConfig.showLoadingIndicator && isLoadingData ? (
+
+ ) : (
+
+ )}
+
+ )}
+
);
diff --git a/platform/app/src/routes/index.tsx b/platform/app/src/routes/index.tsx
index d67e8f612..78ad5db47 100644
--- a/platform/app/src/routes/index.tsx
+++ b/platform/app/src/routes/index.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Routes, Route } from 'react-router-dom';
-import { ErrorBoundary } from '@ohif/ui';
+import { ErrorBoundary } from '@ohif/ui-next';
// Route Components
import DataSourceWrapper from './DataSourceWrapper';
@@ -120,10 +120,7 @@ const createRoutes = ({
function RouteWithErrorBoundary({ route, ...rest }) {
// eslint-disable-next-line react/jsx-props-no-spreading
return (
-
+
{
post_logout_redirect_uri: _makeAbsoluteIfNecessary(post_logout_redirect_uri, baseUri),
});
- const client = firstOpenIdClient.useAuthorizationCodeFlow ? NextClient : LegacyClient
+ const client = firstOpenIdClient.response_type === 'code' ? NextClient : LegacyClient;
return client(openIdConnectConfiguration);
};
diff --git a/platform/app/tailwind.config.js b/platform/app/tailwind.config.js
index 8c40c371f..8837fff20 100644
--- a/platform/app/tailwind.config.js
+++ b/platform/app/tailwind.config.js
@@ -39,16 +39,16 @@ module.exports = {
mono: ['Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'],
},
fontSize: {
- xs: '0.65rem',
- sm: '0.75rem',
- base: '0.8125rem',
- lg: '0.9275rem',
- xl: '1.25rem',
- '2xl': '1.5rem',
- '3xl': '1.875rem',
- '4xl': '2.25rem',
- '5xl': '3rem',
- '6xl': '4rem',
+ xxs: '0.6875rem', // 11px
+ xs: '0.75rem', // 12px
+ sm: '0.8125rem', // 13px
+ base: '0.875rem', // 14px
+ lg: '1rem', // 16px
+ xl: '1.125rem', // 18px
+ '2xl': '1.25rem', // 20px
+ '3xl': '1.375rem', // 22px
+ '4xl': '1.5rem', // 24px
+ '5xl': '1.875rem', // 30px
},
},
};
diff --git a/platform/app/tailwind.css b/platform/app/tailwind.css
index 492349705..510fdf6ec 100644
--- a/platform/app/tailwind.css
+++ b/platform/app/tailwind.css
@@ -2,3 +2,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+
+* {
+ min-height: 0 !important;
+}
diff --git a/platform/core/package.json b/platform/core/package.json
index 106eeb62e..8f6c6d341 100644
--- a/platform/core/package.json
+++ b/platform/core/package.json
@@ -37,7 +37,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
- "@cornerstonejs/dicom-image-loader": "^1.86.0",
+ "@cornerstonejs/dicom-image-loader": "^2.1.13",
"@ohif/ui": "3.9.0-beta.105",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21"
diff --git a/platform/core/src/classes/MetadataProvider.ts b/platform/core/src/classes/MetadataProvider.ts
index 3130a75c4..d9cc2e323 100644
--- a/platform/core/src/classes/MetadataProvider.ts
+++ b/platform/core/src/classes/MetadataProvider.ts
@@ -549,7 +549,7 @@ const WADO_IMAGE_LOADER = {
frameOfReferenceUID: instance.FrameOfReferenceUID,
rows: toNumber(instance.Rows),
columns: toNumber(instance.Columns),
- imageOrientationPatient: toNumber(ImageOrientationPatient),
+ imageOrientationPatient: toNumber(ImageOrientationPatient) || [0, 1, 0, 0, 0, -1],
rowCosines: toNumber(rowCosines || [0, 1, 0]),
isDefaultValueSetForRowCosine: toNumber(rowCosines) ? false : true,
columnCosines: toNumber(columnCosines || [0, 0, -1]),
diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts
index d01635538..6c7d7707e 100644
--- a/platform/core/src/extensions/ExtensionManager.ts
+++ b/platform/core/src/extensions/ExtensionManager.ts
@@ -51,7 +51,7 @@ export interface Extension {
getToolbarModule?: (p: ExtensionParams) => unknown;
getPanelModule?: (p: ExtensionParams) => unknown;
onModeEnter?: (p: AppTypes) => void;
- onModeExit?: () => void;
+ onModeExit?: (p: AppTypes) => void;
}
export type ExtensionRegister = {
@@ -440,7 +440,7 @@ export default class ExtensionManager extends PubSubService {
return extensionModule;
} catch (ex) {
- console.log(ex);
+ console.error(ex);
throw new Error(
`Exception thrown while trying to call ${getModuleFnName} for the ${extensionId} extension`
);
diff --git a/platform/core/src/hooks/useActiveViewportDisplaySets.ts b/platform/core/src/hooks/useActiveViewportDisplaySets.ts
new file mode 100644
index 000000000..f5f425249
--- /dev/null
+++ b/platform/core/src/hooks/useActiveViewportDisplaySets.ts
@@ -0,0 +1,60 @@
+import { useEffect, useState, useCallback } from 'react';
+import { DisplaySet } from '../types';
+
+/**
+ * Hook that listens for changes in the active viewport and its display sets.
+ * It returns the display sets associated with the active viewport.
+ *
+ * @param servicesManager - Services manager instance
+ * @returns Array of display sets for the active viewport
+ */
+const useActiveViewportDisplaySets = ({ servicesManager }): DisplaySet[] => {
+ const [displaySets, setDisplaySets] = useState([]);
+ const { displaySetService, viewportGridService } = servicesManager.services;
+
+ // Move this function outside useEffect and memoize it
+ const getDisplaySetsForViewport = useCallback(
+ (viewportId: string) => {
+ const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId) || [];
+ return displaySetUIDs.map(uid => displaySetService.getDisplaySetByUID(uid)).filter(Boolean);
+ },
+ [displaySetService, viewportGridService]
+ );
+
+ useEffect(() => {
+ // Get initial state
+ const viewportId = viewportGridService.getActiveViewportId();
+ setDisplaySets(getDisplaySetsForViewport(viewportId));
+
+ const handleViewportChange = ({ viewportId }) => {
+ setDisplaySets(getDisplaySetsForViewport(viewportId));
+ };
+
+ const handleGridStateChange = ({ state }) => {
+ const activeViewportId = state.activeViewportId;
+ if (activeViewportId) {
+ setDisplaySets(getDisplaySetsForViewport(activeViewportId));
+ }
+ };
+
+ // Subscribe to viewport changes
+ const subscriptions = [
+ viewportGridService.subscribe(
+ viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
+ handleViewportChange
+ ),
+ viewportGridService.subscribe(
+ viewportGridService.EVENTS.GRID_STATE_CHANGED,
+ handleGridStateChange
+ ),
+ ];
+
+ return () => {
+ subscriptions.forEach(subscription => subscription.unsubscribe());
+ };
+ }, [viewportGridService, getDisplaySetsForViewport]); // Only depend on stable references
+
+ return displaySets;
+};
+
+export default useActiveViewportDisplaySets;
diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js
deleted file mode 100644
index 66d4ac379..000000000
--- a/platform/core/src/index.test.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import * as OHIF from './index';
-
-describe('Top level exports', () => {
- test('have not changed', () => {
- const expectedExports = [
- 'MODULE_TYPES',
- //
- 'CommandsManager',
- 'ExtensionManager',
- 'HotkeysManager',
- 'ServicesManager',
- 'ServiceProvidersManager',
- //
- 'defaults',
- 'utils',
- 'hotkeys',
- 'classes',
- 'default', //
- 'errorHandler',
- 'string',
- 'user',
- 'object',
- 'log',
- 'DICOMWeb',
- 'OHIF',
- //
- 'CineService',
- 'CustomizationService',
- 'Enums',
- 'StateSyncService',
- 'UIDialogService',
- 'UIModalService',
- 'UINotificationService',
- 'UIViewportDialogService',
- 'DisplaySetService',
- 'MeasurementService',
- 'ToolbarService',
- 'Types',
- 'ViewportGridService',
- 'HangingProtocolService',
- 'UserAuthenticationService',
- 'IWebApiDataSource',
- 'DicomMetadataStore',
- 'DisplaySetMessage',
- 'DisplaySetMessageList',
- 'pubSubServiceInterface',
- 'PubSubService',
- 'PanelService',
- 'WorkflowStepsService',
- 'StudyPrefetcherService',
- 'useToolbar',
- ].sort();
-
- const exports = Object.keys(OHIF).sort();
-
- expect(exports).toEqual(expectedExports);
- });
-});
diff --git a/platform/core/src/index.ts b/platform/core/src/index.ts
index 219b32098..031d31d3e 100644
--- a/platform/core/src/index.ts
+++ b/platform/core/src/index.ts
@@ -30,7 +30,6 @@ import {
PubSubService,
UserAuthenticationService,
CustomizationService,
- StateSyncService,
PanelService,
WorkflowStepsService,
StudyPrefetcherService,
@@ -39,6 +38,7 @@ import {
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
import IWebApiDataSource from './DataSources/IWebApiDataSource';
+import useActiveViewportDisplaySets from './hooks/useActiveViewportDisplaySets';
const hotkeys = {
...utils.hotkeys,
@@ -68,7 +68,6 @@ const OHIF = {
//
CineService,
CustomizationService,
- StateSyncService,
UIDialogService,
UIModalService,
UINotificationService,
@@ -85,6 +84,7 @@ const OHIF = {
PubSubService,
PanelService,
useToolbar,
+ useActiveViewportDisplaySets,
WorkflowStepsService,
StudyPrefetcherService,
};
@@ -111,7 +111,6 @@ export {
//
CineService,
CustomizationService,
- StateSyncService,
UIDialogService,
UIModalService,
UINotificationService,
@@ -129,13 +128,15 @@ export {
pubSubServiceInterface,
PubSubService,
Enums,
- Types,
PanelService,
WorkflowStepsService,
StudyPrefetcherService,
useToolbar,
+ useActiveViewportDisplaySets,
};
export { OHIF };
+export type { Types };
+
export default OHIF;
diff --git a/platform/core/src/services/CineService/CineService.ts b/platform/core/src/services/CineService/CineService.ts
index 334187d62..e2e652928 100644
--- a/platform/core/src/services/CineService/CineService.ts
+++ b/platform/core/src/services/CineService/CineService.ts
@@ -15,6 +15,7 @@ class CineService extends PubSubService {
serviceImplementation = {};
startedClips = new Map();
+ closedViewports = new Set();
constructor() {
super(CineService.EVENTS);
@@ -34,9 +35,15 @@ class CineService extends PubSubService {
// Todo: for some reason i need to do this setTimeout since the
// reducer state does not get updated right away and if we publish the
// event and we use the cineService.getState() it will return the old state
- setTimeout(() => {
+ if (isCineEnabled) {
+ this.closedViewports.forEach(viewportId => {
+ this.clearViewportCineClosed(viewportId);
+ });
+ }
+
+ queueMicrotask(() => {
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isCineEnabled });
- }, 0);
+ });
}
public playClip(element, playClipOptions) {
@@ -68,6 +75,19 @@ class CineService extends PubSubService {
return this.serviceImplementation._getSyncedViewports(viewportId);
}
+ public setViewportCineClosed(viewportId) {
+ this.closedViewports.add(viewportId);
+ }
+
+ public isViewportCineClosed(viewportId) {
+ // Todo: we should move towards per viewport cine closed in next release
+ return this.closedViewports.size > 0;
+ }
+
+ public clearViewportCineClosed(viewportId) {
+ this.closedViewports.delete(viewportId);
+ }
+
public setServiceImplementation({
getState: getStateImplementation,
setCine: setCineImplementation,
diff --git a/platform/core/src/services/CustomizationService/CustomizationService.ts b/platform/core/src/services/CustomizationService/CustomizationService.ts
index 282011436..e4f8a7766 100644
--- a/platform/core/src/services/CustomizationService/CustomizationService.ts
+++ b/platform/core/src/services/CustomizationService/CustomizationService.ts
@@ -281,6 +281,30 @@ export default class CustomizationService extends PubSubService {
return result.transform?.(this) || result;
}
+ /**
+ * Helper method to easily add and retrieve customizations
+ * @param id The unique identifier for the customization
+ * @param defaultComponent The default component to use if no customization is set
+ * @param customComponent Optional custom component to set
+ * @returns The custom component if set, otherwise the default component
+ */
+ public getCustomComponent(
+ id: string,
+ defaultComponent: React.ComponentType,
+ customComponent?: React.ComponentType
+ ) {
+ const customization = this.getCustomization(id, {
+ id: `default-${id}`,
+ content: defaultComponent,
+ });
+
+ if (customComponent) {
+ this.setModeCustomization(id, { content: customComponent });
+ }
+
+ return customization.content;
+ }
+
public addModeCustomizations(modeCustomizations): void {
if (!modeCustomizations) {
return;
diff --git a/platform/core/src/services/CustomizationService/types.ts b/platform/core/src/services/CustomizationService/types.ts
index 69a2b1f62..da607dec6 100644
--- a/platform/core/src/services/CustomizationService/types.ts
+++ b/platform/core/src/services/CustomizationService/types.ts
@@ -1,4 +1,4 @@
-import Command from '../../types/Command';
+import { Command } from '../../types/Command';
import { ComponentType } from 'react';
export type Obj = Record;
diff --git a/platform/core/src/services/DisplaySetService/DisplaySet.ts b/platform/core/src/services/DisplaySetService/DisplaySet.ts
deleted file mode 100644
index 88e5111cd..000000000
--- a/platform/core/src/services/DisplaySetService/DisplaySet.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-type DisplaySet {
- displaySetInstanceUID: string;
- StudyInstanceUID: string;
- SeriesInstanceUID?: string;
- SeriesNumber?: string;
- unsupported?: boolean;
- viewportType?: string;
- instances: any[];
- instance?: any;
-}
-
-export default DisplaySet;
diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.ts b/platform/core/src/services/DisplaySetService/DisplaySetService.ts
index 7d72738d4..3bcb79f0a 100644
--- a/platform/core/src/services/DisplaySetService/DisplaySetService.ts
+++ b/platform/core/src/services/DisplaySetService/DisplaySetService.ts
@@ -204,7 +204,7 @@ export default class DisplaySetService extends PubSubService {
}
// If array of instances => One instance.
- const displaySetsAdded = [];
+ const displaySetsAdded = new Array();
if (batch) {
for (let i = 0; i < input.length; i++) {
diff --git a/platform/core/src/services/HangingProtocolService/HPMatcher.js b/platform/core/src/services/HangingProtocolService/HPMatcher.js
index 9d255cc9c..6a874ee47 100644
--- a/platform/core/src/services/HangingProtocolService/HPMatcher.js
+++ b/platform/core/src/services/HangingProtocolService/HPMatcher.js
@@ -50,16 +50,23 @@ const match = (metadataInstance, rules = [], customAttributeRetrievalCallbacks,
readValues[attribute] = fromSrc[from]?.[attribute] ?? instance?.[attribute];
}
+ // handle cases where the constraint is also a custom attribute
+ const resolvedConstraint = resolveConstraintAttributes(
+ readValues,
+ rule.constraint,
+ customAttributeRetrievalCallbacks,
+ fromSrc
+ );
+
// Format the constraint as required by Validate.js
const testConstraint = {
- [attribute]: rule.constraint,
+ [attribute]: resolvedConstraint,
};
// Create a single attribute object to be validated, since metadataInstance is an
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
- let attributeValue = readValues[attribute];
const attributeMap = {
- [attribute]: attributeValue,
+ [attribute]: readValues[attribute],
};
// Use Validate.js to evaluate the constraints on the specified metadataInstance
@@ -118,6 +125,47 @@ const match = (metadataInstance, rules = [], customAttributeRetrievalCallbacks,
};
};
+// New helper function to resolve constraint attributes
+const resolveConstraintAttributes = (
+ readValues,
+ constraint,
+ customAttributeRetrievalCallbacks,
+ fromSrc
+) => {
+ if (typeof constraint !== 'object' || constraint === null) {
+ return constraint;
+ }
+
+ const resolvedConstraint = {};
+ Object.entries(constraint).forEach(([key, value]) => {
+ if (typeof value === 'object' && Object.keys(value).length > 0 && 'attribute' in value) {
+ const attributeName = value.attribute;
+ const attributeFrom = value.from ?? 'metadataInstance';
+
+ if (customAttributeRetrievalCallbacks.hasOwnProperty(attributeName)) {
+ const value = customAttributeRetrievalCallbacks[attributeName].callback.call(
+ null,
+ fromSrc[attributeFrom],
+ fromSrc.options
+ );
+
+ resolvedConstraint[key] = {
+ value,
+ };
+ } else {
+ resolvedConstraint[key] = {
+ value:
+ fromSrc[attributeFrom]?.[attributeName] ?? fromSrc.metadataInstance?.[attributeName],
+ };
+ }
+ } else {
+ resolvedConstraint[key] = value;
+ }
+ }, {});
+
+ return resolvedConstraint;
+};
+
const HPMatcher = {
match,
};
diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts
index 929f31100..10bc77344 100644
--- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts
+++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts
@@ -267,7 +267,6 @@ export default class HangingProtocolService extends PubSubService {
// and the protocol or a function that returns a protocol as the value
const protocols = [];
const keys = this.activeProtocolIds || this.protocols.keys();
- // @ts-ignore
for (const protocolId of keys) {
const protocol = this.getProtocolById(protocolId);
if (protocol) {
@@ -513,12 +512,6 @@ export default class HangingProtocolService extends PubSubService {
_validateProtocol(protocol: HangingProtocol.Protocol): HangingProtocol.Protocol {
protocol.id = protocol.id || protocol.name;
- const defaultViewportOptions = {
- toolGroupId: 'default',
- viewportType: 'stack',
- };
- // Automatically compute some number of attributes if they
- // aren't present. Makes defining new HPs easier.
protocol.name = protocol.name || protocol.id;
const { stages } = protocol;
@@ -545,10 +538,10 @@ export default class HangingProtocolService extends PubSubService {
const { rows, columns } = stage.viewportStructure.properties;
for (let i = 0; i < rows * columns; i++) {
+ const defaultViewport = stage.defaultViewport || protocol.defaultViewport || {};
stage.viewports.push({
viewportOptions: {
- ...defaultViewportOptions,
- // Use 'default' for the first viewport, and UUIDs for the rest.
+ ...defaultViewport.viewportOptions,
viewportId: i === 0 ? 'default' : uuidv4(),
},
displaySets: [],
@@ -557,14 +550,14 @@ export default class HangingProtocolService extends PubSubService {
} else {
// Clone each viewport to ensure independent objects
stage.viewports = stage.viewports.map((viewport, index) => {
+ const defaultViewport = stage.defaultViewport || protocol.defaultViewport || {};
const existingViewportId = viewport.viewportOptions?.viewportId;
return {
...viewport,
viewportOptions: {
- ...(viewport.viewportOptions || defaultViewportOptions),
- // use provided viewportId when available, otherwise use default for first viewport
- // and uuid for the rest
+ ...defaultViewport.viewportOptions,
+ ...viewport.viewportOptions,
viewportId: existingViewportId
? existingViewportId
: index === 0
@@ -766,6 +759,10 @@ export default class HangingProtocolService extends PubSubService {
return viewportsToUpdate;
}
+ public runMatchingRules(metadataArray, matchingRules, options) {
+ return this.protocolEngine.findMatch(metadataArray, matchingRules, options);
+ }
+
private _updateDisplaySetInstanceUIDs(
viewport: HangingProtocol.Viewport,
displaySetSelectorId: string,
@@ -1111,7 +1108,7 @@ export default class HangingProtocolService extends PubSubService {
this.stageIndex = 0;
}
const protocol = this.protocol;
- const stage = protocol.stages[stageIdx];
+ const stage = protocol.stages[stageIdx] ?? protocol.stages[this.stageIndex];
const defaultViewport = stage.defaultViewport || protocol.defaultViewport;
if (!defaultViewport) {
return;
@@ -1407,8 +1404,8 @@ export default class HangingProtocolService extends PubSubService {
}
public areRequiredSelectorsValid(
- displaySetSelectors: HangingProtocol.DisplaySetSelector,
- displaySet: any
+ displaySetSelectors: HangingProtocol.DisplaySetSelector[],
+ displaySet: DisplaySet
): boolean {
let pass = true;
for (const displaySetSelector of displaySetSelectors) {
@@ -1424,7 +1421,7 @@ export default class HangingProtocolService extends PubSubService {
private _validateRequiredSelectors(
displaySetSelector: HangingProtocol.DisplaySetSelector,
- displaySet: any
+ displaySet: DisplaySet
) {
const { seriesMatchingRules } = displaySetSelector;
diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts
index 4ebce5d12..4f823c956 100644
--- a/platform/core/src/services/MeasurementService/MeasurementService.ts
+++ b/platform/core/src/services/MeasurementService/MeasurementService.ts
@@ -32,6 +32,7 @@ import { PubSubService } from '../_shared/pubSubServiceInterface';
/* Measurement schema keys for object validation. */
const MEASUREMENT_SCHEMA_KEYS = [
'uid',
+ 'color',
'data',
'getReport',
'displayText',
@@ -42,6 +43,8 @@ const MEASUREMENT_SCHEMA_KEYS = [
'frameNumber',
'displaySetInstanceUID',
'label',
+ 'isLocked',
+ 'isVisible',
'description',
'type',
'unit',
@@ -58,7 +61,7 @@ const MEASUREMENT_SCHEMA_KEYS = [
'shortestDiameter',
'longestDiameter',
'cachedStats',
- 'selected',
+ 'isSelected',
'textBox',
'referencedImageId',
];
@@ -185,7 +188,7 @@ class MeasurementService extends PubSubService {
return;
}
- measurement.selected = selected;
+ measurement.isSelected = selected;
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
source: measurement.source,
@@ -484,6 +487,10 @@ class MeasurementService extends PubSubService {
/* Convert measurement */
measurement = toMeasurementSchema(sourceAnnotationDetail);
+ if (!measurement) {
+ return;
+ }
+
measurement.source = source;
} catch (error) {
// Todo: handle other
@@ -555,23 +562,23 @@ class MeasurementService extends PubSubService {
* Removes a measurement and broadcasts the removed event.
*
* @param {string} measurementUID The measurement uid
- * @param {MeasurementSource} source The measurement source instance
*/
- remove(measurementUID, source, eventDetails) {
- if (
- !measurementUID ||
- (!this.measurements.has(measurementUID) && !this.unmappedMeasurements.has(measurementUID))
- ) {
- log.warn(`No uid provided, or unable to find measurement by uid.`);
+ remove(measurementUID: string): void {
+ const measurement =
+ this.measurements.get(measurementUID) || this.unmappedMeasurements.get(measurementUID);
+
+ if (!measurementUID || !measurement) {
+ console.debug(`No uid provided, or unable to find measurement by uid.`);
return;
}
+ const source = measurement.source;
+
this.unmappedMeasurements.delete(measurementUID);
this.measurements.delete(measurementUID);
this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, {
source,
measurement: measurementUID,
- ...eventDetails,
});
}
@@ -728,6 +735,57 @@ class MeasurementService extends PubSubService {
_arrayOfObjects = obj => {
return Object.entries(obj).map(e => ({ [e[0]]: e[1] }));
};
+
+ public toggleLockMeasurement(measurementUID: string): void {
+ const measurement = this.measurements.get(measurementUID);
+
+ if (!measurement) {
+ console.debug(`No measurement found for uid: ${measurementUID}`);
+ return;
+ }
+
+ measurement.isLocked = !measurement.isLocked;
+
+ this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
+ source: measurement.source,
+ measurement,
+ notYetUpdatedAtSource: true,
+ });
+ }
+
+ public toggleVisibilityMeasurement(measurementUID: string): void {
+ const measurement = this.measurements.get(measurementUID);
+
+ if (!measurement) {
+ console.debug(`No measurement found for uid: ${measurementUID}`);
+ return;
+ }
+
+ measurement.isVisible = !measurement.isVisible;
+
+ this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
+ source: measurement.source,
+ measurement,
+ notYetUpdatedAtSource: true,
+ });
+ }
+
+ public updateColorMeasurement(measurementUID: string, color: number[]): void {
+ const measurement = this.measurements.get(measurementUID);
+
+ if (!measurement) {
+ console.debug(`No measurement found for uid: ${measurementUID}`);
+ return;
+ }
+
+ measurement.color = color;
+
+ this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
+ source: measurement.source,
+ measurement,
+ notYetUpdatedAtSource: true,
+ });
+ }
}
export default MeasurementService;
diff --git a/platform/core/src/services/StateSyncService/StateSyncService.test.js b/platform/core/src/services/StateSyncService/StateSyncService.test.js
deleted file mode 100644
index 77c23069a..000000000
--- a/platform/core/src/services/StateSyncService/StateSyncService.test.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import StateSyncService from './StateSyncService';
-import log from '../../log';
-
-jest.mock('../../log.js', () => ({
- info: jest.fn(),
- warn: jest.fn(),
- error: jest.fn(),
-}));
-
-const extensionManager = {};
-
-describe('StateSyncService.ts', () => {
- let stateSyncService;
-
- let configuration;
-
- beforeEach(() => {
- log.warn.mockClear();
- jest.clearAllMocks();
- configuration = {};
- stateSyncService = new StateSyncService({
- configuration,
- });
- });
-
- describe('init', () => {
- it('init succeeds', () => {
- stateSyncService.init(extensionManager);
- });
- });
-});
diff --git a/platform/core/src/services/StateSyncService/StateSyncService.ts b/platform/core/src/services/StateSyncService/StateSyncService.ts
deleted file mode 100644
index ef0d6a362..000000000
--- a/platform/core/src/services/StateSyncService/StateSyncService.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { PubSubService } from '../_shared/pubSubServiceInterface';
-import { ExtensionManager } from '../../extensions';
-
-const EVENTS = {};
-
-type Obj = Record;
-
-type StateConfig = {
- /** clearOnModeExit defines state configuration that is cleared automatically on
- * exiting a mode. This clearing occurs after the mode onModeExit,
- * so it is possible to preserve desired state during exit to be restored
- * later.
- */
- clearOnModeExit?: boolean;
-};
-
-type States = {
- [key: string]: Obj;
-};
-
-/**
- */
-export default class StateSyncService extends PubSubService {
- public static REGISTRATION = {
- name: 'stateSyncService',
- create: ({ configuration = {}, commandsManager }) => {
- return new StateSyncService({ configuration, commandsManager });
- },
- };
-
- extensionManager: ExtensionManager;
- configuration: Obj;
- registeredStateSets: {
- [id: string]: StateConfig;
- } = {};
- state: States = {};
-
- constructor({ configuration }) {
- super(EVENTS);
- this.configuration = configuration || {};
- }
-
- public init(extensionManager: ExtensionManager): void {}
-
- /** Registers a new sync store called `id`. The state
- * defines how the state is stored, and any default clearing of the
- * state.
- * A default store has the lifetime of the application.
- * The other available store is cleared `onModeExit`
- */
- public register(id: string, config: StateConfig): void {
- this.registeredStateSets[id] = config;
- this.store({ [id]: {} });
- }
-
- public getState(): Record {
- // TODO - return a proxy to this which is not writable in dev mode
- return this.state;
- }
-
- /**
- * Stores all the new state values contained in states.
- *
- * @param states - is an object containing replacement values to store
- * @returns
- */
- public store(states: States): States {
- Object.keys(states).forEach(stateKey => {
- if (!this.registeredStateSets[stateKey]) {
- throw new Error(`No state ${stateKey} registered`);
- }
- });
- this.state = { ...this.state, ...states };
- return states;
- }
-
- public onModeExit(): void {
- const toReduce = {};
- for (const [key, value] of Object.entries(this.registeredStateSets)) {
- if (value.clearOnModeExit) {
- toReduce[key] = {};
- }
- }
- this.store(toReduce);
- }
-}
diff --git a/platform/core/src/services/StateSyncService/index.ts b/platform/core/src/services/StateSyncService/index.ts
deleted file mode 100644
index 91cc430e5..000000000
--- a/platform/core/src/services/StateSyncService/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import StateSyncService from './StateSyncService';
-
-export default StateSyncService;
diff --git a/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts b/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts
index 3edf20525..5b5853d65 100644
--- a/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts
+++ b/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts
@@ -4,7 +4,16 @@ import ServicesManager from '../ServicesManager';
import ViewportGridService from '../ViewportGridService';
import { DisplaySet } from '../../types';
-const IMAGE_REQUEST_TYPE = 'prefetch';
+enum RequestType {
+ /** Highest priority for loading*/
+ Interaction = 'interaction',
+ /** Second highest priority for loading*/
+ Thumbnail = 'thumbnail',
+ /** Third highest priority for loading, usually used for image loading in the background*/
+ Prefetch = 'prefetch',
+ /** Lower priority, often used for background computations in the worker */
+ Compute = 'compute',
+}
export const EVENTS = {
SERVICE_STARTED: 'event::studyPrefetcherService:started',
@@ -67,7 +76,7 @@ interface ICache {
}
interface IImageLoadPoolManager {
- addRequest (
+ addRequest(
requestFn: () => Promise,
type: string,
additionalDetails: Record,
@@ -81,7 +90,7 @@ interface IImageLoader {
}
type EventSubscription = {
- unsubscribe: () => void
+ unsubscribe: () => void;
};
interface IImageLoadEventsManager {
@@ -122,7 +131,7 @@ class StudyPrefetcherService extends PubSubService {
};
// Properties set by Cornerstone extension (initStudyPrefetcherService)
- public requestType: string = IMAGE_REQUEST_TYPE;
+ public requestType: string = RequestType.Prefetch;
public cache: ICache;
public imageLoadPoolManager: IImageLoadPoolManager;
public imageLoader: IImageLoader;
@@ -181,22 +190,22 @@ class StudyPrefetcherService extends PubSubService {
if (!this._inflightRequests.get(imageId)) {
this._sendNextRequests();
}
- }
+ };
- const fnImageLoadedEventListener = (evt) => {
+ const fnImageLoadedEventListener = evt => {
const { image } = evt.detail;
const { imageId } = image;
this._moveImageIdToLoadedSet(imageId);
fnOnImageLoadCompleted(imageId);
- }
+ };
- const fnImageLoadFailedEventListener = (evt) => {
+ const fnImageLoadFailedEventListener = evt => {
const { imageId } = evt.detail;
this._moveImageIdToFailedSet(imageId);
fnOnImageLoadCompleted(imageId);
- }
+ };
return this.imageLoadEventsManager.addEventListeners(
fnImageLoadedEventListener,
@@ -246,7 +255,7 @@ class StudyPrefetcherService extends PubSubService {
viewportGridActiveViewportIdSubscription,
viewportGridLayoutChangedSubscription,
viewportGridStateChangedSubscription,
- viewportGridViewportreadySubscription
+ viewportGridViewportreadySubscription,
];
}
@@ -263,15 +272,13 @@ class StudyPrefetcherService extends PubSubService {
this._subscriptions = [];
}
- private _syncWithActiveViewport(
- {
- activeViewportId,
- forceRestart
- }:
- {
- activeViewportId?: string,
- forceRestart?: boolean
- } = {}) {
+ private _syncWithActiveViewport({
+ activeViewportId,
+ forceRestart,
+ }: {
+ activeViewportId?: string;
+ forceRestart?: boolean;
+ } = {}) {
const { viewportGridService } = this._servicesManager.services;
const viewportGridServiceState = viewportGridService.getState();
const { viewports } = viewportGridServiceState;
@@ -311,8 +318,13 @@ class StudyPrefetcherService extends PubSubService {
private _areActiveDisplaySetsLoaded() {
const { _activeDisplaySetsInstanceUIDs: displaySetsInstanceUIDs } = this;
- return displaySetsInstanceUIDs.length && displaySetsInstanceUIDs.every(displaySetsInstanceUID =>
- this._displaySetLoadingStates.get(displaySetsInstanceUID).loadingProgress >= 1);
+ return (
+ displaySetsInstanceUIDs.length &&
+ displaySetsInstanceUIDs.every(
+ displaySetsInstanceUID =>
+ this._displaySetLoadingStates.get(displaySetsInstanceUID).loadingProgress >= 1
+ )
+ );
}
private _getClosestDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) {
@@ -363,7 +375,9 @@ class StudyPrefetcherService extends PubSubService {
const { displaySetsCount } = this.config;
const activeDisplaySetsInstanceUIDs = this._activeDisplaySetsInstanceUIDs;
const [activeDisplaySetUID] = activeDisplaySetsInstanceUIDs;
- const activeDisplaySetIndex = displaySets.findIndex(ds => ds.displaySetInstanceUID === activeDisplaySetUID);
+ const activeDisplaySetIndex = displaySets.findIndex(
+ ds => ds.displaySetInstanceUID === activeDisplaySetUID
+ );
const getDisplaySetsFunctionsMap = {
[StudyPrefetchOrder.closest]: this._getClosestDisplaySets,
[StudyPrefetchOrder.downward]: this._getDownwardDisplaySets,
@@ -381,23 +395,21 @@ class StudyPrefetcherService extends PubSubService {
// Remove any active displaySet that may still be in the activeDisplaySetsInstanceUIDs.
// That may happen when activeDisplaySetsInstanceUIDs has more than one element.
- return fnGetDisplaySets.call(this, displaySets, activeDisplaySetIndex).filter(
- ds => !uidsSet.has(ds.displaySetInstanceUID)
- ).slice(0, displaySetsCount);
+ return fnGetDisplaySets
+ .call(this, displaySets, activeDisplaySetIndex)
+ .filter(ds => !uidsSet.has(ds.displaySetInstanceUID))
+ .slice(0, displaySetsCount);
}
private _getDisplaySets() {
const { displaySetService } = this._servicesManager.services;
const displaySets = [...displaySetService.getActiveDisplaySets()];
- let displaySetsToPrefetch = this._getSortedDisplaySetsToPrefetch(displaySets);
+ const displaySetsToPrefetch = this._getSortedDisplaySetsToPrefetch(displaySets);
return { displaySets, displaySetsToPrefetch };
}
- private _updateImageIdsDisplaySetMap(
- displaySetInstanceUID: string,
- imageIds: string[]
- ): void {
+ private _updateImageIdsDisplaySetMap(displaySetInstanceUID: string, imageIds: string[]): void {
for (const imageId of imageIds) {
let displaySetsInstanceUIDsMap = this._imageIdsToDisplaySetsMap.get(imageId);
@@ -451,12 +463,12 @@ class StudyPrefetcherService extends PubSubService {
pendingImageIds,
loadedImageIds,
failedImageIds: new Set(),
- loadingProgress: 0
+ loadingProgress: 0,
};
this._updateDisplaySetLoadingProgress(displaySetLoadingState);
this._displaySetLoadingStates.set(displaySetInstanceUID, displaySetLoadingState);
- this._updateImageIdsDisplaySetMap(displaySetInstanceUID, imageIds)
+ this._updateImageIdsDisplaySetMap(displaySetInstanceUID, imageIds);
// Notify the UI that something is already loaded (eg: update StudyBrowser)
if (loadedImageIds.size) {
diff --git a/platform/core/src/services/ToolBarService/ToolbarService.ts b/platform/core/src/services/ToolBarService/ToolbarService.ts
index 287ab1b9e..f8cc6addc 100644
--- a/platform/core/src/services/ToolBarService/ToolbarService.ts
+++ b/platform/core/src/services/ToolBarService/ToolbarService.ts
@@ -116,8 +116,10 @@ export default class ToolbarService extends PubSubService {
* Removes buttons from the toolbar.
* @param buttonId - The button to be removed.
*/
- public removeButton(buttonId : string ){
- if(this.state.buttons[buttonId]) delete this.state.buttons[buttonId]
+ public removeButton(buttonId: string) {
+ if (this.state.buttons[buttonId]) {
+ delete this.state.buttons[buttonId];
+ }
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
...this.state,
});
@@ -555,6 +557,11 @@ export default class ToolbarService extends PubSubService {
};
getButtonComponentForUIType(uiType: string) {
- return uiType ? this._getButtonUITypes()[uiType]?.defaultComponent ?? null : null;
+ return uiType ? (this._getButtonUITypes()[uiType]?.defaultComponent ?? null) : null;
+ }
+
+ clearButtonSection(buttonSection: string) {
+ this.state.buttonSections[buttonSection] = [];
+ this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state });
}
}
diff --git a/platform/core/src/services/ToolBarService/types.ts b/platform/core/src/services/ToolBarService/types.ts
index 1318e90f3..3abadb49a 100644
--- a/platform/core/src/services/ToolBarService/types.ts
+++ b/platform/core/src/services/ToolBarService/types.ts
@@ -1,12 +1,22 @@
import type { RunCommand } from '../../types/Command';
-export type EvaluatePublic = string | EvaluateFunction | string[];
+export type EvaluatePublic =
+ | string
+ | EvaluateFunction
+ | EvaluateObject
+ | (string | EvaluateFunction | EvaluateObject)[];
export type EvaluateFunction = (props: Record) => {
disabled: boolean;
className: string;
};
+export type EvaluateObject = {
+ name: string;
+ // Allow any additional properties
+ [key: string]: unknown;
+};
+
export type ButtonProps = {
id: string;
icon: string;
diff --git a/platform/core/src/services/UINotificationService/index.ts b/platform/core/src/services/UINotificationService/index.ts
index b49f46fa7..f1f565d81 100644
--- a/platform/core/src/services/UINotificationService/index.ts
+++ b/platform/core/src/services/UINotificationService/index.ts
@@ -1,26 +1,13 @@
-/**
- * A UI Notification
- *
- * @typedef {Object} Notification
- * @property {string} title -
- * @property {string} message -
- * @property {number} [duration=5000] - in ms
- * @property {string} [position="bottomRight"] -"topLeft" | "topCenter | "topRight" | "bottomLeft" | "bottomCenter" | "bottomRight"
- * @property {string} [type="info"] - "info" | "error" | "warning" | "success"
- * @property {boolean} [autoClose=true]
- */
-
-const serviceShowRequestQueue = [];
-
const serviceImplementation = {
- _hide: () => console.warn('hide() NOT IMPLEMENTED'),
+ _hide: () => console.debug('hide() NOT IMPLEMENTED'),
_show: showArguments => {
- serviceShowRequestQueue.push(showArguments);
-
- console.warn('show() NOT IMPLEMENTED');
+ console.debug('show() NOT IMPLEMENTED');
+ return null;
},
};
+type ToastType = 'success' | 'error' | 'info' | 'warning' | 'loading';
+
class UINotificationService {
static REGISTRATION = {
name: 'uiNotificationService',
@@ -44,11 +31,6 @@ class UINotificationService {
}
if (showImplementation) {
serviceImplementation._show = showImplementation;
-
- while (serviceShowRequestQueue.length > 0) {
- const showArguments = serviceShowRequestQueue.pop();
- serviceImplementation._show(showArguments);
- }
}
}
@@ -59,24 +41,103 @@ class UINotificationService {
* @returns undefined
*/
public hide(id: string) {
- return serviceImplementation._hide({ id });
+ return serviceImplementation._hide(id);
}
/**
* Create and show a new UI notification; returns the
- * ID of the created notification.
+ * ID of the created notification. Can also handle promises for loading states.
*
- * @param {Notification} notification { title, message, duration, position, type, autoClose}
- * @returns {number} id
+ * @param {object} notification - The notification object
+ * @param {string} notification.title - The title of the notification
+ * @param {string | function} notification.message - The message content of the notification or a function that returns a message
+ * @param {number} [notification.duration=5000] - The duration to show the notification (in milliseconds)
+ * @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'} [notification.position='bottom-right'] - The position of the notification
+ * @param {ToastType} [notification.type='info'] - The type of the notification
+ * @param {boolean} [notification.autoClose=true] - Whether the notification should auto-close
+ * @param {Promise} [notification.promise] - A promise to track for loading, success, and error states
+ * @param {object} [notification.promiseMessages] - Custom messages for promise states
+ * @param {string} [notification.promiseMessages.loading] - Message to show while promise is pending
+ * @param {string | function} [notification.promiseMessages.success] - Message to show when promise resolves
+ * @param {string | function} [notification.promiseMessages.error] - Message to show when promise rejects
+ * @returns {string} id - The ID of the created notification
*/
show({
title,
message,
duration = 5000,
- position = 'bottomRight',
+ position = 'bottom-right',
type = 'info',
autoClose = true,
- }) {
+ promise,
+ promiseMessages,
+ }: {
+ title: string;
+ message: string | ((data?: any) => string);
+ duration?: number;
+ position?:
+ | 'top-left'
+ | 'top-right'
+ | 'bottom-left'
+ | 'bottom-right'
+ | 'top-center'
+ | 'bottom-center';
+ type?: ToastType;
+ autoClose?: boolean;
+ promise?: Promise;
+ promiseMessages?: {
+ loading?: string;
+ success?: string | ((data: any) => string);
+ error?: string | ((error: any) => string);
+ };
+ }): string {
+ if (promise && promiseMessages) {
+ const loadingId = serviceImplementation._show({
+ title,
+ message: promiseMessages.loading || 'Loading...',
+ type: 'loading',
+ autoClose: false,
+ position,
+ });
+
+ promise.then(
+ data => {
+ const successMessage =
+ typeof promiseMessages.success === 'function'
+ ? promiseMessages.success(data)
+ : promiseMessages.success || 'Success';
+
+ serviceImplementation._show({
+ title,
+ message: successMessage,
+ type: 'success',
+ duration,
+ position,
+ autoClose,
+ });
+ this.hide(loadingId);
+ },
+ error => {
+ const errorMessage =
+ typeof promiseMessages.error === 'function'
+ ? promiseMessages.error(error)
+ : promiseMessages.error || 'Error';
+
+ serviceImplementation._show({
+ title,
+ message: errorMessage,
+ type: 'error',
+ duration,
+ position,
+ autoClose,
+ });
+ this.hide(loadingId);
+ }
+ );
+
+ return loadingId;
+ }
+
return serviceImplementation._show({
title,
message,
diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.ts b/platform/core/src/services/ViewportGridService/ViewportGridService.ts
index 4f31ba051..856bb3223 100644
--- a/platform/core/src/services/ViewportGridService/ViewportGridService.ts
+++ b/platform/core/src/services/ViewportGridService/ViewportGridService.ts
@@ -1,5 +1,4 @@
import { PubSubService } from '../_shared/pubSubServiceInterface';
-import { getPresentationIds, PresentationIds } from './getPresentationIds';
class ViewportGridService extends PubSubService {
public static readonly EVENTS = {
@@ -13,18 +12,75 @@ class ViewportGridService extends PubSubService {
public static REGISTRATION = {
name: 'viewportGridService',
altName: 'ViewportGridService',
- create: ({ configuration = {} }) => {
- return new ViewportGridService();
+ create: ({ configuration = {}, servicesManager }) => {
+ return new ViewportGridService({ servicesManager });
},
};
- public static getPresentationIds = getPresentationIds;
-
serviceImplementation = {};
+ servicesManager: AppTypes.ServicesManager;
+ presentationIdProviders: Map<
+ string,
+ (id: string, { viewport, viewports, isUpdatingSameViewport, servicesManager }) => unknown
+ >;
- constructor() {
+ constructor({ servicesManager }) {
super(ViewportGridService.EVENTS);
+ this.servicesManager = servicesManager;
this.serviceImplementation = {};
+ this.presentationIdProviders = new Map();
+ }
+
+ public addPresentationIdProvider(
+ id: string,
+ provider: (id: string, { viewport, viewports, isUpdatingSameViewport }) => unknown
+ ): void {
+ this.presentationIdProviders.set(id, provider);
+ }
+
+ public getPresentationId(id: string, viewportId: string): string | null {
+ const state = this.getState();
+ const viewport = state.viewports.get(viewportId);
+ return this._getPresentationId(id, {
+ viewport,
+ viewports: state.viewports,
+ });
+ }
+
+ private _getPresentationId(id, { viewport, viewports }) {
+ const isUpdatingSameViewport = [...viewports.values()].some(
+ v =>
+ v.displaySetInstanceUIDs?.toString() === viewport.displaySetInstanceUIDs?.toString() &&
+ v.viewportId === viewport.viewportId
+ );
+
+ const provider = this.presentationIdProviders.get(id);
+ if (provider) {
+ const result = provider(id, {
+ viewport,
+ viewports,
+ isUpdatingSameViewport,
+ servicesManager: this.servicesManager,
+ });
+ return result;
+ }
+ return null;
+ }
+
+ public getPresentationIds({ viewport, viewports }) {
+ // Use the keys of the Map to get all registered provider IDs
+ const registeredPresentationProviders = Array.from(this.presentationIdProviders.keys());
+
+ return registeredPresentationProviders.reduce((acc, id) => {
+ const value = this._getPresentationId(id, {
+ viewport,
+ viewports,
+ });
+ if (value !== null) {
+ acc[id] = value;
+ }
+ return acc;
+ }, {});
}
public setServiceImplementation({
@@ -75,15 +131,24 @@ class ViewportGridService extends PubSubService {
public setActiveViewportId(id: string) {
this.serviceImplementation._setActiveViewport(id);
- this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, {
- viewportId: id,
- });
+
+ // Use queueMicrotask to delay the event broadcast
+ setTimeout(() => {
+ this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, {
+ viewportId: id,
+ });
+ }, 0);
}
- public getState() {
+ public getState(): AppTypes.ViewportGrid.State {
return this.serviceImplementation._getState();
}
+ public getViewportState(viewportId: string) {
+ const state = this.getState();
+ return state.viewports.get(viewportId);
+ }
+
public setViewportIsReady(viewportId, callback) {
this.serviceImplementation._setViewportIsReady(viewportId, callback);
}
@@ -108,19 +173,34 @@ class ViewportGridService extends PubSubService {
public async setDisplaySetsForViewports(props) {
await this.serviceImplementation._setDisplaySetsForViewports(props);
const state = this.getState();
- const viewports = [];
+ const updatedViewports = [];
+
+ const removedViewportIds = [];
for (const viewport of props) {
const updatedViewport = state.viewports.get(viewport.viewportId);
+
if (updatedViewport) {
- viewports.push(updatedViewport);
+ updatedViewports.push(updatedViewport);
+
+ const updatedDisplaySetUIDs = updatedViewport.displaySetInstanceUIDs || [];
+
+ const isCleared = updatedDisplaySetUIDs.length === 0;
+
+ if (isCleared) {
+ removedViewportIds.push(viewport.viewportId);
+ }
} else {
- console.warn("ViewportGridService::Didn't find updated viewport", viewport);
+ removedViewportIds.push(viewport.viewportId);
}
}
- this._broadcastEvent(ViewportGridService.EVENTS.GRID_STATE_CHANGED, {
- state,
- viewports,
+
+ setTimeout(() => {
+ this._broadcastEvent(ViewportGridService.EVENTS.GRID_STATE_CHANGED, {
+ state,
+ viewports: updatedViewports,
+ removedViewportIds,
+ });
});
}
@@ -143,7 +223,7 @@ class ViewportGridService extends PubSubService {
* options that is initially provided as {} (eg to store intermediate state)
* The function returns a viewport object to use at the given position.
*/
- public setLayout({
+ public async setLayout({
numCols,
numRows,
layoutOptions,
@@ -152,7 +232,11 @@ class ViewportGridService extends PubSubService {
findOrCreateViewport = undefined,
isHangingProtocolLayout = false,
}) {
- this.serviceImplementation._setLayout({
+ // Get the previous state before the layout change
+ const prevState = this.getState();
+ const prevViewportIds = new Set(prevState.viewports.keys());
+
+ await this.serviceImplementation._setLayout({
numCols,
numRows,
layoutOptions,
@@ -161,10 +245,26 @@ class ViewportGridService extends PubSubService {
findOrCreateViewport,
isHangingProtocolLayout,
});
- this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, {
- numCols,
- numRows,
- });
+
+ // Use queueMicrotask to ensure the layout changed event is published after
+ setTimeout(() => {
+ // Get the new state after the layout change
+ const state = this.getState();
+ const currentViewportIds = new Set(state.viewports.keys());
+
+ // Determine which viewport IDs have been removed
+ const removedViewportIds = [...prevViewportIds].filter(id => !currentViewportIds.has(id));
+
+ this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, {
+ numCols,
+ numRows,
+ });
+
+ this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, {
+ state,
+ removedViewportIds,
+ });
+ }, 0);
}
public reset() {
@@ -181,11 +281,23 @@ class ViewportGridService extends PubSubService {
this.serviceImplementation._onModeExit();
}
- public set(state) {
- this.serviceImplementation._set(state);
- this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, {
- state,
- });
+ public set(newState) {
+ const prevState = this.getState();
+ const prevViewportIds = new Set(prevState.viewports.keys());
+
+ this.serviceImplementation._set(newState);
+
+ const state = this.getState();
+ const currentViewportIds = new Set(state.viewports.keys());
+
+ const removedViewportIds = [...prevViewportIds].filter(id => !currentViewportIds.has(id));
+
+ setTimeout(() => {
+ this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, {
+ state,
+ removedViewportIds,
+ });
+ }, 0);
}
public getNumViewportPanes() {
@@ -207,5 +319,3 @@ class ViewportGridService extends PubSubService {
}
export default ViewportGridService;
-
-export type { PresentationIds };
diff --git a/platform/core/src/services/ViewportGridService/getPresentationIds.ts b/platform/core/src/services/ViewportGridService/getPresentationIds.ts
deleted file mode 100644
index 835442879..000000000
--- a/platform/core/src/services/ViewportGridService/getPresentationIds.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-const JOIN_STR = '&';
-
-// The default lut presentation id if none defined
-const DEFAULT = 'default';
-
-// This code finds the first unique index to add to the presentation id so that
-// two viewports containing the same display set in the same type of viewport
-// can have different presentation information. This allows comparison of
-// a single display set in two or more viewports, when the user has simply
-// dragged and dropped the view in twice. For example, it allows displaying
-// bone, brain and soft tissue views of a single display set, and to still
-// remember the specific changes to each viewport.
-const addUniqueIndex = (arr, key, viewports, isUpdatingSameViewport) => {
- arr.push(0);
-
- // If we are updating the viewport, we should not increment the index
- if (isUpdatingSameViewport) {
- return;
- }
-
- // The 128 is just a value that is larger than how many viewports we
- // display at once, used as an upper bound on how many unique presentation
- // ID's might exist for a single display set at once.
- for (let displayInstance = 0; displayInstance < 128; displayInstance++) {
- arr[arr.length - 1] = displayInstance;
- const testId = arr.join(JOIN_STR);
- if (
- !Array.from(viewports.values()).find(
- viewport => viewport.viewportOptions?.presentationIds?.[key] === testId
- )
- ) {
- break;
- }
- }
-};
-
-const getLutId = (ds): string => {
- if (!ds || !ds.options) {
- return DEFAULT;
- }
- if (ds.options.id) {
- return ds.options.id;
- }
- const arr = Object.entries(ds.options).map(([key, val]) => `${key}=${val}`);
- if (!arr.length) {
- return DEFAULT;
- }
- return arr.join(JOIN_STR);
-};
-
-export type PresentationIds = {
- positionPresentationId?: string;
- lutPresentationId?: string;
-};
-
-/**
- * Gets a set of presentation IDs for a viewport. The presentation IDs are
- * used to remember the presentation state of the viewport when it is navigated
- * to different layouts.
- *
- * The design of this is setup to allow preserving the view information in the
- * following cases:
- *
- *
- * * If a set of display sets was previously displayed in the same initial
- * position as it is currently being asked to be displayed,
- * then remember the camera position as previously displayed
- *
- * * If a set of display sets was previously displayed with the same initial
- * LUT conditions, then remember the last LUT displayed for that display set
- * and re-apply it.
- *
- * * Otherwise, apply the initial hanging protocol specified LUT and camera
- * position to new display sets.
- *
- * This means generating two presentationId keys:
- *
- * `positionPresentationId`
- *
- * Used for getting the camera/initial position state sync values.
- * This is a combination of:
- * * `viewportOptions.id`
- * * `viewportOptions.orientation`
- * * display set UID's - as displayed for this viewport, excluding seg
- * * a unique index number if the previous key is already displayed
- *
- * `lutPresentationId`
- *
- * Used for getting the voi LUT information. Generated from:
- *
- * * `displaySetOption[0].options` - including the id if present
- * * displaySetUID's
- * * a unique index number if the previously generated key is already
- * displayed.
- *
- * @param viewport requiring a presentation Id
- * @param viewports is the list of viewports being shown. Any presentation ID's
- * among them must not be re-used in order to have each viewport have it's own presentation ID.
- * @returns PresentationIds
- */
-const getPresentationIds = (viewport, viewports): PresentationIds => {
- if (!viewport) {
- return;
- }
- const { viewportOptions, displaySetInstanceUIDs, displaySetOptions } = viewport;
- if (!viewportOptions || !displaySetInstanceUIDs?.length) {
- return;
- }
-
- const { id, orientation } = viewportOptions;
- const lutId = getLutId(displaySetOptions[0]);
- const lutPresentationArr = [lutId];
-
- const positionPresentationArr = [orientation || 'acquisition'];
- if (id) {
- positionPresentationArr.push(id);
- }
-
- if (displaySetOptions.some(ds => ds.options?.blendMode || ds.options?.displayPreset)) {
- positionPresentationArr.push(`custom`);
- }
-
- for (const uid of displaySetInstanceUIDs) {
- positionPresentationArr.push(uid);
- lutPresentationArr.push(uid);
- }
-
- // only add unique index if the viewport is getting inserted and not updated
- const isUpdatingSameViewport = Array.from(viewports.values()).some(v => {
- return (
- v.displaySetInstanceUIDs?.toString() === viewport.displaySetInstanceUIDs?.toString() &&
- v.viewportId === viewport.viewportId
- );
- });
-
- // if it is updating the viewport we should not increment the index since
- // it might be a layer on the fusion or a SEG layer that is added on
- // top of the original display set
- addUniqueIndex(
- positionPresentationArr,
- 'positionPresentationId',
- viewports,
- isUpdatingSameViewport
- );
- addUniqueIndex(lutPresentationArr, 'lutPresentationId', viewports, isUpdatingSameViewport);
-
- const lutPresentationId = lutPresentationArr.join(JOIN_STR);
- const positionPresentationId = positionPresentationArr.join(JOIN_STR);
- return { lutPresentationId, positionPresentationId };
-};
-
-export default getPresentationIds;
-export { getPresentationIds };
diff --git a/platform/core/src/services/ViewportGridService/index.ts b/platform/core/src/services/ViewportGridService/index.ts
index 38f365814..47d083531 100644
--- a/platform/core/src/services/ViewportGridService/index.ts
+++ b/platform/core/src/services/ViewportGridService/index.ts
@@ -1,5 +1,3 @@
import ViewportGridService from './ViewportGridService';
-import type { PresentationIds } from './ViewportGridService';
export default ViewportGridService;
-export type { PresentationIds };
diff --git a/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts b/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts
index 5165e55a1..bd95b5326 100644
--- a/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts
+++ b/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts
@@ -135,6 +135,7 @@ class WorkflowStepsService extends PubSubService {
const toUse = Array.isArray(toolbarButtons) ? toolbarButtons : [toolbarButtons];
toUse.forEach(({ buttonSection, buttons }) => {
+ toolbarService.clearButtonSection(buttonSection);
toolbarService.createButtonSection(buttonSection, buttons);
});
}
diff --git a/platform/core/src/services/index.ts b/platform/core/src/services/index.ts
index 68666a18a..c2df4190e 100644
--- a/platform/core/src/services/index.ts
+++ b/platform/core/src/services/index.ts
@@ -14,7 +14,6 @@ import HangingProtocolService from './HangingProtocolService';
import pubSubServiceInterface, { PubSubService } from './_shared/pubSubServiceInterface';
import UserAuthenticationService from './UserAuthenticationService';
import CustomizationService from './CustomizationService';
-import StateSyncService from './StateSyncService';
import PanelService from './PanelService';
import WorkflowStepsService from './WorkflowStepsService';
import StudyPrefetcherService from './StudyPrefetcherService';
@@ -27,7 +26,6 @@ export {
ServicesManager,
ServiceProvidersManager,
CustomizationService,
- StateSyncService,
UIDialogService,
UIModalService,
UINotificationService,
diff --git a/platform/core/src/types/AppTypes.ts b/platform/core/src/types/AppTypes.ts
index 579dfa313..07db43b48 100644
--- a/platform/core/src/types/AppTypes.ts
+++ b/platform/core/src/types/AppTypes.ts
@@ -5,7 +5,6 @@ import MeasurementServiceType from '../services/MeasurementService';
import ViewportGridServiceType from '../services/ViewportGridService';
import ToolbarServiceType from '../services/ToolBarService';
import DisplaySetServiceType from '../services/DisplaySetService';
-import StateSyncServiceType from '../services/StateSyncService';
import UINotificationServiceType from '../services/UINotificationService';
import UIModalServiceType from '../services/UIModalService';
import WorkflowStepsServiceType from '../services/WorkflowStepsService';
@@ -22,6 +21,18 @@ import ExtensionManagerType from '../extensions/ExtensionManager';
import Hotkey from '../classes/Hotkey';
+import * as CommandTypes from './Command';
+import * as ColorTypes from './Color';
+import * as ConsumerTypes from './Consumer';
+import * as DataSourceTypes from './DataSource';
+import * as DataSourceConfigurationAPITypes from './DataSourceConfigurationAPI';
+import * as DisplaySetTypes from './DisplaySet';
+import * as HangingProtocolTypes from './HangingProtocol';
+import * as IPubSubTypes from './IPubSub';
+import * as PanelModuleTypes from './PanelModule';
+import * as StudyMetadataTypes from './StudyMetadata';
+import * as ViewportGridTypes from './ViewportGridType';
+
import { StepOptions, TourOptions } from 'shepherd.js';
declare global {
@@ -37,7 +48,6 @@ declare global {
export type ViewportGridService = ViewportGridServiceType;
export type UIModalService = UIModalServiceType;
export type UINotificationService = UINotificationServiceType;
- export type StateSyncService = StateSyncServiceType;
export type WorkflowStepsService = WorkflowStepsServiceType;
export type CineService = CineServiceType;
export type UserAuthenticationService = UserAuthenticationServiceType;
@@ -61,7 +71,6 @@ declare global {
viewportGridService?: ViewportGridServiceType;
uiModalService?: UIModalServiceType;
uiNotificationService?: UINotificationServiceType;
- stateSyncService?: StateSyncServiceType;
workflowStepsService?: WorkflowStepsServiceType;
cineService?: CineServiceType;
userAuthenticationService?: UserAuthenticationServiceType;
@@ -70,15 +79,15 @@ declare global {
panelService?: PanelServiceType;
studyPrefetcherService?: StudyPrefetcherServiceType;
}
+
export interface Config {
routerBasename?: string;
- customizationService?: any;
+ customizationService?: CustomizationServiceType;
extensions?: string[];
modes?: string[];
experimentalStudyBrowserSort?: boolean;
defaultDataSourceName?: string;
hotkeys?: Record | Hotkey[];
- useSharedArrayBuffer?: 'AUTO' | 'FALSE' | 'TRUE';
preferSizeOverAccuracy?: boolean;
useNorm16Texture?: boolean;
useCPURendering?: boolean;
@@ -92,8 +101,8 @@ declare global {
interaction?: number;
prefetch?: number;
thumbnail?: number;
+ compute?: number;
};
- disableEditing?: boolean;
maxNumberOfWebWorkers?: number;
acceptHeader?: string[];
investigationalUseDialog?: {
@@ -114,18 +123,21 @@ declare global {
activateViewportBeforeInteraction?: boolean;
autoPlayCine?: boolean;
showStudyList?: boolean;
- whiteLabeling?: any;
- httpErrorHandler?: any;
+ whiteLabeling?: Record;
+ httpErrorHandler?: (error: Error) => void;
addWindowLevelActionMenu?: boolean;
dangerouslyUseDynamicConfig?: {
enabled: boolean;
regex: RegExp;
};
- onConfiguration?: (dicomWebConfig: any, options: any) => any;
- dataSources?: any;
- oidc?: any;
- peerImport?: (moduleId: string) => Promise;
- studyPrefetcher: {
+ onConfiguration?: (
+ dicomWebConfig: Record,
+ options: Record
+ ) => Record;
+ dataSources?: Record;
+ oidc?: Record;
+ peerImport?: (moduleId: string) => Promise>;
+ studyPrefetcher?: {
enabled: boolean;
displaySetsCount: number;
maxNumPrefetchRequests: number;
@@ -145,13 +157,108 @@ declare global {
extensionManager?: ExtensionManager;
config?: Config;
}
+
+ // Add Command namespace to AppTypes
+ export namespace Commands {
+ export type SimpleCommand = CommandTypes.SimpleCommand;
+ export type ComplexCommand = CommandTypes.ComplexCommand;
+ export type Command = CommandTypes.Command;
+ export type RunCommand = CommandTypes.RunCommand;
+ export interface Commands extends CommandTypes.Commands {}
+ }
+
+ // Color types
+ export type RGB = ColorTypes.RGB;
+
+ // Consumer types
+ export type Consumer = ConsumerTypes.Consumer;
+
+ // DataSource types
+ export type DataSourceDefinition = DataSourceTypes.DataSourceDefinition;
+
+ // DataSourceConfigurationAPI types
+ export namespace DataSourceConfiguration {
+ export type BaseDataSourceConfigurationAPIItem =
+ DataSourceConfigurationAPITypes.BaseDataSourceConfigurationAPIItem;
+ export type BaseDataSourceConfigurationAPI =
+ DataSourceConfigurationAPITypes.BaseDataSourceConfigurationAPI;
+ }
+
+ // DisplaySet types
+ export type DisplaySet = DisplaySetTypes.DisplaySet;
+ export type DisplaySetSeriesMetadataInvalidatedEvent =
+ DisplaySetTypes.DisplaySetSeriesMetadataInvalidatedEvent;
+
+ // HangingProtocol types
+ export namespace HangingProtocol {
+ export type DisplaySetInfo = HangingProtocolTypes.DisplaySetInfo;
+ export type ViewportMatchDetails = HangingProtocolTypes.ViewportMatchDetails;
+ export type DisplaySetMatchDetails = HangingProtocolTypes.DisplaySetMatchDetails;
+ export type DisplaySetAndViewportOptions = HangingProtocolTypes.DisplaySetAndViewportOptions;
+ export type DisplayArea = HangingProtocolTypes.DisplayArea;
+ export type SetProtocolOptions = HangingProtocolTypes.SetProtocolOptions;
+ export type HangingProtocolMatchDetails = HangingProtocolTypes.HangingProtocolMatchDetails;
+ export type ConstraintValue = HangingProtocolTypes.ConstraintValue;
+ export type Constraint = HangingProtocolTypes.Constraint;
+ export type MatchingRule = HangingProtocolTypes.MatchingRule;
+ export type ViewportLayoutOptions = HangingProtocolTypes.ViewportLayoutOptions;
+ export type ViewportStructure = HangingProtocolTypes.ViewportStructure;
+ export type DisplaySetSelector = HangingProtocolTypes.DisplaySetSelector;
+ export type SyncGroup = HangingProtocolTypes.SyncGroup;
+ export type CustomOptionAttribute = HangingProtocolTypes.CustomOptionAttribute;
+ export type CustomOption = HangingProtocolTypes.CustomOption;
+ export type initialImageOptions = HangingProtocolTypes.initialImageOptions;
+ export type ViewportOptions = HangingProtocolTypes.ViewportOptions;
+ export type DisplaySetOptions = HangingProtocolTypes.DisplaySetOptions;
+ export type Viewport = HangingProtocolTypes.Viewport;
+ export type StageStatus = HangingProtocolTypes.StageStatus;
+ export type StageActivation = HangingProtocolTypes.StageActivation;
+ export type ProtocolStage = HangingProtocolTypes.ProtocolStage;
+ export type ProtocolNotifications = HangingProtocolTypes.ProtocolNotifications;
+ export type Protocol = HangingProtocolTypes.Protocol;
+ export type ProtocolGenerator = HangingProtocolTypes.ProtocolGenerator;
+ export type HPInfo = HangingProtocolTypes.HPInfo;
+ }
+
+ // IPubSub types
+ export namespace PubSub {
+ export type IPubSub = IPubSubTypes.default;
+ export type Subscription = IPubSubTypes.Subscription;
+ }
+
+ // PanelModule types
+ export namespace PanelModule {
+ export type Panel = PanelModuleTypes.Panel;
+ export type ActivatePanelTriggers = PanelModuleTypes.ActivatePanelTriggers;
+ export type PanelEvent = PanelModuleTypes.PanelEvent;
+ export type ActivatePanelEvent = PanelModuleTypes.ActivatePanelEvent;
+ }
+
+ // StudyMetadata types
+ export namespace StudyMetadata {
+ export type PatientMetadata = StudyMetadataTypes.PatientMetadata;
+ export type StudyMetadata = StudyMetadataTypes.StudyMetadata;
+ export type SeriesMetadata = StudyMetadataTypes.SeriesMetadata;
+ export type InstanceMetadata = StudyMetadataTypes.InstanceMetadata;
+ }
+
+ // ViewportGrid types
+ export namespace ViewportGrid {
+ export type Viewport = ViewportGridTypes.GridViewport;
+ export type Layout = ViewportGridTypes.Layout;
+ export type State = ViewportGridTypes.ViewportGridState;
+ export type Viewports = ViewportGridTypes.GridViewports;
+ export type GridViewportOptions = ViewportGridTypes.GridViewportOptions;
+ }
}
+ export interface PresentationIds {}
+
export type withAppTypes = T &
AppTypes.Services &
AppTypes.Managers & {
- [key: string]: any;
- };
+ [key: string]: unknown;
+ } & AppTypes.Config;
export type withTestTypes = T & AppTypes.Test;
}
diff --git a/platform/core/src/types/DisplaySet.ts b/platform/core/src/types/DisplaySet.ts
index 3bc1cb516..e11cc0bef 100644
--- a/platform/core/src/types/DisplaySet.ts
+++ b/platform/core/src/types/DisplaySet.ts
@@ -5,8 +5,13 @@ export type DisplaySet = {
instances: InstanceMetadata[];
StudyInstanceUID: string;
SeriesInstanceUID?: string;
+ SeriesNumber?: number;
+ SeriesDescription?: string;
numImages?: number;
unsupported?: boolean;
+ Modality?: string;
+ imageIds?: string[];
+ images?: unknown[];
};
export type DisplaySetSeriesMetadataInvalidatedEvent = {
diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts
index 16b846fb2..1e6e2e4ba 100644
--- a/platform/core/src/types/HangingProtocol.ts
+++ b/platform/core/src/types/HangingProtocol.ts
@@ -81,6 +81,7 @@ export type ConstraintValue =
| number
| boolean
| []
+ | string[]
| {
value: string | number | boolean | [];
};
@@ -92,6 +93,7 @@ export type Constraint = {
// A caseless contains
containsI?: string;
contains?: ConstraintValue;
+ doesNotContain?: ConstraintValue;
greaterThan?: ConstraintValue;
};
@@ -101,7 +103,7 @@ export type MatchingRule = {
// Defaults to 1
weight?: number;
attribute: string;
- constraint: Constraint;
+ constraint?: Constraint;
// Not required by default
required?: boolean;
};
@@ -140,6 +142,11 @@ export type DisplaySetSelector = {
studyMatchingRules?: MatchingRule[];
};
+export type OverlaySelector = {
+ id?: string;
+ matchingRules: MatchingRule[];
+};
+
export type SyncGroup = {
type: string;
id: string;
@@ -166,6 +173,7 @@ export type ViewportOptions = {
viewportType?: CustomOption;
id?: string;
orientation?: CustomOption;
+ background?: CustomOption<[number, number, number]>;
viewportId?: string;
displayArea?: DisplayArea;
initialImageOptions?: CustomOption;
@@ -189,9 +197,17 @@ export type DisplaySetOptions = {
options?: Record;
};
+// some options for overlays
+// such as segmentation options
+export type OverlayOptions = {
+ id?: string;
+ options?: Record;
+};
+
export type Viewport = {
viewportOptions: ViewportOptions;
displaySets: DisplaySetOptions[];
+ overlays?: OverlayOptions[];
};
/**
@@ -271,7 +287,7 @@ export type ProtocolNotifications = {
/**
* A protocol is the top level definition for a hanging protocol.
* It is a set of rules about when the protocol can be applied at all,
- * as well as a set of stages that represent indivividual views.
+ * as well as a set of stages that represent individual views.
* Additionally, the display set selectors are used to choose from the existing
* display sets. The hanging protocol definition here does NOT allow
* redefining the display sets to use, but only selects the views to show.
@@ -283,6 +299,8 @@ export type Protocol = {
description?: string;
/** Maps ids to display set selectors to choose display sets */
displaySetSelectors: Record;
+ /** overlay selectors that decide whether an overlay such as segmentation should be shown or not */
+ overlaySelectors?: Record;
/** A default viewport to use for any stage to select new viewport layouts. */
defaultViewport?: Viewport;
stages: ProtocolStage[];
diff --git a/platform/core/src/types/Services.ts b/platform/core/src/types/Services.ts
index 119a2da87..b8a514961 100644
--- a/platform/core/src/types/Services.ts
+++ b/platform/core/src/types/Services.ts
@@ -5,7 +5,6 @@ import {
ViewportGridService,
ToolbarService,
DisplaySetService,
- StateSyncService,
UINotificationService,
UIModalService,
WorkflowStepsService,
@@ -29,7 +28,6 @@ interface Services {
viewportGridService?: ViewportGridService;
uiModalService?: UIModalService;
uiNotificationService?: UINotificationService;
- stateSyncService?: StateSyncService;
workflowStepsService: WorkflowStepsService;
cineService?: CineService;
userAuthenticationService?: UserAuthenticationService;
diff --git a/platform/core/src/types/ViewportGridType.ts b/platform/core/src/types/ViewportGridType.ts
new file mode 100644
index 000000000..1079a8bcd
--- /dev/null
+++ b/platform/core/src/types/ViewportGridType.ts
@@ -0,0 +1,70 @@
+export interface GridViewportOptions {
+ id?: string;
+ viewportId?: string;
+ viewportType?: string;
+ toolGroupId?: string;
+ presentationIds?: AppTypes.PresentationIds;
+ flipHorizontal?: boolean;
+ //
+ orientation?: string;
+ allowUnmatchedView?: boolean;
+ needsRerendering?: boolean;
+ background?: [number, number, number];
+ syncGroups?: unknown[];
+ rotation?: number;
+ initialImageOptions?: unknown;
+ customViewportProps?: Record;
+ //
+ displayArea?: unknown;
+ viewReference?: unknown;
+}
+
+export interface GridViewport {
+ viewportId: string;
+ displaySetInstanceUIDs: string[];
+ viewportOptions: GridViewportOptions;
+ displaySetSelectors: unknown[];
+ displaySetOptions: unknown[];
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ viewportLabel: string | null;
+ isReady: boolean;
+}
+
+export interface Layout {
+ numRows: number;
+ numCols: number;
+ layoutType: string;
+}
+
+export type GridViewports = Map;
+
+export interface ViewportGridState {
+ activeViewportId: string | null;
+ layout: Layout;
+ isHangingProtocolLayout: boolean;
+ viewports: GridViewports;
+}
+
+export type SetDisplaySetsForViewportsProps = Array<{
+ viewportId: string;
+ displaySetInstanceUIDs: string[];
+ viewportOptions?: AppTypes.ViewportGrid.GridViewportOptions;
+ displaySetOptions?: Array<{
+ id?: string;
+ voi?: {
+ windowWidth: number;
+ windowCenter: number;
+ };
+ voiInverted?: boolean;
+ blendMode?: string;
+ slabThickness?: number;
+ colormap?: {
+ name: string;
+ opacity?: number;
+ };
+ displayPreset?: string;
+ }>;
+}>;
diff --git a/platform/core/src/types/index.ts b/platform/core/src/types/index.ts
index 3e1608790..e01e64f71 100644
--- a/platform/core/src/types/index.ts
+++ b/platform/core/src/types/index.ts
@@ -7,7 +7,6 @@ import type {
BaseDataSourceConfigurationAPI,
BaseDataSourceConfigurationAPIItem,
} from './DataSourceConfigurationAPI';
-import type DisplaySet from '../services/DisplaySetService/DisplaySet';
export type * from '../services/ViewportGridService';
export type * from '../services/CustomizationService/types';
@@ -32,5 +31,4 @@ export {
DataSourceDefinition,
BaseDataSourceConfigurationAPI,
BaseDataSourceConfigurationAPIItem,
- DisplaySet,
};
diff --git a/platform/core/src/utils/combineFrameInstance.ts b/platform/core/src/utils/combineFrameInstance.ts
index 087f97b42..6385bfbd0 100644
--- a/platform/core/src/utils/combineFrameInstance.ts
+++ b/platform/core/src/utils/combineFrameInstance.ts
@@ -50,7 +50,10 @@ const combineFrameInstance = (frame, instance) => {
// Todo: we should cache this combined instance somewhere, maybe add it
// back to the dicomMetaStore so we don't have to do this again.
- return newInstance;
+ return {
+ ...newInstance,
+ ImagePositionPatient: newInstance.ImagePositionPatient ?? [0, 0, frameNumber],
+ };
} else {
return instance;
}
diff --git a/platform/core/src/utils/createStudyBrowserTabs.ts b/platform/core/src/utils/createStudyBrowserTabs.ts
index 0d55149e3..259558ac0 100644
--- a/platform/core/src/utils/createStudyBrowserTabs.ts
+++ b/platform/core/src/utils/createStudyBrowserTabs.ts
@@ -12,7 +12,12 @@
* @returns tabs - The prop object expected by the StudyBrowser component
*/
-export function createStudyBrowserTabs(primaryStudyInstanceUIDs, studyDisplayList, displaySets, recentTimeframeMS = 31536000000) {
+export function createStudyBrowserTabs(
+ primaryStudyInstanceUIDs,
+ studyDisplayList,
+ displaySets,
+ recentTimeframeMS = 31536000000
+) {
const primaryStudies = [];
const allStudies = [];
diff --git a/platform/core/src/utils/getImageId.js b/platform/core/src/utils/getImageId.js
index 51e057b91..d40f127d7 100644
--- a/platform/core/src/utils/getImageId.js
+++ b/platform/core/src/utils/getImageId.js
@@ -24,6 +24,10 @@ export default function getImageId(instance, frame, thumbnail = false) {
return;
}
+ if (instance.imageId && frame === undefined) {
+ return instance.imageId;
+ }
+
if (typeof instance.getImageId === 'function') {
return instance.getImageId();
}
diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js
index 22ae507ae..85d18f71d 100644
--- a/platform/core/src/utils/index.test.js
+++ b/platform/core/src/utils/index.test.js
@@ -41,7 +41,6 @@ describe('Top level exports', () => {
'resolveObjectPath',
'hierarchicalListUtils',
'progressTrackingUtils',
- 'subscribeToNextViewportGridChange',
'uuidv4',
'addAccessors',
].sort();
diff --git a/platform/core/src/utils/index.ts b/platform/core/src/utils/index.ts
index e5725f748..74c9cdd38 100644
--- a/platform/core/src/utils/index.ts
+++ b/platform/core/src/utils/index.ts
@@ -36,7 +36,6 @@ import {
sortingCriteria,
seriesSortCriteria,
} from './sortStudy';
-import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange';
import { splitComma, getSplitParam } from './splitComma';
import { createStudyBrowserTabs } from './createStudyBrowserTabs';
import { sopClassDictionary } from './sopClassDictionary';
@@ -81,7 +80,6 @@ const utils = {
debounce,
roundNumber,
downloadCSVReport,
- subscribeToNextViewportGridChange,
splitComma,
getSplitParam,
generateAcceptHeader,
diff --git a/platform/core/src/utils/roundNumber.js b/platform/core/src/utils/roundNumber.js
index 64f614a86..3747e5159 100644
--- a/platform/core/src/utils/roundNumber.js
+++ b/platform/core/src/utils/roundNumber.js
@@ -13,10 +13,10 @@
function roundNumber(value, precision = 2) {
if (Array.isArray(value)) {
- return value.map((v) => roundNumber(v, precision)).join(", ");
+ return value.map(v => roundNumber(v, precision)).join(', ');
}
- if (value === undefined || value === null || value === "") {
- return "NaN";
+ if (value === undefined || value === null || value === '') {
+ return 'NaN';
}
value = Number(value);
const absValue = Math.abs(value);
@@ -27,16 +27,16 @@ function roundNumber(value, precision = 2) {
absValue >= 100
? precision - 2
: absValue >= 10
- ? precision - 1
- : absValue >= 1
- ? precision
- : absValue >= 0.1
- ? precision + 1
- : absValue >= 0.01
- ? precision + 2
- : absValue >= 0.001
- ? precision + 3
- : precision + 4;
+ ? precision - 1
+ : absValue >= 1
+ ? precision
+ : absValue >= 0.1
+ ? precision + 1
+ : absValue >= 0.01
+ ? precision + 2
+ : absValue >= 0.001
+ ? precision + 3
+ : precision + 4;
return value.toFixed(fixedPrecision);
}
diff --git a/platform/core/src/utils/subscribeToNextViewportGridChange.ts b/platform/core/src/utils/subscribeToNextViewportGridChange.ts
deleted file mode 100644
index 6ffbc4a1c..000000000
--- a/platform/core/src/utils/subscribeToNextViewportGridChange.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { ViewportGridService } from '../services';
-
-/**
- * Subscribes to the very next LAYOUT_CHANGED event that
- * is not currently on the event queue. The subscriptions are made on a 'zero'
- * timeout so as to avoid responding to any of those events currently on the event queue.
- * The subscription persists only for a single invocation of either event.
- * Once either event is fired, the subscriptions are unsubscribed.
- * @param viewportGridService the viewport grid service to subscribe to
- * @param gridChangeCallback the callback
- */
-function subscribeToNextViewportGridChange(
- viewportGridService: ViewportGridService,
- gridChangeCallback: (arg: unknown) => void
-): void {
- const subscriber = () => {
- const callback = (callbackProps: unknown) => {
- subscriptions.forEach(subscription => subscription.unsubscribe());
- gridChangeCallback(callbackProps);
- };
-
- const subscriptions = [
- viewportGridService.subscribe(viewportGridService.EVENTS.LAYOUT_CHANGED, callback),
- ];
- };
-
- window.setTimeout(subscriber, 0);
-}
-
-export { subscribeToNextViewportGridChange };
diff --git a/platform/core/src/utils/toNumber.js b/platform/core/src/utils/toNumber.js
index 986a8d518..b0d3bd0dd 100644
--- a/platform/core/src/utils/toNumber.js
+++ b/platform/core/src/utils/toNumber.js
@@ -6,7 +6,7 @@
*/
export default function toNumber(val) {
if (Array.isArray(val)) {
- return val.map(v => (v !== undefined ? Number(v) : v));
+ return [...val].map(v => (v !== undefined ? Number(v) : v));
} else {
return val !== undefined ? Number(val) : val;
}
diff --git a/platform/docs/docs/configuration/dataSources/dicom-web.md b/platform/docs/docs/configuration/dataSources/dicom-web.md
index 8e2d49250..d6fd1ec1c 100644
--- a/platform/docs/docs/configuration/dataSources/dicom-web.md
+++ b/platform/docs/docs/configuration/dataSources/dicom-web.md
@@ -185,17 +185,6 @@ For DICOM video and PDF it has been found that Orthanc delivers multipart, while
To learn more about how you can configure the OHIF Viewer, check out our
[Configuration Guide](../index.md).
-### DICOM Upload
-See the [`dicomUploadEnabled`](#dicomuploadenabled) data source configuration option.
-
-Don't forget to add the customization to the config as well
-
-```js
-customizationService: {
- dicomUploadComponent:
- '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
-},
-```
### DICOM PDF
See the [`singlepart`](#singlepart) data source configuration option.
diff --git a/platform/docs/docs/development/webWorkers.md b/platform/docs/docs/development/webWorkers.md
new file mode 100644
index 000000000..7fa3bed1a
--- /dev/null
+++ b/platform/docs/docs/development/webWorkers.md
@@ -0,0 +1,191 @@
+---
+sidebar_position: 13
+sidebar_label: Web Workers
+---
+# Web Worker Implementation Guide
+
+## Overview
+Web Workers enable running computationally intensive tasks in background threads without blocking the UI. This guide explains how to implement them step by step.
+
+## Basic Setup
+
+### 1. Create Your Worker File
+First, create a worker file with your background tasks:
+
+```javascript
+// myWorker.js
+import { expose } from 'comlink';
+
+const obj = {
+ // Simple task
+ basicCalculation({ data }) {
+ // Your computation here
+ return result;
+ },
+
+ // Task with progress updates
+ longRunningTask({ data }, progressCallback) {
+ const total = data.length;
+
+ for (let i = 0; i < total; i++) {
+ // Your processing logic
+
+ if (progressCallback) {
+ const progress = Math.round((i / total) * 100);
+ progressCallback(progress);
+ }
+ }
+
+ return result;
+ }
+};
+
+expose(obj);
+```
+
+### 2. Register the Worker
+
+In the main thread, can be your service, commands module, etc.
+
+```javascript
+import { getWebWorkerManager } from '@cornerstonejs/core';
+
+const workerManager = getWebWorkerManager();
+
+// Define worker creation function
+const workerFn = () => {
+ return new Worker(
+ new URL('./myWorker.js', import.meta.url),
+ { name: 'my-worker' }
+ );
+};
+
+// Registration options
+const options = {
+ maxWorkerInstances: 1, // Number of concurrent workers
+ autoTerminateOnIdle: {
+ enabled: true,
+ idleTimeThreshold: 3000, // Terminate after 3s idle
+ },
+};
+
+// Register the worker
+workerManager.registerWorker('my-worker', workerFn, options);
+```
+
+:::info
+It is recommended to register the worker in top of the commands module. So that it
+gets registered before any commands that need to use the worker.
+:::
+
+### 3. Execute Tasks
+
+```javascript
+// Basic execution
+try {
+ const result = await workerManager.executeTask(
+ 'my-worker',
+ 'basicCalculation',
+ { data: myData }
+ );
+} catch (error) {
+ console.error('Task failed:', error);
+}
+
+// Execution with progress callback
+try {
+ const result = await workerManager.executeTask(
+ 'my-worker',
+ 'longRunningTask',
+ { data: myData },
+ {
+ callbacks: [
+ (progress) => {
+ console.log(`Progress: ${progress}%`);
+ }
+ ]
+ }
+ );
+} catch (error) {
+ console.error('Task failed:', error);
+}
+```
+
+## Progress Events (Optional)
+
+If you want to show progress in your UI as a loading spinner, you can implement a progress event system:
+
+### 1. Publish Progress Events
+
+```javascript
+// Helper to trigger progress events
+const publishProgress = (eventTarget, progress, taskId) => {
+ triggerEvent(eventTarget, 'WEB_WORKER_PROGRESS', {
+ progress, // number 0-100
+ type: 'YOUR_TASK_TYPE', // can be any string identifier
+ id: taskId, // unique task identifier
+ });
+};
+
+// Usage in your application
+async function runTaskWithProgress(data) {
+ // Start progress
+ publishProgress(eventTarget, 0, data.id);
+
+ try {
+ const result = await workerManager.executeTask(
+ 'my-worker',
+ 'longRunningTask',
+ { data },
+ {
+ callbacks: [
+ (progress) => {
+ publishProgress(eventTarget, progress, data.id);
+ }
+ ]
+ }
+ );
+
+ // Complete progress
+ publishProgress(eventTarget, 100, data.id);
+
+ return result;
+ } catch (error) {
+ console.error('Task failed:', error);
+ throw error;
+ }
+}
+```
+
+Note: Publishing the `WEB_WORKER_PROGRESS` event on Cornerstone's `eventTarget` will automatically trigger the built-in loading spinner. This gives users visual feedback while your worker runs in the background.
+
+
+## Multiple Methods in One Worker
+
+You can define multiple related methods in a single worker file:
+
+```javascript
+// complexWorker.js
+import { expose } from 'comlink';
+
+const obj = {
+ processingMethod1({ data }, progressCallback) {
+ // Implementation
+ },
+
+ processingMethod2({ data }, progressCallback) {
+ // Implementation
+ },
+
+ processingMethod3({ data }, progressCallback) {
+ // Implementation
+ },
+
+ // Shared helper methods
+ _internalHelper() {
+ // Helper logic
+ }
+};
+
+expose(obj);
+```
diff --git a/platform/docs/docs/faq/technical.md b/platform/docs/docs/faq/technical.md
index 33a50df51..4b446e5ab 100644
--- a/platform/docs/docs/faq/technical.md
+++ b/platform/docs/docs/faq/technical.md
@@ -347,3 +347,19 @@ customizationService.addModeCustomizations([
:::note
Notice the arrays and objects, the values are arrays
:::
+
+
+## How do I change the cine auto mount behavior
+
+You can change the cine auto mount behavior by adding the `autoCineModalities` mode customization, the value is an array of modalities that should be mounted with cine.
+
+By default the viewer will mount with cine enabled for `OT` and `US` modalities.
+
+```js
+customizationService.addModeCustomizations([
+ {
+ id: 'autoCineModalities',
+ modalities: ['OT', 'US'],
+ },
+]);
+```
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/0-general.md b/platform/docs/docs/migration-guide/3p8-to-3p9/0-general.md
new file mode 100644
index 000000000..117e96ce4
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/0-general.md
@@ -0,0 +1,257 @@
+---
+id: 0-general
+title: General
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+
+# React 18 Migration Guide
+As we upgrade to React 18, we're making some exciting changes to improve performance and developer experience. This guide will help you navigate the key updates and ensure your custom extensions and modes are compatible with the new version.
+What's Changing?
+
+
+
+
+```md
+- React 17
+- Using `defaultProps`
+- `babel-inline-svg` for SVG imports
+```
+
+
+
+
+```md
+- React 18
+- Default parameters for props
+- `svgr` for SVG imports
+```
+
+
+
+
+
+## Update React version:
+In your custom extensions and modes, change the version of react and react-dom to ^18.3.1.
+
+## Replace defaultProps with default parameters:
+
+
+
+
+```jsx
+const MyComponent = ({ prop1, prop2 }) => {
+ return {prop1} {prop2}
+}
+
+MyComponent.defaultProps = {
+ prop1: 'default value',
+ prop2: 'default value'
+}
+```
+
+
+
+
+```jsx
+const MyComponent = ({ prop1 = 'default value', prop2 = 'default value' }) => {
+ return {prop1} {prop2}
+}
+```
+
+
+
+## Update SVG imports:
+
+You might need to update your SVG imports to use the `ReactComponent` syntax, if you want to use the old Icon component. However, we have made a significant change to how we handle Icons, read the UI Migration Guide for more information.
+
+
+
+
+```javascript
+import arrowDown from './../../assets/icons/arrow-down.svg';
+```
+
+
+
+
+```javascript
+import { ReactComponent as arrowDown } from './../../assets/icons/arrow-down.svg';
+```
+
+
+
+
+---
+
+## Polyfill.io
+
+We have removed the Polyfill.io script from the Viewer. If you require polyfills, you can add them to your project manually. This change primarily affects Internet Explorer, which Microsoft has already [ended support for](https://learn.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge#is-internet-explorer-11-the-last-version-of-internet-explorer-).
+
+
+
+---
+
+## Crosshairs
+
+They now have new colors in their associated viewports in the MPR view. However, you can turn this feature off.
+
+To disable it, remove the configuration from the `initToolGroups` in your mode.
+
+```
+{
+ configuration: {
+ viewportIndicators: true,
+ viewportIndicatorsConfig: {
+ circleRadius: 5,
+ xOffset: 0.95,
+ yOffset: 0.05,
+ },
+ }
+}
+```
+
+---
+
+
+## useAuthorizationCodeFlow
+
+`useAuthorizationCodeFlow` config is deprecated
+
+now internally we detect the authorizationCodeFlow if the response_type is equal to `code`
+
+you can remove the config from the appConfig
+
+---
+
+## StackScrollMouseWheel -> StackScroll Tool + Mouse bindings
+
+If you previously used:
+
+```js
+{ toolName: toolNames.StackScrollMouseWheel, bindings: [] }
+```
+
+in your `initToolGroups`, you should now use:
+
+```js
+{
+ toolName: toolNames.StackScroll,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
+}
+```
+
+This change allows for more flexible mouse bindings and keyboard combinations.
+
+## VolumeRotateMouseWheel -> VolumeRotate Tool + Mouse bindings
+
+Before:
+
+```js
+{
+ toolName: toolNames.VolumeRotateMouseWheel,
+ configuration: {
+ rotateIncrementDegrees: 5,
+ },
+},
+```
+
+Now:
+
+```js
+{
+ toolName: toolNames.VolumeRotate,
+ bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
+ configuration: {
+ rotateIncrementDegrees: 5,
+ },
+},
+```
+
+---
+
+## DicomUpload
+
+The DICOM upload functionality in OHIF has been refactored to use the standard customization service pattern. Now you don't need to put
+
+`customizationService: { dicomUploadComponent: '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent', },`
+
+in your config, we will automatically add that if you have `dicomUploadEnabled`
+
+---
+
+## Viewport and Modality Support for Toolbar Buttons
+
+Previously, toolbar buttons had limited support for disabling themselves based on the active viewport type (e.g., `volume3d`, `video`, `sr`) or the modality of the displayed data (e.g., `US`, `SM`). This led to inconsistencies and sometimes enabled tools in contexts where they weren't applicable.
+
+The new implementation introduces more robust and flexible evaluators to control the enabled/disabled state of toolbar buttons based on viewport types and modalities.
+
+**Key Changes**
+
+1. **New Evaluators:** New evaluators have been added to the `getToolbarModule`:
+ - `evaluate.viewport.supported`: Disables a button if the active viewport's type is listed in the `unsupportedViewportTypes` property.
+ - `evaluate.modality.supported`: Disables a button based on the modalities of the displayed data. It checks for both `unsupportedModalities` (exclusion) and `supportedModalities` (inclusion).
+2. **Removal of Legacy Evaluators:**
+ - Evaluators such as `evaluate.not.sm`, `evaluate.action.not.video`, `evaluate.not3D`, and `evaluate.isUS` have been removed. Migrate your toolbar button definitions to use the new evaluators mentioned above.
+
+
+**Replace Legacy Evaluators:**
+ - Replace `evaluate.not.sm` with:
+
+ ```json
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['sm'],
+ }
+ ```
+
+ - Replace `evaluate.action.not.video` with:
+
+ ```json
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['video'],
+ }
+ ```
+
+ - Replace `evaluate.not3D` with:
+
+ ```json
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['volume3d'],
+ }
+ ```
+
+ - Replace `evaluate.isUS` with:
+
+ ```json
+ {
+ name: 'evaluate.modality.supported',
+ supportedModalities: ['US'],
+ }
+ ```
+
+
+Example Migration
+
+Before:
+
+```json
+evaluate: ['evaluate.cine', 'evaluate.not3D'],
+```
+
+After
+
+```json
+evaluate: [
+ 'evaluate.cine',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['volume3d'],
+ },
+],
+```
+
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md
new file mode 100644
index 000000000..c8a92efa0
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md
@@ -0,0 +1,48 @@
+---
+id: seg-new-arch
+title: New Architecture
+---
+
+
+## New Architecture
+
+* **Viewport-Centric Architecture**
+ * Previous: Segmentations were tied to toolGroups
+ * Now: Segmentations are tied directly to viewports
+ * Impact: More granular control but requires significant code changes
+
+* **Representation Management**
+ * Previous: Required managing segmentation representation UIDs
+ * Now: Uses simpler segmentationId + type combination
+ * Impact: Simplified but requires API updates
+
+
+
+If you are not familiar with the difference between a segmentation and a segmentation representation, below
+
+
+Read More
+
+In Cornerstone3DTools, we have decoupled the concept of a Segmentation from a Segmentation Representation. This means that from one Segmentation we can create multiple Segmentation Representations. For instance, a Segmentation Representation of a 3D Labelmap, can be created from a Segmentation data, and a Segmentation Representation of a Contour can be created from the same Segmentation data. This way we have decouple the presentational aspect of a Segmentation from the underlying data.
+
+
+Similar relationship structure has been adapted in popular medical imaging softwares such as 3D Slicer with the addition of polymorph segmentation.
+
+- https://github.com/PerkLab/PolySeg
+- https://www.slicer.org/
+
+
+
+
+
+
+
+
+### Architecture Overview
+
+The new architecture in Cornerstone3D 2.0 makes a clear distinction between:
+
+* A segmentation (the data structure containing segments)
+* A segmentation representation (how that segmentation is visualized in a specific viewport)
+
+Let's now review what has changed
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md
new file mode 100644
index 000000000..06601b7cd
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md
@@ -0,0 +1,433 @@
+---
+id: seg-api
+title: SegmentationService API
+---
+
+
+
+Below we will review the changes to the API of the `SegmentationService`
+
+# SegmentationService API
+
+## Events
+
+SEGMENTATION_UPDATED -> SEGMENTATION_MODIFIED
+
+
+Just a rename to match the cornerstone terminology
+
+## VolumeId vs SegmentationId
+
+Previously, we used the SegmentationId as the VolumeId for volume-based segmentations, which led to confusion and issues.
+
+Now, we have two separate IDs: one for the segmentation and one for the volume.
+
+`segmentationService.getLabelmapVolume(segmentationId)` will return the volume associated with the segmentation.
+
+If your code uses `cache.getVolume(segmentationId)`, update it to use the new `getLabelmapVolume` method.
+
+
+## getSegmentation(segmentationId)
+
+remains the same it will return the segmentation object = cornerstone segmentation object with the following properties:
+
+```js
+/**
+ * Global Segmentation Data which is used for the segmentation
+ */
+type Segmentation = {
+ /** segmentation id */
+ segmentationId: string;
+ /** segmentation label */
+ label: string;
+ segments: {
+ [segmentIndex: number]: Segment;
+ };
+ /**
+ * Representations of the segmentation. Each segmentation "can" be viewed
+ * in various representations. For instance, if a DICOM SEG is loaded, the main
+ * representation is the labelmap. However, for DICOM RT the main representation
+ * is contours, and other representations can be derived from the contour (currently
+ * only labelmap representation is supported)
+ */
+ representationData: RepresentationsData;
+ /**
+ * Segmentation level stats, Note each segment can have its own stats
+ * This is used for caching stats for the segmentation level
+ */
+ cachedStats: { [key: string]: unknown };
+};
+
+export type Segment = {
+ /** segment index */
+ segmentIndex: number;
+ /** segment label */
+ label: string;
+ /** is segment locked for editing */
+ locked: boolean;
+ /** cached stats for the segment, e.g., pt suv mean, max etc. */
+ cachedStats: { [key: string]: unknown };
+ /** is segment active for editing, at the same time only one segment can be active for editing */
+ active: boolean;
+};
+```
+
+
+
+Compared to Cornerstone3D 1.x
+
+Previously this function was returning this
+
+```js
+export type Segmentation = {
+ segmentationId: string;
+ type: Enums.SegmentationRepresentations;
+ label: string;
+ activeSegmentIndex: number;
+ segmentsLocked: Set;
+ cachedStats: { [key: string]: number };
+ segmentLabels: { [key: string]: string };
+ representationData: SegmentationRepresentationData;
+};
+
+```
+
+As you can see `segmentLabels`, `segmentsLocked`, `activeSegmentIndex`, are all gathered under the new `segments` object. We now have support for per segment cachedStats as well.
+
+
+
+---
+
+## getSegmentations
+
+It provides all segmentations in the state. Previously, it accepted a `filterNonhydrated` flag, but since we've moved away from hydration and every loaded segmentation is now hydrated by default, it returns all segmentations.
+
+
+
+
+---
+
+## getActiveSegmentation
+
+
+After migrating to viewport-specific segmentations, different viewports can have distinct active segmentations for editing. The panel will always display the active segmentation when the active viewport changes.
+
+Before (3.8)
+
+```js
+// Returns full segmentation object
+public getActiveSegmentation(): Segmentation {
+ const segmentations = this.getSegmentations();
+ return segmentations.find(segmentation => segmentation.isActive);
+}
+```
+
+After (3.9)
+
+```js
+public getActiveSegmentation(viewportId: string): Segmentation | null {
+ return cstSegmentation.activeSegmentation.getActiveSegmentation(viewportId);
+}
+```
+
+
+Key Changes
+
+1. **Viewport Specificity**
+ - Before: Global active segmentation across all tool groups
+ - After: Active segmentation per viewport
+2. **Required Parameters**
+ - Before: No parameters needed
+ - After: Requires viewportId parameter
+
+
+
+
+Migration Examples
+
+**Before:**
+
+```js
+// Get active segmentation
+const activeSegmentation = segmentationService.getActiveSegmentation();
+if (activeSegmentation) {
+ console.log('Active segmentation:', activeSegmentation.segmentationId);
+ console.log('Active segment:', activeSegmentation.activeSegmentIndex);
+}
+```
+
+**After:**
+
+```js
+// Get active segmentation for specific viewport
+const activeSegmentation = segmentationService.getActiveSegmentation('viewport1');
+
+```
+
+
+
+---
+
+## getToolGroupIdsWithSegmentation
+
+is now -> `getViewportIdsWithSegmentation` as you guessed
+
+
+
+## setActiveSegmentationForToolGroup
+
+-> setActiveSegmentation
+
+
+
+**Before (OHIF 3.8)**
+
+```js
+setActiveSegmentationForToolGroup(
+ segmentationId: string,
+ toolGroupId?: string,
+ suppressEvents?: boolean
+): void
+```
+
+**After (OHIF 3.9)**
+
+```js
+setActiveSegmentation(
+ viewportId: string,
+ segmentationId: string
+): void
+```
+
+
+Migration Examples
+
+1. **Basic Usage Update**
+
+ ```js
+ // Before - OHIF 3.8
+ segmentationService.setActiveSegmentationForToolGroup(
+ segmentationId,
+ toolGroupId
+ );
+ // After - OHIF 3.9
+ segmentationService.setActiveSegmentation(
+ viewportId,
+ segmentationId
+ );
+ ```
+
+
+
+
+
+---
+
+
+## addSegment
+
+The `addSegment` method in OHIF 3.9 has been updated to handle segmentation properties in a viewport-centric way, removing tool group dependencies and simplifying the configuration structure.
+
+
+**Before (OHIF 3.8)**
+
+```js
+addSegment(
+ segmentationId: string,
+ config: {
+ segmentIndex?: number;
+ toolGroupId?: string;
+ properties?: {
+ label?: string;
+ color?: ohifTypes.RGB;
+ opacity?: number;
+ visibility?: boolean;
+ isLocked?: boolean;
+ active?: boolean;
+ };
+ }
+): void
+```
+
+**After (OHIF 3.9)**
+
+```js
+addSegment(
+ segmentationId: string,
+ config: {
+ segmentIndex?: number;
+ label?: string;
+ isLocked?: boolean;
+ active?: boolean;
+ color?: csTypes.Color;
+ visibility?: boolean;
+ }
+): void
+```
+
+
+Key Changes
+
+1. **Configuration Structure**
+ - Removed double nested `properties` object
+ - Configuration options now at top level
+ - Removed `toolGroupId` parameter
+ - Removed `opacity` parameter (now part of color)
+2. **Segment Index Generation**
+ - Changed from length-based to max-value-based indexing
+ - More reliable for non-sequential segment indices
+3. **Color Handling**
+ - Color now includes alpha channel (opacity)
+ - Applied to all relevant viewports automatically
+
+
+
+
+
+
+Migration Examples
+
+1. **Basic Segment Creation**
+
+ ```js
+ // Before - OHIF 3.8
+ segmentationService.addSegment(segmentationId, {
+ properties: {
+ label: 'Segment 1'
+ }
+ });
+ // After - OHIF 3.9
+ segmentationService.addSegment(segmentationId, {
+ label: 'Segment 1'
+ });
+ ```
+
+2. **Creating Segment with Color**
+
+ ```js
+ // Before - OHIF 3.8
+ segmentationService.addSegment(segmentationId, {
+ properties: {
+ color: [255, 0, 0],
+ opacity: 255
+ }
+ });
+ // After - OHIF 3.9
+ segmentationService.addSegment(segmentationId, {
+ color: [255, 0, 0, 255] // RGB + Alpha
+ });
+ ```
+
+3. **Setting Visibility and Lock Status**
+
+ ```js
+ // Before - OHIF 3.8
+ segmentationService.addSegment(segmentationId, {
+ toolGroupId: 'myToolGroup',
+ properties: {
+ visibility: true,
+ isLocked: true
+ }
+ });
+ // After - OHIF 3.9
+ segmentationService.addSegment(segmentationId, {
+ visibility: true,
+ isLocked: true
+ });
+ ```
+
+4. **Complete Configuration Example**
+
+ ```js
+ // Before - OHIF 3.8
+ segmentationService.addSegment(segmentationId, {
+ segmentIndex: 1,
+ toolGroupId: 'myToolGroup',
+ properties: {
+ label: 'Tumor',
+ color: [255, 0, 0],
+ opacity: 200,
+ visibility: true,
+ isLocked: false,
+ active: true
+ }
+ });
+ // After - OHIF 3.9
+ segmentationService.addSegment(segmentationId, {
+ segmentIndex: 1,
+ label: 'Tumor',
+ color: [255, 0, 0, 200], // RGB + Alpha
+ visibility: true,
+ isLocked: false,
+ active: true
+ });
+ ```
+
+
+
+
+
+
+
+Important Changes
+
+1. **Tool Group Removal**
+ ```js
+ // Before - OHIF 3.8
+ segmentationService.addSegment(segmentationId, {
+ toolGroupId: 'myToolGroup'
+ // ... other properties
+ });
+ // After - OHIF 3.9
+ // No tool group needed - automatically applies to all relevant viewports
+ segmentationService.addSegment(segmentationId, {
+ // ... properties
+ });
+ ```
+
+2. **Segment Index Generation**
+ ```js
+ // Before - OHIF 3.8
+ // Used array length
+ segmentIndex = segmentation.segments.length === 0 ? 1 : segmentation.segments.length;
+ // After - OHIF 3.9
+ // Uses highest existing index + 1
+ segmentIndex = Math.max(...Object.keys(csSegmentation.segments).map(Number)) + 1;
+ ```
+
+3. **Color and Opacity**
+ ```js
+ // Before - OHIF 3.8
+ segmentationService.addSegment(segmentationId, {
+ properties: {
+ color: [255, 0, 0],
+ opacity: 200
+ }
+ });
+
+ // After - OHIF 3.9
+ segmentationService.addSegment(segmentationId, {
+ color: [255, 0, 0, 200] // Combined color and opacity
+ });
+ ```
+
+
+
+
+---
+
+---
+
+## getActiveSegment
+
+now requires viewportId, since we have moved away from global active segmentation to viewport specific one
+
+**API Changes**
+
+```js
+// Before
+getActiveSegment(): Segment
+
+// After
+getActiveSegment(viewportId: string): Segment | null
+```
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md
new file mode 100644
index 000000000..522259276
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md
@@ -0,0 +1,189 @@
+---
+id: seg-representation
+title: Segmentation Representations
+---
+
+
+
+
+## Segmentation Representation Management API
+
+```js
+addSegmentationRepresentationToToolGroup
+removeSegmentationRepresentationFromToolGroup
+getSegmentationRepresentationsForToolGroup
+```
+
+In Cornerstone3D 2.0, segmentation representation management has shifted from a tool group-centric approach to a viewport-centric approach. This architectural change provides better control over segmentation rendering and simplifies the mental model for managing segmentations.
+
+
+### Adding Segmentation Representations
+
+**Before (3.8)**:
+
+```js
+// Tool group-based approach
+await segmentation.addSegmentationRepresentationToToolGroup(
+ toolGroupId,
+ segmentationId,
+ hydrateSegmentation,
+ csToolsEnums.SegmentationRepresentations.Labelmap
+);
+```
+
+**After (3.9)**:
+
+```js
+// Viewport-centric approach
+await segmentation.addSegmentationRepresentation(
+ viewportId,
+ {
+ segmentationId: segmentationId,
+ type: csToolsEnums.SegmentationRepresentations.Labelmap,
+ }
+);
+```
+
+### Removing Segmentation Representations
+
+**Before** :
+
+```js
+// Remove specific representations from a tool group
+segmentation.removeSegmentationRepresentationFromToolGroup(
+ toolGroupId,
+ [segmentationRepresentationUID]
+);
+// Remove all representations from a tool group
+segmentation.removeSegmentationRepresentationFromToolGroup(toolGroupId);
+```
+
+**After**
+
+```js
+// Remove specific representation from a viewport
+segmentation.removeSegmentationRepresentation(
+ viewportId,
+ {
+ segmentationId: segmentationId,
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+ }
+);
+// Remove all representations from a viewport
+segmentation.removeSegmentationRepresentations(viewportId);
+```
+
+### Getting Segmentation Representations
+
+**Before**:
+
+```js
+// Get representations for a tool group
+const representations = segmentation.getSegmentationRepresentationsForToolGroup(toolGroupId);
+```
+
+**After** :
+
+```js
+// Get all representations for a viewport
+const representations = segmentation.getSegmentationRepresentations(viewportId);
+
+// Get specific type of representations
+const labelmapReps = segmentation.getSegmentationRepresentations(viewportId, {
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+});
+
+// Get representations for specific segmentation
+const segmentationReps = segmentation.getSegmentationRepresentations(viewportId, {
+ segmentationId: segmentationId
+});
+
+// Get specific representation
+const representation = segmentation.getSegmentationRepresentation(viewportId, {
+ segmentationId: segmentationId,
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+});
+```
+
+### Understanding the Specifier Pattern
+
+The Cornerstone3D 2.0 (OHIF 3.9) API introduces a "specifier" pattern that provides more flexible and precise control over segmentation representations. A specifier is an object that can include:
+
+```js
+type Specifier = {
+ segmentationId?: string; // The ID of the segmentation
+ type?: SegmentationRepresentations; // The type of representation (Labelmap, Contour, etc.)
+}
+```
+
+The specifier pattern allows for:
+
+1. **Precise Targeting**: You can target specific segmentations and representation types
+ - Allows direct access to individual segmentations
+ - Enables filtering by representation type
+
+2. **Flexible Querying**: You can get all representations of a certain type or for a specific segmentation
+ - Query by segmentation ID
+ - Query by representation type
+ - Combine queries for specific needs
+
+3. **Granular Control**: You can manage representations at different levels of specificity
+ - Viewport level control
+ - Segmentation level control
+ - Individual representation type control
+
+### Examples of Specifier Usage
+
+```js
+// Get all labelmap representations in a viewport
+const labelmaps = segmentation.getSegmentationRepresentations(viewportId, {
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+});
+
+// Get all representations of a specific segmentation (including contour, labelmap, surface)
+const segReps = segmentation.getSegmentationRepresentations(viewportId, {
+ segmentationId: 'seg123'
+});
+
+// Get a specific representation
+const specificRep = segmentation.getSegmentationRepresentation(viewportId, {
+ segmentationId: 'seg123',
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+});
+```
+
+
+Benefits of the New Approach
+
+1. **Direct Viewport Control**:
+ - Each viewport can have its own unique representation configuration
+ - No need to create separate tool groups for different viewport representations
+2. **Simpler Mental Model**:
+ - Representations are directly tied to where they're displayed
+ - No intermediate tool group layer to manage
+3. **More Flexible Rendering**:
+ - Each viewport can render the same segmentation differently
+ - Better support for multiple views of the same data
+4. **Improved Type Safety**:
+ - Specifier pattern provides better TypeScript support
+ - More explicit API with clearer intentions
+
+
+
+
+Migration Tips
+
+1. **Replace Tool Group References**:
+ - Search your codebase for `toolGroupId` references in segmentation code
+ - Replace with appropriate `viewportId` references
+2. **Update Event Handlers**:
+ - Update any code listening for segmentation events
+ - Events now include viewportId instead of toolGroupId
+3. **Review Representation Management**:
+ - Identify where you manage segmentation representations
+ - Convert to using the new viewport-centric methods
+4. **Consider Viewport Context**:
+ - Think about segmentation representation in terms of viewport display
+ - Use specifiers to target specific representations when needed
+
+
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md
new file mode 100644
index 000000000..d96f9a014
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md
@@ -0,0 +1,211 @@
+---
+id: seg-creation
+title: SegmentationService Creation
+---
+
+
+
+## createSegmentationForDisplaySet
+
+is now -> `createLabelmapForDisplaySet`
+
+Since we are moving towards segmentations be contours as well, this is renamed to clearly state the purpose.
+Since OHIF 3.9 introduced Stack Segmentation support, we no longer generate a volume-based labelmap or convert the viewport to a volume viewport by default. Our default creation is now stack-based.
+
+API Changes
+- `createSegmentationForDisplaySet` has been renamed to `createLabelmapForDisplaySet`.
+- Pass a `displaySet` object instead of a `displaySetInstanceUID`. This change enhances type safety and flexibility, accommodating future updates to the `displaySetService`.
+
+**Before (OHIF 3.8)**
+
+```js
+async createSegmentationForDisplaySet(
+ displaySetInstanceUID: string,
+ options?: {
+ segmentationId: string;
+ FrameOfReferenceUID: string;
+ label: string;
+ }
+): Promise
+```
+
+**After (OHIF 3.9)**
+
+```js
+// Method 1: Display Set Based
+async createLabelmapForDisplaySet(
+ displaySet: DisplaySet,
+ options?: {
+ segmentationId?: string;
+ label: string;
+ segments?: {
+ [segmentIndex: number]: Partial
+ };
+ }
+): Promise
+```
+
+
+
+Migration Examples
+
+
+```js
+// Before - OHIF 3.8
+const segmentationId = await segmentationService.createSegmentationForDisplaySet(
+ displaySetInstanceUID,
+ {
+ label: 'My Segmentation'
+ }
+);
+```
+
+```js
+// After - OHIF 3.9
+// Option 1: If you have a display set UID
+const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
+
+const segmentationId = await segmentationService.createLabelmapForDisplaySet(
+ displaySet,
+ {
+ label: 'My Segmentation'
+ }
+);
+```
+
+
+
+---
+
+## createSegmentationForRTDisplaySet
+
+
+**Before (OHIF 3.8)**
+
+```js
+async createSegmentationForRTDisplaySet(
+ rtDisplaySet,
+ segmentationId?: string,
+ suppressEvents = false
+): Promise
+```
+
+**After (OHIF 3.9)**
+
+```js
+async createSegmentationForRTDisplaySet(
+ rtDisplaySet,
+ options: {
+ segmentationId?: string;
+ type: SegmentationRepresentations; // not required, defaults to Contour
+ }
+): Promise
+```
+
+
+
+Migration Examples
+
+if you were not passing segmentationId, you don't need to change anything
+
+
+```js
+// Before - OHIF 3.8
+const segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
+ rtDisplaySet
+);
+
+// After - OHIF 3.9
+const segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
+ rtDisplaySet,
+);
+```
+
+if you were passing segmentationId, you need to update the API to pass an options object and set the segmentationId in there.
+
+```js
+// Before - OHIF 3.8
+const segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
+ rtDisplaySet,
+ 'custom-id',
+);
+// After - OHIF 3.9
+const segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
+ rtDisplaySet,
+ {
+ segmentationId: 'custom-id',
+ type: csToolsEnums.SegmentationRepresentations.Contour
+ }
+);
+```
+
+
+
+---
+
+
+## createSegmentationForSEGDisplaySet Changes
+
+**Before (OHIF 3.8)**
+
+```js
+async createSegmentationForSEGDisplaySet(
+ segDisplaySet,
+ segmentationId?: string,
+ suppressEvents = false
+): Promise
+```
+
+**After (OHIF 3.9)**
+
+```js
+async createSegmentationForSEGDisplaySet(
+ segDisplaySet,
+ options: {
+ segmentationId?: string;
+ type: SegmentationRepresentations; // not required, defaults to Labelmap
+ }
+): Promise
+```
+
+
+Migration Examples
+
+1. **Basic Usage Update**
+
+ ```
+ // Before - OHIF 3.8
+ const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
+ segDisplaySet
+ );
+ // After - OHIF 3.9
+ const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
+ segDisplaySet,
+ {
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+ }
+ );
+ ```
+
+2. **Custom Configuration**
+
+ ```
+ // Before - OHIF 3.8
+ const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
+ segDisplaySet,
+ 'custom-id',
+ false
+ );
+ // After - OHIF 3.9
+ const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
+ segDisplaySet,
+ {
+ segmentationId: 'custom-id',
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+ }
+ );
+ ```
+
+
+
+---
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md
new file mode 100644
index 000000000..ebe16f7c1
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md
@@ -0,0 +1,193 @@
+---
+id: seg-service-mod
+title: SegmentationService Modifications
+---
+
+
+---
+
+
+## Segmentation Representation Management API
+
+```js
+addSegmentationRepresentationToToolGroup
+removeSegmentationRepresentationFromToolGroup
+getSegmentationRepresentationsForToolGroup
+```
+
+In Cornerstone3D 2.0, segmentation representation management has shifted from a tool group-centric approach to a viewport-centric approach. This architectural change provides better control over segmentation rendering and simplifies the mental model for managing segmentations.
+
+
+### Adding Segmentation Representations
+
+**Before (3.8)**:
+
+```js
+// Tool group-based approach
+await segmentation.addSegmentationRepresentationToToolGroup(
+ toolGroupId,
+ segmentationId,
+ hydrateSegmentation,
+ csToolsEnums.SegmentationRepresentations.Labelmap
+);
+```
+
+**After (3.9)**:
+
+```js
+// Viewport-centric approach
+await segmentation.addSegmentationRepresentation(
+ viewportId,
+ {
+ segmentationId: segmentationId,
+ type: csToolsEnums.SegmentationRepresentations.Labelmap,
+ }
+);
+```
+
+### Removing Segmentation Representations
+
+**Before** :
+
+```js
+// Remove specific representations from a tool group
+segmentation.removeSegmentationRepresentationFromToolGroup(
+ toolGroupId,
+ [segmentationRepresentationUID]
+);
+// Remove all representations from a tool group
+segmentation.removeSegmentationRepresentationFromToolGroup(toolGroupId);
+```
+
+**After**
+
+```js
+// Remove specific representation from a viewport
+segmentation.removeSegmentationRepresentation(
+ viewportId,
+ {
+ segmentationId: segmentationId,
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+ }
+);
+// Remove all representations from a viewport
+segmentation.removeSegmentationRepresentations(viewportId);
+```
+
+### Getting Segmentation Representations
+
+**Before**:
+
+```js
+// Get representations for a tool group
+const representations = segmentation.getSegmentationRepresentationsForToolGroup(toolGroupId);
+```
+
+**After** :
+
+```js
+// Get all representations for a viewport
+const representations = segmentation.getSegmentationRepresentations(viewportId);
+
+// Get specific type of representations
+const labelmapReps = segmentation.getSegmentationRepresentations(viewportId, {
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+});
+
+// Get representations for specific segmentation
+const segmentationReps = segmentation.getSegmentationRepresentations(viewportId, {
+ segmentationId: segmentationId
+});
+
+// Get specific representation
+const representation = segmentation.getSegmentationRepresentation(viewportId, {
+ segmentationId: segmentationId,
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+});
+```
+
+### Understanding the Specifier Pattern
+
+The Cornerstone3D 2.0 (OHIF 3.9) API introduces a "specifier" pattern that provides more flexible and precise control over segmentation representations. A specifier is an object that can include:
+
+```js
+type Specifier = {
+ segmentationId?: string; // The ID of the segmentation
+ type?: SegmentationRepresentations; // The type of representation (Labelmap, Contour, etc.)
+}
+```
+
+The specifier pattern allows for:
+
+1. **Precise Targeting**: You can target specific segmentations and representation types
+ - Allows direct access to individual segmentations
+ - Enables filtering by representation type
+
+2. **Flexible Querying**: You can get all representations of a certain type or for a specific segmentation
+ - Query by segmentation ID
+ - Query by representation type
+ - Combine queries for specific needs
+
+3. **Granular Control**: You can manage representations at different levels of specificity
+ - Viewport level control
+ - Segmentation level control
+ - Individual representation type control
+
+### Examples of Specifier Usage
+
+```js
+// Get all labelmap representations in a viewport
+const labelmaps = segmentation.getSegmentationRepresentations(viewportId, {
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+});
+
+// Get all representations of a specific segmentation (including contour, labelmap, surface)
+const segReps = segmentation.getSegmentationRepresentations(viewportId, {
+ segmentationId: 'seg123'
+});
+
+// Get a specific representation
+const specificRep = segmentation.getSegmentationRepresentation(viewportId, {
+ segmentationId: 'seg123',
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+});
+```
+
+
+Benefits of the New Approach
+
+1. **Direct Viewport Control**:
+ - Each viewport can have its own unique representation configuration
+ - No need to create separate tool groups for different viewport representations
+2. **Simpler Mental Model**:
+ - Representations are directly tied to where they're displayed
+ - No intermediate tool group layer to manage
+3. **More Flexible Rendering**:
+ - Each viewport can render the same segmentation differently
+ - Better support for multiple views of the same data
+4. **Improved Type Safety**:
+ - Specifier pattern provides better TypeScript support
+ - More explicit API with clearer intentions
+
+
+
+
+Migration Tips
+
+1. **Replace Tool Group References**:
+ - Search your codebase for `toolGroupId` references in segmentation code
+ - Replace with appropriate `viewportId` references
+2. **Update Event Handlers**:
+ - Update any code listening for segmentation events
+ - Events now include viewportId instead of toolGroupId
+3. **Review Representation Management**:
+ - Identify where you manage segmentation representations
+ - Convert to using the new viewport-centric methods
+4. **Consider Viewport Context**:
+ - Think about segmentation representation in terms of viewport display
+ - Use specifiers to target specific representations when needed
+
+
+
+
+---
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md
new file mode 100644
index 000000000..266206bf1
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md
@@ -0,0 +1,362 @@
+---
+id: seg-style
+title: SegmentationService Style
+---
+
+
+## Style
+
+
+### setSegmentVisibility
+
+since visibility is viewport concern and representation is what is being toggled ->
+
+**Before (OHIF 3.8)**
+
+```js
+setSegmentVisibility(
+ segmentationId: string,
+ segmentIndex: number,
+ isVisible: boolean,
+ toolGroupId?: string
+): void
+```
+
+**After (OHIF 3.9)**
+
+```js
+setSegmentVisibility(
+ viewportId: string,
+ segmentationId: string,
+ segmentIndex: number,
+ isVisible: boolean,
+ type?: SegmentationRepresentations
+): void
+```
+
+
+Migration Example
+
+```js
+// Before
+segmentationService.setSegmentVisibility(
+ 'segmentation1',
+ 1,
+ true,
+ 'toolGroup1'
+);
+// After
+segmentationService.setSegmentVisibility(
+ 'viewport1',
+ 'segmentation1',
+ 1,
+ true
+);
+```
+
+**Getting Viewport IDs**
+
+When you need to update visibility across multiple viewports:
+
+```js
+// Before
+const toolGroupIds = ['toolGroup1', 'toolGroup2'];
+toolGroupIds.forEach(toolGroupId => {
+ segmentationService.setSegmentVisibility(
+ 'segmentation1',
+ 1,
+ true,
+ toolGroupId
+ );
+});
+// After
+const viewportIds = segmentationService.getViewportIdsWithSegmentation('segmentation1');
+viewportIds.forEach(viewportId => {
+ segmentationService.setSegmentVisibility(
+ viewportId,
+ 'segmentation1',
+ 1,
+ true
+ );
+});
+```
+
+
+
+
+
+### get/set Configuration -> get/setStyle
+
+The segmentation configuration system has been completely redesigned:
+
+- Moved from global/toolGroup configuration to viewport-specific styles
+- Split rendering of inactive segmentations into separate API
+- More granular control over styles at different levels (global, segmentation, viewport, segment)
+
+
+**Before (OHIF 3.8)**
+
+```js
+interface SegmentationConfig {
+ brushSize: number;
+ brushThresholdGate: number;
+ fillAlpha: number;
+ fillAlphaInactive: number;
+ outlineWidthActive: number;
+ renderFill: boolean;
+ renderInactiveSegmentations: boolean;
+ renderOutline: boolean;
+ outlineOpacity: number;
+ outlineOpacityInactive: number;
+}
+```
+
+**After (OHIF 3.9)**
+
+```js
+// Style Types
+interface StyleSpecifier {
+ viewportId?: string;
+ segmentationId?: string;
+ type: SegmentationRepresentations;
+ segmentIndex?: number;
+}
+interface LabelmapStyle {
+ renderOutline: boolean;
+ outlineWidth: number;
+ renderFill: boolean;
+ fillAlpha: number;
+ outlineAlpha: number;
+ // ....
+}
+// Functions
+getStyle(specifier: StyleSpecifier): LabelmapStyle | ContourStyle | SurfaceStyle;
+setStyle(specifier: StyleSpecifier, style: LabelmapStyle | ContourStyle | SurfaceStyle): void;
+setRenderInactiveSegmentations(viewportId: string, renderInactive: boolean): void;
+getRenderInactiveSegmentations(viewportId: string): boolean;
+```
+
+
+**Before:**
+
+```js
+// Get global configuration
+const config = segmentationService.getConfiguration();
+console.log(config.fillAlpha, config.renderOutline);
+// Get tool group specific config
+const toolGroupConfig = segmentationService.getConfiguration('toolGroup1');
+```
+
+**After:**
+
+```js
+// Get global style for labelmap
+const labelmapStyle = segmentationService.getStyle({
+ type: SegmentationRepresentations.Labelmap
+});
+// Get viewport-specific style
+const viewportStyle = segmentationService.getStyle({
+ viewportId: 'viewport1',
+ type: SegmentationRepresentations.Labelmap
+});
+// Get segmentation-specific style
+const segmentationStyle = segmentationService.getStyle({
+ segmentationId: 'seg1',
+ type: SegmentationRepresentations.Labelmap
+});
+// Get segment-specific style
+const segmentStyle = segmentationService.getStyle({
+ segmentationId: 'seg1',
+ type: SegmentationRepresentations.Labelmap,
+ segmentIndex: 1
+});
+```
+
+
+
+**Setting Configuration/Style**
+
+**Before:**
+
+```js
+segmentationService.setConfiguration({
+ fillAlpha: 0.5,
+ outlineWidthActive: 2,
+ renderOutline: true,
+ renderFill: true,
+ renderInactiveSegmentations: true
+});
+```
+
+**After:**
+
+```js
+// Set global style
+segmentationService.setStyle(
+ { type: SegmentationRepresentations.Labelmap },
+ {
+ fillAlpha: 0.5,
+ outlineWidth: 2,
+ renderOutline: true,
+ renderFill: true
+ }
+);
+// Set viewport-specific style
+segmentationService.setStyle(
+ {
+ viewportId: 'viewport1',
+ type: SegmentationRepresentations.Labelmap
+ },
+ {
+ fillAlpha: 0.5,
+ outlineWidth: 2
+ }
+);
+// Handle inactive segmentations separately
+segmentationService.setRenderInactiveSegmentations('viewport1', true);
+```
+
+
+
+Migration Examples
+
+**Combining Multiple Style Settings**
+
+**Before:**
+
+```js
+segmentationService.setConfiguration({
+ fillAlpha: 0.5,
+ fillAlphaInactive: 0.2,
+ outlineWidthActive: 2,
+ outlineOpacity: 1,
+ outlineOpacityInactive: 0.5,
+ renderOutline: true,
+ renderFill: true,
+ renderInactiveSegmentations: true
+});
+```
+
+**After:**
+
+```js
+// Set base style
+segmentationService.setStyle(
+ { type: SegmentationRepresentations.Labelmap },
+ {
+ fillAlpha: 0.5,
+ outlineWidth: 2,
+ outlineAlpha: 1,
+ renderOutline: true,
+ renderFill: true
+ }
+);
+```
+
+
+
+
+
+**Set inactive rendering per viewport**
+
+```js
+segmentationService.setRenderInactiveSegmentations('viewport1', true);
+// Set style for inactive segments if needed
+segmentationService.setStyle(
+ {
+ viewportId: 'viewport1',
+ type: SegmentationRepresentations.Labelmap,
+ segmentationId: 'seg1'
+ },
+ {
+ fillAlpha: 0.2,
+ outlineAlpha: 0.5
+ }
+);
+```
+
+---
+
+
+
+## setSegmentRGBAColor , setSegmentOpacity, setSegmentRGBA
+Previously, the SegmentationService had multiple redundant methods for setting colors and opacity (`setSegmentRGBA`, `setSegmentColor`, `setSegmentOpacity`). This led to confusion and potential state inconsistencies between the service and Cornerstone.js Tools.
+
+The old methods (`setSegmentRGBA`, `setSegmentRGBA`, and `setSegmentOpacity`) are now removed.
+
+
+1. Replace `setSegmentRGBAColor`, `setSegmentRGBA`, and `setSegmentOpacity` calls: Replace all instances of the old methods with the new `setSegmentColor` method. Note that you now need to provide the `viewportId` as the first argument since segment color is managed per viewport and representation in cornerstone3D.
+
+
+**Before**
+
+```js
+// Old API:
+segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, rgbaColor, toolGroupId);
+segmentationService.setSegmentRGBA(segmentationId, segmentIndex, rgbaColor, toolGroupId);
+segmentationService.setSegmentOpacity(segmentationId, segmentIndex, opacity, toolGroupId);
+```
+
+**After**
+
+```js
+// New API:
+segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color); // color is an array of [red, green, blue, alpha]
+```
+
+The new `color` argument is an array representing the RGBA color, where the alpha component determines the opacity. Since the Cornerstone Tools library handles segment color per viewport and representation, we require the `viewportId` as an argument now.
+
+
+
+2. **Retrieve Segment Color using** `getSegmentColor`: The new `getSegmentColor` provides a way to fetch the color of a segment within a specific viewport.
+
+```js
+const color = segmentationService.getSegmentColor(viewportId, segmentationId, segmentIndex); //returns [r, g, b, a]
+```
+
+
+---
+
+
+
+## ToggleSegmentationVisibility
+
+In Cornerstone3D v2.x, `toggleSegmentationVisibility` has been replaced with `toggleSegmentationRepresentationVisibility`. This change reflects the fact that
+a representation is what is being toggled, not the segmentation.
+
+
+**Before (OHIF 3.8)**
+
+```js
+// Toggle visibility for a segmentation globally
+segmentationService.toggleSegmentationVisibility(segmentationId);
+```
+
+**After (OHIF 3.9)**
+
+```js
+// Toggle visibility for a segmentation representation in a specific viewport
+segmentationService.toggleSegmentationRepresentationVisibility(viewportId, {
+ segmentationId: segmentationId,
+ type: csToolsEnums.SegmentationRepresentations.Labelmap
+});
+```
+
+**Migration Steps**
+
+1. Update all calls to `toggleSegmentationVisibility` to use `toggleSegmentationRepresentationVisibility`
+2. Add the required `viewportId` parameter
+3. Add a `type` parameter specifying the representation type (e.g., Labelmap, Contour)
+4. If you were toggling visibility across all viewports, you'll need to loop through the viewports:
+
+
+
+Additional Notes
+
+
+- Each viewport can now have independent visibility settings for the same segmentation
+- The visibility state is specific to the representation type (Labelmap, Contour, etc.)
+- To check current visibility, use `getSegmentationRepresentationVisibility(viewportId, { segmentationId, type })`
+
+
+---
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md
new file mode 100644
index 000000000..1236787e1
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md
@@ -0,0 +1,374 @@
+---
+id: seg-other
+title: Other Changes
+---
+
+
+
+
+## addOrUpdateSegmentation
+
+This was a public method but there is a good chance you were not using it
+
+
+**Before (OHIF 3.8)**
+
+```js
+// Before
+addOrUpdateSegmentation(
+ segmentation: Segmentation,
+ suppressEvents = false,
+ notYetUpdatedAtSource = false
+): string
+```
+
+**After**
+
+```js
+addOrUpdateSegmentation(
+ segmentationInput: SegmentationPublicInput | Partial
+)
+```
+
+### Data Structure Changes
+
+The segmentation object that was used previously was a custom segmentation object that was used internally by the SegmentationService. But
+we have moved to the cornerstone public segmentation input type.
+
+**Before:**
+
+```js
+const segmentation = {
+ id: 'segmentation1',
+ type: SegmentationRepresentations.Labelmap,
+ isActive: true,
+ activeSegmentIndex: 1,
+ segments: [
+ {
+ segmentIndex: 1,
+ color: [255, 0, 0],
+ isVisible: true,
+ isLocked: false,
+ opacity: 255
+ }
+ ],
+ label: 'Segmentation 1',
+ cachedStats: {},
+ representationData: {
+ LABELMAP: {
+ volumeId: 'volume1',
+ referencedVolumeId: 'reference1'
+ }
+ }
+};
+```
+
+
+**After:**
+
+This matches the cornerstone public segmentation input type.
+
+```js
+const segmentationInput = {
+ segmentationId: 'segmentation1',
+ representation: {
+ type: SegmentationRepresentations.Labelmap,
+ data: {
+ imageIds: segmentationImageIds,
+ referencedVolumeId: 'reference1'
+ }
+ },
+ config: {
+ label: 'Segmentation 1',
+ segments: {
+ 1: {
+ label: 'Segment 1',
+ active: true,
+ locked: false
+ }
+ }
+ }
+};
+```
+
+
+Migration Examples
+
+
+```js
+// Before
+const newSegmentation = {
+ id: 'seg1',
+ type: SegmentationRepresentations.Labelmap,
+ segments: [...],
+ representationData: {
+ LABELMAP: {
+ volumeId: 'volume1',
+ referencedVolumeId: 'reference1'
+ }
+ }
+};
+segmentationService.addOrUpdateSegmentation(newSegmentation);
+
+// After
+segmentationService.addOrUpdateSegmentation({
+ segmentationId: 'seg1',
+ representation: {
+ type: SegmentationRepresentations.Labelmap,
+ data: {
+ imageIds: segmentationImageIds,
+ referencedVolumeId: 'reference1'
+ }
+ },
+ config: {
+ segments: {
+ 1: {
+ label: 'Segment 1',
+ active: true
+ }
+ }
+ }
+});
+```
+
+
+**Updating Existing Segmentation**
+
+```js
+// Before
+const updatedSegmentation = {
+ ...existingSegmentation,
+ segments: [...modifiedSegments],
+ activeSegmentIndex: 2
+};
+segmentationService.addOrUpdateSegmentation(updatedSegmentation);
+
+// After
+segmentationService.addOrUpdateSegmentation({
+ segmentationId: 'seg1',
+ config: {
+ segments: {
+ 2: { active: true },
+ }
+ }
+});
+```
+
+
+
+
+## loadSegmentationsForViewport
+
+same as addOrUpdateSegmentation, you should pass in the new segmentation data structure.
+
+For instance
+
+**Before**
+
+```js
+const segmentations = [
+ {
+ id: '1',
+ label: 'Segmentations',
+ segments: labels.map((label, index) => ({
+ segmentIndex: index + 1,
+ label
+ })),
+ isActive: true,
+ activeSegmentIndex: 1,
+ },
+];
+
+commandsManager.runCommand('loadSegmentationsForViewport', {
+ segmentations,
+});
+```
+
+
+
+**After**
+
+```js
+
+const labels = ['Segment 1', 'Segment 2', 'Segment 3'];
+
+const segmentations = [
+ {
+ segmentationId: '1',
+ representation: {
+ type: Enums.SegmentationRepresentations.Labelmap,
+ },
+ config: {
+ label: 'Segmentations',
+ segments: labels.reduce((acc, label, index) => {
+ acc[index + 1] = {
+ label,
+ active: index === 0, // First segment is active
+ locked: false,
+ };
+ return acc;
+ }, {}),
+ },
+ },
+];
+
+commandsManager.runCommand('loadSegmentationsForViewport', {
+ segmentations,
+});
+```
+
+
+---
+
+
+
+
+## highlightSegment
+
+**Before (OHIF 3.8)**
+
+```js
+// Before (v1.x)
+highlightSegment(
+ segmentationId: string,
+ segmentIndex: number,
+ toolGroupId?: string,
+ alpha = 0.9,
+ animationLength = 750,
+ hideOthers = true,
+ highlightFunctionType = 'ease-in-out'
+)
+
+```
+
+**After (OHIF 3.9)**
+
+```js
+highlightSegment(
+ segmentationId: string,
+ segmentIndex: number,
+ viewportId?: string, // notice viewportId instead of toolGroupId
+ alpha = 0.9,
+ animationLength = 750,
+ hideOthers = true,
+ highlightFunctionType = 'ease-in-out'
+)
+```
+
+
+Key Changes
+
+1. Removed `toolGroupId` in favor of `viewportId`
+2. If no viewportId is provided, highlights in all relevant viewports
+
+
+
+
+Migration Examples
+
+**Basic Usage**
+
+```js
+// Before
+segmentationService.highlightSegment(
+ 'seg1',
+ 1,
+ 'toolGroup1',
+ 0.9,
+ 750,
+ true,
+);
+// After
+segmentationService.highlightSegment(
+ 'seg1',
+ 1,
+ 'viewport1',
+ 0.9,
+ 750,
+ true
+);
+```
+
+**Highlighting in Multiple Views**
+
+```js
+// Before
+const toolGroupIds = ['toolGroup1', 'toolGroup2'];
+toolGroupIds.forEach(toolGroupId => {
+ segmentationService.highlightSegment(
+ 'seg1',
+ 1,
+ toolGroupId
+ );
+});
+// After - Method 1: Let service handle multiple viewports
+segmentationService.highlightSegment('seg1', 1);
+// After - Method 2: Explicitly specify viewports
+const viewportIds = ['viewport1', 'viewport2'];
+viewportIds.forEach(viewportId => {
+ segmentationService.highlightSegment(
+ 'seg1',
+ 1,
+ viewportId
+ );
+});
+```
+
+
+---
+
+## jumpToSegmentCenter
+
+**Before (OHIF 3.8)**
+
+```js
+jumpToSegmentCenter(
+ segmentationId: string,
+ segmentIndex: number,
+ toolGroupId?: string,
+ highlightAlpha = 0.9,
+ highlightSegment = true,
+ animationLength = 750,
+ highlightHideOthers = false,
+ highlightFunctionType = 'ease-in-out'
+)
+```
+
+**After (OHIF 3.9)**
+
+```js
+jumpToSegmentCenter(
+ segmentationId: string,
+ segmentIndex: number,
+ viewportId? string, // notice viewportId instead of toolGroupId
+ highlightAlpha = 0.9,
+ highlightSegment = true,
+ animationLength = 750,
+ highlightHideOthers = false,
+ highlightFunctionType = 'ease-in-out'
+)
+```
+
+
+Key Changes
+
+1. Removed `toolGroupId` parameter infavor of viewportId
+2. Automatically handles relevant viewports if `viewportId` not provided
+
+
+```
+// Before
+segmentationService.jumpToSegmentCenter(
+ 'seg1',
+ 1,
+ 'toolGroup1'
+);
+// After
+segmentationService.jumpToSegmentCenter(
+ 'seg1',
+ 1,
+ 'viewportId1'
+);
+```
+
+
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/index.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/index.md
new file mode 100644
index 000000000..0eea04b41
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/index.md
@@ -0,0 +1,11 @@
+---
+id: segmentation-index
+title: Segmentation
+sidebar_position: 1
+---
+
+:::info
+This migration involves significant architectural changes to the segmentation system. While we typically aim for incremental updates, the shift from a tool group-centric to a viewport-centric architecture was necessary to support OHIF 3.9's advanced visualization capabilities, and more flexible segmentation handling.
+
+Don't worry - we'll guide you through each change step by step!
+:::
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/2-Renamings.md b/platform/docs/docs/migration-guide/3p8-to-3p9/2-Renamings.md
new file mode 100644
index 000000000..6d8b1ce0f
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/2-Renamings.md
@@ -0,0 +1,28 @@
+---
+id: 2-renamings
+title: Renamings
+sidebar_position: 2
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+
+## Panel Measurements
+
+The panel in the default extension is renamed from `measure` to `measurements` to be more consistent with the rest of the extensions.
+
+**Action Needed**
+
+Update any references to the `measure` panel to `measurements` in your code.
+
+Find and replace
+
+
+
+ @ohif/extension-default.panelModule.measure
+
+
+ @ohif/extension-default.panelModule.measurements
+
+
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/3-DataSources.md b/platform/docs/docs/migration-guide/3p8-to-3p9/3-DataSources.md
new file mode 100644
index 000000000..2d14adf53
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/3-DataSources.md
@@ -0,0 +1,40 @@
+---
+id: 3-data-sources
+title: Data Sources
+sidebar_position: 3
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+## BulkDataURI Configuration
+
+We've updated the configuration for BulkDataURI to provide more flexibility and control. This guide will help you migrate from the old configuration to the new one.
+
+### What's Changing?
+
+
+
+
+```javascript
+useBulkDataURI: false,
+```
+
+
+
+
+```javascript
+bulkDataURI: {
+ enabled: true,
+ // Additional configuration **options**
+},
+```
+
+
+
+
+
+**Additional Notes:**
+- The new configuration allows for more granular control over BulkDataURI behavior.
+- You can now add custom URL prefixing logic using the startsWith and prefixWith properties.
+- This change enables easier correction of retrieval URLs, especially in scenarios where URLs pass through multiple systems.
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/4-Measurements.md b/platform/docs/docs/migration-guide/3p8-to-3p9/4-Measurements.md
new file mode 100644
index 000000000..d4b89faf3
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/4-Measurements.md
@@ -0,0 +1,43 @@
+---
+title: Measurements
+---
+
+
+## Display Text
+
+
+Previously, `displayText` for measurements was often a simple string or an array of strings. This approach made it difficult to distinguish between primary measurement values (e.g., length, area) and secondary information (e.g., series number, instance number). It also limited styling options for differentiating these types of information.
+
+The new approach introduces a structured object for `displayText`, consisting of `primary` and `secondary` arrays. This separation allows for better organization and presentation of measurement information. The `primary` array is intended for the main measurement values (on the left), while the `secondary` array is for contextual information like series and instance numbers (on the right)
+
+### Migration Steps
+
+If you have custom measurement tools or modify existing ones, you need to update the `getDisplayText` functions within the `measurementServiceMappings` to return a structured object in the new format.
+
+**Update Measurement Mappings:** If your extension defines custom measurement tools or modifies existing ones, update the `getDisplayText` functions within the `measurementServiceMappings` to return a structured object in the new format.
+
+```js
+// Old Implementation (example for Length tool)
+function getDisplayText(mappedAnnotations, displaySet, customizationService) {
+ // ...
+ return `${roundedLength} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`;
+}
+// New Implementation
+function getDisplayText(mappedAnnotations, displaySet) {
+ // ...
+ return {
+ primary: [`${roundedLength} ${unit}`], // Primary measurement value
+ secondary: [`S: ${SeriesNumber}${instanceText}${frameText}`], // Secondary information
+ };
+}
+```
+
+---
+
+### selected property
+
+`selected` property on measurements is now renamed to `isSelected` to match the rest of `isLocked` , `isVisible` naming convention.
+
+Migration: you probably don't need to perform any migration
+
+---
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md b/platform/docs/docs/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md
new file mode 100644
index 000000000..7b85dafc0
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md
@@ -0,0 +1,40 @@
+---
+id: viewport-action-corner
+title: ViewportActionCorner
+---
+
+
+
+
+## Key Changes and Rationale
+
+Previously, the `ViewportActionCornersService` used the `setComponent` or `setComponents` methods to add components to viewport corners. These methods, when used with multiple components, would essentially overwrite existing components at the same location, unless great care was taken with the `indexPriority` property. This made it difficult to reliably position multiple components within the same corner.
+
+The new approach introduces the methods `addComponent` and `addComponents`, which insert components into the viewport corners based on an optional `indexPriority` property and provide predictable ordering based on the relative `indexPriority` of the components already at the corner. If no `indexPriority` is given, components are added to the end (for the left side) or the beginning (for the right side) by default.
+
+### Migration Steps
+
+**Update Component Addition Methods:** Replace calls to `setComponent` and `setComponents` with `addComponent` and `addComponents`, respectively.
+
+```js
+// Old API
+viewportActionCornersService.setComponent({
+ viewportId,
+ id: 'myComponent',
+ component: ,
+ location: viewportActionCornersService.LOCATIONS.topRight
+});
+```
+
+**New API**
+
+```js
+viewportActionCornersService.addComponent({
+ viewportId,
+ id: 'myComponent',
+ component: ,
+ location: viewportActionCornersService.LOCATIONS.topRight,
+ indexPriority: 1, // indexPriority is now optional and determines placement order within the corner
+});
+
+```
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/5-StateSyncService.md b/platform/docs/docs/migration-guide/3p8-to-3p9/5-StateSyncService.md
new file mode 100644
index 000000000..8505a342e
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/5-StateSyncService.md
@@ -0,0 +1,343 @@
+---
+id: state-sync-service
+title: StateSyncService
+---
+
+
+## Migrating from StateSyncService to Zustand Stores
+
+The `StateSyncService` has been deprecated in favor of more modern and efficient state management using Zustand stores. This migration guide outlines the reasons for the change and provides step-by-step instructions on how to migrate your extension or mode from using `StateSyncService` to Zustand.
+
+## Why Migrate?
+
+The `StateSyncService` had limitations:
+
+- **Limited Reactivity:** Updates weren't always reactive, requiring manual re-renders.
+- **Lack of Granularity:** It stored large chunks of state, hindering performance.
+- **Complexity:** Managing and syncing state across components was cumbersome.
+
+Zustand offers several advantages:
+
+- **Lightweight and Fast:** Zustand is a minimal and performant state management library.
+- **Granular Control:** Create individual stores for specific data, improving reactivity and performance.
+- **Simplified API:** Easy-to-use hooks for subscribing and updating state.
+
+## Migration Steps:
+
+1. **Identify State to Migrate:** Determine which parts of your extension or mode rely on the `StateSyncService`. Typical examples include:
+ - **Viewport Presentations:** LUT and position information for viewports.
+ - **Layout State:** Custom grid layouts and one-up toggling.
+ - **Synchronizers:** State for cross-viewport synchronization.
+ - **UI State:** UI-specific settings.
+2. **Replace StateSyncService Usage:** In your extension or mode:
+ - **Import Zustand Stores:** Import the new stores you created.
+ - **Replace** `getState()` and `store()`: Use the Zustand hooks (`useStore`, `set`, `get`) to access and update state in your components.
+ - **Handle Presentation IDs:** Implement logic for generating and managing presentation IDs within your stores or relevant components. This can involve using unique keys based on viewport options, display sets, and unique indices. See the `presentationUtils.ts` file for example implementations.
+ - **Rehydrate State:** On mode entry, rehydrate your Zustand stores with any relevant persisted state from localStorage or other storage mechanisms.
+ - **Clear State on Mode Exit:** Ensure you clear your Zustand stores appropriately on mode exit to prevent memory leaks.
+
+
+
+### `LutPresentationStore`
+
+
+**Before (StateSyncService):**
+
+```js
+const stateSyncService = servicesManager.services.stateSyncService;
+const lutPresentationStore = stateSyncService.getState().lutPresentationStore;
+const lutPresentation = lutPresentationStore[presentationId];
+// ...to update
+stateSyncService.store({
+ lutPresentationStore: {
+ ...lutPresentationStore,
+ [presentationId]: newLutPresentation,
+ },
+});
+```
+
+**After (Zustand):**
+
+```js
+import { useLutPresentationStore } from '../stores/useLutPresentationStore';
+const { lutPresentationStore, setLutPresentation } = useLutPresentationStore();
+const lutPresentation = lutPresentationStore[presentationId];
+// ...to update
+setLutPresentation(presentationId, newLutPresentation);
+```
+
+The `getPresentationId` for `lutPresentationStore` was previously registered in `platform/core`. Now, the Zustand store provides this functionality.
+
+```js
+// Fetch getPresentationId functions from respective Zustand stores
+const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState();
+
+// Register presentation id providers
+viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId);
+```
+
+
+---
+
+### `PositionPresentationStore`
+
+**Before (StateSyncService):**
+
+```js
+const stateSyncService = servicesManager.services.stateSyncService;
+const positionPresentationStore = stateSyncService.getState().positionPresentationStore;
+const positionPresentation = positionPresentationStore[presentationId];
+// ...to update
+stateSyncService.store({
+ positionPresentationStore: {
+ ...positionPresentationStore,
+ [presentationId]: newPositionPresentation,
+ },
+});
+```
+
+**After (Zustand):**
+
+```js
+import { usePositionPresentationStore } from '../stores/usePositionPresentationStore';
+const { positionPresentationStore, setPositionPresentation } = usePositionPresentationStore();
+const positionPresentation = positionPresentationStore[presentationId];
+// ...to update
+setPositionPresentation(presentationId, newPositionPresentation);
+```
+
+Similar to lutPresentationId, the PositionPresentationId is also registered from outside
+
+```js
+
+ const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState();
+
+ // register presentation id providers
+ viewportGridService.addPresentationIdProvider(
+ 'positionPresentationId',
+ getPositionPresentationId
+ );
+```
+
+---
+
+### `ViewportGridStore`
+
+**Before (StateSyncService):**
+
+```js
+const stateSyncService = servicesManager.services.stateSyncService;
+const viewportGridStore = stateSyncService.getState().viewportGridStore;
+const gridState = viewportGridStore[storeId];
+// ...to update
+stateSyncService.store({
+ viewportGridStore: {
+ ...viewportGridStore,
+ [storeId]: newGridState,
+ },
+});
+```
+
+**After (Zustand):**
+
+```js
+import { useViewportGridStore } from '../stores/useViewportGridStore';
+const { viewportGridState, setViewportGridState } = useViewportGridStore();
+const gridState = viewportGridState[storeId];
+// ...to update
+setViewportGridState(storeId, newGridState);
+```
+
+---
+
+### `DisplaySetSelectorStore`
+
+
+**Before (StateSyncService):**
+
+```js
+const stateSyncService = servicesManager.services.stateSyncService;
+const displaySetSelectorMap = stateSyncService.getState().displaySetSelectorMap;
+const displaySetUID = displaySetSelectorMap[selectorKey];
+// ...to update
+stateSyncService.store({
+ displaySetSelectorMap: {
+ ...displaySetSelectorMap,
+ [selectorKey]: newDisplaySetUID,
+ },
+});
+```
+
+**After (Zustand):**
+
+```js
+import { useDisplaySetSelectorStore } from '../stores/useDisplaySetSelectorStore';
+const { displaySetSelectorMap, setDisplaySetSelector } = useDisplaySetSelectorStore();
+const displaySetUID = displaySetSelectorMap[selectorKey];
+// ...to update
+setDisplaySetSelector(selectorKey, newDisplaySetUID);
+```
+
+---
+
+### `HangingProtocolStageIndexStore`
+
+
+**Before (StateSyncService):**
+
+```js
+const stateSyncService = servicesManager.services.stateSyncService;
+const hangingProtocolStageIndexMap = stateSyncService.getState().hangingProtocolStageIndexMap;
+const hpInfo = hangingProtocolStageIndexMap[cacheId];
+// ...to update
+stateSyncService.store({
+ hangingProtocolStageIndexMap: {
+ ...hangingProtocolStageIndexMap,
+ [cacheId]: newHpInfo,
+ },
+});
+```
+
+**After (Zustand):**
+
+```js
+import { useHangingProtocolStageIndexStore } from '../stores/useHangingProtocolStageIndexStore';
+const { hangingProtocolStageIndexMap, setHangingProtocolStageIndex } = useHangingProtocolStageIndexStore();
+const hpInfo = hangingProtocolStageIndexMap[cacheId];
+// ...to update
+setHangingProtocolStageIndex(cacheId, newHpInfo);
+```
+
+---
+
+### `ToggleHangingProtocolStore`
+
+
+**Before (StateSyncService):**
+
+```js
+const stateSyncService = servicesManager.services.stateSyncService;
+const toggleHangingProtocol = stateSyncService.getState().toggleHangingProtocol;
+const previousHpInfo = toggleHangingProtocol[storedHanging];
+// ...to update
+stateSyncService.store({
+ toggleHangingProtocol: {
+ ...toggleHangingProtocol,
+ [storedHanging]: newHpInfo,
+ },
+});
+```
+
+**After (Zustand):**
+
+```js
+import { useToggleHangingProtocolStore } from '../stores/useToggleHangingProtocolStore';
+const { toggleHangingProtocol, setToggleHangingProtocol } = useToggleHangingProtocolStore();
+const previousHpInfo = toggleHangingProtocol[storedHanging];
+// ...to update
+setToggleHangingProtocol(storedHanging, newHpInfo);
+```
+
+---
+
+### `ToggleOneUpViewportGridStore`
+
+
+**Before (StateSyncService):**
+
+```js
+const stateSyncService = servicesManager.services.stateSyncService;
+const toggleOneUpViewportGridStore = stateSyncService.getState().toggleOneUpViewportGridStore;
+const previousGridState = toggleOneUpViewportGridStore.layout; // Assuming layout was a property
+// ...to update
+stateSyncService.store({
+ toggleOneUpViewportGridStore: newGridState,
+});
+```
+
+**After (Zustand):**
+
+```js
+import { useToggleOneUpViewportGridStore } from '../stores/useToggleOneUpViewportGridStore';
+const { toggleOneUpViewportGridStore, setToggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore();
+const previousGridState = toggleOneUpViewportGridStore; // No nested layout property
+// ...to update
+setToggleOneUpViewportGridStore(newGridState);
+```
+
+---
+
+### `UIStateStore`
+
+
+**Before (StateSyncService):**
+
+```js
+const stateSyncService = servicesManager.services.stateSyncService;
+const uiState = stateSyncService.getState().uiStateStore[someUIKey];
+// ...to update
+stateSyncService.store({
+ uiStateStore: {
+ ...stateSyncService.getState().uiStateStore,
+ [someUIKey]: newUIState,
+ },
+});
+```
+
+**After (Zustand):**
+
+```js
+import { useUIStateStore } from '../stores/useUIStateStore';
+const { uiState, setUIState } = useUIStateStore();
+const currentUIState = uiState[someUIKey];
+// ...to update
+setUIState(someUIKey, newUIState);
+```
+
+---
+
+### `ViewportsByPositionStore`
+
+
+**Before (StateSyncService):**
+
+```js
+const stateSyncService = servicesManager.services.stateSyncService;
+const viewportsByPosition = stateSyncService.getState().viewportsByPosition;
+const cachedViewport = viewportsByPosition[positionId];
+// ...to update
+stateSyncService.store({
+ viewportsByPosition: {
+ ...viewportsByPosition,
+ [positionId]: newViewport,
+ },
+});
+```
+
+**After (Zustand):**
+
+```js
+import { useViewportsByPositionStore } from '../stores/useViewportsByPositionStore';
+const { viewportsByPosition, setViewportsByPosition } = useViewportsByPositionStore();
+const cachedViewport = viewportsByPosition[positionId];
+// ...to update
+setViewportsByPosition(positionId, newViewport);
+```
+
+---
+
+### `SegmentationPresentationStore`
+
+**After (Zustand):**
+
+```js
+import { useSegmentationPresentationStore } from '../stores/useSegmentationPresentationStore';
+const { segmentationPresentationStore, setSegmentationPresentation } =
+ useSegmentationPresentationStore();
+// ...to update
+setSegmentationPresentation(presentationId, newSegmentationPresentation);
+// You likely have functions within the store like:
+// addSegmentationPresentation
+// setSegmentationVisibility
+// etc.
+```
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/6-RTSTRUCT.md b/platform/docs/docs/migration-guide/3p8-to-3p9/6-RTSTRUCT.md
new file mode 100644
index 000000000..f13bf9130
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/6-RTSTRUCT.md
@@ -0,0 +1,18 @@
+---
+id: 6-rtstruct
+title: RTSTRUCT
+sidebar_position: 6
+---
+
+
+
+# RTStructure Set has transitioned from VTK actors to SVG.
+
+We have transitioned from VTK-based rendering to SVG-based rendering for RTStructure Set contours. This change should not require any modifications to your codebase. We anticipate improved stability and speed in our contour rendering.
+
+As a result of this update, viewports rendering RTStructure Sets will no longer convert to volume viewports. Instead, they will remain as stack viewports.
+
+
+Read more in Pull Requests:
+- https://github.com/OHIF/Viewers/pull/4074
+- https://github.com/OHIF/Viewers/pull/4157
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/7-UI.md b/platform/docs/docs/migration-guide/3p8-to-3p9/7-UI.md
new file mode 100644
index 000000000..2dbb01c42
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/7-UI.md
@@ -0,0 +1,140 @@
+---
+title: UI
+---
+
+
+## `UINotificationService`
+
+
+We've switched our custom notification service to the Sonner component from https://sonner.emilkowal.ski/
+
+### 1. Toast Positions (Kebab-Case)
+
+Toast positions are now defined using kebab-case instead of camelCase. For instance, `topRight` becomes `top-right`, `bottomRight` becomes `bottom-right`, etc. Ensure your position strings are updated accordingly.
+
+**Old API:**
+
+```js
+uiNotificationService.show({
+ title: 'My Title',
+ message: 'My Message',
+ duration: 3000,
+ position: 'topRight',
+ type: 'error',
+ autoClose: true,
+});
+```
+
+
+**New API:**
+
+```js
+uiNotificationService.show({
+ title: 'My Title',
+ message: 'My Message',
+ duration: 3000,
+ position: 'top-right', // Note the change to kebab-case
+ type: 'error',
+ autoClose: true,
+});
+```
+
+### 2. Promise Support
+
+The `show()` method now supports promises, enabling you to display loading notifications and automatically update them based on the promise's resolution or rejection. This significantly simplifies asynchronous operation feedback.
+
+**Example:**
+
+```js
+const myPromise = someAsyncOperation();
+const notificationId = uiNotificationService.show({
+ title: 'Loading Data',
+ message: 'Fetching data from server...',
+ type: 'info',
+ promise: myPromise,
+ promiseMessages: {
+ loading: 'Fetching...',
+ success: (data) => `Data loaded: ${data.length} items`, // Access promise result
+ error: (error) => `Failed to load data: ${error.message}`, // Access error details
+ },
+});
+// Optionally hide notification manually if needed
+// myPromise.finally(() => uiNotificationService.hide(notificationId));
+```
+
+### 3. `hide()` API Change
+
+The `hide()` method no longer takes an options object. It only accepts the notification ID as a string argument.
+
+**Old API:**
+
+```js
+uiNotificationService.hide({ id: notificationId });
+```
+
+**New API:**
+
+```js
+uiNotificationService.hide(notificationId);
+```
+
+
+---
+
+
+## Viewport Pane Tailwindcss class
+
+Previously, when targeting the viewport pane to add custom CSS, you likely used `group-hover:visible` with the viewportPane having a `group` class.
+
+The naming was confusing as we added more groups, so we renamed it to `group/pane`. Now you can apply `group-hover/pane` for better clarity.
+
+
+---
+
+## Header Component
+
+
+Header Component has been refactored in the @ohif/ui-next package.
+
+
+**Before**
+
+
+```js
+function Header({
+ children,
+ menuOptions,
+ isReturnEnabled,
+ onClickReturnButton,
+ isSticky,
+ WhiteLabeling,
+ showPatientInfo,
+ servicesManager,
+ Secondary,
+ appConfig,
+ ...props
+}: withAppTypes): ReactNode
+```
+
+**After**
+
+```js
+function Header({
+ children,
+ menuOptions,
+ isReturnEnabled,
+ onClickReturnButton,
+ isSticky,
+ WhiteLabeling,
+ PatientInfo,
+ Secondary,
+ ...props
+}: HeaderProps): ReactNode
+```
+
+The `PatientInfo` component is now preferred, and the `showPatientInfo` prop has been removed. The previous method depended on `servicesManager`, which was cumbersome because the UI shouldn't need to interact with `servicesManager`.
+
+All the DropDown and Icons are now in the @ohif/ui-next package.
+
+
+---
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/8-Refactorings.md b/platform/docs/docs/migration-guide/3p8-to-3p9/8-Refactorings.md
new file mode 100644
index 000000000..795b3e3c6
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/8-Refactorings.md
@@ -0,0 +1,120 @@
+---
+title: Refactoring
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+
+
+
+
+## Panel Segmentation
+
+is now moved from `@ohif/extension-cornerstone-dicom-seg` to `@ohif/extension-cornerstone`.
+
+
+The cornerstone extension now provides the panelSegmentation feature, which was previously part of the cornerstone-dicom-seg extension. This change is logical as panelSegmentation handles more than just DICOM. It can process various formats, including custom formats from the backend and potentially NIFTI format in the future.
+
+
+Before in your modes you were using
+
+```js
+'@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
+```
+
+
+Now you should use it via
+
+
+```js
+'@ohif/extension-cornerstone.panelModule.panelSegmentation',
+```
+
+---
+
+## `callInputDialog` and `colorPickerDialog` and `showLabelAnnotationPopup`
+
+Due to the excessive number of `callInputDialog` instances, we centralized them. You can now import them from `@ohif/extension-default`.
+
+
+```js
+import { showLabelAnnotationPopup, callInputDialog, colorPickerDialog } from '@ohif/extension-default';
+```
+
+
+---
+
+## disableEditing
+
+The configuration has moved from appConfig to allow more precise control over component disabling. To disable editing for segmentation and measurements, add the following settings:
+
+
+**Before: **
+
+```js
+customizationService.addModeCustomizations([
+ {
+ id: 'segmentation.panel',
+ disableEditing: true,
+ },
+]);
+```
+
+**Now **
+
+```js
+customizationService.addModeCustomizations([
+ // To disable editing in the SegmentationTable
+ {
+ id: 'PanelSegmentation.disableEditing',
+ disableEditing: true,
+ },
+ // To disable editing in the MeasurementTable
+ {
+ id: 'PanelMeasurement.disableEditing',
+ disableEditing: true,
+ },
+])
+```
+
+
+---
+
+## Customization Ids
+
+The primary reason for this migration is to improve modularity and maintainability in configuration management, as we plan to focus more on the customization service in the near future.
+
+**Before**
+
+```js
+customizationService.addModeCustomizations([
+ {
+ id: 'segmentation.panel',
+ segmentationPanelMode: 'expanded',
+ addSegment: false,
+ onSegmentationAdd: () => {
+ commandsManager.run('createNewLabelmapFromPT');
+ },
+ },
+]);
+```
+
+
+**Now**
+
+```js
+customizationService.addModeCustomizations([
+ {
+ id: 'PanelSegmentation.tableMode',
+ mode: 'expanded',
+ },
+ {
+ id: 'PanelSegmentation.onSegmentationAdd',
+ onSegmentationAdd: () => {
+ commandsManager.run('createNewLabelmapFromPT');
+ },
+ },
+]);
+
+```
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/9-other.md b/platform/docs/docs/migration-guide/3p8-to-3p9/9-other.md
new file mode 100644
index 000000000..5bd8b735d
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/9-other.md
@@ -0,0 +1,99 @@
+---
+title: Other Changes
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+
+## External Libraries
+Some libraries are loaded via dynamic import. You can provide a global function
+`browserImport` the allows loading of dynamic imports without affecting the
+webpack build. This import looks like:
+
+```html
+
+```
+
+and belongs in the root html file for your application.
+You then need to remove `dependencies` on the external import, and add a reference
+to the external import in your `pluginConfig.json` file.
+
+### Example plugin config for `dicom-microscopy-viewer`
+The example below imports the `dicom-microscopy-viewer` for use as an external
+dependency. The example is part of the default `pluginConfig.json` file.
+
+```json
+ "public": [
+ {
+ "directory": "./platform/public"
+ },
+ {
+ "packageName": "dicom-microscopy-viewer",
+ "importPath": "/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js",
+ "globalName": "dicomMicroscopyViewer",
+ "directory": "./node_modules/dicom-microscopy-viewer/dist/dynamic-import"
+ }
+ ]
+```
+
+This defines two directory modules, whose contents are copied unchanged to the
+output build directory. It then defines the `dicom-microscopy-viewer` using
+the `packageName` element as being a module which is imported dynamically.
+Then, the import path passed into the browserImportFunction above is
+specified, and then how to access the import itself, via the `window.dicomMicroscopyViewer`
+global name reference.
+
+### Referencing External Imports
+The appConfig either defines or has a default peerImport function which can be
+used to load references to the modules defined in the pluginConfig file. See
+the example in `init.tsx` for the cornerstone extension for how this is passed
+into CS3D for loading the whole slide imaging library.
+
+
+
+---
+
+
+
+---
+
+
+---
+
+
+## Use of ViewReference for navigation
+When navigating to measurements and storing/remembering navigation positions,
+the `viewport.getViewReference` is used to get a position, and `viewport.isReferenceViewable`
+used to check if a reference can be applied, and finally `viewport.setViewReference` to
+navigate to a view. Note that this changes the behaviour of navigation between
+MPR and Stack viewports, and also enables navigation of video and microscopy
+viewports in CS3D. This can cause some unexpected behaviour depending on how the
+frame of reference values are configured to allow for navigation.
+
+The isReferenceViewable is used to determine when a view or measurement can be
+shown on a given view. For stack versus volume viewports, this can cause unexpected
+behaviour to be seen depending on how the view reference was fetched.
+
+### `getViewReference` with `forFrameOfReference`
+When a view reference is fetched with the for frame of reference flag set to true,
+a reference will be returned which can be displayed on any viewport containing
+the same frame of reference and encompassing the given FOR and able to display the required
+orientation. Without this flag, a view reference is returned which will be
+displayed on a stack with the given image id, or a volume containing said image id
+or the specified volume.
+
+### `isReferenceViewable` with navigation and/or orientation
+The is reference viewable will return false unless the given reference is directly
+viewable in the viewport as is. However, it can be passed various flags to determine
+whether the reference could be displayed if the viewport was modified in various ways,
+for example, by changing the position or orientation of the viewport. This allows
+checking for degrees of closeness so that the correct viewport can be chosen.
+
+Note that this may result in displaying a measurement from one viewport on a completely
+different viewport, for example, showing a Probe tool from the stack viewport on
+an MPR view.
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/index.md b/platform/docs/docs/migration-guide/3p8-to-3p9/index.md
new file mode 100644
index 000000000..56e414e87
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p8-to-3p9/index.md
@@ -0,0 +1,7 @@
+---
+id: 3p8-to-3p9
+title: 3.8 -> 3.9
+sidebar_position: 1
+---
+
+Here are the changes you need to make to migrate from 3.8 to 3.9.
diff --git a/platform/docs/docs/migration-guide/from-3p8-to-3p9-beta.md b/platform/docs/docs/migration-guide/from-3p8-to-3p9-beta.md
deleted file mode 100644
index d430f06a1..000000000
--- a/platform/docs/docs/migration-guide/from-3p8-to-3p9-beta.md
+++ /dev/null
@@ -1,309 +0,0 @@
----
-sidebar_position: 1
-sidebar_label: 3.8 -> 3.9-beta
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-
-# Migration Guide
-
-
-## React 18 Migration Guide
-As we upgrade to React 18, we're making some exciting changes to improve performance and developer experience. This guide will help you navigate the key updates and ensure your custom extensions and modes are compatible with the new version.
-What's Changing?
-
-
-
-```md
-- React 17
-- Using `defaultProps`
-- `babel-inline-svg` for SVG imports
-```
-
-
-
-
-```md
-- React 18
-- Default parameters for props
-- `svgr` for SVG imports
-```
-
-
-
-
-
-### Run newer yarn version
-You must be running a newer yarn version for react 18.
-It isn't clear the exact yarn required.
-
-### Update React version:
-In your custom extensions and modes, change the version of react and react-dom to ^18.3.1.
-
-### Replace defaultProps with default parameters:
-
-
-
-
-```jsx
-const MyComponent = ({ prop1, prop2 }) => {
- return {prop1} {prop2}
-}
-
-MyComponent.defaultProps = {
- prop1: 'default value',
- prop2: 'default value'
-}
-```
-
-
-
-
-```jsx
-const MyComponent = ({ prop1 = 'default value', prop2 = 'default value' }) => {
- return {prop1} {prop2}
-}
-```
-
-
-
-### Update SVG imports:
-
-
-
-
-```javascript
-import arrowDown from './../../assets/icons/arrow-down.svg';
-```
-
-
-
-
-```javascript
-import { ReactComponent as arrowDown } from './../../assets/icons/arrow-down.svg';
-```
-
-
-
-
-
-
-
----
-
-
-
-## Renaming
-
-The panel in the default extension is renamed from `measure` to `measurements` to be more consistent with the rest of the extensions.
-
-**Action Needed**
-
-Update any references to the `measure` panel to `measurements` in your code.
-
-Find and replace
-
-
-
- @ohif/extension-default.panelModule.measure
-
-
- @ohif/extension-default.panelModule.measurements
-
-
-
-
-
-
----
-
-
-
-## RTStructure Set has transitioned from VTK actors to SVG.
-
-We have transitioned from VTK-based rendering to SVG-based rendering for RTStructure Set contours. This change should not require any modifications to your codebase. We anticipate improved stability and speed in our contour rendering.
-
-As a result of this update, viewports rendering RTStructure Sets will no longer convert to volume viewports. Instead, they will remain as stack viewports.
-
-
-Read more in Pull Requests:
-- https://github.com/OHIF/Viewers/pull/4074
-- https://github.com/OHIF/Viewers/pull/4157
-
-
-
----
-
-
-
-## Crosshairs
-
-They now have new colors in their associated viewports in the MPR view. However, you can turn this feature off.
-
-To disable it, remove the configuration from the `initToolGroups` in your mode.
-
-```
-{
- configuration: {
- viewportIndicators: true,
- viewportIndicatorsConfig: {
- circleRadius: 5,
- xOffset: 0.95,
- yOffset: 0.05,
- },
- }
-}
-```
-
-
-
----
-
-
-
-## External Libraries
-Some libraries are loaded via dynamic import. You can provide a global function
-`browserImport` the allows loading of dynamic imports without affecting the
-webpack build. This import looks like:
-
-```
-
-```
-
-and belongs in the root html file for your application.
-You then need to remove `dependencies` on the external import, and add a reference
-to the external import in your `pluginConfig.json` file.
-
-### Example plugin config for `dicom-microscopy-viewer`
-The example below imports the `dicom-microscopy-viewer` for use as an external
-dependency. The example is part of the default `pluginConfig.json` file.
-
-```
- "public": [
- {
- "directory": "./platform/public"
- },
- {
- "packageName": "dicom-microscopy-viewer",
- "importPath": "/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js",
- "globalName": "dicomMicroscopyViewer",
- "directory": "./node_modules/dicom-microscopy-viewer/dist/dynamic-import"
- }
- ]
-```
-
-This defines two directory modules, whose contents are copied unchanged to the
-output build directory. It then defines the `dicom-microscopy-viewer` using
-the `packageName` element as being a module which is imported dynamically.
-Then, the import path passed into the browserImportFunction above is
-specified, and then how to access the import itself, via the `window.dicomMicroscopyViewer`
-global name reference.
-
-### Referencing External Imports
-The appConfig either defines or has a default peerImport function which can be
-used to load references to the modules defined in the pluginConfig file. See
-the example in `init.tsx` for the cornerstone extension for how this is passed
-into CS3D for loading the whole slide imaging library.
-
-### Usage of Dynamic Imports
-
-
-## BulkDataURI Configuration
-
-We've updated the configuration for BulkDataURI to provide more flexibility and control. This guide will help you migrate from the old configuration to the new one.
-
-### What's Changing?
-
-
-
-
-```javascript
-useBulkDataURI: false,
-```
-
-
-
-
-```javascript
-bulkDataURI: {
- enabled: true,
- // Additional configuration options
-},
-```
-
-
-
-
-
-Additional Notes:
-- The new configuration allows for more granular control over BulkDataURI behavior.
-- You can now add custom URL prefixing logic using the startsWith and prefixWith properties.
-- This change enables easier correction of retrieval URLs, especially in scenarios where URLs pass through multiple systems.
-
-
-
-
----
-
-
-
-## Polyfill.io
-
-We have removed the Polyfill.io script from the Viewer. If you require polyfills, you can add them to your project manually. This change primarily affects Internet Explorer, which Microsoft has already [ended support for](https://learn.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge#is-internet-explorer-11-the-last-version-of-internet-explorer-).
-
-
-
-
----
-
-
-
-## Dynamic Modules
-
-TBD
-
-## Renaming some interfaces
-A few interfaces are being renamed to simple types to reflect the fact that
-they don't contain methods and are thus more properly simple types.
-
-* IDisplaySet renamed to DisplaySet
- * Adding some field declarations to agree with actual usage
-
-
-## Use of ViewReference for navigation
-When navigating to measurements and storing/remembering navigation positions,
-the `viewport.getViewReference` is used to get a position, and `viewport.isReferenceViewable`
-used to check if a reference can be applied, and finally `viewport.setViewReference` to
-navigate to a view. Note that this changes the behaviour of navigation between
-MPR and Stack viewports, and also enables navigation of video and microscopy
-viewports in CS3D. This can cause some unexpected behaviour depending on how the
-frame of reference values are configured to allow for navigation.
-
-The isReferenceViewable is used to determine when a view or measurement can be
-shown on a given view. For stack versus volume viewports, this can cause unexpected
-behaviour to be seen depending on how the view reference was fetched.
-
-### `getViewReference` with `forFrameOfReference`
-When a view reference is fetched with the for frame of reference flag set to true,
-a reference will be returned which can be displayed on any viewport containing
-the same frame of reference and encompassing the given FOR and able to display the required
-orientation. Without this flag, a view reference is returned which will be
-displayed on a stack with the given image id, or a volume containing said image id
-or the specified volume.
-
-### `isReferenceViewable` with navigation and/or orientation
-The is reference viewable will return false unless the given reference is directly
-viewable in the viewport as is. However, it can be passed various flags to determine
-whether the reference could be displayed if the viewport was modified in various ways,
-for example, by changing the position or orientation of the viewport. This allows
-checking for degrees of closeness so that the correct viewport can be chosen.
-
-Note that this may result in displaying a measurement from one viewport on a completely
-different viewport, for example, showing a Probe tool from the stack viewport on
-an MPR view.
diff --git a/platform/docs/docs/platform/extensions/modules/toolbar.md b/platform/docs/docs/platform/extensions/modules/toolbar.md
index 94fdb22fc..b353a0304 100644
--- a/platform/docs/docs/platform/extensions/modules/toolbar.md
+++ b/platform/docs/docs/platform/extensions/modules/toolbar.md
@@ -142,7 +142,13 @@ this pattern, where multiple toolbar buttons are using the same evaluator but wi
You can choose to set up multiple evaluators for a single button. This comes in handy when you need to assess the button according to various conditions. For example, we aim to prevent the Cine player from showing up on the 3D viewport, so we have:
```js
-evaluate: ['evaluate.cine', 'evaluate.not3D'],
+evaluate: [
+ 'evaluate.cine',
+ {
+ name: 'evaluate.viewport.supported',
+ unsupportedViewportTypes: ['volume3d'],
+ },
+],
```
You can even come up with advanced evaluators such as:
diff --git a/platform/docs/docs/platform/services/data/SegmentationService.md b/platform/docs/docs/platform/services/data/SegmentationService.md
index bd99e12de..31e4b6726 100644
--- a/platform/docs/docs/platform/services/data/SegmentationService.md
+++ b/platform/docs/docs/platform/services/data/SegmentationService.md
@@ -20,11 +20,10 @@ There are seven events that get publish in `MeasurementService`:
| Event | Description |
| --------------------- | ------------------------------------------------------ |
-| SEGMENTATION_UPDATED | Fires when a segmentation is updated e.g., segment added, removed etc.|
+| SEGMENTATION_MODIFIED | Fires when a segmentation is updated e.g., segment added, removed etc.|
| SEGMENTATION_DATA_MODIFIED | Fires when the segmentation data changes |
| SEGMENTATION_ADDED | Fires when a new segmentation is added to OHIF |
| SEGMENTATION_REMOVED | Fires when a segmentation is removed from OHIF |
-| SEGMENTATION_CONFIGURATION_CHANGED | Fires when a segmentation configuration is changed |
| SEGMENT_LOADING_COMPLETE | Fires when a segment group adds its pixel data to the volume |
| SEGMENTATION_LOADING_COMPLETE | Fires when the full segmentation volume is filled with its segments |
@@ -33,7 +32,7 @@ There are seven events that get publish in `MeasurementService`:
### Segmentation Creation
-- `createSegmentationForDisplaySet`: based on a reference displaySet, create a new segmentation. E.g., create a new segmentation based on a CT series
+- `createEmptyLabelmapForDisplaySetUID`: based on a reference displaySet, create a new segmentation. E.g., create a new segmentation based on a CT series
- `createSegmentationForSEGDisplaySet`: given a segDisplaySet loaded by a sopClassHandler, create a new segmentation
- `addSegmentationRepresentationToToolGroup`: given the toolGroupId, add the given segmentationId to the toolGroup.
diff --git a/platform/docs/docs/platform/services/data/StateSyncService.md b/platform/docs/docs/platform/services/data/StateSyncService.md
deleted file mode 100644
index 49fdbf0c3..000000000
--- a/platform/docs/docs/platform/services/data/StateSyncService.md
+++ /dev/null
@@ -1,76 +0,0 @@
----
-sidebar_position: 8
-sidebar_label: State Sync Service
----
-
-# State Sync Service
-
-## Overview
-The state sync service is designed to allow short and long term memory of things such as
-annotations applied, last annotation state, hanging protocol viewport state,
-window level etc. This allows for better interaction with things like navigation
-between hanging protocols, ensuring that the previously displayed layouts
-can be redisplayed after returning to a given hanging protocol.
-
-Currently, all the state sync service configurations have one of the following two
-lifetimes. See the mode description for general information on the mode lifetime.
-
-* Application load - when the application is restarted, the state is lost
-* `clearOnModeExit` - which stores state until the mode onModeExit is called, and then throws away the remaining state. This is useful for mode specific information.
-
-### TODO work - add more storage locations
-It is expected to add a few more storage locations, which will store to various
-locations on updates:
-
-* User specific server store - to store things between application restarts at the user level
-* Browser state store - to store things in the browser local state, to recover after crashing.
-* Study specific server store - to store things relevant to a given study between application restarts, on the server.
-
-## Events
-
-Currently the service does not fire events.
-
-## API
-
-- `register`: to create a new named state storage
-- `reduce`: to apply a set of changes to several states at once
-- `getState`: to retrieve the current state
-- `onModeExit`: clears the states configured as clearOnModeExit states
-
-### register
-The register call is typically added to an extension to create a new
-syncable state. A typical call is shown below, registering the viewport
-grid store state as a modal state.
-
-```javascript
- stateSyncService.register('viewportGridStore', { clearOnModeExit: true });
-```
-
-### getState
-The `getState` call returns an object containing all of the registered states,
-by id. The values can be read directly, but should not be modified.
-
-### reduce
-The `reduce` call is used to apply a set of updates to various states. The
-updates are performed for every state as a simply "set" call.
-
-### onModeExit
-When the Mode is exited, the onModeExit is called on the sync state, and this
-clears all states registered with `clearOnModeExit: true`.
-To avoid clearing the state, the mode definition should store any transient
-state in the mode onModeExit and recover it in the `mode.onModeEnter`.
-
-## OHIF Registered State Sync Stores
-There are a number of defined stores here. It is recommended to update this
-list as state stores are added:
-
-### Default Extension Stores
-
-* `viewportGridStore` has viewport grid restore information for returning to an earlier grid layout.
-* `reuseIdMap` has a map of names to display sets for preserving user changes to hp display set selections.
-* `hanging` has a map of the hanging protocol stage information applied (HPInfo)
-
-### Cornerstone Extension Stores
-
-* `lutPresentationStore` has the cornerstone LUT (window level) presentation state information
-* `positionPresentationStore` has the cornerstone viewport position (camera, initial image) information
diff --git a/platform/docs/docs/platform/services/ui/viewport-action-menu.md b/platform/docs/docs/platform/services/ui/viewport-action-menu.md
new file mode 100644
index 000000000..5a0592535
--- /dev/null
+++ b/platform/docs/docs/platform/services/ui/viewport-action-menu.md
@@ -0,0 +1,30 @@
+---
+sidebar_position: 8
+sidebar_label: Viewport Action Corners
+---
+
+# Viewport Action Corners Service
+
+The Viewport Action Corners Service is a powerful tool for managing interactive components in the corners of viewports within the OHIF viewer. This service allows developers to dynamically add, remove, and organize various UI elements such as menus, buttons, or custom components in specific locations around the viewport.
+
+## Overview
+
+The Viewport Action Corners Service extends the PubSubService and provides methods to:
+
+- Add single or multiple components to viewport corners
+- Clear components from a specific viewport
+- Manage the state of viewport corner components
+
+## Key Features
+
+- **Flexible Positioning**: Components can be placed in top-left, top-right, bottom-left, or bottom-right corners of the viewport.
+- **Priority Ordering**: Components can be assigned priority indices for ordering within a corner.
+- **Viewport-Specific**: Actions are associated with specific viewports, allowing for individualized control.
+- **Dynamic Updates**: Components can be added or removed at runtime, enabling context-sensitive UI elements.
+
+## Usage
+
+To use the Viewport Action Corners Service, you typically interact with it through the `servicesManager`. Here's a basic example of how to add a component:
+
+
+Take a look at how we add window level menu to the top right corner of the viewport in the `OHIFCornerstoneViewport` component.
diff --git a/platform/docs/docusaurus.config.js b/platform/docs/docusaurus.config.js
index 4f295febd..092b18377 100644
--- a/platform/docs/docusaurus.config.js
+++ b/platform/docs/docusaurus.config.js
@@ -11,10 +11,26 @@ const path = require('path');
const fs = require('fs');
const versions = fs.readFileSync('../../version.txt', 'utf8').split('\n');
+const ArchivedVersionsDropdownItems = [
+ {
+ version: '2.0-deprecated',
+ href: 'https://v2.docs.ohif.org',
+ isExternal: true,
+ },
+ {
+ version: '1.0-deprecated',
+ href: 'https://v1.docs.ohif.org',
+ isExternal: true,
+ },
+];
+
const baseUrl = process.env.BASE_URL || '/';
/** @type {import('@docusaurus/types').DocusaurusConfig} */
module.exports = {
+ future: {
+ experimental_faster: true,
+ },
title: 'OHIF',
tagline: 'Open-source web-based medical imaging platform',
organizationName: 'Open Health Imaging Foundation',
@@ -47,22 +63,6 @@ module.exports = {
// path.resolve(__dirname, './pluginOHIFWebpackConfig.js'),
// /path.resolve(__dirname, './postcss.js'),
'docusaurus-plugin-image-zoom', // 3rd party plugin for image click to pop
- [
- '@docusaurus/plugin-client-redirects',
- {
- fromExtensions: ['html'],
- redirects: [
- {
- // we need this for https://cloud.google.com/healthcare/docs/how-tos/dicom-viewers
- to: '/2.0-deprecated/deployment/recipes/google-cloud-healthcare',
- from: [
- '/connecting-to-image-archives/google-cloud-healthcare',
- '/connecting-to-image-archives/google-cloud-healthcare.html',
- ],
- },
- ],
- },
- ],
[
'@docusaurus/plugin-ideal-image',
{
@@ -200,9 +200,16 @@ module.exports = {
value: '
',
},
{
- to: '/versions',
- label: 'All versions',
+ type: 'html',
+ className: 'dropdown-archived-versions',
+ value: 'Archived versions',
},
+ ...ArchivedVersionsDropdownItems.map(item => ({
+ label: `${item.version} `,
+ href: item.href,
+ target: item.isExternal ? '_blank' : undefined,
+ rel: item.isExternal ? 'noopener noreferrer' : undefined,
+ })),
],
},
{
diff --git a/platform/docs/package.json b/platform/docs/package.json
index aee01682b..51f06a0fb 100644
--- a/platform/docs/package.json
+++ b/platform/docs/package.json
@@ -41,15 +41,16 @@
]
},
"dependencies": {
- "@docusaurus/core": "3.5.2",
- "@docusaurus/plugin-client-redirects": "3.5.2",
- "@docusaurus/plugin-google-gtag": "3.5.2",
- "@docusaurus/plugin-ideal-image": "3.5.2",
- "@docusaurus/plugin-pwa": "3.5.2",
- "@docusaurus/preset-classic": "3.5.2",
- "@docusaurus/remark-plugin-npm2yarn": "3.5.2",
- "@docusaurus/theme-classic": "3.5.2",
- "@docusaurus/theme-live-codeblock": "3.5.2",
+ "@docusaurus/core": "3.6.0",
+ "@docusaurus/plugin-client-redirects": "3.6.0",
+ "@docusaurus/plugin-google-gtag": "3.6.0",
+ "@docusaurus/plugin-ideal-image": "3.6.0",
+ "@docusaurus/plugin-pwa": "3.6.0",
+ "@docusaurus/preset-classic": "3.6.0",
+ "@docusaurus/remark-plugin-npm2yarn": "3.6.0",
+ "@docusaurus/theme-classic": "3.6.0",
+ "@docusaurus/theme-live-codeblock": "3.6.0",
+ "@docusaurus/faster": "3.6.0",
"@mdx-js/react": "3.0.1",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.1",
diff --git a/platform/docs/src/css/custom.css b/platform/docs/src/css/custom.css
index fa0b0ad77..5d7b1b8eb 100644
--- a/platform/docs/src/css/custom.css
+++ b/platform/docs/src/css/custom.css
@@ -537,3 +537,130 @@ div[class^='announcementBar_'] {
);
font-weight: 700;
}
+
+/* #__docusaurus {
+ height: 100%;
+} */
+
+.dropdown-separator {
+ border-top: 1px solid #808080;
+}
+
+/* flex items , center */
+.dropdown__link {
+ display: flex;
+ align-items: center;
+}
+
+.footer__link-item {
+ display: flex;
+ align-items: center;
+}
+
+/* add proper ui link styling */
+
+/* Bullet point styling */
+ul {
+ list-style-type: disc;
+ padding-left: 1.5rem;
+ margin: 1rem 0;
+}
+
+ul li {
+ margin-bottom: 0.5rem;
+}
+
+/* Nested bullet points */
+ul ul {
+ list-style-type: circle;
+ margin: 0.5rem 0;
+}
+
+/* For documentation bullet points specifically */
+.markdown ul {
+ list-style-type: disc;
+ padding-left: 1.5rem;
+}
+
+.markdown ul li {
+ margin-bottom: 0.5rem;
+}
+
+/* Markdown link styling */
+.markdown a {
+ color: #0066cc;
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.markdown a:hover {
+ color: #0051a3;
+ text-decoration: underline;
+}
+
+/* Dark mode link styling */
+html[data-theme='dark'] .markdown a {
+ color: #66b3ff;
+}
+
+html[data-theme='dark'] .markdown a:hover {
+ color: #99ccff;
+}
+
+/* Horizontal rule styling */
+.markdown hr {
+ height: 1px;
+ border: none;
+ background: linear-gradient(to right, #0066cc, #66b3ff);
+ margin: 2rem 0;
+ opacity: 0.6;
+}
+
+/* Dark mode horizontal rule */
+html[data-theme='dark'] .markdown hr {
+ background: linear-gradient(to right, #66b3ff, #99ccff);
+}
+
+/* Markdown code block styling */
+.markdown pre {
+ font-size: 0.9rem;
+}
+
+.theme-code-block {
+ font-size: 0.9rem;
+}
+
+/* Target both light and dark themes */
+[data-theme='light'] .theme-code-block,
+[data-theme='dark'] .theme-code-block {
+ font-size: 0.9rem;
+}
+
+/* Dropdown menu positioning and interaction fixes */
+.dropdown {
+ position: relative;
+}
+
+.dropdown__menu {
+ top: 100%;
+ margin-top: 0;
+ padding-top: 0.5rem;
+}
+
+/* Add a hover area to prevent menu from disappearing */
+.dropdown__menu::before {
+ content: '';
+ position: absolute;
+ top: -10px;
+ left: 0;
+ right: 0;
+ height: 10px;
+}
+
+/* Ensure menu stays visible while hovering */
+.dropdown:hover .dropdown__menu,
+.dropdown__menu:hover {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
diff --git a/platform/docs/src/pages/patterns/patterns-measurements.tsx b/platform/docs/src/pages/patterns/patterns-measurements.tsx
index a0ea5cc8d..343bd908d 100644
--- a/platform/docs/src/pages/patterns/patterns-measurements.tsx
+++ b/platform/docs/src/pages/patterns/patterns-measurements.tsx
@@ -9,6 +9,7 @@ import {
} from '../../../../ui-next/src/components/Accordion';
import { DataRow } from '../../../../ui-next/src/components/DataRow';
import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data';
+import BrowserOnly from '@docusaurus/BrowserOnly';
interface DataItem {
id: number;
@@ -36,95 +37,95 @@ export default function Measurements() {
};
const organSegmentationGroup = dataList.find(
- (listGroup: ListGroup) => listGroup.type === 'Organ Segmentation'
+ listGroup => listGroup.type === 'Organ Segmentation'
);
- const roiToolsGroup = dataList.find((listGroup: ListGroup) => listGroup.type === 'ROI Tools');
+ const roiToolsGroup = dataList.find(listGroup => listGroup.type === 'ROI Tools');
- if (!organSegmentationGroup) {
- return Organ Segmentation data not found.
;
- }
-
- if (!roiToolsGroup) {
- return ROI Tools data not found.
;
+ if (!organSegmentationGroup || !roiToolsGroup) {
+ return null; // Avoid rendering until these groups are ready.
}
return (
-
- {/* Simulated Panel List for "Segmentation" */}
-
-
- {/* Segmentation Tools */}
-
-
- Measurements
-
-
-
-
2024-Jan-01
-
- Study title lorem ipsum
-
-
+
+ {() => (
+
+ {/* Simulated Panel List for "Segmentation" */}
+
+
+ {/* Segmentation Tools */}
+
+
+ Measurements
+
+
+
+
2024-Jan-01
+
+ Study title lorem ipsum
+
+
-
-
-
-
-
-
-
- {roiToolsGroup.items.map((item, index) => {
- const compositeId = `${roiToolsGroup.type}-${item.id}-panel`; // Ensure unique composite ID
- return (
- handleAction(compositeId, action)}
- isSelected={selectedRowId === compositeId}
- onSelect={() => handleRowSelect(compositeId)}
- />
- );
- })}
-
-
-
+
+
+
+
+
+
+
+ {roiToolsGroup.items.map((item, index) => {
+ const compositeId = `${roiToolsGroup.type}-${item.id}-panel`; // Ensure unique composite ID
+ return (
+ handleAction(compositeId, action)}
+ isSelected={selectedRowId === compositeId}
+ onSelect={() => handleRowSelect(compositeId)}
+ />
+ );
+ })}
+
+
+
- {/* Additional Findings */}
-
-
- Additional Findings
-
-
-
-
-
-
-
-
+ {/* Additional Findings */}
+
+
+ Additional Findings
+
+
+
+
+
+
+
+
+ )}
+
);
}
diff --git a/platform/docs/src/pages/patterns/patterns-segmentation.tsx b/platform/docs/src/pages/patterns/patterns-segmentation.tsx
index 49f6c07e2..04d0088f7 100644
--- a/platform/docs/src/pages/patterns/patterns-segmentation.tsx
+++ b/platform/docs/src/pages/patterns/patterns-segmentation.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import React, { useState } from 'react';
import { DataRow } from '../../../../ui-next/src/components/DataRow';
@@ -34,6 +36,7 @@ import { Label } from '../../../../ui-next/src/components/Label';
import { Input } from '../../../../ui-next/src/components/Input';
import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs';
import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data';
+import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
interface DataItem {
id: number;
@@ -74,232 +77,234 @@ export default function SegmentationPanel() {
return (
-
- {/* Segmentation Tools */}
-
-
- Segmentation Tools
-
-
-
-
-
+
+
+ {/* Segmentation Tools */}
+
+
+ Segmentation Tools
+
+
+
+
+
- {/* Segmentation List */}
-
-
- Segmentation List
-
-
-
- {/* Header Controls */}
-
-
-
-
-
-
-
-
- Create New Segmentation
-
-
- Manage Current Segmentation
-
-
- Remove from Viewport
-
-
-
- Rename
-
-
-
-
- Export & Download
-
-
-
- Export DICOM SEG
- Download DICOM SEG
- Download DICOM RTSTRUCT
-
-
-
-
-
-
- Delete
-
-
-
-
-
-
-
- {/* Appearance Settings */}
-
-
-
-
- Appearance Settings
-
-
-
-
-
- {/* Display Label with Selected Tab */}
-
Show: {selectedTab}
- {/* Tabs Controls */}
-
+
+ Segmentation List
+
+
+
+ {/* Header Controls */}
+
+
+
+
- {/* Opacity Slider */}
-
-
-
-
-
- {/* Border Slider */}
-
-
-
-
-
- {/* Sync Changes Switch */}
-
-
-
-
-
- {/* Display Inactive Segmentations Switch */}
-
-
-
-
- {/* Additional Opacity Slider */}
-
-
-
-
-
-
-
-
- {/* Action Buttons */}
-
-
-
-
-
+
+
+
+
+
+
+ Create New Segmentation
+
+
+ Manage Current Segmentation
+
+
+ Remove from Viewport
+
+
+
+ Rename
+
+
+
+
+ Export & Download
+
+
+
+ Export DICOM SEG
+ Download DICOM SEG
+ Download DICOM RTSTRUCT
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
- {/* Data Rows */}
-
- {organSegmentationGroup.items.map((item, index) => {
- const compositeId = `${organSegmentationGroup.type}-${item.id}-panel`; // Ensure unique composite ID
- return (
- handleAction(compositeId, action)}
- isSelected={selectedRowId === compositeId}
- onSelect={() => handleRowSelect(compositeId)}
- />
- );
- })}
-
-
-
-
+ {/* Appearance Settings */}
+
+
+
+
+ Appearance Settings
+
+
+
+
+
+ {/* Display Label with Selected Tab */}
+
Show: {selectedTab}
+ {/* Tabs Controls */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Opacity Slider */}
+
+
+
+
+
+ {/* Border Slider */}
+
+
+
+
+
+ {/* Sync Changes Switch */}
+
+
+
+
+
+ {/* Display Inactive Segmentations Switch */}
+
+
+
+
+ {/* Additional Opacity Slider */}
+
+
+
+
+
+
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+ {/* Data Rows */}
+
+ {organSegmentationGroup.items.map((item, index) => {
+ const compositeId = `${organSegmentationGroup.type}-${item.id}-panel`; // Ensure unique composite ID
+ return (
+ handleAction(compositeId, action)}
+ isSelected={selectedRowId === compositeId}
+ onSelect={() => handleRowSelect(compositeId)}
+ />
+ );
+ })}
+
+
+
+
+
);
diff --git a/platform/docs/src/pages/patterns/patterns-tmtv.tsx b/platform/docs/src/pages/patterns/patterns-tmtv.tsx
index 0040971b5..b792a8fe4 100644
--- a/platform/docs/src/pages/patterns/patterns-tmtv.tsx
+++ b/platform/docs/src/pages/patterns/patterns-tmtv.tsx
@@ -1,25 +1,12 @@
import React, { useState } from 'react';
import { Button } from '../../../../ui-next/src/components/Button';
-import {
- Select,
- SelectGroup,
- SelectValue,
- SelectTrigger,
- SelectContent,
- SelectLabel,
- SelectItem,
- SelectSeparator,
- SelectScrollUpButton,
- SelectScrollDownButton,
-} from '../../../../ui-next/src/components/Select';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
- DropdownMenuLabel,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
@@ -39,9 +26,9 @@ import { Slider } from '../../../../ui-next/src/components/Slider';
import { Switch } from '../../../../ui-next/src/components/Switch';
import { Label } from '../../../../ui-next/src/components/Label';
import { Input } from '../../../../ui-next/src/components/Input';
-import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../ui-next/src/components/Tabs';
-
-import { ChevronDownIcon } from '@radix-ui/react-icons';
+import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs';
+import { TooltipProvider } from '../../../../ui-next/src';
+import BrowserOnly from '@docusaurus/BrowserOnly';
interface DataItem {
id: number;
@@ -88,316 +75,320 @@ export default function TMTVPatterns() {
}
return (
-
-
-
- {/* Segmentation Tools */}
-
-
- Segmentation Tools
-
-
-
-
-
- {/* Segmentation List */}
-
-
- Segmentation List
-
-
- {/* Appearance Settings */}
-
-
-
-
- Appearance Settings
-
+
+ {() => (
+
+
+
+ {/* Segmentation Tools */}
+
+
+ Segmentation Tools
-
-
- {/* Display Label with Selected Tab */}
-
Show: {selectedTab}
- {/* Tabs Controls */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Opacity Slider */}
-
-
-
-
-
- {/* Border Slider */}
-
-
-
-
-
- {/* Sync Changes Switch */}
-
-
-
-
-
- {/* Display Inactive Segmentations Switch */}
-
-
-
-
- {/* Additional Opacity Slider */}
-
-
-
-
-
-
+
- {/* TMTV1 Group */}
-
-
-
- {/* Left Group: DropdownMenu and TMTV1 Label */}
-
-
-
+ {/* Segmentation List */}
+
+
+ Segmentation List
+
+
+ {/* Appearance Settings */}
+
+
+
+
+ Appearance Settings
+
+
+
+
+
+ {/* Display Label with Selected Tab */}
+
Show: {selectedTab}
+ {/* Tabs Controls */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Opacity Slider */}
+
+
+
+
+
+ {/* Border Slider */}
+
+
+
+
+
+ {/* Sync Changes Switch */}
+
+
+
+
+
+ {/* Display Inactive Segmentations Switch */}
+
+
+
+
+ {/* Additional Opacity Slider */}
+
+
+
+
+
+
+
+
+ {/* TMTV1 Group */}
+
+
+
+ {/* Left Group: DropdownMenu and TMTV1 Label */}
+
+
+
+
+
+
+
+
+ Add Segment
+
+
+
+
+ Remove from Viewport
+
+
+
+ Rename
+
+
+
+ Hide or Show all Segments
+
+
+
+
+ Export & Download
+
+
+
+ Export DICOM SEG
+ Download DICOM SEG
+ Download DICOM RTSTRUCT
+
+
+
+
+
+
+ Delete
+
+
+
+
TMTV1 Segmentation
+
+
-
-
-
-
- Add Segment
-
-
-
-
- Remove from Viewport
-
-
-
- Rename
-
-
-
- Hide or Show all Segments
-
-
-
-
- Export & Download
-
-
-
- Export DICOM SEG
- Download DICOM SEG
- Download DICOM RTSTRUCT
-
-
-
-
-
-
- Delete
-
-
-
-
TMTV1 Segmentation
-
-
-
-
-
-
-
- {/* Data Rows for TMTV1 */}
-
- {tmvGroup.items.map((item, index) => {
- const compositeId = `${tmvGroup.type}-${item.id}-panel`; // Ensure unique composite ID
- return (
- handleAction(compositeId, action)}
- isSelected={selectedRowId === compositeId}
- onSelect={() => handleRowSelect(compositeId)}
- />
- );
- })}
-
-
-
- {/* TMTV2 Group */}
-
-
-
-
-
-
-
-
-
-
-
- Add Segment
-
-
-
-
- Remove from Viewport
-
-
-
- Rename
-
-
-
- Hide or Show all Segments
-
-
-
-
- Export & Download
-
-
-
- Export DICOM SEG
- Download DICOM SEG
- Download DICOM RTSTRUCT
-
-
-
-
-
-
- Delete
-
-
-
+
+
+
+
+ {/* Data Rows for TMTV1 */}
+
+ {tmvGroup.items.map((item, index) => {
+ const compositeId = `${tmvGroup.type}-${item.id}-panel`; // Ensure unique composite ID
+ return (
+ handleAction(compositeId, action)}
+ isSelected={selectedRowId === compositeId}
+ onSelect={() => handleRowSelect(compositeId)}
+ />
+ );
+ })}
+
+
+
+ {/* TMTV2 Group */}
+
+
+
+
+
+
+
+
+
+
+
+ Add Segment
+
+
+
+
+ Remove from Viewport
+
+
+
+ Rename
+
+
+
+ Hide or Show all Segments
+
+
+
+
+ Export & Download
+
+
+
+ Export DICOM SEG
+ Download DICOM SEG
+ Download DICOM RTSTRUCT
+
+
+
+
+
+
+ Delete
+
+
+
-
TMTV2 Segmentation
-
-
-
-
-
-
-
- {/* Data Rows for TMTV2 */}
-
- {tmv2Group.items.map((item, index) => {
- const compositeId = `${tmv2Group.type}-${item.id}-panel`; // Ensure unique composite ID
- return (
-
handleAction(compositeId, action)}
- isSelected={selectedRowId === compositeId}
- onSelect={() => handleRowSelect(compositeId)}
- />
- );
- })}
+ TMTV2 Segmentation
+
+
+
+
+
+
+
+ {/* Data Rows for TMTV2 */}
+
+ {tmv2Group.items.map((item, index) => {
+ const compositeId = `${tmv2Group.type}-${item.id}-panel`; // Ensure unique composite ID
+ return (
+ handleAction(compositeId, action)}
+ isSelected={selectedRowId === compositeId}
+ onSelect={() => handleRowSelect(compositeId)}
+ />
+ );
+ })}
+
+
+
+ {/* Footer or Additional Information */}
+
+ TMTV
+ 21.555 mL
- {/* Footer or Additional Information */}
-
- TMTV
- 21.555 mL
-
-
-
-
-
-
+
+
+
+ )}
+
);
}
diff --git a/platform/docs/src/pages/versions.js b/platform/docs/src/pages/versions.js
new file mode 100644
index 000000000..54e777994
--- /dev/null
+++ b/platform/docs/src/pages/versions.js
@@ -0,0 +1,63 @@
+import React from 'react';
+import Layout from '@theme/Layout';
+import Link from '@docusaurus/Link';
+
+export default function Versions() {
+ const versions = [
+ {
+ version: 'Version 1',
+ status: 'deprecated',
+ description: 'Built with Meteor as a full stack application.',
+ },
+ {
+ version: 'Version 2',
+ status: 'deprecated',
+ description: 'Front end image viewer built with React',
+ },
+ {
+ version: 'Version 3.x-beta',
+ status: 'master branch',
+ description: 'With latest bug fixes and features but not yet released (released under beta)',
+ },
+ {
+ version: 'Version 3.x',
+ status: 'release branch',
+ description: 'Released version of the OHIF platform which is more stable and tested',
+ },
+ ];
+
+ return (
+
+
+
Versions
+
+
+ As we are increasing the efforts to make the OHIF platform more robust and up-to-date with
+ the latest software engineering practices, here we are listing the versions of the OHIF
+ platform that we are currently supporting, and the versions that have been deprecated.
+
+
+
Product Version
+
+
Currently we have four product versions:
+
+
+ {versions.map((item, index) => (
+ -
+ {item.version} ({item.status}): {item.description}
+
+ ))}
+
+
+
+ You can read more about the differences between the versions in the{' '}
+ development section of the
+ documentation to understand which version is more suitable for your use case.
+
+
+
+ );
+}
diff --git a/platform/docs/src/pages/versions.md b/platform/docs/src/pages/versions.md
deleted file mode 100644
index 7f985e56f..000000000
--- a/platform/docs/src/pages/versions.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Versions
-
-As we are increasing the efforts to make the OHIF platform more robust and up-to-date
-with the latest software engineering practices, here we are listing the versions of
-the OHIF platform that we are currently supporting, and the versions that have been
-deprecated.
-
-## Product Version
-
-
-Currently we have three product versions:
-
-- Version 1 (deprecated): Built with Meteor as a full stack application.
-- Version 2 (deprecated): Front end image viewer built with React
-- Version 3.x-beta (master branch): With latest bug fixes and features but not yet released (released under beta)
-- Version 3.x (release branch): Released version of the OHIF platform which is more stable and tested
-
-
-You can read more about the differences between the versions in the [development section](../../docs/development/getting-started#branches) of the documentation
-to understand which version is more suitable for your use case.
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/_category_.json
deleted file mode 100644
index c1a514219..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "I Want to ...",
- "position": 8
-}
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-logo-to-the-viewer.md b/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-logo-to-the-viewer.md
deleted file mode 100644
index 53942222e..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-logo-to-the-viewer.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Add a Logo to the Viewer
-
-The OHIF Framework provides [**ohif-header**](https://github.com/OHIF/Viewers/tree/master/Packages/ohif-header) package to add a header into application layout. **ohif-header** package is designed as a [custom block helper](http://blazejs.org/api/spacebars.html#Custom-Block-Helpers) named **header** to define your own header context.
-
-If you would like to add SVG logo to header please follow these steps.
-
-1. Add your SVG logo into public folder.
-
-2. Add **header** content block which will be located on the top of the application layout into your application's main template. You can also pass some parameters to **header** content block to customize the header.
-
- * headerClasses: the list of classes which will be applied to header element
- * brandHref: the url of the logo to link
-
-
-``` html
-{{#header headerClasses="header-big bg-blue" brandHref="your logo link"}}
-...
-{{/header}}
-```
-
-
-1. Create a section called as **brand** in **header** content block and add your logo content which is displayed on the left side of the header as default into section **brand**.
-
-
- ``` html
- {{#header}}
- {{#section "brand"}}
-
-
-
-
-
Logo Text
- {{/section}}
- {{/header}}
- ```
-
-
- For example, see how it works in [OHIF Viewer](https://github.com/OHIF/Viewers/blob/master/OHIFViewer/client/components/ohifViewer/ohifViewer.html#L2)
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-tool-to-the-viewer.md b/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-tool-to-the-viewer.md
deleted file mode 100644
index a55a32628..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/I-want-to/add-a-tool-to-the-viewer.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# Add a Tool to the Viewer
-
-To add a tool to the Viewer there are a few steps:
-
-1. Add the tool itself to the repository.
-
- If you're using something from Cornerstone Tools you can skip this step.
-
- Some examples of custom tools can be found in the lesion tracker: https://github.com/OHIF/Viewers/tree/master/Packages/ohif-lesiontracker/client/compatibility
-
-2. Add the toolbar button itself to the array of tools in the Toolbar:
- https://github.com/OHIF/Viewers/blob/574a6d02b090b8b2f020430c5919f8377b8316c6/OHIFViewer/client/components/toolbarSection/toolbarSection.js
-
-3. **A:** Add it to the toolManager (if it's a tool, such as length / angle):
-
- https://github.com/OHIF/Viewers/blob/574a6d02b090b8b2f020430c5919f8377b8316c6/Packages/lesiontracker/client/tools.js#L2
-
- ** --- OR --- **
-
- **B:** Add it to the functionList if it's a command (e.g. toggle CINE play, or Invert the current viewport):
-
- https://github.com/OHIF/Viewers/blob/574a6d02b090b8b2f020430c5919f8377b8316c6/OHIFViewer/client/components/viewer/viewer.js#L12
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/Standalone-Installation-Instructions.md b/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/Standalone-Installation-Instructions.md
deleted file mode 100644
index fb6213184..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/Standalone-Installation-Instructions.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# OHIF Standalone Installation Instructions For Windows Server 2016:
-
-**Note: Turn-Off Windows Defender Realtime protection during this process**
-
-1. Install Chocolatey
-
- a. Go to this URL for instructions:
-
- https://chocolatey.org/install#install-with-cmdexe
-
- b. Execute this command in the cmd line as Admin:
- ```
- @"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
- ```
-
- c. Type `choco -?` to ensure the installation is OK.
-2. Install Meteor
-
- a. Type the command: `choco install meteor`
-3. Install node.js
-
- a. Download MSI from: https://nodejs.org/en/download/
-4. Install MongoDB
-
- a. Download MSI from:
-https://www.mongodb.com/download-center/community
-5. Install Git
-
- a. https://git-scm.com/download/win
-6. Download the OHIF Viewer repository from GitHub, or use Git to clone it (recommended)
-
- a. GitHub Repo: https://github.com/OHIF/Viewers
-
- b. GIT Clone command: `git clone https://github.com/OHIF/Viewers`
-7. Set the Meteor Packages folder environment variable
-
- a. Go to Advanced System Settings
-
- b. Under Advanced, click on the "Environment Variables..." button
-
- c. Under System Variables, click "New..."
-
- d. Set the following:
-
- i. Variable name: METEOR_PACKAGE_DIRS
-
- ii. Variable Value:
- example: C:\OHIF\Viewers\Packages
-8. Using the command line, navigate/cs to the standalone viewer folder, example: `cd C:\OHIF\Viewers\StandaloneViewer\StandaloneViewer`
-9. run the command: `meteor npm install`
-10. Run the command: `meteor`
-
- a. if you get this error "Error: EPERM: operation not permitted, unlink" Or,
-
- b. You feel the build is stale for a very long time > 10min with no visual indication, restart the cmd prompt and repeat this step (`meteor`)
-
-# Troubleshooting:
-
-1. If you get an error: Error: EPERM: operation not permitted, unlink `c:\xxxxx`
-
- a. https://github.com/phoenixframework/phoenix/issues/2464
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/_category_.json
deleted file mode 100644
index 787010486..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Extras",
- "position": 13
-}
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/environment-installations.md b/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/environment-installations.md
deleted file mode 100644
index 09dd39744..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/environment-installations.md
+++ /dev/null
@@ -1,345 +0,0 @@
----
-title: Development Environment Installation
----
-
-**Development Environment Installation (Ubuntu)**
-
-**CONFIDENTIAL DOCUMENT**
-
-This is a confidential document and property of Radical Imaging LLC. It shall not be transmitted, copied or sent to anyone without prior authorization.
-
-**Index**
-
-[[TOC]]
-
-
-
-| Acronyms and definitions |
- |
-
-
-| Acronym/Definition |
-Meaning |
-
-
-| N/A |
-N/A |
-
-
-
-
-
-
-| Referenced Documents |
- |
-
-
-| Doc No |
-Doc Title |
-
-
-| DOC00001 |
-Radical Imaging Official Documents Template |
-
-
-
-
-# PURPOSE AND OBJECTIVES
-
-The purpose of this document is to describe the development environment installation of OHIF viewer/Lesion Tracker on Linux Ubuntu.
-
-# DEVELOPMENT ENVIRONMENT INSTALLATION
-
-## Docker
-
-Docker is an open source software platform to create, deploy and manage virtualized application containers on common operating systems, with several allied tools.
-
-To install Docker, follow the instructions below:
-
-1. Open a terminal, and update the apt package index, by running the following command:
-
-sudo apt-get update
-
-Expected result:
-
-
-
-2. Install packages to allow apt to use a repository over HTTPS, by running the following command:
-
-sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
-
-Expected result:
-
-
-
-3. Add Docker’s official GPG key, by running the following command:
-
-curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
-
-Expected result:
-
-
-
-4. Verify that you now have the key with the fingerprint **9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88**, by searching for the last 8 characters of the fingerprint after running the following command:
-
-sudo apt-key fingerprint 0EBFCD88
-
-Expected result:
-
-
-
-5. Use the following command to set up the stable repository.
-
-sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
-
-Expected result:
-
-
-
-6. Install the latest version of Docker CE, by running the following command:
-
-sudo apt-get install docker-ce
-
-Expected result:
-
-
-
-7. Verify that Docker CE is installed correctly by running the hello-world image:
-
-sudo docker run hello-world
-
-Expected result:
-
-
-
-**Note**: Additional information about Docker installation on Linux Ubuntu OS can be foud at [https://docs.docker.com/install/linux/docker-ce/ubuntu/](https://docs.docker.com/install/linux/docker-ce/ubuntu/).
-
-## DCM4CHE
-
-Dcm4che ([https://www.dcm4che.org/](https://www.dcm4che.org/)) is a collection of open source applications and utilities for healthcare.
-
-To install Dcm4che, follow the instructions below:
-
-1. Before start, you need to create the following folders on your *Home *directory:
-
-* DCM4CHEE
-
-* dcm4chee-arc
-
-* db
-
-* ldap
-
-* slapd.d
-
-* storage
-
-* wildfly
-
-
-
-2. Create the following files on the DCM4CHEE folder, with the correspondent contents:
-
-docker-compose.env
-
-```
-STORAGE_DIR=/storage/fs1
-POSTGRES_DB=pacsdb
-POSTGRES_USER=pacs
-POSTGRES_PASSWORD=pacs
-```
-
-
-docker-compose.yml
-
-```
-version: "3"
-services:
-ldap:
-image: dcm4che/slapd-dcm4chee:2.4.44-14.1
-logging:
-driver: json-file
-options:
-max-size: "10m"
-ports:
-- "389:389"
-env_file: docker-compose.env
-volumes:
-- /etc/localtime:/etc/localtime:ro
-- /etc/timezone:/etc/timezone:ro
-- ~/dcm4chee-arc/ldap:/var/lib/ldap
-- ~/dcm4chee-arc/slapd.d:/etc/ldap/slapd.d
-db:
-image: dcm4che/postgres-dcm4chee:10.4-14
-logging:
-driver: json-file
-options:
-max-size: "10m"
-ports:
-- "5432:5432"
-env_file: docker-compose.env
-volumes:
-- /etc/localtime:/etc/localtime:ro
-- /etc/timezone:/etc/timezone:ro
-- ~/dcm4chee-arc/db:/var/lib/postgresql/data
-arc:
-image: dcm4che/dcm4chee-arc-psql:5.14.1
-logging:
-driver: json-file
-options:
-max-size: "10m"
-ports:
-- "8080:8080"
-- "8443:8443"
-- "9990:9990"
-- "11112:11112"
-- "2575:2575"
-env_file: docker-compose.env
-environment:
-WILDFLY_CHOWN: /opt/wildfly/standalone /storage
-WILDFLY_WAIT_FOR: ldap:389 db:5432
-depends_on:
-- ldap
-- db
-volumes:
-- /etc/localtime:/etc/localtime:ro
-- /etc/timezone:/etc/timezone:ro
-- ~/dcm4chee-arc/wildfly:/opt/wildfly/standalone
-- ~/dcm4chee-arc/storage:/storage
-```
-
-Files/Folder will become like the image above:
-
-
-
-1. Open a terminal inside DCM4CHE folder, and run the following command:
-
-sudo snap install docker
-
-Expected result:
-
-
-
-4. Still inside the DCM4CHE folder, run the following command:
-
-sudo docker-compose -p dcm4chee up -d
-
-Expected result:
-
-
-
-1. The following commands can be used to control DCM4CHE:
-
-Stop all 3 containers: docker-compose -p dcm4chee stop
-
-Start all 3 containers again: docker-compose -p dcm4chee start
-
-Stop and delete all 3 containers: docker-compose -p dcm4chee down
-
-2. Now it is necessary to import some DICOM studies to DCM4CHE. Before importing these studies, you will need to download them. To do so, access the address [http://34.224.187.57:3000/studylist](http://34.224.187.57:3000/studylist), right click one or more studies, and select the option Export:
-
-
-
-3. Confirm the study export on the modal window that will appear:
-
-
-
-4. The study(ies) export will begin. You can follow the exporting progress:
-
-
-
-5. After the exporting progress, the study(ies) will be downloaded in a file called "studies.zip". Create a folder named “Studies” on your Home folder, and extract this and any other studies you download in this folder:
-
-
-
-6. Open the terminal, go to the DCM4CHE folder, and run the following command in order to send the studies to DCM4CHE:
-
-docker run -v ~/Studies/:/tmp --rm --network=dcm4chee_default dcm4che/dcm4che-tools:5.14.0 storescu -cDCM4CHEE@arc:11112 /tmp
-
-Expected result:
-
-
-
-7. Now, go to your browser and access the URL [http://localhost:8080/dcm4chee-arc/ui2/](http://localhost:8080/dcm4chee-arc/ui2/). Once opened, click on the refresh icon on the most right:
-
-
-
-8. The studies received by DCM4CHE will be shown:
-
-
-
-9. Open the terminal, go to your Home folder, and run the following command in order to clone viewers repository to your local:
-
-git clone https://github.com/OHIF/Viewers.git
-
-Expected result:
-
-
-
-10. Then, enter the Viewers folder, and run the following command in order to make sure that you are in master branch:
-
-git checkout master
-
-Expected result:
-
-
-
-11. Now go to OHIFViewer folder and run the following command:
-
-meteor npm install
-
-Expected result:
-
-
-
-12. Still on the OHIFViewer folder, run the following command:
-
-./bin/dcm4cheeDICOMWeb.sh
-
-Expected result:
-
-
-
-13. Now, go to your browser and access the URL [http://localhost:3000](http://localhost:3000). Once opened, change the Study Date filter to start on the year 2000:
-
-
-
-14. The imported studies will be show. Double click a study to open it:
-
-
-
-## Meteor
-
-Meteor is a JavaScript web framework that allows for rapid prototyping and produces cross-platform code.
-
-To install Meteor, follow the instructions below:
-
-**_Remark_***: This guide covers the Linux Ubuntu version. Installation instructions can be different on other operating systems.*
-
-1. Open a terminal, and run the following command:
-
-curl https://install.meteor.com/ | sh
-
-Expected result:
-
-
-
-## Starting OHIF Viewer after system restart
-
-These are the steps to run OHIFVIewer after system restart:.
-
-1. Open terminal
-
-2. Access the DCM4CHEE folder, on your Home folder
-
-3. Run the following command in order to start docker containers:
-
-docker-compose -p dcm4chee start
-
-4. Access the Viewers/OHIFViewer folder, on your Home folder
-
-5. Run the following command in order to run OHIF Viewer:
-
-./bin/dcm4cheeDICOMWeb.sh
-
-6. Access the address [http://localhost:3000](http://localhost:3000)
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/usage.md b/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/usage.md
deleted file mode 100644
index cf5932ff9..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/OHIF-Viewer/usage.md
+++ /dev/null
@@ -1,96 +0,0 @@
----
-title: Standalone Viewer
----
-
-# Standalone Viewer
-
-## Quick Start
-
-Install dependencies:
-
-```bash
-meteor npm install
-```
-
-Run the application:
-
-```bash
-METEOR_PACKAGE_DIRS="../../Packages" ROOT_URL=http://localhost:3000 meteor
-```
-
-Open your web browser and navigate to one of the following URLs to test the standalone viewer application:
-
-```bash
-http://localhost:3000/testId
-```
-
-Or, to load DICOMs:
-```bash
-http://localhost:3000/testDICOMs
-```
-
-### To Build for the Client
-
-It is possible to build this standalone viewer to run as a client-only bundle of HTML, JavaScript, and CSS.
-
-1. First, install [meteor-build-client-fixed2](https://www.npmjs.com/package/meteor-build-client-fixed2).
-
- ````bash
- sudo npm install -g meteor-build-client-fixed2
- ````
-
-2. Next, build the client bundle into an output folder ("myOutputFolder") with a base URL ("localhost:3000"). In production, this would be the URL where the Viewer is available.
-
- ````
- METEOR_PACKAGE_DIRS="../../Packages" meteor-build-client-fixed2 ../myOutputFolder -u localhost:3000 --legacy
- ````
-
-3. Test the bundled client-side package locally.
-
- Note: You will need to have Python installed to run the test server for this case. It is not a typical simple HTTP server. The bundled script redirects all URLs following the base URL to index.html. It will then use the routes defined in your application to handle the URL parameters.
-
- In our case, this means it will request a JSON file at baseURL/api/[id parameter].
-
- So if you navigate to http://localhost:3000/sampleJPEG.json the application will retrieve the JSON from http://localhost:3000/api/sampleJPEG.json and use it to populate the viewer. If something appears to be broken, make sure you retrieve a JSON file at the /api URL.
-
- Create the api folder for your data
-
- ````bash
- cd myOutputFolder
- mkdir api
- ````
-
- Copy your data into the folder
-
- ````bash
- cp ../etc/sample* api/
- ````
-
- Run the server
-
- ```` bash
- python ../etc/redirectingSimpleServer.py
- ````
-
- Open your web browser and navigate to http://localhost:3000/sampleJPEG.json or http://localhost:3000/sampleDICOM.json
-
- ## Authorization Header
-
- A ```token``` fragment parameter can be specified. If present this value will be used for http bearer authorization when making requests for the above JSON, and when retrieving images using the Cornerstone Image Loaders.
-
- Example :
-
- ```
- http://localhost:3000/sampleDICOM.json#token=1a2b3c4d
- ```
-
-
-### Testing the Sample client-only build
-For the sake of simplicity we have also included a pre-built client-only version of the standalone viewer, which can be found in the SampleClientOnlyBuild folder.
-
-You can test this with:
-
- ```` bash
- cd SampleClientOnlyBuild
- python ../etc/redirectingSimpleServer.py
- ````
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/README.md b/platform/docs/versioned_docs/version-1.0-deprecated/README.md
deleted file mode 100644
index b7fe4256a..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/README.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-id: Introduction
-slug: /
-sidebar_position: 1
----
-
-##### Looking for your Deploy Preview? -
Deploy Preview for Viewer
-
-The [Open Health Imaging Foundation](https://www.ohif.org) is developing an open
-source framework for constructing web-based medical imaging applications. The
-application framework is built using modern HTML / CSS / JavaScript and uses
-[Cornerstone](https://cornerstonejs.org/) at its core to display and manipulate
-medical images. It is built with Meteor, a Node.js-based full-stack JavaScript
-platform.
-
-This documentation concerns the OHIF framework itself and its three example
-applications: the OHIF Viewer, Lesion Tracker, and the Standalone Viewer.
-
-## The **OHIF Viewer**: A general purpose DICOM Viewer ([demo](http://viewer.ohif.org/))
-
-
-
-The Open Health Imaging Foundation intends to provide a simple general purpose
-DICOM Viewer which can be easily extended for specific uses. The primary purpose
-of the OHIF Viewer is to serve as a testing ground for the underlying packages
-and the [Cornerstone](https://cornerstonejs.org/) family of libraries.
-
-## **Lesion Tracker**: An oncology-focused imaging application ([demo](http://lesiontracker.ohif.org/))
-
-
-
-The Lesion Tracker is designed to facilitate quantitative assessments of tumour
-burden over time. It is similar in scope to the ePAD Imaging Platform
-(https://epad.stanford.edu/), developed at Stanford Medicine.
-
-## Study List & DICOM Connectivity
-
-
-
-The solution provides a study list and other resources for connecting to PACS
-and other Image Archives through standard communication approaches (DICOM Web,
-DICOM Messages).
-
-## Standalone Viewer ([demo](https://ohif-viewer.s3-website.eu-central-1.amazonaws.com/?url=https://raw.githubusercontent.com/OHIF/Viewers/master/StandaloneViewer/etc/sampleDICOM.json))
-
-The Standalone Viewer offers only the client-side portions of the OHIF Viewer
-with the Study List pages removed. This single-page viewer can be hosted as a
-static site (e.g. on Amazon S3, Azure Blob Storage, or Github Pages), and easily
-integrated with existing back-end DICOM storage systems. Alternative
-[Cornerstone](https://cornerstonejs.org/) Image Loaders can be included to allow
-your viewer to support non-DICOM objects (e.g. PNG, JPEG).
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Assessment_Progress.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Assessment_Progress.png
deleted file mode 100644
index 7d4021cf7..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Assessment_Progress.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Association_Dialog.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Association_Dialog.png
deleted file mode 100644
index ec21eea3b..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Association_Dialog.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Audit_Trails.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Audit_Trails.png
deleted file mode 100644
index 2296825b3..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Audit_Trails.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CINE.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CINE.png
deleted file mode 100644
index 612898adb..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CINE.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Change_Password.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Change_Password.png
deleted file mode 100644
index ac7f7cff2..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Change_Password.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CompareMode.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CompareMode.png
deleted file mode 100644
index d46678e07..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_CompareMode.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Comparison.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Comparison.png
deleted file mode 100644
index c9c91b914..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Comparison.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Configuration_Menu.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Configuration_Menu.png
deleted file mode 100644
index 8aaac07ac..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Configuration_Menu.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Conformance_Check.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Conformance_Check.png
deleted file mode 100644
index 409ddf5a2..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Conformance_Check.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Download.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Download.png
deleted file mode 100644
index 5ee99d709..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Download.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Ellipse.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Ellipse.png
deleted file mode 100644
index cd21fe816..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Ellipse.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipH.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipH.png
deleted file mode 100644
index e79adb120..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipH.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipV.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipV.png
deleted file mode 100644
index 99404b9ae..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_FlipV.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Forgot_Password.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Forgot_Password.png
deleted file mode 100644
index 4114d898f..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Forgot_Password.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Generate_Report.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Generate_Report.png
deleted file mode 100644
index b224172b2..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Generate_Report.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_HUD_Panel.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_HUD_Panel.png
deleted file mode 100644
index ca8d6aa4b..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_HUD_Panel.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Image_Viewer.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Image_Viewer.png
deleted file mode 100644
index 37d8a8201..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Image_Viewer.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_After_Prerequisites.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_After_Prerequisites.png
deleted file mode 100644
index d0e808ea2..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_After_Prerequisites.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Desktop_Shortcuts.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Desktop_Shortcuts.png
deleted file mode 100644
index c5fb9c3f3..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Desktop_Shortcuts.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Final.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Final.png
deleted file mode 100644
index 720f532ea..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Final.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Finish.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Finish.png
deleted file mode 100644
index 5656990eb..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Finish.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Initial.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Initial.png
deleted file mode 100644
index 90457fa2b..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Initial.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Installation.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Installation.png
deleted file mode 100644
index 78fa9d246..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Installation.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Uninstall.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Uninstall.png
deleted file mode 100644
index 2935db087..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Launch_Uninstall.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_License_Aggrement.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_License_Aggrement.png
deleted file mode 100644
index db2e2465e..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_License_Aggrement.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Prerequisites.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Prerequisites.png
deleted file mode 100644
index b0fbe2ed4..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Prerequisites.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Select_Location.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Select_Location.png
deleted file mode 100644
index 9d216e655..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Select_Location.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Services.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Services.png
deleted file mode 100644
index cef807778..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Services.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Successful.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Successful.png
deleted file mode 100644
index 15d9a9da9..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Successful.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Uninstall.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Uninstall.png
deleted file mode 100644
index 4d9060f82..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Installer_Uninstall.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Invert.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Invert.png
deleted file mode 100644
index e4f2e81d5..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Invert.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Key_Timepoints.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Key_Timepoints.png
deleted file mode 100644
index 79b02e63a..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Key_Timepoints.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Keyboard_Shortcuts.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Keyboard_Shortcuts.png
deleted file mode 100644
index ae00ff995..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Keyboard_Shortcuts.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Login.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Login.png
deleted file mode 100644
index 58250b654..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Login.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Logout.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Logout.png
deleted file mode 100644
index 04c488fa6..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Logout.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Magnify.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Magnify.png
deleted file mode 100644
index c96db713d..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Magnify.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Measurements.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Measurements.png
deleted file mode 100644
index a9f0ba537..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Measurements.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Need_Account.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Need_Account.png
deleted file mode 100644
index 3493b4d6c..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Need_Account.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Select_Location.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Select_Location.png
deleted file mode 100644
index 7eaa3e932..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Select_Location.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Tool.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Tool.png
deleted file mode 100644
index 7c2951246..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_NonTarget_Tool.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Patient.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Patient.png
deleted file mode 100644
index 13d38194b..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Patient.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Study.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Study.png
deleted file mode 100644
index c2a7071e9..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Select_Study.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Study.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Study.png
deleted file mode 100644
index b302d28eb..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Delete_Study.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Drag_and_Drop.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Drag_and_Drop.png
deleted file mode 100644
index 3cbc0c96d..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Drag_and_Drop.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Start_Upload.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Start_Upload.png
deleted file mode 100644
index 2fa7850d6..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Start_Upload.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload.png
deleted file mode 100644
index b6f34a910..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload_Result.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload_Result.png
deleted file mode 100644
index b545b1c69..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Orthanc_Upload_Result.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Pan.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Pan.png
deleted file mode 100644
index 161a4f17a..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Pan.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Quick_Switch_Tool.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Quick_Switch_Tool.png
deleted file mode 100644
index 478369c0e..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Quick_Switch_Tool.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Remove_Associate.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Remove_Associate.png
deleted file mode 100644
index 4d3f580c6..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Remove_Associate.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Report_PDF.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Report_PDF.png
deleted file mode 100644
index cb65fd0c3..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Report_PDF.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Reset.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Reset.png
deleted file mode 100644
index e1e94fed2..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Reset.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Response_Criteria.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Response_Criteria.png
deleted file mode 100644
index 01662fe12..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Response_Criteria.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Rotate_Right.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Rotate_Right.png
deleted file mode 100644
index 246d3862a..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Rotate_Right.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save1.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save1.png
deleted file mode 100644
index 040617fd0..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save1.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save2.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save2.png
deleted file mode 100644
index 615156f4c..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Save2.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Scroll.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Scroll.png
deleted file mode 100644
index f8e0e3e54..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Scroll.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Select_Associate.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Select_Associate.png
deleted file mode 100644
index 8916b0876..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Select_Associate.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Server_Info.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Server_Info.png
deleted file mode 100644
index 4944121aa..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Server_Info.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Multiple.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Multiple.png
deleted file mode 100644
index 6a0f757f6..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Multiple.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Single.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Single.png
deleted file mode 100644
index a9ee8eace..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StackScroll_Single.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Studies_Panel.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Studies_Panel.png
deleted file mode 100644
index ad170a732..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Studies_Panel.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StudyList.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StudyList.png
deleted file mode 100644
index 3f126c784..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_StudyList.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target.png
deleted file mode 100644
index 24fc066fc..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_CR.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_CR.png
deleted file mode 100644
index f760cfaf7..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_CR.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Delete.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Delete.png
deleted file mode 100644
index dafbadb5c..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Delete.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Label.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Label.png
deleted file mode 100644
index 5c9a9fe30..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Label.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Rename.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Rename.png
deleted file mode 100644
index 4d71bf308..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_Rename.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_UN.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_UN.png
deleted file mode 100644
index e311ea0c3..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Target_UN.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Temp_Tool.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Temp_Tool.png
deleted file mode 100644
index 8f92b2ead..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Temp_Tool.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Themes.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Themes.png
deleted file mode 100644
index 5ae104f43..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Themes.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Lesion.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Lesion.png
deleted file mode 100644
index aa4a026ed..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Lesion.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Series_Details.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Series_Details.png
deleted file mode 100644
index 4bac8b1ec..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Series_Details.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Study.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Study.png
deleted file mode 100644
index dc546b153..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_View_Study.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Viewer.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Viewer.png
deleted file mode 100644
index effc7da90..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Viewer.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL.png
deleted file mode 100644
index d703e8b20..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL_Presets.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL_Presets.png
deleted file mode 100644
index 464912753..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_WL_Presets.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Zoom.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Zoom.png
deleted file mode 100644
index bdca718f1..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/LesionTracker/LT_Zoom.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/lesionTracker.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/lesionTracker.png
deleted file mode 100644
index effc7da90..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/lesionTracker.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/viewer.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/viewer.png
deleted file mode 100644
index 21eacd59a..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/viewer.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/worklist.png b/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/worklist.png
deleted file mode 100644
index 3f126c784..000000000
Binary files a/platform/docs/versioned_docs/version-1.0-deprecated/assets/img/worklist.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/_category_.json
deleted file mode 100644
index 5192fa6ac..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Connecting to ImageArchives",
- "position": 3
-}
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dcm4chee-with-docker.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dcm4chee-with-docker.md
deleted file mode 100644
index 73aec35f7..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dcm4chee-with-docker.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-sidebar_position: 4
----
-# DCM4CHEE with Docker
-
-1. Install Docker (https://www.docker.com/)
-2. Follow the DCM4CHEE Guidelines for Running on Docker.
-
- The easiest path is to use Docker-Compose which will start and stop multiple containers for you. There are excellent instructions provided by the DCM4CHEE team on the 'light archive' repository:
-
- https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker#use-docker-compose
-
- * Create docker-compose.yml and docker-compose.env files
- * Start the containers:
-
- ```` bash
- docker-compose start
- ````
-
- **Note:** If you are running this on Mac OSX you will probably need to change the default docker-compose.yml file slightly. Specifically, the paths that refer to /var/local/ will likely need to be changed to /opt/
-
-3. Run the OHIF Viewer or Lesion Tracker using the dcm4cheeDIMSE.json configuration file
-
- ````bash
- cd OHIFViewer
- PACKAGE_DIRS="../Packages" meteor --settings ../config/dcm4cheeDIMSE.json
- ````
-
-## Web Service URLs from DCM4CHEE:
-Original source here: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker#web-service-urls
-
- - Archive UI: `http://localhost:8080/dcm4chee-arc/ui` - if secured, login with
-
- Username | Password | Role
- --- | --- | ---
- `user` | `user` | `user`
- `admin` | `admin` | `user` + `admin`
- - Keycloak Administration Console: `http://localhost:8080/auth`, login with Username: `admin`, Password: `admin`.
- - Wildfly Administration Console: `http://localhost:9990`, login with Username: `admin`, Password: `admin`.
- - Kibana UI: `http://localhost:5601`
- - DICOM QIDO-RS Base URL: `http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs`
- - DICOM STOW-RS Base URL: `http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs`
- - DICOM WADO-RS Base URL: `http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs`
- - DICOM WADO-URI: `http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/wado`
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dicomweb.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dicomweb.md
deleted file mode 100644
index 1166b2b43..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dicomweb.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-sidebar_position: 2
----
-# DICOM Web
-
-[DICOMWeb](https://en.wikipedia.org/wiki/DICOMweb) refers to RESTful DICOM Services and is a recently standardized set of guidelines for exchanging medical images and imaging metadata over the internet. Not all archives fully support it yet, but it is gaining wider adoption.
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dimse.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dimse.md
deleted file mode 100644
index 22c5a1d70..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/dimse.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-sidebar_position: 3
----
-# DICOM Message Service Element
-
-DIMSE Stands for [DICOM Message Service Element](http://dicom.nema.org/medical/dicom/current/output/chtml/part07/chapter_7.html) and is the standard method through which DICOM archives communicate. We support this messaging standard for the retrieval of study, series, and instance metadata because it is widely support. For certain PACS systems, it also (currently) provides faster query results than DICOMWeb.
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/google-cloud-healthcare.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/google-cloud-healthcare.md
deleted file mode 100644
index da60a8cfb..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/google-cloud-healthcare.md
+++ /dev/null
@@ -1,90 +0,0 @@
----
-sidebar_position: 6
----
-# Google Cloud Healthcare
-
-> The [Google Cloud Healthcare API](https://cloud.google.com/healthcare/) is a powerful option for storing medical imaging data in the cloud.
-
-An alternative to deploying your own PACS is to use a software-as-a-service provider such as Google Cloud. The Cloud Healthcare API promises to be a scalable, secure, cost effective image storage solution for those willing to store their data in the cloud. It offers an [almost-entirely complete DICOMWeb API](https://cloud.google.com/healthcare/docs/dicom) which requires tokens generated via the [OAuth 2.0 Sign In flow](https://developers.google.com/identity/sign-in/web/sign-in). Images can even be transcoded on the fly if this is desired. The Cloud Healthcare API is a very attractive option because it allows us to avoid deploying the Meteor server entirely. We can just deploy OHIF as a client-only static site application.
-
-## Setup a Google Cloud Healthcare Project
-
-1. Create a Google Cloud account
-1. Create a project in Google Cloud
-1. Enable the [Cloud Healthcare API](https://cloud.google.com/healthcare/) for your project.
-1. (Optional): Create a Dataset and Data Store for storing your DICOM data
-1. Enable the [Cloud Resource Manager API](https://cloud.google.com/resource-manager/) for your project.
-
- *Note:* If you are having trouble finding the APIs, use the search box at the top of the Cloud console.
-
-1. Go to APIs & Services > Credentials to create an OAuth Consent screen and fill in your application details.
-
- - Under Scopes for Google APIs, click "manually paste scopes".
- - Add the following scopes:
- - https://www.googleapis.com/auth/cloudplatformprojects.readonly
- - https://www.googleapis.com/auth/cloud-healthcare
-
-1. Go to APIs & Services > Credentials to create a new set of credentials:
- - Choose the "Web Application" type
- - Set up an [OAuth 2.0 Client ID](https://support.google.com/cloud/answer/6158849?hl=en)
-
- - Add your domain (e.g. ```http://localhost:3000```) to Authorized JavaScript origins.
- - Add your domain, plus `_oauth/google` (e.g. ```http://localhost:3000/_oauth/google```) to Authorized Redirect URIs.
- - Save your Client ID for later.
-1. (Optional): Enable Public Datasets that are being hosted by Google: https://cloud.google.com/healthcare/docs/resources/public-datasets/
-
-## Run the viewer with your OAuth Client ID
-
-1. Open the `config/oidc-googleCloud.json` file and change `YOURCLIENTID` to your Client ID value.
-1. Run the OHIF Viewer using the oidc-googleCloud.json configuration file
-
-````bash
-cd OHIFViewer
-METEOR_PACKAGE_DIRS="../Packages" meteor npm install
-METEOR_PACKAGE_DIRS="../Packages" meteor --settings ../config/oidc-googleCloud.json
-````
-
-## Running via Docker
-
-OHIF is also providing a Docker container which can connect to Google Cloud Healthcare with a Client ID which is provided at runtime. This is a very simple method to get up and running. Internally, the container is running [Nginx](https://nginx.org/) to serve the [Standalone Viewer](../OHIF-Viewer/usage.md).
-
-1. Install Docker (https://www.docker.com/)
-1. Run the Docker container, providing a Client ID as an environment variable. Client IDs look like `xyz.apps.googleusercontent.com`.
-
-````bash
-docker run --env CLIENT_ID=$CLIENT_ID --publish 3000:80 ohif/viewer-google-cloud:latest
-````
-
-## Building the ohif/viewer-google-cloud Docker Image
-
-The [ohif/viewer-google-cloud](https://cloud.docker.com/u/ohif/repository/docker/ohif/viewer-google-cloud) Docker image is built as follows. The Dockerfile and nginx.conf are in the `/dockersupport/viewer-google-cloud` folder.
-
-1. [Install Meteor](https://www.meteor.com/install)
-1. Clone the repository
-```bash
-git clone https://github.com/OHIF/Viewers.git
-cd Viewers
-```
-
-1. Install meteor-build-client-fixed2 so you can build the Standalone Viewer
-```bash
-npm install -g meteor-build-client-fixed2
-```
-
-1. Build the Standalone client-only OHIF Viewer
-```bash
-cd OHIFViewer/
-METEOR_PACKAGE_DIRS="../Packages" meteor npm install
-METEOR_PACKAGE_DIRS="../Packages" meteor-build-client-fixed2 ../dockersupport/viewer-google-cloud/build -s ../config/oidc.json
-```
-
-1. Build the Docker image
-```bash
-cd ../dockersupport/viewer-google-cloud
-docker build -t ohif/viewer-google-cloud .
-```
-
-1. Run the Docker image using an OAuth Client ID
-```bash
-docker run --env CLIENT_ID={$someID}.apps.googleusercontent.com --publish 3000:80 ohif/viewer-google-cloud
-```
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/options.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/options.md
deleted file mode 100644
index e9c29971b..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/options.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-sidebar_position: 1
----
-# Connecting to Image Archives
-
-We support DIMSE and DICOMWeb. Which one to use is up to you and depends on your PACS system. DICOMWeb requires no setup on the PACS-side whatsoever, whereas DIMSE may require you to add the 'OHIFDCM' aeTitle to the known DICOM Modalities of your Archive. This is the case for Orthanc, for example (See https://github.com/OHIF/Viewers/wiki/Orthanc-with-DIMSE).
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/orthanc-with-docker.md b/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/orthanc-with-docker.md
deleted file mode 100644
index 07f9d2dea..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/connecting-to-image-archives/orthanc-with-docker.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-sidebar_position: 5
----
-# Orthanc with Docker
-
-Depending on whether or not you want uploaded studies to persist in Orthanc after Docker has been closed, there are two different methods for starting the Docker image:
-
-## Temporary data storage
-This command will start an instance of the jodogne/orthanc-plugins Docker image. *All data will be removed when the instance is stopped!*
-
-````
-docker run --rm -p 4242:4242 -p 8042:8042 jodogne/orthanc-plugins
-````
-
-## Persistent data storage
-In order to allow your data to persist after the instance is stopped, you first need to create an image and attached data volume with Docker. The steps are as follows:
-
-1. Create a persistent data volume for Orthanc to use
-
- ````
- docker create --name sampledata -v /sampledata jodogne/orthanc-plugins
- ````
-
- **Note: On Windows, you need to use an absolute path for the data volume, like so:**
-
- ````
- docker create --name sampledata -v '//C/Users/erik/sampledata' jodogne/orthanc-plugins
- ````
-
-2. Run Orthanc from Docker with the data volume attached
-
- ````
- docker run --volumes-from sampledata -p 4242:4242 -p 8042:8042 jodogne/orthanc-plugins
- ````
-
-3. Upload your data and it will be persisted
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/contributing.md b/platform/docs/versioned_docs/version-1.0-deprecated/contributing.md
deleted file mode 100644
index d0afb8973..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/contributing.md
+++ /dev/null
@@ -1,55 +0,0 @@
----
-id: contributing
-sidebar_position: 2
----
-
-# Contributing
-
-### I would like to contribute code - how do I do this?
-
-Fork the repository, make your change and submit a pull request.
-
-### Any guidance on submitting changes?
-
-While we do appreciate code contributions, triaging and integrating contributed
-code changes can be very time consuming. Please consider the following tips when
-working on your pull requests:
-
-- Functionality is appropriate for the repository. Consider posting on the forum
- if you are not sure.
-- Code quality is acceptable. We don't have coding standards defined, but make
- sure it passes ESLint and looks like the rest of the code in the repository.
-- Quality of design is acceptable. This is a bit subjective so you should
- consider posting on the forum for specific guidance.
-- The scope of the pull request is not too large. Please consider separate pull
- requests for each feature as big pull requests are very time consuming to
- understand.
-
-We will provide feedback on your pull requests as soon as possible. Following
-the tips above will help ensure your changes are reviewed.
-
-### Testing contribution pull requests
-
-OHIF uses [netlify](https://netlify.com) so that pull requests are autogenerated
-and available for testing.
-
-For example,
-[this url](https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json)
-allows you to test
-[pull request 237, the request that created this FAQ entry,](https://github.com/OHIF/Viewers/pull/237)
-using data pulled from Amazon S3.
-
-Replacing the number 237 in the link below with your pull request number should
-let you test it as well and you can use this link for discussions on github
-without requiring reviewers to download and build your branch.
-
-```
-https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json
-```
-
-If you have made a documentation change, a link like this will let you preview
-the gitbook generated by the pull request:
-
-```
-https://deploy-preview-237--ohif.netlify.com/contributing.html
-```
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/data/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/data/_category_.json
deleted file mode 100644
index f216db497..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/data/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Data Organization",
- "position": 4
-}
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/data/data-hierarchy.md b/platform/docs/versioned_docs/version-1.0-deprecated/data/data-hierarchy.md
deleted file mode 100644
index ceaee45b9..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/data/data-hierarchy.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Data Hierarchy
-
-## Studies, Series, Instances, Frames
-
-## Display Sets
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/data/image-viewport.md b/platform/docs/versioned_docs/version-1.0-deprecated/data/image-viewport.md
deleted file mode 100644
index 9d480566c..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/data/image-viewport.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Image Viewport
-
-### Main Viewport Component
-
-The [imageViewerViewport](https://github.com/OHIF/Viewers/tree/master/Packages/ohif-viewerbase/client/components/viewer/imageViewerViewport) component
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/data/measurements-and-annotations.md b/platform/docs/versioned_docs/version-1.0-deprecated/data/measurements-and-annotations.md
deleted file mode 100644
index 574f11845..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/data/measurements-and-annotations.md
+++ /dev/null
@@ -1 +0,0 @@
-# Measurements and Annotations
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/data/tool-management.md b/platform/docs/versioned_docs/version-1.0-deprecated/data/tool-management.md
deleted file mode 100644
index 399b04c8b..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/data/tool-management.md
+++ /dev/null
@@ -1 +0,0 @@
-# Tool Management
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/deployment/_category_.json
deleted file mode 100644
index 0b0fa4cc8..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Deployment",
- "position": 7
-}
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/building-for-production.md b/platform/docs/versioned_docs/version-1.0-deprecated/deployment/building-for-production.md
deleted file mode 100644
index d09a49f46..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/building-for-production.md
+++ /dev/null
@@ -1,152 +0,0 @@
-# Building for Production
-
-**This tutorial considers the current folder as `OHIFViewer/`**.
-
-### Dependencies
-
-First, you need to have installed:
-
-- [Node.js](https://nodejs.org/) and [NPM](https://www.npmjs.com/)
-- [Meteor](https://www.meteor.com/)
-- [MongoDB](https://www.mongodb.com/)
-
-**MongoDB** is actually **not required** if you have a **remote MongoDB**. If your database is **local** then you need to have it running.
-
-### Check your packages
-
-Inside of the viewer folder make sure NPM packages are updated for production:
-
-```bash
-npm install --production
-```
-
-### Building
-
-There are two ways of building OHIF Viewer for a specific DICOM server: **Automatically** or **Manually**.
-
-For both, we use the `meteor build` (check it's [docs](https://guide.meteor.com/deployment.html#custom-deployment)) app and **Orthanc Dicom Web Server**.
-
-For `meteor build` it's necessary to inform an output folder `myOutputFolder`.
-**Remember to change `myOutputFolder` to your folder location.**
-
-OHIF Viewer will be built using **Orthanc DICOM Web server** configuration file `../config/orthancDICOMWeb.json` to set `METEOR_SETTINGS` environment var.
-
-*To run with another server, just point to the corresponding `.json` located in `config` folder or create your own.*
-
-#### Build automatically
-
-After this step, go directly to [Prepare to run production build](#prepare-to-run-production-build).
-
-##### Non-windows users
-
-```bash
-METEOR_PACKAGE_DIRS="../Packages" METEOR_SETTINGS=$(cat ../config/orthancDICOMWeb.json) meteor build --directory myOutputFolder
-```
-
-##### Windows users
-
-Since there is no `cat` command in Windows `cmd`, use Windows `PowerShell` (at least version 3.0) instead.
-
-In `PowerShell`, open a new shell as an `admin`:
-
-```bash
-Start-Process powershell -Verb runAs
-```
-
-Then:
-
-**Remember to change `OHIFViewerFolderLocation` to OHIFViewer's folder location.**
-
- ```bash
-cd OHIFViewerFolderLocation
-$settings = Get-Content ..\config\orthancDICOMWeb.json -Raw
-$settings = $settings -replace "`n","" -replace "`r","" -replace " ",""
-[Environment]::SetEnvironmentVariable("METEOR_SETTINGS", $settings, "Machine")
-SET METEOR_PACKAGE_DIRS="../Packages"
-meteor build --directory myOutputFolder
-```
-
-#### Build manually
-
-OHIF Viewer will be built normally, but with no DICOM Server information, which needs to be added when running the build. This is described in [Manually adding DICOM Server to the Viewer](#manually-adding-dicom-server-to-the-viewer).
-
-##### Non-windows users
-
-```bash
-METEOR_PACKAGE_DIRS="../Packages" meteor build --directory myOutputFolder
-```
-
-##### Windows users in `cmd`
-
-```bash
-SET METEOR_PACKAGE_DIRS="../Packages"
-meteor build --directory myOutputFolder
-```
-
-### Prepare to run production build
-
-If everything went ok, `meteor build` created a `bundle` folder inside `myOutputFolder`.
-
-**Remember to change `myOutputFolder` to your folder location.**
-
-Go to that folder:
-
-```bash
-cd myOutputFolder/bundle
-```
-
-Now install the **NPM dependencies**:
-
-```bash
-cd programs/server
-npm install
-```
-
-### To run production build
-
-Go back to the `bundle` folder:
-
-```bash
-cd ../..
-```
-or (**Remember to change `myOutputFolder` to your folder location.**):
-
-```bash
-cd myOutputFolder/bundle
-```
-
-3 environment variables are set before running Node.js:
-- `MONGO_URL`: is the url to MongoDB. If it's **local**, you need to have it running
-- `ROOT_URL`: the hostname where you can access your Viewer in the browser
-- `PORT`: the port the Viewer will run
-
-This way, the Viewer can be accessed in `http://localhost:3000`, with MongoDB running locally using 27017 port (it's default).
-
-##### Non-windows users
-
-```bash
-MONGO_URL=mongodb://localhost:27017/myapp ROOT_URL=http://localhost PORT=3000 node main.js
-```
-
-##### Windows users in `cmd`
-
-```bash
-SET MONGO_URL=mongodb://localhost:27017/myapp
-SET ROOT_URL=http://localhost
-SET PORT=3000
-node main.js
-```
-
-### Manually adding DICOM Server to the Viewer
-
-If DICOM Server was configured automatically during the building process, skip this step.
-
-Access the viewer `http://localhost:3000` and toggle the **Options** menu, located at top right corner. Select **Server Information** option.
-
-In **Server Information** dialog click on **Add a new server** button and fill the fields accordingly to the DICOM Server to be added.
-
-In case of doubts about any field in this dialog, use `config/orthancDICOMWeb.json` as reference.
-
-After filling the form, click on the **Save** button and the new server will be listed. Make sure to activate it by clicking on the left most button (a checkbox button) in the **Actions** column.
-
-Refresh the page and Viewer will be connected to the DICOM Server.
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/security.md b/platform/docs/versioned_docs/version-1.0-deprecated/deployment/security.md
deleted file mode 100644
index 8dbb2f9bc..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/deployment/security.md
+++ /dev/null
@@ -1 +0,0 @@
-# Security
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/_category_.json
deleted file mode 100644
index fd00c612d..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Essentials",
- "position": 2
-}
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/architecture.md b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/architecture.md
deleted file mode 100644
index c4a58d1bf..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/architecture.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-sidebar_position: 2
----
-# Architecture
-
-## Meteor
-[Meteor](https://www.meteor.com/) is built on top of [Node.js](https://nodejs.org/en/), adding reactive templates ([Blaze](https://guide.meteor.com/blaze.html)), a [publish/subscribe](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) mechanism (via [WebSockets](https://en.wikipedia.org/wiki/WebSocket) and the [Distributed Data Protocol](https://blog.meteor.com/introducing-ddp-6b40c6aff27d), and a fully integrated client-server [MongoDB](https://www.mongodb.com/) making it easy to create reactive data elements in the UI.
-
-## Structure of an OHIF Application
-The OHIF Framework is built as a set of small Meteor packages which can be included as necessary in the final application. Since the logic and templates are largely pushed into packages, the actual application-specific code for each Viewer is relatively short.
-
-In brief, to create a new Viewer application, you need to:
-* Define the existing pages and the routes which will lead to them
-* Define the overall application layout
-* Include child templates from the OHIF Packages into your application layout
-* Specify desired template options as necessary (e.g. which tools should appear in the toolbar)
-
-User interface components for your application can be defined in any View layer supported by Meteor (Officially: [Angular](https://www.meteor.com/tutorials/angular/creating-an-app), [React](https://www.meteor.com/tutorials/react/creating-an-app), [Blaze](https://www.meteor.com/tutorials/blaze/creating-an-app), Unofficially: [Vue](https://github.com/meteor-vue/vue-meteor)).
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/configuration.md b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/configuration.md
deleted file mode 100644
index e44d91fe3..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/configuration.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-sidebar_position: 3
----
-# Configurations
-
-There are a few pre-defined settings files that you can find in [config](https://github.com/OHIF/Viewers/tree/master/config) folder. These are Meteor files and all settings can be accessed as showed on [Meteor Website](https://docs.meteor.com/api/core.html#Meteor-settings). UI settings are also available as OHIF.uiSettings. See the [schema](https://github.com/OHIF/Viewers/blob/131d64854cb2eceff056a15ccb12c34b9e2baaa7/Packages/ohif-servers/both/schema/servers.js) for more information.
-
-## Server settings
-
-* `dropCollections (boolean)`: the server will drop all Mongo collections when it's set to `true` as soon as it has finished starting. This is useful for demo and development environments.
-
-## UI Settings
-
-* `studyListDateFilterNumDays (integer)`: define the default date filter (range) on study list. If it's 02/15/2017 and this config is set to 5 then it will search for studies between `02/10/2017` and `02/15/2017`.
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/installation.md b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/installation.md
deleted file mode 100644
index 2423c793a..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/installation.md
+++ /dev/null
@@ -1,67 +0,0 @@
----
-sidebar_position: 1
----
-# Installation
-
-## Getting the Code
-
-Either clone the repository using Git:
-
-````bash
-git clone git@github.com:OHIF/Viewers.git
-````
-
-or [Download the latest Master as a ZIP File](https://github.com/OHIF/Viewers/archive/master.zip).
-
-## Set up a DICOM server
-
-1. Choose and install an Image Archive
-2. Upload some data into your archive (e.g. with DCMTK's [storescu](http://support.dcmtk.org/docs/storescu.html) or your archive's web interface)
-3. Keep the server running
-
-#### Open Source DICOM Image Archive Options
-
-
-
-| Archive | Installation |
-| ----------- | ----------- |
-| [DCM4CHEE Archive 5.x](https://github.com/dcm4che/dcm4chee-arc-light) | [Installation with Docker](https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker) |
-| [Orthanc](https://www.orthanc-server.com/) | [Installation with Docker](http://book.orthanc-server.com/users/docker.html) |
-| [DICOMcloud](https://github.com/DICOMcloud/DICOMcloud) (**DICOM Web only**) | [Installation](https://github.com/DICOMcloud/DICOMcloud#running-the-code) |
-| [OsiriX](http://www.osirix-viewer.com/) (**Mac OSX only**) | Text |
-| [Horos](https://www.horosproject.org/) (**Mac OSX only**) | Text |
-
-
-
-
-
-*Feel free to make a Pull Request if you want to add to this list.*
-
-## Set up and test the OHIF Viewer (or LesionTracker) application:
-1. [Install Meteor](https://www.meteor.com/install)
-2. Open a new terminal tab in one of the Application directories (OHIFViewer or LesionTracker)
-3. Instruct Meteor to install all dependent NPM Packages
-
- ````bash
- METEOR_PACKAGE_DIRS="../Packages" meteor npm install
- ````
-
-4. Run Meteor using one of the available configuration files.
-
- ````bash
- METEOR_PACKAGE_DIRS="../Packages" meteor --settings ../config/orthancDICOMWeb.json
- ````
-
- **Note:** On Windows, you may need to set PACKAGE_DIRS="../Packages" in your Environment Variables in your operating system settings.
-
-5. Launch the OHIF Viewer / Lesion Tracker Study List by visiting [http://localhost:3000/](http://localhost:3000/) in a web browser.
-
- **If everything is working correctly, you should see the Study List from your archive when you visit the Study List.**
-
-6. Double-click on a Study in the Study List to launch it in the Viewer
-
- **If everything is working correctly, you should see your study load into the Viewer.**
-
-#### Troubleshooting
-* If you receive a *"No Studies Found"* message and do not see your studies, try changing the Study Date filters to a wider range.
-* If you see any errors in your server console, check the Troubleshooting page for more in depth advice.
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/meteor-packages.md b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/meteor-packages.md
deleted file mode 100644
index a9fd4d26b..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/meteor-packages.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# Meteor Packages
-
-### Commands (*ohif-commands*)
-### Core (*ohif-core*)
-
-## Cornerstone Package (*ohif-cornerstone*)
-
-This package contains a number of front-end libraries that help us build web-based medical imaging applications.
-
-These are:
-- [dicomParser](https://github.com/cornerstonejs/dicomParser):
-A lightweight JavaScript library for parsing DICOM P10 byte streams in modern web browsers (IE10+), Node.js, and Meteor.
-
-- [Cornerstone Core](https://github.com/cornerstonejs/cornerstone):
-A lightweight JavaScript library for displaying medical images in modern web browsers that support the HTML5 canvas element.
-
-- [Cornerstone Tools](https://github.com/cornerstonejs/cornerstoneTools):
-A library built on top of cornerstone that provides a set of common tools needed in medical imaging to work with images and stacks of images
-
-- [Cornerstone Math](https://github.com/cornerstonejs/cornerstoneMath):
-Math and computational geometry functionality for Cornerstone
-
-- [Cornerstone WADO Image Loader](https://github.com/cornerstonejs/cornerstoneWADOImageLoader):
-A Cornerstone Image Loader for DICOM P10 instances over HTTP. This can be used to integrate cornerstone with WADO-URI servers or any other HTTP based server that returns DICOM P10 instances (e.g. Orthanc or custom servers).
-
-- [Hammer.js](https://github.com/hammerjs/hammer.js):
-A JavaScript library for multi-touch gestures
-
-### Design (*ohif-design*)
-### DICOM Services (*ohif-dicom-services*)
-It contains a number of helper functions for retrieving common value types (e.g. JSON, patient name, image frame) from a DICOM image. This package is for server-side usage.
-
-### Hanging Protocols (*ohif-hanging-protocols*)
-### Header (*ohif-header*)
-### Hotkeys (*ohif-hotkeys*)
-
-### Lesion Tracker (*ohif-lesiontracker*)
-This package stores all of the oncology-specific tools and functions developed for the Lesion Tracker application. Here we store, for example, the Target measurement and Non-target pointer tools that are used to monitor tumour burden over time.
-
-This package also stores Meteor components for the interactive lesion table used in the Lesion Tracker, and dialog boxes for the callbacks attached to the Target and Non-target tools.
-
-### Logging (*ohif-log*)
-### Logging (*ohif-measurements*)
-### Metadata (*ohif-metadata*)
-### Polyfilling Functionality (*ohif-polyfill*)
-
-### Select Tree UI (*ohif-select-tree*)
-### Server Settings UI (*ohif-servers*)
-### Studies (*ohif-studies*)
-### Study List UI (*ohif-study-list*)
-### Common Themes (*ohif-themes-common*)
-### Theming (*ohif-themes*)
-### User Management (*ohif-user-management*)
-### User (*ohif-user*)
-
-### Basic Viewer Components (*ohif-viewerbase*)
-This is the largest package in the repository. It holds a large number of reusable Meteor components that are used to build both the OHIF Viewer and Lesion Tracker.
-
-### WADO Proxy (*ohif-wadoproxy*)
-Proxy for CORS
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/troubleshooting.md b/platform/docs/versioned_docs/version-1.0-deprecated/essentials/troubleshooting.md
deleted file mode 100644
index ca2e47f5f..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/essentials/troubleshooting.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-sidebar_position: 4
----
-# Troubleshooting
-
-## Common Problems
-
-| Problem | Most Common Reasons |
-| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Can't retrieve Study List over DICOMWeb | 1. QIDO root URL is incorrect 2. DICOM Web is not enabled on PACS |
-| Can't retrieve Study List over DIMSE | 1. PACS is not configured to allow connections from OHIF Meteor Server |
-| Can't retrieve images | 1. WADO Root URL is incorrect 2. DICOM Web is not enabled on PACS 3. HTTP Basic Authentication username and password are incorrect or not provided. |
-
-
-
-## Debugging Steps
-### Can't retrieve Study List over DICOMWeb
-
-1. Check that you can query your PACS using an alternative DICOM Web client (e.g. cURL, or a Web Browser). If you cannot, then your PACS is configured incorrectly. Refer to the documentation of the image archive.
-2.
-
-
-### Can't retrieve Study List over DIMSE
-
-### Can't retrieve images
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/_category_.json
deleted file mode 100644
index c845acf43..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Example Applications",
- "position": 9
-}
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/lesion-tracker.md b/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/lesion-tracker.md
deleted file mode 100644
index 11fe6b818..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/lesion-tracker.md
+++ /dev/null
@@ -1 +0,0 @@
-# Lesion Tracker
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/ohif-viewer.md b/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/ohif-viewer.md
deleted file mode 100644
index e6c0db4cb..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/ohif-viewer.md
+++ /dev/null
@@ -1 +0,0 @@
-# OHIF Viewer
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/standalone-viewer.md b/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/standalone-viewer.md
deleted file mode 100644
index 2ff802c4f..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/example-applications/standalone-viewer.md
+++ /dev/null
@@ -1 +0,0 @@
-# Standalone Viewer
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/faq/general.md b/platform/docs/versioned_docs/version-1.0-deprecated/faq/general.md
deleted file mode 100644
index 90c7f1994..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/faq/general.md
+++ /dev/null
@@ -1,88 +0,0 @@
-# General
-
-### How do I file a bug?
-
-We accept and triage bug reports through Github primarily.
-
-1. [Create a Github account](https://github.com/join)
-2. Search the current [Issue List](https://github.com/OHIF/Viewers/issues) to
- ensure you are not creating a duplicate issue.
-
-If your issue already exists, post a comment to show us that this issue also
-affects you.
-
-3. If no prior issue exists,
- [Create a New Issue](https://github.com/OHIF/Viewers/issues/new) on the
- repository.
-
-Some tips for filing a new issue:
-
-- **Make sure your issue is reproducible**: If we try to reproduce your issue
- given your provided steps and we cannot reproduce it, we will not be able to
- fix it. _Nobody wants to spend time guessing how to reproduce your issue!_
- Before filing, please reproduce your issue more than once and clearly describe
- the steps taken.
-- **If you are reporting a user interface issue, provide screenshots**: A
- picture is worth a thousand words. If your issue concerns the UI, screenshots
- will help us identify the issue dramatically faster since it can be extremely
- challenging to describe UI bugs with text. _You should still clearly describe
- the steps that you took to produce the issue_.
-- **Include platform & environment**: Your operating system, web browser, and
- web browser version are highly relevant for many bugs. Please provide these
- with all bug reports.
-- **Include expected and actual result**: Tell us what you expected to happen,
- and what actually happened. If you don't do this, we might not consider it a
- bug.
-
-### How can I request a new feature?
-
-At the moment we are in the process of defining our roadmap and will do our best
-to communicate this to the community. If your requested feature is on the
-roadmap, then it will most likely be built at some point. If it is not, you are
-welcome to build it yourself and [contribute it](../contributing.md). If you
-have resources and would like to fund the development of a feature, please
-[contact us](https://www.ohif.org).
-
-### Who should I contact about Academic Collaborations?
-
-[Gordon J. Harris](http://www.dfhcc.harvard.edu/insider/member-detail/member/gordon-j-harris-phd/)
-at Massachusetts General Hospital is the primary contact for any academic
-collaborators. We are always happy to hear about new groups interested in using
-the OHIF framework, and may be able to provide development support if the
-proposed collaboration has an impact on cancer research.
-
-### Do you offer commercial support?
-
-The Open Health Imaging Foundation does not offer commercial support, however,
-some community members do offer consulting services. You can search our
-[Community Forum](https://community.ohif.org/) for more information.
-
-### I emailed my question to you directly and you did not respond. Why not?
-
-Emailing developers directly is not a shortcut to faster support. Please file
-your issues and questions on Github so that everyone can benefit from the
-discussion and solutions.
-
-### Do your Viewers have [510(k) Clearance](https://www.fda.gov/MedicalDevices/DeviceRegulationandGuidance/HowtoMarketYourDevice/PremarketSubmissions/PremarketNotification510k/) from the U.S. F.D.A or [CE Marking](https://ec.europa.eu/growth/single-market/ce-marking_en) from the European Commission?
-
-**NO.** The OHIF Viewer, Lesion Tracker, and Standalone Viewer, **NOT** F.D.A.
-cleared or CE Marked. It is the users responsibility to ensure compliance with
-applicable rules and regulations. The
-[License](https://github.com/OHIF/Viewers/blob/master/LICENSE) for the OHIF
-Framework does not prevent your company or group from seeking F.D.A. clearance
-for a product built using the framework.
-
-If you have gone this route (or are going there), please let us know because we
-would be interested to hear about your experience.
-
-### Are your Viewers [HIPAA](https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act) Compliant?
-
-**NO.** The OHIF Viewer, Lesion Tracker, and Standalone Viewer **DO NOT**
-fulfill all of the criteria to become HIPAA Compliant. It is the users
-responsibility to ensure compliance with applicable rules and regulations.
-
-The Lesion Tracker application demonstrates some available components and
-features (e.g. [Audit Trail](../lesion-tracker/audit-trail.md), automatic
-logoff, and unique user identification for
-[User Accounts](../lesion-tracker/user-accounts.md)) which could be used by
-third parties seeking HIPAA compliance.
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/faq/technical.md b/platform/docs/versioned_docs/version-1.0-deprecated/faq/technical.md
deleted file mode 100644
index f7cc314f2..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/faq/technical.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Technical
-## Why is your framework built with Meteor?
-
-At the time of project conception, Meteor was a simple way to begin using bleeding edge web technologies without putting significant effort into configuration and build processes. Although Meteor does have some drawbacks, it remains a simple all-in-one solution.
-
-## Do you have any plans to stop using Meteor?
-
-We have considered migrating templates from Blaze (http://blazejs.org/) to React (https://reactjs.org/) or Vue (https://vuejs.org/), simply because these decouple the view layer from the remainder of the application. Blaze currently lacks [Stand-alone Support](http://blazejs.org/#Better-Stand-alone-Support) which means that our templates are not reusable outside of a Meteor application. This is certainly a downside, but the resource cost to migrate every template is significant.
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/layout/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/layout/_category_.json
deleted file mode 100644
index 1da2602ba..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/layout/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Viewport Layout",
- "position": 5
-}
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/layout/hanging-protocols.md b/platform/docs/versioned_docs/version-1.0-deprecated/layout/hanging-protocols.md
deleted file mode 100644
index 319bda35f..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/layout/hanging-protocols.md
+++ /dev/null
@@ -1 +0,0 @@
-# Hanging Protocols
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/layout/layout-management.md b/platform/docs/versioned_docs/version-1.0-deprecated/layout/layout-management.md
deleted file mode 100644
index 3756edd1c..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/layout/layout-management.md
+++ /dev/null
@@ -1 +0,0 @@
-# Layout Management
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/_category_.json
deleted file mode 100644
index 1f5374a5c..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Lesion Tracker",
- "position": 11
-}
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/audit-trail.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/audit-trail.md
deleted file mode 100644
index 09726ddaa..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/audit-trail.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-sidebar_position: 9
----
-# Audit Trail
-
-1. To view Audit Trails, select **View Audit Log** on the Configuration menu.
-2. To filter audit logs for a column or multiple columns, type or select in the
- related column or columns.
-
-
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/installation-on-windows.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/installation-on-windows.md
deleted file mode 100644
index 03d07a021..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/installation-on-windows.md
+++ /dev/null
@@ -1,112 +0,0 @@
----
-sidebar_position: 2
----
-
-# Installation on Windows
-
-On Windows, the easiest way to install LesionTracker is using the provided
-Windows Installer. You can download the latest version of LesionTracker
-installer from [OHIF](https://ohif.org)
-
-## Install
-
-1. After you have obtained the latest version of LesionTracker installer,
- double-click the installer to install the application.
-2. When you start the installer, setup wizard will pop up.
-
-
-
-3. Click **Install** to start the installation.
-4. LesionTracker will install prerequisites if they do not exist before
- installing the application.
-
-
-
-5. Click the **Next** if you accept the installation.
-
-
-
-6. Read the license agreement, and if you agree, click **I accept the terms in
- the License Agreement** and then click the **Next**.
-
-
-
-7. Select a location to install LesionTracker and click the **Next** to
- continue.
-
-
-
-8. Click the **Install** to start the installation and the installation will be
- started.
-
-
-
-9. Click **Finish** to complete the installation.
-
-
-
-10. When the installation is done,
-
-- You will see the final dialog which gives the URLs in order to connect to
- LesionTracker Viewer and Orthanc Server.
-
- - It will launch computer's default browser and display LesionTracker Viewer
- if you click 'http://localhost:3000'
- - It will launch computer's default browser and enables to upload studies to
- Orthanc Server if you click 'http://localhost:8042'
-
- 
-
-- Also, LesionTracker installer will create two shortcuts named LesionTracker
- and Orthanc Server on the Desktop.
-
- - It will launch computer's default browser and display LesionTracker image
- viewer if you click LesionTracker application on the Desktop.
- - It will launch computer's default browser and enables to upload studies to
- Orthanc Server if you click Orthanc Server application.
-
- 
-
-11. Click the **Done** and **Close** to close the installation window.
-
-
-
-12. After the installation is completed,
-
-- You can find the project folder in C:\Program Files\OHIF\LesionTracker if you
- use the default installation location.
-- You will find 4 program executables which eases managing the services in the
- project folder:
-
- - **Start Services.exe:** Starts LesionTracker, MongoDB and Orthanc services
- manually.
- - **Stop Services.exe:** Stops LesionTracker, MongoDB and Orthanc services
- manually.
- - **Turn Off Auto Services.exe:** Configures not to start LesionTracker,
- MongoDB and Orthanc services automatically when Windows starts up.
- - **Turn On Auto Services.exe:** Configures to start LesionTracker, MongoDB
- and Orthanc services automatically when Windows starts up.
-
- 
-
-## Uninstall
-
-1. Open the Start Menu.
-2. Click **Settings**.
-3. Click **Apps** on the Settings menu.
-4. Select **Apps & features** from the left panel.
-5. Find the LesionTracker in programs list and click LesionTracker, then click
- the **Uninstall**.
-
-
-
-6. Click **Uninstall** from the LesionTracker Setup dialog.
-
-
-
-When the uninstallation is done, the uninstallation will remove;
-
-- Prerequisites: MongoDB, nodejs and Orthanc Server
-- The project folder, OHIF, from C:\Program Files
-- Background Services: LesionTracker Server, MongoDB and Orthanc services
-- Shortcuts: LesionTracker and Orthanc Server shortcuts on the Desktop
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/manage-studies-in-orthanc.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/manage-studies-in-orthanc.md
deleted file mode 100644
index 2bc8f01fe..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/manage-studies-in-orthanc.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
-sidebar_position: 3
----
-# Manage Studies in Orthanc
-Orthanc is an open-source, simple and powerful standalone DICOM server which eases DICOM scripting and data management for clinical routine and medical research. LesionTracker installer installs Orthanc as a service and runs automatically in the background. Orthanc serves a web interface, Orthanc Explorer, and it can be accessed through http://localhost:8042 in your web browser or the Orthanc Server shortcut which is created by LesionTracker installer and loads http://localhost:8042 in your default browser.
-
-## Upload a study into Orthanc using Orthanc Explorer
-
-1. Click **Upload** button at the top-right of Orthanc Explorer and you will be brought to the upload page.
-
- 
-
-2. Select the DICOM files and drag those files into Orthanc Explorer.
-
- 
-
-3. Click **Start the upload** and wait until the upload is complete.
-
- 
-
-4. Click **Patients** to view the uploaded study.
-
- 
-
-## Delete a study from Orthanc using Orthanc Explorer
-
-1. Select a patient from Patients list.
-
- 
-
-2. Select a study of the patient.
-
- 
-
-3. Select **Delete this study** in the Interact menu.
-
- 
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/server-management.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/server-management.md
deleted file mode 100644
index 1389a6afb..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/server-management.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-sidebar_position: 8
----
-# Server Management
-
-LesionTracker supports configuration of multiple servers and allows to switch between them. To configure the servers, click **Server Information** on the Configuration Menu.
-
-
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/study-and-timepoint-management.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/study-and-timepoint-management.md
deleted file mode 100644
index 5a0edc457..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/study-and-timepoint-management.md
+++ /dev/null
@@ -1,78 +0,0 @@
----
-sidebar_position: 5
----
-# Study and Timepoint Management
-
-
-
-1. Study List allows to sort each column. To sort studies, click the desired
- column header.
-2. Study List is filterable for each column and you can filter by more than one
- column by typing in the column field. To filter a column, type the text in
- the desired column header field and press Enter.
-3. Pagination is provided for the Study List at the bottom of the page. You can
- change the number of rows per page or the current page.
-
-## Study List Context Menu
-
-When you right-click on the study row, Study Context Menu will pop up. Study
-Context Menu includes operations at the study level.
-
-There are a couple of ways to view a study:
-
-- Simply double-click on the study row.
-- Select **View** option on the Study Context Menu, then you will be redirected
- to Viewer page.
-
-
-
-### Timepoint Association
-
-1. To link a study with a timepoint, right-click on the study row and select
- **Associate** option on the Study Context Menu. You will see Study
- Association dialog.
-2. To link multiple studies at the same time, hold Shift key and select studies
- you would like to associate, then right-click and select Associate on the
- Study Context Menu
-
-
-
-The Study Association dialog allows you to select and unselect Timepoint Type
-for the selected study or studies. Also, it will include all studies within 14
-days of your selected studies, in case you forgot to select a study. After you
-identify timepoints for the selected study or studies, click **Save** to save
-the association.
-
-
-
-### Remove Timepoint Association
-
-To unlink a timepoint association with a study, right-click on the study row and
-select **Remove Association** option on the Study Context Menu. Remove
-Association will be disabled if the study is already dissociated.
-
-
-
-### View Series Details
-
-To view details of related series, right-click on the study row and select
-**View Series Details** option on the Study Context Menu, then Series Details
-dialog will pop up. Series Details dialog gives a summary of the selected study.
-
-
-
-### Anonymize (Under Development)
-
-...
-
-### Send (Under Development)
-
-...
-
-### Export (Under Development)
-
-...
-
-### Delete (Under Development)
-
-...
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-accounts.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-accounts.md
deleted file mode 100644
index 94bcd16b4..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-accounts.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-sidebar_position: 4
----
-# User Accounts
-
-## Sign In
-
-LesionTracker requires authentication to access the viewer.
-If you are already a registered user, enter Your Email and Password and click **Sign In**
-
-
-
-## Register An Account
-
-If you are a new user, click **Need An Account** and fill the all fields and click **Join Now**
-
-
-
-Note: There is a Test Drive button on the top-right of Login page. Test Drive is used to skip registration process and allows to use LesionTracker with a test account. This will be removed in the next versions.
-
-## Forgot Password
-
-To reset the password, enter Your Email and click **Send Reminder**. An email that includes a link to reset your password will be sent to your email address.
-
-
-
-## Change Password
-
-To change password:
-
-1. Click Configuration menu icon at the top-right of the workspace then select **Change Password**
-2. Enter your password and click **Change Password**
-
-
-
-## Logout
-
-To logout, click **Logout** on the Configuration menu.
-
-
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-preferences.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-preferences.md
deleted file mode 100644
index bce9dd661..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/user-preferences.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-sidebar_position: 7
----
-# User Preferences
-To view User Preferences dialog, select **Preferences** on the Configuration menu. User Preferences dialog includes a couple of tabs:
-
-
-
-#### Keyboard Shortcuts
-There are a number of default shortcuts for the defined tools. To change the shortcut for the tool, type the desired key into the tool field.
-
-
-
-#### Window Width & Level Presets
-There are a number of default Window Width/Level settings in the Window W/L tab. To create a new setting, enter the desired column field.
-
-
-
-## Color Themes
-
-1. To modify the colors in the viewer, select **Themes** from Configuration menu.
-2. Select a theme then click **Apply theme** to change the theme.
-
-
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/using-the-viewer.md b/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/using-the-viewer.md
deleted file mode 100644
index d24414527..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/lesion-tracker/using-the-viewer.md
+++ /dev/null
@@ -1,273 +0,0 @@
----
-sidebar_position: 6
----
-# Using the Viewer
-
-After you double-click a study, the Lesion Tracking imaging application will be launched. The basic patient and study data is displayed in the Image Viewer.
-
-
-
-### Switching Studies and Series
-There are two methods to switch studies and series inside each viewport:
-
-#### Studies Panel
-
-Studies Panel at the left side of the workspace allows you to change studies and series. Click **Studies** button to toggle Studies Panel. You can view images in two ways:
-
-1. Double-click the desired series thumbnail.
-2. Drag and drop the desired series thumbnail on the image viewer.
-
-
-
-#### Quick Switch Tool
-
-Quick Switch Tool in the top-middle of the workspace allows you to switch studies and series for the active image viewer easily. Selected series will be colored by the theme colors.
-
-
-
-## Image Manipulation Tools
-Image Manipulation Tools are available in the toolbar at the top of the workspace.
-
-### Zoom Tool
-
-There are a couple of ways to zoom the image:
-
-1. Select **Zoom Tool** and down on the image with the left mouse button.
-2. Down on the image the right mouse button without selecting Zoom Tool.
-
-
-
-### Window Width & Level
-
-There are a couple of ways to change the window/level of the image:
-
-1. Select **Levels Tool** and drag on the image with the left mouse button. Dragging up decreases the level and dragging left decreases the window width.
-
- 
-
-2. Alternatively, you can use default Window/Level Presets which are defined in User Preferences dialog or create your window level values.
-
- 
-
-### Pan Tool
-
-There are a couple of ways to pan the image:
-1. Select **Pan Tool** and move the image around the screen with the left mouse button.
-2. Move the image around the screen with the middle mouse button without selecting Pan Tool.
-
-
-
-### Stack Scroll Tool
-
-There are a number of ways to scroll images:
-1. Scroll up and down the mouse wheel.
-2. Drag the scroll bar at the right side of the image viewer.
-3. Use Up and Down buttons on the keyboard.
-
-
-
-## Lesion Tracking Basics
-### View Case Response Criteria
-
-LesionTracker supports Conformance Checks for RECIST 1.1 and click **Trial** to view Response Criteria details.
-
-_Note:_ Trial button is available if the study is associated with a timepoint.
-
-
-
-### Measurements Table
-
-To view measurements and manipulate the lesions, click **Measurements** button at the top-right of the workspace. The Measurements table will be displayed at the right side of the workspace. To hide the Measurements the table, click Measurements button.
-
-_Note:_ Measurements button is available if the study is associated with a timepoint.
-
-
-
-### Target Tool
-
-1. To create a target, select **Target Tool** and click on the image.
-2. Hold, drag and release the mouse button to create the target.
-
- 
-
-3. To label the target, click **Add Label** button.
-
- 
-
-4. To resize the measurement, hover the mouse over the end points of the target then click and drag.
-5. To rename the lesion, click the lesion number in the Measurements table then click **Rename**
-
- 
-
-6. To delete the lesion, click the lesion number in the Measurements table then click **Delete**. It will delete the lesion from the Measurements table and the image.
-
- 
-
-#### Completely Resolved (CR) Target Tool
-To make a lesion completely resolved, click Target Tool and select **CR Target** from Target Tool pop up.
-
-
-
-#### Unknown (UN) Target Tool
-To mark a lesion as unknown, click Target Tool and select ** UN Target** from Target Tool pop up.
-
-
-
-### Non-Target Tool
-
-1. To create a non-target, select **Non-Target Tool** and click on the image.
-2. Hold, drag and release the mouse button to create a non-target.
-
- 
-
-3. Select Lesion Location and Lesion Location Response then click **Confirm**.
-
- 
-
-4. To resize the measurement, hover the mouse over the end points of the non-target then click and drag.
-5. To rename the lesion, click the lesion number in the Measurements table then click **Rename**
-6. To delete the lesion, click the lesion number in the Measurements table then click **Delete**. It will delete the lesion from the Measurements table and the image.
-
-### View Lesion
-
-To view the lesion on the image, click the measurement record in the Measurements table then the image which includes the lesion will be displayed by activating the selected lesion.
-
-
-
-### Conformance Checks
-
-Any lesion that will conflict with Response Criteria for RECIST 1.1 will be flagged in the Measurements table. To view criteria non-conformities, click the icon at the left of the lesion record in the Measurements table.
-
-
-
-### Generating PDF Reports
-
-1. To generate reports of measurements, click **Generate Report** button at the bottom of the Measurements table.
-
-
-
-2. A PDF file which includes target and non-target measurements with annotated image snapshots will be created.
-
-
-
-### Saving Measurements
-
-There a couple of ways to save changes:
-1. Click **Save** button at the top-right of the workspace.
-
- 
-
-2. Click **Study list** to save changes and go back to Study List.
-
- 
-
-### Timepoint Comparison
-
-LesionTracker allows to compare time points and manipulate the measurements on the timepoint images. To launch the compare mode:
-
-1. Associate a study with Follow-up timepoint then double-click Follow-up study. Follow-up and Baseline studies will be displayed in 1x2 layout and Baseline study will show the image which includes the first target measurement as default.
-2. Measurements in the Follow-up must be created in the same order as they were on Baseline.
-
- 
-
-#### Comparison
-
- **Comparison** tab in the Measurements table allows you to display lesions in the timepoint compared.
-
-
-
-#### Key Timepoints
-
-**Key Timepoints** tab in the Measurements table allows you to display the lesions by the timepoints at the same time.
-
-
-
-#### Image Review
-
-To compare the lesions in Baseline and Follow-up, click the lesion record in the Measurements table. The recorded lesions will be displayed on the Baseline and Follow-up images
-
-#### Stack Scroll Tool**
-
-To scroll between two series synchronously, click More button and select **Stack Scroll Tool**.
-
-
-
-#### Case Progress
-
-Case Progress icon displays the progress of target and non-target lesions which are assessed out of total lesions. Number in the middle of the icon shows the number of lesions which are not assessed.
-
-
-
-#### Heads Up Display (HUD) Panel
-
-To have a quick view of the Measurements table, click **HUD** button at the top-right of the workspace. You can easily move and resize the HUD Panel on the workspace.
-
-
-
-Note: HUD button is only available if the study is associated with a timepoint.
-
-### Additional Tools
-#### Temporary Length Measurement
-To check the size of a lesion without adding to the Measurements table, select **Temp Tool**.
-
-
-#### Stack Scroll Tool
-To scroll images:
-
-1. Click More button and select **Stack Scroll Tool**.
-2. Drag down and up on the image viewer with the left mouse button.
-
-
-
-#### Reset Image Tool
-To reset the geometric orientation, position and magnification of the image in the active image viewer, click More button and select **Reset Tool**.
-
-
-#### Rotate Right Tool
-To rotate the image in the active image viewer 90 degree clockwise, click More button and select **Rotate Right Tool**.
-
-
-#### Flip H Tool
-To flip the image in the active image viewer horizontally, click More button and select **Flip H Tool**.
-
-
-#### Flip V Tool
-To flip the image in the active image viewer vertically, click More button and select **Flip V Tool**.
-
-
-#### Invert Tool
-To invert the image in the active image viewer, click More button and select **Invert Tool**.
-
-
-#### Magnify Tool
-Magnify Tool allows to zoom in a part of the image without changing zoom level of the whole image.
-
-1. Click More button and select **Magnify Tool**.
-2. Click and drag around the image with the left mouse button.
-
-
-
-#### 3.7.9. Ellipse Tool
-Ellipse Tool allows you to draw elliptical annotation with mean and standard deviation values.
-
-1. Click More button and select **Ellipse Tool** then click on the image.
-2. Hold, move and release the mouse button to create the annotation.
-
-
-
-#### Save Screenshot
-To save a screenshot of the current active viewport:
-
-1. Click More button and click **Save Screenshot**.
-2. Set image name and sizes and click Download.
-
-
-
-#### CINE Tool
-CINE Tool allows to play through stacks of images in the active viewport.
-
-1. Click More button and click **CINE Tool**. The CINE dialog will pop up.
-2. Click the desired media options from the CINE dialog.
-3. To close CINE dialog, click **CINE Tool**.
-
-
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/packages/_category_.json b/platform/docs/versioned_docs/version-1.0-deprecated/packages/_category_.json
deleted file mode 100644
index 55e5c9e73..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/packages/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Packages",
- "position": 6
-}
diff --git a/platform/docs/versioned_docs/version-1.0-deprecated/packages/measurements.md b/platform/docs/versioned_docs/version-1.0-deprecated/packages/measurements.md
deleted file mode 100644
index 4ac6f667f..000000000
--- a/platform/docs/versioned_docs/version-1.0-deprecated/packages/measurements.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Measurements Package (ohif-measurements)
-
-## Package design
-
-## Usage
-
-## How to define a Measurement tool
-
-## How to validate Measurements
-
-## Data exchange concepts
-
-## Longitudinal Measurements
-
-## Timepoints
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/Architecture.md b/platform/docs/versioned_docs/version-2.0-deprecated/Architecture.md
deleted file mode 100644
index d61e3ffa1..000000000
--- a/platform/docs/versioned_docs/version-2.0-deprecated/Architecture.md
+++ /dev/null
@@ -1,151 +0,0 @@
----
-sidebar_position: 5
-title: Architecture
----
-
-# Architecture
-
-Looking to extend your instance of the OHIF Viewer? Want learn how to reuse _a
-portion_ of the Viewer in your own application? Or maybe you want to get
-involved and draft or suggest a new feature? Regardless, you're in the right
-place!
-
-The OHIF Viewer aims to be decoupled, configurable, and extensible; while this
-allows our code to be used in more ways, it also increases complexity. Below, we
-aim to demistify that complexity by providing insight into how our Viewer is
-architected, and the role each of it's dependent libraries plays.
-
-
-
-The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a
-[`monorepo`][monorepo]. This means that this repository, instead of containing a
-single project, contains many projects. If you explore our project structure,
-you'll see the following:
-
-```bash
-.
-├── extensions
-│ ├── _example # Skeleton of example extension
-│ ├── cornerstone # 2D images w/ Cornerstone.js
-│ ├── dicom-html # Structured Reports as HTML in viewport
-│ ├── dicom-microscopy # Whole slide microscopy viewing
-│ ├── dicom-pdf # View DICOM wrapped PDFs in viewport
-│ └── vtk # MPR and Volume support w/ VTK.js
-│
-├── platform
-│ ├── core # Business Logic
-│ ├── i18n # Internationalization Support
-│ ├── ui # React component library
-│ └── viewer # Connects platform and extension projects
-│
-├── ... # misc. shared configuration
-├── lerna.json # MonoRepo (Lerna) settings
-├── package.json # Shared devDependencies and commands
-└── README.md
-```
-
-The `platform` directory contains the business logic library, component library,
-and the application library that combines them to create a powerful medical
-imaging viewer.
-
-The `extensions` directory contains many packages that can be registered with
-`@ohif/core`'s `ExtensionManager` to expand an application's supported features
-and functionality.
-
-
-
-
-This diagram is a conceptual illustration of how the Viewer is architected.
-
-1. (optional) `extensions` can be registered with `@ohif/core`'s
- `ExtensionManager`
-2. `@ohif/core` provides business logic and a way for `@ohif/viewer` to access
- registered extensions
-3. The `@ohif/viewer` composes and provides data to components from our
- component library (`@ohif/ui`)
-4. The `@ohif/viewer` can be built and served as a stand-alone PWA, or as an
- embeddable package ([`@ohif/viewer`][viewer-npm])
-
-## Business Logic
-
-The [`@ohif/core`][core-github] project offers pre-packaged solutions for
-features common to Web-based medical imaging viewers. For example:
-
-- Hotkeys
-- DICOM Web requests
-- Hanging Protocols
-- Managing a study's measurements
-- Managing a study's DICOM metadata
-- [A flexible pattern for extensions](./extensions/index.md)
-- And many others
-
-It does this while remaining decoupled from any particular view library or
-rendering logic. While we use it to power our React Viewer, it can be used with
-Vue, React, Vanilla JS, or any number of other frameworks.
-
-## React Component Library
-
-[`@ohif/ui`][ui-github] is a React Component library that contains the reusable
-components that power the OHIF Viewer. It allows us to build, compose, and test
-components in isolation; easing the development process by reducing the need to
-stand-up a local PACS with test case data.
-
-Extension authors can also use these same components when building their
-extension's UI; allowing for a consistent look and feel with the rest of the
-application.
-
-[Check out our component library!](https://react.ohif.org/)
-
-## Extensions & Configuration
-
-While OHIF maintains several high value and commonly requested features in its
-own extensions, there are many instances where one may wish to further extend
-the viewer. Some common use cases include:
-
-- Adding AI/ML tools and insights
-- Custom workflows for guided diagnosis
-- Collecting specific annotations for training data or reports
-- Authentication and granular permissions
-- Teleconsultation workflow, image comments, and tracking
-- Adding surgical templating tools and reports
-- and many others
-
-We expose common integration points via [extensions](./extensions/index.md) to
-make this possible. The viewer and many of our own extensions also offer
-[configuration][configuration]. For a list of extensions maintained by OHIF,
-[check out this helpful table](./extensions/index.md#maintained-extensions).
-
-If you find yourself thinking "I wish the Viewer could do X", and you can't
-accomplish it with an extension today, create a GitHub issue! We're actively
-looking for ways to improve our extensibility ^\_^
-
-[Click here to read more about extensions!](./extensions/index.md)
-
-## Common Questions
-
-> When should I use the packaged source `@ohif/viewer` versus building a PWA
-> from the source?
-
-...
-
-> Can I create my own Viewer using Vue.js or Angular.js?
-
-You can, but you will not be able to leverage as much of the existing code and
-components. `@ohif/core` could still be used for business logic, and to provide
-a model for extensions. `@ohif/ui` would then become a guide for the components
-you would need to recreate.
-
-
-
-
-[monorepo]: https://github.com/OHIF/Viewers/issues/768
-[viewers-project]: https://github.com/OHIF/Viewers
-[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
-[pwa]: https://developers.google.com/web/progressive-web-apps/
-[configuration]: ./configuring/index.md
-[extensions]: ./extensions/index.md
-[core-github]: https://github.com/OHIF/viewers/platform/core
-[ui-github]: https://github.com/OHIF/Viewers/tree/master/platform/ui
-
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/README.md b/platform/docs/versioned_docs/version-2.0-deprecated/README.md
deleted file mode 100644
index 14e4553fa..000000000
--- a/platform/docs/versioned_docs/version-2.0-deprecated/README.md
+++ /dev/null
@@ -1,74 +0,0 @@
----
-id: Introduction
-slug: /
-sidebar_position: 1
----
-
-
-
-
- Looking for a Live Demo?
-
-
-
-
-
-
-
-
-> ATTENTION! You are looking at the docs for the `React` version of the OHIF
-> Viewer. If you're looking for the `Meteor` version's documentation (now
-> deprecated), select it's version from the dropdown box in the top right corner
-> of this page.
-
-The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source,
-web-based, medical imaging viewer. It can be configured to connect to Image
-Archives that support [DicomWeb][dicom-web], and offers support for mapping to
-proprietary API formats. OHIF maintained extensions add support for viewing,
-annotating, and reporting on DICOM images in 2D (slices) and 3D (volumes).
-
-
-
-
The OHIF Viewer: A general purpose DICOM Viewer (Live Demo)
-
-The Open Health Imaging Foundation intends to provide a simple general purpose
-DICOM Viewer which can be easily extended for specific uses. If you find
-yourself unable to extend the viewer for your purposes, please reach out via our
-[GitHub issues][gh-issues]. We are actively seeking feedback on ways to improve
-our integration and extension points.
-
-## Where to next?
-
-Check out these helpful links:
-
-- Ready to dive into some code? Check out our
- [Getting Started Guide](./development/getting-started.md).
-- We're an active, vibrant community.
- [Learn how you can be more involved.](./development/contributing.md)
-- Feeling lost? Read our [help page](./help.md).
-
-
-
-
-[ohif-org]: https://www.ohif.org
-[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
-[gh-issues]: https://github.com/OHIF/Viewers/issues
-
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/architecture-diagram.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/architecture-diagram.png
deleted file mode 100644
index 1c43d0108..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/architecture-diagram.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/dialog-example.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/dialog-example.gif
deleted file mode 100644
index b6f9754c4..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/dialog-example.gif and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-diagram.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-diagram.png
deleted file mode 100644
index c71be8692..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-diagram.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-panel.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-panel.gif
deleted file mode 100644
index fd173bd70..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-panel.gif and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar-nested.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar-nested.gif
deleted file mode 100644
index d89a75ab3..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar-nested.gif and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar.gif
deleted file mode 100644
index 88c313f3d..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-toolbar.gif and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-viewport.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-viewport.png
deleted file mode 100644
index 0ecffda06..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/extensions-viewport.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/homePage.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/homePage.png
deleted file mode 100644
index 9ae0624cc..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/homePage.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/lesionTracker.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/lesionTracker.png
deleted file mode 100644
index effc7da90..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/lesionTracker.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/modal-example.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/modal-example.gif
deleted file mode 100644
index 834705de1..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/modal-example.gif and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/netlify-drop.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/netlify-drop.gif
deleted file mode 100644
index 98634e088..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/netlify-drop.gif and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/notification-example.gif b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/notification-example.gif
deleted file mode 100644
index 34d564cb4..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/notification-example.gif and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/viewer.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/viewer.png
deleted file mode 100644
index 21eacd59a..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/viewer.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/worklist.png b/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/worklist.png
deleted file mode 100644
index 3f126c784..000000000
Binary files a/platform/docs/versioned_docs/version-2.0-deprecated/assets/img/worklist.png and /dev/null differ
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/_category_.json b/platform/docs/versioned_docs/version-2.0-deprecated/configuring/_category_.json
deleted file mode 100644
index eef358871..000000000
--- a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Configuring",
- "position": 4
-}
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/data-source.md b/platform/docs/versioned_docs/version-2.0-deprecated/configuring/data-source.md
deleted file mode 100644
index 6de5fa157..000000000
--- a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/data-source.md
+++ /dev/null
@@ -1,170 +0,0 @@
----
-sidebar_position: 2
----
-# Data Source
-
-After following the steps outlined in
-[Getting Started](./../development/getting-started.md), you'll notice that the
-OHIF Viewer has data for several studies and their images. You didn't add this
-data, so where is it coming from?
-
-By default, the viewer is configured to connect to a remote server hosted by the
-nice folks over at [dcmjs.org][dcmjs-org]. While convenient for getting started,
-the time may come when you want to develop using your own data either locally or
-remotely.
-
-## Set up a local DICOM server
-
-> ATTENTION! Already have a remote or local server? Skip to the
-> [configuration section](#configuration-learn-more) below.
-
-While the OHIF Viewer can work with any data source, the easiest to configure
-are the ones that follow the [DICOMWeb][dicom-web] spec.
-
-1. Choose and install an Image Archive
-2. Upload data to your archive (e.g. with DCMTK's [storescu][storescu] or your
- archive's web interface)
-3. Keep the server running
-
-For our purposes, we will be using `Orthanc`, but you can see a list of
-[other Open Source options](#open-source-dicom-image-archives) below.
-
-### Requirements
-
-- Docker
- - [Docker for Mac](https://docs.docker.com/docker-for-mac/)
- - [Docker for Windows (recommended)](https://docs.docker.com/docker-for-windows/)
- - [Docker Toolbox for Windows](https://docs.docker.com/toolbox/toolbox_install_windows/)
-
-_Not sure if you have `docker` installed already? Try running `docker --version`
-in command prompt or terminal_
-
-> If you are using `Docker Toolbox` you need to change the _PROXY_DOMAIN_
-> parameter in _platform/viewer/package.json_ to http://192.168.99.100:8042 or
-> the ip docker-machine ip throws. This is the value [`WebPack`][webpack-proxy]
-> uses to proxy requests
-
-### Running Orthanc
-
-_Start Orthanc:_
-
-```bash
-# Runs orthanc so long as window remains open
-yarn run orthanc:up
-```
-
-_Upload your first Study:_
-
-1. Navigate to
- [Orthanc's web interface](http://localhost:8042/ui/app/index.html#/) at
- `http://localhost:8042/ui/app/index.html#/` in a web browser.
-2. In the left you can see the upload button where you can drag and drop your DICOM files
-
-#### Orthanc: Learn More
-
-You can see the `docker-compose.yml` file this command runs at
-[`
/.docker/Nginx-Orthanc/`][orthanc-docker-compose], and more on
-Orthanc for Docker in [Orthanc's documentation][orthanc-docker].
-
-### Connecting to Orthanc
-
-Now that we have a local Orthanc instance up and running, we need to configure
-our web application to connect to it. Open a new terminal window, navigate to
-this repository's root directory, and run:
-
-```bash
-# If you haven't already, enable yarn workspaces
-yarn config set workspaces-experimental true
-
-# Restore dependencies
-yarn install
-
-# Run our dev command, but with the local orthanc config
-yarn run dev:orthanc
-```
-
-#### Configuration: Learn More
-
-> For more configuration fun, check out the
-> [Essentials Configuration](./index.md) guide.
-
-Let's take a look at what's going on under the hood here. `yarn run dev:orthanc`
-is running the `dev:orthanc` script in our project's `package.json`. That script
-is:
-
-```js
-cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker-nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js -w
-```
-
-- `cross-env` sets three environment variables
- - PROXY_TARGET: `/dicom-web`
- - PROXY_DOMAIN: `http://localhost:8042`
- - APP_CONFIG: `config/docker-nginx-orthanc.js`
-- `webpack-dev-server` runs using the `.webpack/webpack.pwa.js` configuration
- file. It will watch for changes and update as we develop.
-
-`PROXY_TARGET` and `PROXY_DOMAIN` tell our development server to proxy requests
-to `Orthanc`. This allows us to bypass CORS issues that normally occur when
-requesting resources that live at a different domain.
-
-The `APP_CONFIG` value tells our app which file to load on to `window.config`.
-Here is what that
-configuration looks like:
-
-```js title="/platform/viewer/public/config/default.js"
-window.config = {
- routerBasename: '/',
- servers: {
- dicomWeb: [
- {
- name: 'Orthanc',
- wadoUriRoot: 'http://localhost:8899/wado',
- qidoRoot: 'http://localhost:8899/dicom-web',
- wadoRoot: 'http://localhost:8899/dicom-web',
- qidoSupportsIncludeField: false,
- imageRendering: 'wadors',
- thumbnailRendering: 'wadors',
- },
- ],
- },
-};
-```
-
-To learn more about how you can configure the OHIF Viewer, check out our
-[Configuration Guide](./index.md).
-
-## Open Source DICOM Image Archives
-
-Our example uses `Orthanc`, but there are a lot of options available to you.
-Here are some of the more popular ones:
-
-| Archive | Installation |
-| --------------------------------------------- | ---------------------------------- |
-| [DCM4CHEE Archive 5.x][dcm4chee] | [W/ Docker][dcm4chee-docker] |
-| [Orthanc][orthanc] | [W/ Docker][orthanc-docker] |
-| [DICOMcloud][dicomcloud] (**DICOM Web only**) | [Installation][dicomcloud-install] |
-| [OsiriX][osirix] (**Mac OSX only**) | Desktop Client |
-| [Horos][horos] (**Mac OSX only**) | Desktop Client |
-
-_Feel free to make a Pull Request if you want to add to this list._
-
-
-
-
-[dcmjs-org]: https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado
-[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
-[storescu]: http://support.dcmtk.org/docs/storescu.html
-[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy
-[orthanc-docker-compose]: https://github.com/OHIF/Viewers/tree/master/.docker/Nginx-Orthanc
-
-[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light
-[dcm4chee-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker
-[orthanc]: https://www.orthanc-server.com/
-[orthanc-docker]: http://book.orthanc-server.com/users/docker.html
-[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud
-[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code
-[osirix]: http://www.osirix-viewer.com/
-[horos]: https://www.horosproject.org/
-
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/index.md b/platform/docs/versioned_docs/version-2.0-deprecated/configuring/index.md
deleted file mode 100644
index 3d602fa40..000000000
--- a/platform/docs/versioned_docs/version-2.0-deprecated/configuring/index.md
+++ /dev/null
@@ -1,124 +0,0 @@
----
-sidebar_position: 1
----
-
-# Configuration
-
-> This step assumes you have an imaging archive. If you need assistance setting
-> one up, check out the [`Data Source` Guide](./data-source.md) or a deployment
-> recipe that contains an open Image Archive
-
-
-
-### Configuration Files
-
-The configuration for our viewer is in the `platform/viewer/public/config`
-directory. Our build process knows which configuration file to use based on the
-`APP_CONFIG` environment variable. By default, its value is
-[`config/default.js`][default-config]. The majority of the viewer's features,
-and registered extension's features, are configured using this file.
-
-**Embedded Use Note:**
-
-Alternatively, when using the `umd` bundle for embedded use cases, these same
-values are what you'll pass to `installViewer` method:
-
-`OHIFStandaloneViewer.installViewer(window.config)`
-
-### Environment Variables
-
-We use environment variables at build and dev time to change the Viewer's
-behavior. We can update the `HTML_TEMPLATE` to easily change which extensions
-are registered, and specify a different `APP_CONFIG` to connect to an
-alternative data source (or even specify different default hotkeys).
-
-| Environment Variable | Description | Default |
-| -------------------- | -------------------------------------------------------------------------------------------------- | ------------------- |
-| `HTML_TEMPLATE` | Which [HTML template][html-templates] to use as our web app's entry point. Specific to PWA builds. | `index.html` |
-| `PUBLIC_URL` | The route relative to the host that the app will be served from. Specific to PWA builds. | `/` |
-| `APP_CONFIG` | Which [configuration file][config-file] to copy to output as `app-config.js` | `config/default.js` |
-| `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` |
-| `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` |
-
-## How do I configure my project?
-
-The simplest way is to update the existing default config:
-
-
-```js title="/platform/viewer/public/config/default.js"
-window.config = {
- routerBasename: '/',
- servers: {
- dicomWeb: [
- {
- name: 'DCM4CHEE',
- wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
- qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
- wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
- qidoSupportsIncludeField: true,
- imageRendering: 'wadors',
- thumbnailRendering: 'wadors',
- },
- ],
- },
-};
-```
-
-The configuration can also be written as a JS Function in case you need to inject dependencies like external services:
-
-```js
-window.config = ({ servicesManager } = {}) => {
- const { UIDialogService } = servicesManager.services;
- return {
- cornerstoneExtensionConfig: {
- tools: {
- ArrowAnnotate: {
- configuration: {
- getTextCallback: (callback, eventDetails) => UIDialogService.create({...
- }
- }
- },
- },
- routerBasename: '/',
- servers: {
- dicomWeb: [
- {
- name: 'DCM4CHEE',
- wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
- qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
- wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
- qidoSupportsIncludeField: true,
- imageRendering: 'wadors',
- thumbnailRendering: 'wadors',
- },
- ],
- },
- };
-};
-```
-
-You can also create a new config file and specify its path relative to the build
-output's root by setting the `APP_CONFIG` environment variable. You can set the
-value of this environment variable a few different ways:
-
-- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~
- - Previous `react-scripts` functionality that we need to duplicate with
- `dotenv-webpack`
-- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~
- - Previous `react-scripts` functionality that we need to duplicate with
- `dotenv-webpack`
-- Using the `cross-env` package in an npm script:
- - `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"`
-
-After updating the configuration, `yarn run build` to generate updated build
-output.
-
-
-
-
-[default-config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/default.js
-[html-templates]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/html-templates
-[config-files]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config
-
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/index.md b/platform/docs/versioned_docs/version-2.0-deprecated/deployment/index.md
deleted file mode 100644
index 5444026d7..000000000
--- a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/index.md
+++ /dev/null
@@ -1,309 +0,0 @@
----
-sidebar_position: 1
-sidebar_label: Overview
----
-# Deployment
-
-The OHIF Viewer can be embedded in other web applications via it's [packaged
-script source][viewer-npm], or served up as a stand-alone PWA ([progressive web
-application][pwa-url]) by building and hosting a collection of static assets. In
-either case, you will need to configure your instance of the Viewer so that it
-can connect to your data source (the database or PACS that provides the data
-your Viewer will display).
-
-
-Our goal is to make deployment as simple and painless as possible; however,
-there is an inherent amount of complexity in configuring and deploying web
-applications. If you find yourself a little lost, please don't hesitate to
-[reach out for help](/help.md)
-
-## Deployment Scenarios
-
-### Embedded Viewer
-
-The quickest and easiest way to get the OHIF Viewer up and running is to embed
-it into an existing web application. It allows us to forego a "build step", and
-add a powerful medical imaging viewer to an existing web page using only a few
-include tags.
-
-- Read more about it here: [Embedded Viewer](./recipes/embedded-viewer.md)
-- And check out our [live demo on CodeSandbox][code-sandbox]
-
-
-
-
-### Stand-alone Viewer
-
-Deploying the OHIF Viewer as a stand-alone web application provides many
-benefits, but comes at the cost of time and complexity. Some benefits include:
-
-_Today:_
-
-- Leverage [extensions](/extensions/index.md) to drop-in powerful new features
-- Add routes and customize the viewer's workflow
-- Finer control over styling and whitelabeling
-
-_In the future:_
-
-- The ability to package the viewer for [App Store distribution][app-store]
-- Leverage `service-workers` for offline support and speed benefits from caching
-
-#### Hosted Static Assets
-
-At the end of the day, a production OHIF Viewer instance is a collection of
-HTML, CSS, JS, Font Files, and Images. We "build" those files from our
-`source code` with configuration specific to our project. We then make those
-files publicly accessible by hosting them on a Web Server.
-
-If you have not deployed a web application before, this may be a good time to
-[reach out for help](/help.md), as these steps assume prior web development and
-deployment experience.
-
-##### Part 1 - Build Production Assets
-
-"Building", or creating, the files you will need is the same regardless of the
-web host you choose. You can find detailed instructions on how to configure and
-build the OHIF Viewer in our
-["Build for Production" guide](./recipes/build-for-production.md).
-
-##### Part 2 - Host Your App
-
-There are a lot of [benefits to hosting static assets][host-static-assets] over
-dynamic content. You can find instructions on how to host your build's output
-via one of these guides:
-
-_Drag-n-drop_
-
-- [Netlify: Drop](/deployment/recipes/static-assets.md#netlify-drop)
-
-_Easy_
-
-- [Surge.sh](/deployment/recipes/static-assets.md#surgesh)
-- [GitHub Pages](/deployment/recipes/static-assets.md#github-pages)
-
-_Advanced_
-
-- [AWS S3 + Cloudfront](/deployment/recipes/static-assets.md#aws-s3--cloudfront)
-- [GCP + Cloudflare](/deployment/recipes/static-assets.md#gcp--cloudflare)
-- [Azure](/deployment/recipes/static-assets.md#azure)
-
-## Data
-
-The OHIF Viewer is able to connect to any data source that implements the [DICOM
-Web Standard][dicom-web-standard]. [DICOM Web][dicom-web] refers to RESTful
-DICOM Services -- a recently standardized set of guidelines for exchanging
-medical images and imaging metadata over the internet. Not all archives fully
-support it yet, but it is gaining wider adoption.
-
-### Configure Connection
-
-If you have an existing archive and intend to host the OHIF Viewer at the same
-domain name as your archive, then connecting the two is as simple as following
-the steps laid out in our
-[Configuration Essentials Guide](./../configuring/index.md).
-
-#### What if I don't have an imaging archive?
-
-We provide some guidance on configuring a local image archive in our
-[Data Source Essentials](./../configuring/data-source.md) guide. Hosting an
-archive remotely is a little trickier. You can check out some of our
-[advanced recipes](#recipes) for modeled setups that may work for you.
-
-#### What if I intend to host the OHIF Viewer at a different domain?
-
-There are two important steps to making sure this setup works:
-
-1. Your Image Archive needs to be exposed, in some way, to the open web. This
- can be directly, or through a `reverse proxy`, but the Viewer needs _some
- way_ to request it's data.
-2. \* Your Image Archive needs to have appropriate CORS (Cross-Origin Resource
- Sharing) Headers
-
-> \* Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional
-> HTTP headers to tell a browser to let a web application running at one origin
-> (domain) have permission to access selected resources from a server at a
-> different origin. - [MDN Web Docs: Web - Http - CORS][cors]
-
-Most image archives do not provide either of these features "out of the box".
-It's common to use IIS, Nginx, or Apache to route incoming requests and append
-appropriate headers. You can find an example of this setup in our
-[Nginx + Image Archive Deployment Recipe](./recipes/nginx--image-archive.md).
-
-#### What if my archive doesn't support DicomWeb?
-
-It's possible to supply all Study data via JSON format, in the event you do not have a DicomWeb endpoint.
-You can host all of the relevant files on any web accessible server (Amazon S3, Azure Blob Storage, Local file server etc.)
-
-This JSON is supplied via the '?url=' query parameter.
-It should reference an endpoint that returns **application/json** formatted text.
-
-If you do not have an API, you can simply return a text file containing the JSON from any web server.
-
-
-You tell the OHIF viewer to use JSON by appending the `'?url='` query to the `/Viewer` route:
-
-eg. `https://my-test-ohif-server/viewer?url=https://my-json-server/study-uid.json`
-
-The returned JSON object must contain a single root object with a 'studies' array.
-
-
-*Sample JSON format:*
-```json
-{
- "studies": [
- {
- "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78",
- "StudyDescription": "BRAIN SELLA",
- "StudyDate": "20010108",
- "StudyTime": "120022",
- "PatientName": "MISTER^MR",
- "PatientId": "832040",
- "series": [
- {
- "SeriesDescription": "SAG T-1",
- "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
- "SeriesNumber": 2,
- "SeriesDate": "20010108",
- "SeriesTime": "120318",
- "Modality": "MR",
- "instances": [
- {
- "metadata": {
- "Columns": 512,
- "Rows": 512,
- "InstanceNumber": 3,
- "AcquisitionNumber": 0,
- "PhotometricInterpretation": "MONOCHROME2",
- "BitsAllocated": 16,
- "BitsStored": 16,
- "PixelRepresentation": 1,
- "SamplesPerPixel": 1,
- "PixelSpacing": [0.390625, 0.390625],
- "HighBit": 15,
- "ImageOrientationPatient": [0,1,0,0,0,-1],
- "ImagePositionPatient": [11.600000,-92.500000, 98.099998],
- "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470",
- "ImageType": ["ORIGINAL","PRIMARY","OTHER"],
- "Modality": "MR",
- "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.124",
- "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
- "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78"
- },
- "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.124.dcm"
- }
- ]
- }
- ]
- }
- ]
-}
-```
-More info on this JSON format can be found here [Issue #1500](https://github.com/OHIF/Viewers/issues/1500)
-
-
-**Implementation Notes:**
-
-1. When hosting the viewer, you will also need to host a /viewer route on the server - or the browser may not be able to find the route.
-2. For each instance url (dicom object) in the returned JSON, you must prefix the `url` with `dicomweb:` in order for the cornerstone image loader to retrieve it correctly.
- eg. `https://image-server/my-image.dcm` ---> `dicomweb:https://image-server/my-image.dcm`
-3. The JSON format above is compatible with >= v3.7.8 of the application. Older versions of the viewer used a different JSON format. As of 20/04/20 the public [https://viewer.ohif.org/] is a pre 3.0 version that does not support this format yet.
-4. The JSON format is case-sensitive. Please ensure you have matched casing with the naturalised Dicom format referenced in [Issue #1500](https://github.com/OHIF/Viewers/issues/1500).
-
-*CORS Issues (Cross-Origin Resource Sharing)*
-
-If you host a JSON API or Images on a different domain from the the app itself, you will likely have CORS issues. This will also happen when testing from Localhost and reaching out to remote servers.
-Even if the domain is the same, different ports, subdomains or protocols (https vs http) will also cause CORS errors.
-You will to need add a configuration on each server hosting these assets to allow your App server origin.
-
-For example:
-
-Lets assume your application is hosted on `https://my-ohif-server.com`.
-
-Your JSON API is hosted on `https://my-json-api.aws.com`
-
-And your images are stored on Amazon S3 at `https://my-s3-bucket.aws.com`
-
-When you first start your application, browsing to `https://my-ohif-server.com/viewer?url=https://my-json-api.aws.com/api/my-json-study-info.json`, you will likely get a CORS error in the browser console as it tries to connect to `https://my-json-api.aws.com`.
-
-Adding a setting on the JSON server to allow the CORS origin = `https://my-ohif-server.com` should solve this.
-
-Next, you will likely get a similar CORS error, as the browser tries to go to `https://my-s3-bucket.aws.com`.
-You will need to go to the S3 bucket configuration, and add a CORS setting to allow origin = `https://my-ohif-server.com`.
-
-Essentially, whenever the application connects to a remote resource, you will need to add the applications url to the allowed CORS Origins on that resource. Adding an origin similar to https://localhost:3000 will also allow for local testing.
-
-
-### Securing Your Data
-
-> Feeling lost? Securing your data is important, and it can be hard to tell if
-> you've gotten it right. Don't hesitate to work with professional auditors, or
-> [enlist help from experts](./../help.md).
-
-The OHIF Viewer can be configured to work with authorization servers that
-support one or more of the OpenID-Connect authorization flows. The Viewer finds
-it's OpenID-Connect settings on the `oidc` configuration key. You can set these
-values following the instructions laid out in the
-[Configuration Essentials Guide](./../configuring/index.md).
-
-_Example OpenID-Connect Settings:_
-
-```js
-window.config = {
- ...
- oidc: [
- {
- // ~ REQUIRED
- // Authorization Server URL
- authority: 'http://127.0.0.1/auth/realms/ohif',
- client_id: 'ohif-viewer',
- redirect_uri: 'http://127.0.0.1/callback', // `OHIFStandaloneViewer.js`
- response_type: 'code', // "Authorization Code Flow"
- scope: 'openid', // email profile openid
- // ~ OPTIONAL
- post_logout_redirect_uri: '/logout-redirect.html',
- },
- ],
-}
-```
-
-You can find an example of this setup in our
-[User Account Control Deployment Recipe](./recipes/user-account-control.md).
-
-#### Choosing a Flow for the Viewer
-
-In general, we recommend using the "Authorization Code Flow" ( [see
-`response_type=code` here][code-flows]); however, the "Implicit Flow" ( [see
-`response_type=token` here][code-flows]) can work if additional precautions are
-taken. If the flow you've chosen produces a JWT Token, it's validity can be used
-to secure access to your Image Archive as well.
-
-### Recipes
-
-We've included a few recipes for common deployment scenarios. There are many,
-many possible configurations, so please don't feel limited to these setups.
-Please feel free to suggest or contribute your own recipes.
-
-- Script Include
- - [Embedding the Viewer](./recipes/embedded-viewer.md)
-- Stand-Alone
- - [Build for Production](./recipes/build-for-production.md)
- - [Static](./recipes/static-assets.md)
- - [Nginx + Image Archive](./recipes/nginx--image-archive.md)
- - [User Account Control](./recipes/user-account-control.md)
-
-
-
-
-[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
-[pwa-url]: https://developers.google.com/web/progressive-web-apps/
-[static-assets-url]: https://www.maxcdn.com/one/visual-glossary/static-content/
-[app-store]: https://medium.freecodecamp.org/i-built-a-pwa-and-published-it-in-3-app-stores-heres-what-i-learned-7cb3f56daf9b
-[dicom-web-standard]: https://www.dicomstandard.org/dicomweb/
-[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
-[host-static-assets]: https://www.netlify.com/blog/2016/05/18/9-reasons-your-site-should-be-static/
-[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
-[code-flows]: https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660
-[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
-
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/_category_.json b/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/_category_.json
deleted file mode 100644
index 5b36cc15d..000000000
--- a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Recipes",
- "position": 2
-}
diff --git a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/embedded-viewer.md b/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/embedded-viewer.md
deleted file mode 100644
index b3a125659..000000000
--- a/platform/docs/versioned_docs/version-2.0-deprecated/deployment/recipes/embedded-viewer.md
+++ /dev/null
@@ -1,179 +0,0 @@
----
-sidebar_position: 1
----
-# Embedded Viewer
-
-The quickest and easiest way to get the OHIF Viewer up and running is to embed
-it into an existing web application. It allows us to forego a "build step", and
-add a powerful medical imaging viewer to an existing web page using only a few
-include tags. Here's how it works:
-
-
-
-
-
-
-
-
-1. Create a new web page or template that includes the following external
- dependencies:
-
-
-
-
- - Create a JS Object or Function to hold the OHIF Viewer's configuration. Here are some
- example values that would allow the viewer to hit our public PACS:
-
-
-```js
-// Set before importing `ohif-viewer` (JS Object)
-window.config = {
- // default: '/'
- routerBasename: '/',
- servers: {
- dicomWeb: [
- {
- name: 'DCM4CHEE',
- wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
- qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
- wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
- qidoSupportsIncludeField: true,
- imageRendering: 'wadors',
- thumbnailRendering: 'wadors',
- },
- ],
- },
-};
-```
-
-To learn more about how you can configure the OHIF Viewer, check out our
-[Configuration Guide](../../configuring/index.md).
-
--
- Render the viewer in the web page's target
div
-
-
-```js
-// Made available by the `@ohif/viewer` script included in step 1
-var containerId = 'id-of-div-to-render-component-to';
-var componentRenderedOrUpdatedCallback = function() {
- console.log('OHIF Viewer rendered/updated');
-};
-window.OHIFViewer.installViewer(
- window.config,
- containerId,
- componentRenderedOrUpdatedCallback
-);
-```
-
-You can see a live example of this recipe in [this CodeSandbox][code-sandbox].
-
-## Add Extensions
-
-The UMD build of the OHIF Viewer is a "light weight" build that only contains
-the core extensions required for basic 2D image viewing. It's possible to add
-other extensions at runtime.
-
-This only requires us to include a single script tag, and add it using the
-`extensions` key to our config. In this practical example, we register our
-popular whole slide microscopy extension:
-
-```html
-
-
-
-
-```
-
-You can see an example of a slide microscopy study in the viewer [with the
-extension enabled here][whole-slide-ext-demo] ([source code][ext-code-sandbox])
-and [without it here][whole-slide-base-demo] ([source code][code-sandbox]).
-
-You can read more about extensions and how to create your own in our
-[extensions guide](/extensions/index.md).
-
-#### FAQ
-
-> I'm having trouble getting this to work. Where can I go for help?
-
-First, check out this fully functional [CodeSandbox][code-sandbox] example. If
-you're still having trouble, feel free to search or GitHub issues. Can't find
-anything related your problem? Create a new one.
-
-> My application's styles are impacting the OHIF Viewer's look and feel. What
-> can I do?
-
-When you include stylesheets and scripts, they are added globally. This has the
-potential of causing conflicts with other scripts and styles on the page. To
-prevent this, `embed` the viewer in a new/empty web page. Have that working?
-Good. Now `embed` that new page using an
-[`