OHIF with Cornerstone3D 2.0 (#4347)

This commit is contained in:
Alireza authored and GitHub committed 2024-11-06 07:15:27 -05:00
1 parent 8981a61b8c
commit 7ce7127d7c
803 files changed
+32760 -15893

No files matched your search

@@ -1,10 +1,9 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { useResizeDetector } from 'react-resize-detector';
import PropTypes from 'prop-types';
import * as cs3DTools from '@cornerstonejs/tools';
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import { Notification, useViewportDialog, AllInOneMenu } from '@ohif/ui';
import { AllInOneMenu, Notification, useViewportDialog } from '@ohif/ui';
import type { Types as csTypes } from '@cornerstonejs/core';
import { setEnabledElement } from '../state';
@@ -17,8 +16,10 @@ import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
import { useAppConfig } from '@state';
import { LutPresentation, PositionPresentation } from '../types/Presentation';
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
const STACK = 'stack';
@@ -28,428 +29,401 @@ const STACK = 'stack';
*/
let cacheJumpToMeasurementEvent;
function areEqual(prevProps, nextProps) {
if (nextProps.needsRerendering) {
return false;
}
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
return false;
}
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
return false;
}
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
return false;
}
if (prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType) {
return false;
}
if (nextProps.viewportOptions.needsRerendering) {
return false;
}
const prevDisplaySets = prevProps.displaySets;
const nextDisplaySets = nextProps.displaySets;
if (prevDisplaySets.length !== nextDisplaySets.length) {
return false;
}
for (let i = 0; i < prevDisplaySets.length; i++) {
const prevDisplaySet = prevDisplaySets[i];
const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
);
if (!foundDisplaySet) {
return false;
}
// check they contain the same image
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
return false;
}
// check if their imageIds are the same
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
return false;
}
}
}
}
return true;
}
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
// Then we don't need to worry about the re-renders if the props change.
const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
const {
displaySets,
dataSource,
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
// eslint-disable-next-line react/prop-types
onElementDisabled,
isJumpToMeasurementDisabled = false,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
// eslint-disable-next-line react/prop-types
initialImageIndex,
// if the viewport is part of a hanging protocol layout
// we should not really rely on the old synchronizers and
// you see below we only rehydrate the synchronizers if the viewport
// is not part of the hanging protocol layout. HPs should
// define their own synchronizers. Since the synchronizers are
// viewportId dependent and
// eslint-disable-next-line react/prop-types
isHangingProtocolLayout,
} = props;
const OHIFCornerstoneViewport = React.memo(
(
props: withAppTypes<{
viewportId: string;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportGrid.GridViewportOptions;
initialImageIndex: number;
}>
) => {
const {
displaySets,
dataSource,
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
// eslint-disable-next-line react/prop-types
onElementDisabled,
isJumpToMeasurementDisabled = false,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
// eslint-disable-next-line react/prop-types
initialImageIndex,
// if the viewport is part of a hanging protocol layout
// we should not really rely on the old synchronizers and
// you see below we only rehydrate the synchronizers if the viewport
// is not part of the hanging protocol layout. HPs should
// define their own synchronizers. Since the synchronizers are
// viewportId dependent and
// eslint-disable-next-line react/prop-types
isHangingProtocolLayout,
} = props;
const viewportId = viewportOptions.viewportId;
const viewportId = viewportOptions.viewportId;
if (!viewportId) {
throw new Error('Viewport ID is required');
}
// Make sure displaySetOptions has one object per displaySet
while (displaySetOptions.length < displaySets.length) {
displaySetOptions.push({});
}
// Since we only have support for dynamic data in volume viewports, we should
// handle this case here and set the viewportType to volume if any of the
// displaySets are dynamic volumes
viewportOptions.viewportType = displaySets.some(ds => ds.isDynamicVolume && ds.isReconstructable)
? 'volume'
: viewportOptions.viewportType;
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
const [appConfig] = useAppConfig();
const {
displaySetService,
toolbarService,
toolGroupService,
syncGroupService,
cornerstoneViewportService,
cornerstoneCacheService,
stateSyncService,
viewportActionCornersService,
} = servicesManager.services;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(() => {
if (elementRef.current) {
cornerstoneViewportService.resize();
setImageScrollBarHeight();
if (!viewportId) {
throw new Error('Viewport ID is required');
}
}, [elementRef]);
const cleanUpServices = useCallback(
viewportInfo => {
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
// Make sure displaySetOptions has one object per displaySet
while (displaySetOptions.length < displaySets.length) {
displaySetOptions.push({});
}
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
// Since we only have support for dynamic data in volume viewports, we should
// handle this case here and set the viewportType to volume if any of the
// displaySets are dynamic volumes
viewportOptions.viewportType = displaySets.some(
ds => ds.isDynamicVolume && ds.isReconstructable
)
? 'volume'
: viewportOptions.viewportType;
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
const [appConfig] = useAppConfig();
viewportActionCornersService.clear(viewportId);
},
[viewportId]
);
const {
displaySetService,
toolbarService,
toolGroupService,
syncGroupService,
cornerstoneViewportService,
segmentationService,
cornerstoneCacheService,
viewportActionCornersService,
} = servicesManager.services;
const elementEnabledHandler = useCallback(
evt => {
// check this is this element reference and return early if doesn't match
if (evt.detail.element !== elementRef.current) {
return;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(() => {
if (elementRef.current) {
cornerstoneViewportService.resize();
setImageScrollBarHeight();
}
}, [elementRef]);
const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
setEnabledElement(viewportId, element);
setEnabledVPElement(element);
const cleanUpServices = useCallback(
viewportInfo => {
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
const renderingEngineId = viewportInfo.getRenderingEngineId();
const toolGroupId = viewportInfo.getToolGroupId();
const syncGroups = viewportInfo.getSyncGroups();
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
segmentationService.clearSegmentationRepresentations(viewportId);
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
viewportActionCornersService.clear(viewportId);
},
[
viewportId,
segmentationService,
syncGroupService,
toolGroupService,
viewportActionCornersService,
]
);
const synchronizersStore = stateSyncService.getState().synchronizersStore;
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
// If the viewport used to have a synchronizer, re apply it again
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
}
if (onElementEnabled) {
onElementEnabled(evt);
}
},
[viewportId, onElementEnabled, toolGroupService]
);
// disable the element upon unmounting
useEffect(() => {
cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
setImageScrollBarHeight();
return () => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (!viewportInfo) {
return;
}
cornerstoneViewportService.storePresentation({ viewportId });
// This should be done after the store presentation since synchronizers
// will get cleaned up and they need the viewportInfo to be present
cleanUpServices(viewportInfo);
if (onElementDisabled) {
onElementDisabled(viewportInfo);
}
cornerstoneViewportService.disableElement(viewportId);
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
}, []);
// subscribe to displaySet metadata invalidation (updates)
// Currently, if the metadata changes we need to re-render the display set
// for it to take effect in the viewport. As we deal with scaling in the loading,
// we need to remove the old volume from the cache, and let the
// viewport to re-add it which will use the new metadata. Otherwise, the
// viewport will use the cached volume and the new metadata will not be used.
// Note: this approach does not actually end of sending network requests
// and it uses the network cache
useEffect(() => {
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async ({
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
invalidateData,
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
if (!invalidateData) {
const elementEnabledHandler = useCallback(
evt => {
// check this is this element reference and return early if doesn't match
if (evt.detail.element !== elementRef.current) {
return;
}
const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
setEnabledElement(viewportId, element);
setEnabledVPElement(element);
const renderingEngineId = viewportInfo.getRenderingEngineId();
const toolGroupId = viewportInfo.getToolGroupId();
const syncGroups = viewportInfo.getSyncGroups();
toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
// we don't need reactivity here so just use state
const { synchronizersStore } = useSynchronizersStore.getState();
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
// If the viewport used to have a synchronizer, re apply it again
_rehydrateSynchronizers(viewportId, syncGroupService);
}
if (onElementEnabled && typeof onElementEnabled === 'function') {
onElementEnabled(evt);
}
},
[viewportId, onElementEnabled, toolGroupService]
);
// disable the element upon unmounting
useEffect(() => {
cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
setImageScrollBarHeight();
return () => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData();
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
viewportData,
invalidatedDisplaySetInstanceUID,
dataSource,
displaySetService
);
const keepCamera = true;
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
if (!viewportInfo) {
return;
}
}
);
return () => {
unsubscribe();
};
}, [viewportId]);
useEffect(() => {
// handle the default viewportType to be stack
if (!viewportOptions.viewportType) {
viewportOptions.viewportType = STACK;
}
cornerstoneViewportService.storePresentation({ viewportId });
const loadViewportData = async () => {
const viewportData = await cornerstoneCacheService.createViewportData(
displaySets,
viewportOptions,
dataSource,
initialImageIndex
);
// This should be done after the store presentation since synchronizers
// will get cleaned up and they need the viewportInfo to be present
cleanUpServices(viewportInfo);
// The presentation state will have been stored previously by closing
// a viewport. Otherwise, this viewport will be unchanged and the
// presentation information will be directly carried over.
const state = stateSyncService.getState();
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
if (onElementDisabled && typeof onElementDisabled === 'function') {
onElementDisabled(viewportInfo);
}
const { presentationIds } = viewportOptions;
const presentations = {
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
cornerstoneViewportService.disableElement(viewportId);
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
let measurement;
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
measurement = cacheJumpToMeasurementEvent.measurement;
// Delete the position presentation so that viewport navigates direct
presentations.positionPresentation = null;
cacheJumpToMeasurementEvent = null;
}
}, []);
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
// so we need to re-render (force update via change of the needsRerendering) so that React
// does the diffing and decides we should render this again (although the id and element has not changed)
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
if (viewportOptions.needsRerendering) {
viewportOptions.needsRerendering = false;
}
// subscribe to displaySet metadata invalidation (updates)
// Currently, if the metadata changes we need to re-render the display set
// for it to take effect in the viewport. As we deal with scaling in the loading,
// we need to remove the old volume from the cache, and let the
// viewport to re-add it which will use the new metadata. Otherwise, the
// viewport will use the cached volume and the new metadata will not be used.
// Note: this approach does not actually end of sending network requests
// and it uses the network cache
useEffect(() => {
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async ({
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
invalidateData,
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
if (!invalidateData) {
return;
}
cornerstoneViewportService.setViewportData(
viewportId,
viewportData,
viewportOptions,
displaySetOptions,
presentations
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData();
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
viewportData,
invalidatedDisplaySetInstanceUID,
dataSource,
displaySetService
);
const keepCamera = true;
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
}
}
);
if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
return () => {
unsubscribe();
};
}, [viewportId]);
useEffect(() => {
// handle the default viewportType to be stack
if (!viewportOptions.viewportType) {
viewportOptions.viewportType = STACK;
}
};
loadViewportData();
}, [viewportOptions, displaySets, dataSource]);
const loadViewportData = async () => {
const viewportData = await cornerstoneCacheService.createViewportData(
displaySets,
viewportOptions,
dataSource,
initialImageIndex
);
/**
* There are two scenarios for jump to click
* 1. Current viewports contain the displaySet that the annotation was drawn on
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
* and we need to change the viewports displaySet for jumping.
* Since measurement_jump happens via events and listeners, the former case is handled
* by the measurement_jump direct callback, but the latter case is handled first by
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const presentations = getViewportPresentations(viewportId, viewportOptions);
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
elementRef,
viewportId,
servicesManager
);
let measurement;
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
measurement = cacheJumpToMeasurementEvent.measurement;
// Delete the position presentation so that viewport navigates direct
presentations.positionPresentation = null;
cacheJumpToMeasurementEvent = null;
}
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
// so we need to re-render (force update via change of the needsRerendering) so that React
// does the diffing and decides we should render this again (although the id and element has not changed)
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
if (viewportOptions.needsRerendering) {
viewportOptions.needsRerendering = false;
}
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportId]);
cornerstoneViewportService.setViewportData(
viewportId,
viewportData,
viewportOptions,
displaySetOptions,
presentations
);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
// Doing an === check here because the default config value when not set is true
if (appConfig.addWindowLevelActionMenu === false) {
return;
}
if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
}
};
// TODO: In the future we should consider using the customization service
// to determine if and in which corner various action components should go.
const wlActionMenu = getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
loadViewportData();
}, [viewportOptions, displaySets, dataSource]);
/**
* There are two scenarios for jump to click
* 1. Current viewports contain the displaySet that the annotation was drawn on
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
* and we need to change the viewports displaySet for jumping.
* Since measurement_jump happens via events and listeners, the former case is handled
* by the measurement_jump direct callback, but the latter case is handled first by
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
elementRef,
viewportId,
servicesManager
);
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
// Doing an === check here because the default config value when not set is true
if (appConfig.addWindowLevelActionMenu === false) {
return;
}
const location = viewportActionCornersService.LOCATIONS.topRight;
// TODO: In the future we should consider using the customization service
// to determine if and in which corner various action components should go.
viewportActionCornersService.addComponent({
viewportId,
id: 'windowLevelActionMenu',
component: getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
location,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
}),
location,
});
viewportActionCornersService.addComponent({
viewportId,
id: 'segmentation',
component: getViewportDataOverlaySettingsMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
location,
}),
location,
});
}, [
displaySets,
viewportId,
viewportActionCornersService,
servicesManager,
commandsManager,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
appConfig,
]);
const { ref: resizeRef } = useResizeDetector({
onResize,
});
viewportActionCornersService.setComponent({
viewportId,
id: 'windowLevelActionMenu',
component: wlActionMenu,
location: viewportActionCornersService.LOCATIONS.topRight,
indexPriority: -100,
});
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
const { ref: resizeRef } = useResizeDetector({
onResize,
});
return (
<React.Fragment>
<div className="viewport-wrapper">
<div
className="cornerstone-viewport-element"
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
ref={el => {
resizeRef.current = el;
elementRef.current = el;
}}
></div>
<CornerstoneOverlays
viewportId={viewportId}
toolBarService={toolbarService}
element={elementRef.current}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
<CinePlayer
enabledVPElement={enabledVPElement}
viewportId={viewportId}
servicesManager={servicesManager}
/>
</div>
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
onKeyPress={viewportDialogState.onKeyPress}
return (
<React.Fragment>
<div className="viewport-wrapper">
<div
className="cornerstone-viewport-element"
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
ref={el => {
resizeRef.current = el;
elementRef.current = el;
}}
></div>
<CornerstoneOverlays
viewportId={viewportId}
toolBarService={toolbarService}
element={elementRef.current}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
)}
</div>
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
<OHIFViewportActionCorners viewportId={viewportId} />
</React.Fragment>
);
}, areEqual);
<CinePlayer
enabledVPElement={enabledVPElement}
viewportId={viewportId}
servicesManager={servicesManager}
/>
<ActiveViewportBehavior
viewportId={viewportId}
servicesManager={servicesManager}
/>
</div>
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
onKeyPress={viewportDialogState.onKeyPress}
/>
)}
</div>
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
<OHIFViewportActionCorners viewportId={viewportId} />
</React.Fragment>
);
},
areEqual
);
function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
const { measurementService, cornerstoneViewportService } = servicesManager.services;
@@ -542,12 +516,15 @@ function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesM
}
}
function _rehydrateSynchronizers(
synchronizersStore: { [key: string]: unknown },
viewportId: string,
syncGroupService: any
) {
synchronizersStore[viewportId].forEach(synchronizerObj => {
function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) {
const { synchronizersStore } = useSynchronizersStore.getState();
const synchronizers = synchronizersStore[viewportId];
if (!synchronizers) {
return;
}
synchronizers.forEach(synchronizerObj => {
if (!synchronizerObj.id) {
return;
}
@@ -597,17 +574,91 @@ function _rehydrateSynchronizers(
// Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
OHIFCornerstoneViewport.propTypes = {
displaySets: PropTypes.array.isRequired,
dataSource: PropTypes.object.isRequired,
viewportOptions: PropTypes.object,
displaySetOptions: PropTypes.arrayOf(PropTypes.any),
servicesManager: PropTypes.object.isRequired,
onElementEnabled: PropTypes.func,
isJumpToMeasurementDisabled: PropTypes.bool,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIdOrIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
function areEqual(prevProps, nextProps) {
if (nextProps.needsRerendering) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering');
return false;
}
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change');
return false;
}
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: orientation change');
return false;
}
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: toolGroupId change');
return false;
}
if (
nextProps.viewportOptions.viewportType &&
prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType
) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportType change');
return false;
}
if (nextProps.viewportOptions.needsRerendering) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportOptions.needsRerendering');
return false;
}
const prevDisplaySets = prevProps.displaySets;
const nextDisplaySets = nextProps.displaySets;
if (prevDisplaySets.length !== nextDisplaySets.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length mismatch');
return false;
}
for (let i = 0; i < prevDisplaySets.length; i++) {
const prevDisplaySet = prevDisplaySets[i];
const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
);
if (!foundDisplaySet) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySet not found');
return false;
}
// check they contain the same image
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: images length mismatch');
return false;
}
// check if their imageIds are the same
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: imageId mismatch');
return false;
}
}
}
}
return true;
}
// Helper function to check if display sets have changed
function haveDisplaySetsChanged(prevDisplaySets, currentDisplaySets) {
if (prevDisplaySets.length !== currentDisplaySets.length) {
return true;
}
return currentDisplaySets.some((currentDS, index) => {
const prevDS = prevDisplaySets[index];
return currentDS.displaySetInstanceUID !== prevDS.displaySetInstanceUID;
});
}
export default OHIFCornerstoneViewport;
@@ -2,9 +2,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ViewportOverlay } from '@ohif/ui';
import { InstanceMetadata } from '@ohif/core/src/types';
import type { InstanceMetadata } from '@ohif/core/src/types';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
@@ -45,14 +45,14 @@ const OverlayItemComponents = {
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
};
const studyDateItem = {
id: 'StudyDate',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Study date',
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
contentF: ({ referenceInstance, formatters: { formatDate } }) => formatDate(referenceInstance.StudyDate),
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
formatDate(referenceInstance.StudyDate),
};
const seriesDescriptionItem = {
@@ -63,15 +63,19 @@ const seriesDescriptionItem = {
condition: ({ referenceInstance }) => {
return referenceInstance && referenceInstance.SeriesDescription;
},
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
};
const topLeftItems = { id: 'cornerstoneOverlayTopLeft', items: [studyDateItem, seriesDescriptionItem] };
const topLeftItems = {
id: 'cornerstoneOverlayTopLeft',
items: [studyDateItem, seriesDescriptionItem],
};
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
const bottomLeftItems = {
id: 'cornerstoneOverlayBottomLeft', items: [
id: 'cornerstoneOverlayBottomLeft',
items: [
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
@@ -79,12 +83,12 @@ const bottomLeftItems = {
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: (props) => {
condition: props => {
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
return activeToolName === 'Zoom';
},
},
]
],
};
const bottomRightItems = {
@@ -94,7 +98,7 @@ const bottomRightItems = {
id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
]
],
};
/**
@@ -145,19 +149,18 @@ function CustomizableViewportOverlay({
// append functionality. This code enables the historical usage, but
// the recommended functionality is to append to the default values in
// cornerstoneOverlay rather than defining individual items.
const topLeftCustomization = customizationService.getCustomization(
'cornerstoneOverlayTopLeft'
) || cornerstoneOverlay?.topLeftItems;
const topRightCustomization = customizationService.getCustomization(
'cornerstoneOverlayTopRight'
) || cornerstoneOverlay?.topRightItems;
const bottomLeftCustomization = customizationService.getCustomization(
'cornerstoneOverlayBottomLeft'
) || cornerstoneOverlay?.bottomLeftItems;
const bottomRightCustomization = customizationService.getCustomization(
'cornerstoneOverlayBottomRight'
) || cornerstoneOverlay?.bottomRightItems;
const topLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
cornerstoneOverlay?.topLeftItems;
const topRightCustomization =
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
cornerstoneOverlay?.topRightItems;
const bottomLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
cornerstoneOverlay?.bottomLeftItems;
const bottomRightCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
cornerstoneOverlay?.bottomRightItems;
const instanceNumber = useMemo(
() =>
@@ -177,13 +180,12 @@ function CustomizableViewportOverlay({
return {
displaySets,
displaySet,
instance: instances[imageIndex],
instance: instances?.[imageIndex],
instances,
referenceInstance,
};
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
/**
* Updating the VOI when the viewport changes its voi
*/
@@ -307,7 +309,8 @@ function CustomizableViewportOverlay({
<>
{items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}>
{(!item?.condition || item.condition(props)) && _renderOverlayItem(item, props) || null}
{((!item?.condition || item.condition(props)) && _renderOverlayItem(item, props)) ||
null}
</div>
))}
</>
@@ -316,7 +319,6 @@ function CustomizableViewportOverlay({
[_renderOverlayItem]
);
return (
<ViewportOverlay
topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
@@ -335,7 +337,9 @@ function getDisplaySets(viewportData, displaySetService) {
if (!viewportData?.data?.length) {
return null;
}
const displaySets = viewportData.data.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID)).filter(it => !!it);
const displaySets = viewportData.data
.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID))
.filter(it => !!it);
if (!displaySets.length) {
return null;
}
@@ -382,7 +386,6 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
return parseInt(instanceNumber);
}
// Since volume viewports can be in any view direction, they can render
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
@@ -400,6 +403,11 @@ function _getInstanceNumberFromVolume(
// Todo: support fusion of acquisition plane which has instanceNumber
const { volume } = volumes[0];
if (!volume) {
return;
}
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
@@ -443,9 +451,10 @@ function OverlayItem(props) {
style={{ color, background }}
title={title}
>
{label ? (<span className="mr-1 shrink-0">{label}</span>) : null}
{label ? <span className="mr-1 shrink-0">{label}</span> : null}
<span className="ml-1 mr-2 shrink-0">{value}</span>
</div>);
</div>
);
}
/**
@@ -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
@@ -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 '';
}
+472 -14
View File
@@ -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 {
@@ -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({
@@ -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 (
<div className="bg-muted flex h-full w-[262px] flex-col rounded p-3">
<span className="text-muted-foreground mb-2 text-xs font-semibold">Current Viewport</span>
<ul className="space-y-1">
{activeSegmentations.map(segmentation => (
<li
key={segmentation.id}
className="flex items-center text-sm"
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground mr-2"
onClick={() => removeSegmentationFromViewport(segmentation.segmentationId)}
>
<Icons.Minus className="h-6 w-6" />
</Button>
<span className="text-foreground flex-grow">{segmentation.label}</span>
{segmentation.visible ? (
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={() =>
toggleSegmentationRepresentationVisibility(
segmentation.segmentationId,
segmentation.type
)
}
>
<Icons.Hide className="h-6 w-6" />
</Button>
) : (
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={() =>
toggleSegmentationRepresentationVisibility(
segmentation.segmentationId,
segmentation.type
)
}
>
<Icons.Show className="h-6 w-6" />
</Button>
)}
</li>
))}
</ul>
{availableSegmentations.length > 0 && (
<>
<Separator className="bg-input mb-3" />
<span className="text-muted-foreground mb-2 text-xs font-semibold">Available</span>
<ul className="space-y-1">
{availableSegmentations.map(segmentation => (
<li
key={segmentation.segmentation.segmentationId}
className="flex items-center text-sm"
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground mr-2"
onClick={() =>
addSegmentationToViewport(segmentation.segmentation.segmentationId)
}
>
<Icons.Plus className="h-6 w-6" />
</Button>
<span className="text-foreground/60">{segmentation.segmentation.label}</span>
</li>
))}
</ul>
</>
)}
</div>
);
}
export default ViewportSegmentationMenu;
@@ -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 (
<Popover>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<Button
variant="ghost"
size="icon"
>
<Icons.ViewportViews
className={classNames(
'text-highlight',
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
)}
/>
</Button>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={-15}
sideOffset={5}
>
<ViewportSegmentationMenu
className="w-full"
viewportId={viewportId}
displaySets={displaySets}
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
</PopoverContent>
</Popover>
);
}
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' };
}
};
@@ -0,0 +1,11 @@
import React, { ReactNode } from 'react';
import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper';
export function getViewportDataOverlaySettingsMenu(
props: withAppTypes<{
viewportId: string;
element: HTMLElement;
}>
): ReactNode {
return <ViewportSegmentationMenuWrapper {...props} />;
}
@@ -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 (
<PanelSection title="Window Level">
@@ -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),
@@ -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);
};
@@ -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];
}
@@ -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<Record<string, Array<WindowLevelPreset>>>;
verticalDirection: AllInOneMenu.VerticalDirection;
horizontalDirection: AllInOneMenu.HorizontalDirection;
colorbarProperties: ColorbarProperties;
displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>;
@@ -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 && (
<Colorbar
viewportId={viewportId}
displaySets={displaySets.filter(ds => !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({
<Colormap
colormaps={colormaps}
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager}
servicesManager={servicesManager}
/>
@@ -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 (
<WindowLevelActionMenu
viewportId={viewportId}
@@ -35,8 +35,8 @@ export const ViewportActionCornersContext = createContext(DEFAULT_STATE);
export function ViewportActionCornersProvider({ children, service }) {
const viewportActionCornersReducer = (state, action) => {
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<ActionComponentInfo>) => {
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 (
@@ -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',
},
@@ -0,0 +1,90 @@
import React from 'react';
import { Toolbox } from '@ohif/ui-next';
import PanelSegmentation from './panels/PanelSegmentation';
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
const wrappedPanelSegmentation = ({ configuration }) => {
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
}}
/>
);
};
const wrappedPanelSegmentationNoHeader = ({ configuration }) => {
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
}}
/>
);
};
const wrappedPanelSegmentationWithTools = ({ configuration }) => {
return (
<>
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="segmentationToolbox"
title="Segmentation Tools"
configuration={{
...configuration,
}}
/>
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
}}
/>
</>
);
};
return [
{
name: 'activeViewportWindowLevel',
component: () => {
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
},
},
{
name: 'panelSegmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentation,
},
{
name: 'panelSegmentationNoHeader',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationNoHeader,
},
{
name: 'panelSegmentationWithTools',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationWithTools,
},
];
};
export default getPanelModule;
@@ -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,
+55 -49
View File
@@ -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);
@@ -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<ViewportSegmentationRepresentation>({
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;
}
@@ -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;
}
@@ -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<SegmentationData[]>([]);
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;
}
+23 -34
View File
@@ -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: [
{
+39 -21
View File
@@ -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 => <DicomUpload {...props} />,
});
return init.call(this, props);
},
getToolbarModule,
getPanelModule({ servicesManager }) {
return [
{
name: 'activeViewportWindowLevel',
component: () => {
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
},
},
];
},
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;
+79 -88
View File
@@ -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<void> {
appConfig,
}: withAppTypes): Promise<void> {
// 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() {
@@ -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];
@@ -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,
@@ -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);
}
);
@@ -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');
}
@@ -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 (
<>
<div
className="invisible-scrollbar overflow-y-auto overflow-x-hidden"
ref={measurementsPanelRef}
data-cy={'trackedMeasurements-panel'}
>
<MeasurementTable
title="Measurements"
data={measurements}
onClick={jumpToImage}
onDelete={removeMeasurement}
onToggleVisibility={toggleVisibilityMeasurement}
onToggleLocked={toggleLockMeasurement}
onRename={renameMeasurement}
// onColor={changeColorMeasurement}
>
<MeasurementTable.Header>
{customHeader && (
<>
{typeof customHeader === 'function'
? customHeader({
additionalFindings,
measurements,
})
: customHeader}
</>
)}
</MeasurementTable.Header>
<MeasurementTable.Body />
</MeasurementTable>
{additionalFindings.length > 0 && (
<MeasurementTable
data={additionalFindings}
title="Additional Findings"
onClick={jumpToImage}
onDelete={removeMeasurement}
onToggleVisibility={toggleVisibilityMeasurement}
onToggleLocked={toggleLockMeasurement}
onRename={renameMeasurement}
// onColor={changeColorMeasurement}
>
<MeasurementTable.Body />
</MeasurementTable>
)}
</div>
</>
);
}
@@ -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 (
<>
<SegmentationTable
disabled={disabled}
data={segmentationsWithRepresentations}
mode={SegmentationTableMode}
title="Segmentations"
exportOptions={exportOptions}
disableEditing={disableEditing}
onSegmentationAdd={onSegmentationAdd}
onSegmentationClick={handlers.onSegmentationClick}
onSegmentationDelete={handlers.onSegmentationDelete}
showAddSegment={showAddSegment}
onSegmentAdd={handlers.onSegmentAdd}
onSegmentClick={handlers.onSegmentClick}
onSegmentEdit={handlers.onSegmentEdit}
onSegmentationEdit={handlers.onSegmentationEdit}
onSegmentColorClick={handlers.onSegmentColorClick}
onSegmentDelete={handlers.onSegmentDelete}
onToggleSegmentVisibility={handlers.onToggleSegmentVisibility}
onToggleSegmentLock={handlers.onToggleSegmentLock}
onToggleSegmentationRepresentationVisibility={
handlers.onToggleSegmentationRepresentationVisibility
}
onSegmentationDownload={handlers.onSegmentationDownload}
storeSegmentation={handlers.storeSegmentation}
onSegmentationDownloadRTSS={handlers.onSegmentationDownloadRTSS}
setStyle={handlers.setStyle}
toggleRenderInactiveSegmentations={handlers.toggleRenderInactiveSegmentations}
onSegmentationRemoveFromViewport={handlers.onSegmentationRemoveFromViewport}
setFillAlpha={handlers.setFillAlpha}
setOutlineWidth={handlers.setOutlineWidth}
setRenderFill={handlers.setRenderFill}
setRenderOutline={handlers.setRenderOutline}
setFillAlphaInactive={handlers.setFillAlphaInactive}
renderInactiveSegmentations={handlers.getRenderInactiveSegmentations()}
>
{children}
<SegmentationTable.Config />
<SegmentationTable.AddSegmentationRow />
{SegmentationTableMode === 'collapsed' ? (
<SegmentationTable.Collapsed>
<SegmentationTable.SelectorHeader />
<SegmentationTable.AddSegmentRow />
<SegmentationTable.Segments />
</SegmentationTable.Collapsed>
) : (
<SegmentationTable.Expanded>
<SegmentationTable.Header />
{/* <SegmentationTable.AddSegmentRow /> */}
<SegmentationTable.Segments />
</SegmentationTable.Expanded>
)}
</SegmentationTable>
</>
);
}
@@ -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) {
@@ -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<string, unknown>,
viewportOptions: AppTypes.ViewportGrid.GridViewportOptions,
dataSource: unknown,
initialImageIndex?: number
): Promise<StackViewportData | VolumeViewportData> {
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<StackViewportData> {
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;
File diff suppressed because it is too large. Load diff
@@ -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<string, any>;
// 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<Segment>;
// the set of segments that are locked
segmentsLocked: Array<number>;
// 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 };
@@ -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}`);
}
}
@@ -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,
});
};
@@ -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<ActionComponentInfo>) {
this.serviceImplementation._setComponents(components);
public addComponents(components: Array<ActionComponentInfo>) {
this.serviceImplementation._addComponents(components);
}
public clear(viewportId: string) {
@@ -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;
@@ -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;
}
@@ -0,0 +1,4 @@
export { useLutPresentationStore } from './useLutPresentationStore';
export { usePositionPresentationStore } from './usePositionPresentationStore';
export { useSegmentationPresentationStore } from './useSegmentationPresentationStore';
export { useSynchronizersStore } from './useSynchronizersStore';
@@ -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 };
@@ -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<string, LutPresentation>;
/**
* 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<LutPresentationState>()(
DEBUG_STORE
? devtools(createLutPresentationStore, { name: 'LutPresentationStore' })
: createLutPresentationStore
);
@@ -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<string, PositionPresentation>;
/**
* 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<PositionPresentationState>()(
DEBUG_STORE
? devtools(createPositionPresentationStore, { name: 'PositionPresentationStore' })
: createPositionPresentationStore
);
@@ -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<string, SegmentationPresentation>;
/**
* 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<SegmentationPresentationStore>()(
DEBUG_STORE
? devtools(createSegmentationPresentationStore, { name: 'Segmentation Presentation Store' })
: createSegmentationPresentationStore
);
@@ -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<string, SynchronizerInfo[]>;
/**
* 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<SynchronizersState>()(
DEBUG_STORE
? devtools(createSynchronizersStore, { name: 'SynchronizersStore' })
: createSynchronizersStore
);
@@ -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,
@@ -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;
@@ -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';
@@ -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;
@@ -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<string, Types.ViewportProperties> | Types.ViewportProperties;
// either a single object with the properties itself or a map of properties with volumeId keys
properties: Record<string, Types.ViewportProperties> | 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;
@@ -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;
@@ -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;
@@ -1,158 +0,0 @@
import React from 'react';
import { Input, Dialog, ButtonEnums, LabellingFlow } from '@ohif/ui';
/**
*
* @param {*} data
* @param {*} data.text
* @param {*} data.label
* @param {*} event
* @param {*} callback
* @param {*} isArrowAnnotateInputDialog
* @param {*} dialogConfig
* @param {string?} dialogConfig.dialogTitle - title of the input dialog
* @param {string?} dialogConfig.inputLabel - show label above the input
*/
function callInputDialog(
uiDialogService,
data,
callback,
isArrowAnnotateInputDialog = true,
dialogConfig: any = {}
) {
const dialogId = 'dialog-enter-annotation';
const label = data ? (isArrowAnnotateInputDialog ? data.text : data.label) : '';
const {
dialogTitle = 'Annotation',
inputLabel = 'Enter your annotation',
validateFunc = value => true,
} = dialogConfig;
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
if (typeof validateFunc === 'function' && !validateFunc(value.label)) {
return;
}
callback(value.label, 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: dialogTitle,
value: { label },
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 (
<Input
autoFocus
className="border-primary-main bg-black"
type="text"
id="annotation"
label={inputLabel}
labelClassName="text-white text-[14px] leading-[1.2]"
value={value.label}
onChange={event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
}}
onKeyPress={event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
}
}}
/>
);
},
},
});
}
}
export function callLabelAutocompleteDialog(uiDialogService, callback, dialogConfig, labelConfig) {
const exclusive = labelConfig ? labelConfig.exclusive : false;
const dropDownItems = labelConfig ? labelConfig.items : [];
const { validateFunc = value => true } = dialogConfig;
const labellingDoneCallback = value => {
if (typeof value === 'string') {
if (typeof validateFunc === 'function' && !validateFunc(value)) {
return;
}
callback(value, 'save');
} else {
callback('', 'cancel');
}
uiDialogService.dismiss({ id: 'select-annotation' });
};
uiDialogService.create({
id: 'select-annotation',
centralize: true,
isDraggable: false,
showOverlay: true,
content: LabellingFlow,
contentProps: {
labellingDoneCallback: labellingDoneCallback,
measurementData: { label: '' },
componentClassName: {},
labelData: dropDownItems,
exclusive: exclusive,
},
});
}
export function showLabelAnnotationPopup(measurement, uiDialogService, labelConfig) {
const exclusive = labelConfig ? labelConfig.exclusive : false;
const dropDownItems = labelConfig ? labelConfig.items : [];
return new Promise<Map<any, any>>((resolve, reject) => {
const labellingDoneCallback = value => {
uiDialogService.dismiss({ id: 'select-annotation' });
if (typeof value === 'string') {
measurement.label = value;
}
resolve(measurement);
};
uiDialogService.create({
id: 'select-annotation',
isDraggable: false,
showOverlay: true,
content: LabellingFlow,
defaultPosition: {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
},
contentProps: {
labellingDoneCallback: labellingDoneCallback,
measurementData: measurement,
componentClassName: {},
labelData: dropDownItems,
exclusive: exclusive,
},
});
});
}
export default callInputDialog;
@@ -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();
@@ -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';
@@ -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) {
@@ -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;
};
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
@@ -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;
}
@@ -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;
@@ -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;
}
@@ -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;
}
@@ -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;
@@ -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;
}
@@ -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;
@@ -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;
@@ -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;
}
@@ -0,0 +1,5 @@
import { locking } from '@cornerstonejs/tools/annotation';
export const getIsLocked = annotationUID => {
return locking.isAnnotationLocked(annotationUID);
};
@@ -0,0 +1,6 @@
import { visibility } from '@cornerstonejs/tools/annotation';
export const getIsVisible = annotationUID => {
const isVisible = visibility.isAnnotationVisible(annotationUID);
return isVisible;
};
@@ -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],
};
}
@@ -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;