feat(data overlay): add foreground background control plus orientation change in the UI and add RTDOSE support (#5007)
This commit is contained in:
1 parent
c1fced0573
commit
cd4e6408bc
105 files changed
+3117
-788
No files matched your search
@@ -7,17 +7,14 @@ import {
|
||||
Icons,
|
||||
Button,
|
||||
} from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* The default sub-menu appearance and setup is defined here, but this can be
|
||||
* replaced by
|
||||
*/
|
||||
const getMenuItemsDefault = ({
|
||||
commandsManager,
|
||||
items,
|
||||
servicesManager,
|
||||
...props
|
||||
}: withAppTypes) => {
|
||||
const getMenuItemsDefault = ({ commandsManager, items, ...props }: withAppTypes) => {
|
||||
const { servicesManager } = useSystem();
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
// This allows replacing the default child item for menus, whereas the entire
|
||||
@@ -36,7 +33,7 @@ const getMenuItemsDefault = ({
|
||||
onClick: ({ commandsManager, ...props }: withAppTypes) => () => void;
|
||||
};
|
||||
}) => (
|
||||
<DropdownMenuItem onClick={() => item.onClick({ commandsManager, ...props })}>
|
||||
<DropdownMenuItem onClick={() => item.onClick({ commandsManager, servicesManager, ...props })}>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.iconName && <Icons.ByName name={item.iconName} />}
|
||||
<span>{item.label}</span>
|
||||
@@ -76,12 +73,8 @@ const getMenuItemsDefault = ({
|
||||
* @returns Component bound to the bindProps
|
||||
*/
|
||||
export default function MoreDropdownMenu(bindProps) {
|
||||
const {
|
||||
menuItemsKey,
|
||||
getMenuItems = getMenuItemsDefault,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
} = bindProps;
|
||||
const { menuItemsKey, getMenuItems = getMenuItemsDefault, commandsManager } = bindProps;
|
||||
const { servicesManager } = useSystem();
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const items = customizationService.getCustomization(menuItemsKey);
|
||||
|
||||
@@ -3,6 +3,12 @@ import { Types, DicomMetadataStore } from '@ohif/core';
|
||||
import { ContextMenuController } from './CustomizableContextMenu';
|
||||
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||
import reuseCachedLayouts from './utils/reuseCachedLayouts';
|
||||
import {
|
||||
configureViewportForLayerAddition,
|
||||
configureViewportForLayerRemoval,
|
||||
canAddDisplaySetToViewport,
|
||||
DERIVED_OVERLAY_MODALITIES,
|
||||
} from './utils/layerConfigurationUtils';
|
||||
import findViewportsByPosition, {
|
||||
findOrCreateViewport as layoutFindOrCreate,
|
||||
} from './findViewportsByPosition';
|
||||
@@ -51,6 +57,137 @@ const commandsModule = ({
|
||||
const contextMenuController = new ContextMenuController(servicesManager, commandsManager);
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Adds a display set as a layer to the specified viewport
|
||||
*
|
||||
* @param options.viewportId - The ID of the viewport to add the layer to
|
||||
* @param options.displaySetInstanceUID - The UID of the display set to add as a layer
|
||||
* @param options.removeFirst - Optional flag to remove the display set first if it's already added
|
||||
*/
|
||||
addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => {
|
||||
if (!viewportId || !displaySetInstanceUID) {
|
||||
console.warn('Missing required parameters for addDisplaySetAsLayer command');
|
||||
return;
|
||||
}
|
||||
|
||||
const { displaySetService, viewportGridService, hangingProtocolService } =
|
||||
servicesManager.services;
|
||||
|
||||
// Get the display set
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!displaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current display sets for the viewport
|
||||
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
// Check if we can add this display set to the viewport
|
||||
const canAdd = canAddDisplaySetToViewport({
|
||||
viewportId,
|
||||
displaySetInstanceUID,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
if (!canAdd) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the display set to the viewport
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportId,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Configure each viewport for layer addition
|
||||
updatedViewports.forEach(viewport => {
|
||||
configureViewportForLayerAddition({
|
||||
viewport,
|
||||
displaySetInstanceUID,
|
||||
currentDisplaySetUIDs,
|
||||
servicesManager,
|
||||
});
|
||||
});
|
||||
|
||||
// Update position presentation
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs,
|
||||
});
|
||||
|
||||
// Run command to update viewports
|
||||
commandsManager.run('setDisplaySetsForViewports', {
|
||||
viewportsToUpdate: updatedViewports,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a display set layer from the specified viewport
|
||||
*
|
||||
* @param options.viewportId - The ID of the viewport to remove the layer from
|
||||
* @param options.displaySetInstanceUID - The UID of the display set to remove
|
||||
*/
|
||||
removeDisplaySetLayer: ({ viewportId, displaySetInstanceUID }) => {
|
||||
if (!viewportId || !displaySetInstanceUID) {
|
||||
console.warn('Missing required parameters for removeDisplaySetLayer command');
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
hangingProtocolService,
|
||||
segmentationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
// Get the display set
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!displaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current display sets for the viewport
|
||||
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
// If the display set is not in the viewport, no need to remove it
|
||||
if (!currentDisplaySetUIDs.includes(displaySetInstanceUID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a segmentation and handle accordingly
|
||||
const isSegmentation = DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality);
|
||||
if (isSegmentation) {
|
||||
segmentationService.removeSegmentationRepresentations(viewportId, {
|
||||
segmentationId: displaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportId,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Configure each viewport for layer removal
|
||||
updatedViewports.forEach(viewport => {
|
||||
configureViewportForLayerRemoval({
|
||||
viewport,
|
||||
displaySetInstanceUID,
|
||||
currentDisplaySetUIDs,
|
||||
servicesManager,
|
||||
});
|
||||
});
|
||||
|
||||
// Update position presentation
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs,
|
||||
});
|
||||
|
||||
// Update the viewports
|
||||
commandsManager.run('setDisplaySetsForViewports', {
|
||||
viewportsToUpdate: updatedViewports,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Runs a command in multi-monitor mode. No-op if not multi-monitor.
|
||||
*/
|
||||
@@ -644,6 +781,8 @@ const commandsModule = ({
|
||||
openDICOMTagViewer: actions.openDICOMTagViewer,
|
||||
updateViewportDisplaySet: actions.updateViewportDisplaySet,
|
||||
scrollActiveThumbnailIntoView: actions.scrollActiveThumbnailIntoView,
|
||||
addDisplaySetAsLayer: actions.addDisplaySetAsLayer,
|
||||
removeDisplaySetLayer: actions.removeDisplaySetLayer,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,6 +14,26 @@ export default {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'addAsLayer',
|
||||
label: 'Add as Layer',
|
||||
iconName: 'ViewportViews',
|
||||
onClick: ({ commandsManager, displaySetInstanceUID, servicesManager }: withAppTypes) => {
|
||||
const { viewportGridService } = servicesManager.services;
|
||||
|
||||
// Get the active viewport
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
if (!activeViewportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the new command to add the display set as a layer
|
||||
commandsManager.runCommand('addDisplaySetAsLayer', {
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUID,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
'studyBrowser.sortFunctions': [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { utils, classes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { id } from './id';
|
||||
import getDisplaySetMessages from './getDisplaySetMessages';
|
||||
import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries';
|
||||
@@ -99,6 +100,11 @@ const makeDisplaySet = instances => {
|
||||
averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
|
||||
isDynamicVolume,
|
||||
dynamicVolumeInfo,
|
||||
supportsWindowLevel: true,
|
||||
label:
|
||||
instance.SeriesDescription ||
|
||||
`${i18n.t('Series')} ${instance.SeriesNumber} - ${i18n.t(instance.Modality)}`,
|
||||
FrameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
});
|
||||
|
||||
// Sort the images in this series if needed
|
||||
@@ -260,6 +266,7 @@ const sopClassUids = [
|
||||
sopClassDictionary.LegacyConvertedEnhancedPETImageStorage,
|
||||
sopClassDictionary.RTImageStorage,
|
||||
sopClassDictionary.EnhancedUSVolumeStorage,
|
||||
sopClassDictionary.RTDoseStorage,
|
||||
];
|
||||
|
||||
function getSopClassHandlerModule(appContextParam) {
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { CommandsManager, ExtensionManager } from '@ohif/core';
|
||||
import LineChartViewport from './Components/LineChartViewport/index';
|
||||
|
||||
const getViewportModule = ({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
}: {
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
commandsManager: CommandsManager;
|
||||
extensionManager: ExtensionManager;
|
||||
}) => {
|
||||
const getViewportModule = () => {
|
||||
return [
|
||||
{
|
||||
name: 'chartViewport',
|
||||
|
||||
@@ -22,11 +22,11 @@ export default function init({
|
||||
cineService.EVENTS.CINE_STATE_CHANGED,
|
||||
]);
|
||||
// Add
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handlePETImageMetadata);
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handleScalingModules);
|
||||
|
||||
// If the metadata for PET has changed by the user (e.g. manually changing the PatientWeight)
|
||||
// we need to recalculate the SUV Scaling Factors
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handlePETImageMetadata);
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handleScalingModules);
|
||||
|
||||
// Adds extra custom attributes for use by hanging protocols
|
||||
registerHangingProtocolAttributes({ servicesManager });
|
||||
@@ -71,7 +71,7 @@ export default function init({
|
||||
});
|
||||
}
|
||||
|
||||
const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
const handleScalingModules = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
const { instances } = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID);
|
||||
|
||||
if (!instances?.length) {
|
||||
@@ -80,12 +80,41 @@ const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
|
||||
const modality = instances[0].Modality;
|
||||
|
||||
if (!modality || modality !== 'PT') {
|
||||
const allowedModality = ['PT', 'RTDOSE'];
|
||||
|
||||
if (!allowedModality.includes(modality)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = instances.map(instance => instance.imageId);
|
||||
const instanceMetadataArray = [];
|
||||
|
||||
if (modality === 'RTDOSE') {
|
||||
const DoseGridScaling = instances[0].DoseGridScaling;
|
||||
const DoseSummation = instances[0].DoseSummation;
|
||||
const DoseType = instances[0].DoseType;
|
||||
const DoseUnit = instances[0].DoseUnit;
|
||||
const NumberOfFrames = instances[0].NumberOfFrames;
|
||||
const imageId = imageIds[0];
|
||||
|
||||
// add scaling module to the metadata
|
||||
// since RTDOSE is always a multiframe we should add the scaling module to each frame
|
||||
for (let i = 0; i < NumberOfFrames; i++) {
|
||||
const frameIndex = i + 1;
|
||||
|
||||
// Todo: we should support other things like wadouri, local etc
|
||||
const newImageId = `${imageId.replace(/\/frames\/\d+$/, '')}/frames/${frameIndex}`;
|
||||
metadataProvider.addCustomMetadata(newImageId, 'scalingModule', {
|
||||
DoseGridScaling,
|
||||
DoseSummation,
|
||||
DoseType,
|
||||
DoseUnit,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// try except block to prevent errors when the metadata is not correct
|
||||
try {
|
||||
imageIds.forEach(imageId => {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
export const DERIVED_OVERLAY_MODALITIES = ['SEG', 'RTSTRUCT'];
|
||||
export const DEFAULT_COLORMAP = 'hsv';
|
||||
export const DEFAULT_OPACITY = 0.9;
|
||||
export const DEFAULT_OPACITY_PERCENT = DEFAULT_OPACITY * 100;
|
||||
|
||||
/**
|
||||
* Get modality-specific color and opacity settings from the customization service
|
||||
*/
|
||||
export function getModalityOverlayColormap(customizationService, modality) {
|
||||
const modalityOverlayDefaultColorMaps = customizationService?.getCustomization(
|
||||
'cornerstone.modalityOverlayDefaultColorMaps'
|
||||
) || { defaultSettings: {} };
|
||||
|
||||
return (
|
||||
modalityOverlayDefaultColorMaps.defaultSettings[modality] || {
|
||||
colormap: DEFAULT_COLORMAP,
|
||||
opacity: DEFAULT_OPACITY,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create display set options based on modality and opacity settings
|
||||
*/
|
||||
export function createColormapOverlayDisplaySetOptions(displaySet, opacity, customizationService) {
|
||||
if (displaySet.Modality === 'SEG') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const modalitySettings = getModalityOverlayColormap(customizationService, displaySet.Modality);
|
||||
return {
|
||||
colormap: {
|
||||
name: modalitySettings.colormap || DEFAULT_COLORMAP,
|
||||
opacity: opacity / 100, // Convert from percentage to 0-1 range
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure viewport for adding a display set layer
|
||||
*/
|
||||
export function configureViewportForLayerAddition(params: {
|
||||
viewport: any;
|
||||
displaySetInstanceUID: string;
|
||||
currentDisplaySetUIDs: string[];
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}): any {
|
||||
const { viewport, displaySetInstanceUID, currentDisplaySetUIDs, servicesManager } = params;
|
||||
const { cornerstoneViewportService, displaySetService, customizationService } =
|
||||
servicesManager.services;
|
||||
|
||||
const { viewportId } = viewport;
|
||||
|
||||
// Set the display set UIDs for the viewport
|
||||
const allDisplaySetInstanceUIDs = [...currentDisplaySetUIDs, displaySetInstanceUID];
|
||||
viewport.displaySetInstanceUIDs = allDisplaySetInstanceUIDs;
|
||||
|
||||
if (!viewport.viewportOptions) {
|
||||
viewport.viewportOptions = {};
|
||||
}
|
||||
|
||||
const requestedLayerDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
if (!viewport.viewportOptions.orientation) {
|
||||
viewport.viewportOptions.orientation = cornerstoneViewportService.getOrientation(viewportId);
|
||||
}
|
||||
|
||||
// Do not force volume for SEG and RTSTRUCT if there is only one display set
|
||||
if (requestedLayerDisplaySet.isOverlayDisplaySet && currentDisplaySetUIDs.length === 1) {
|
||||
viewport.viewportOptions.viewportType = 'stack';
|
||||
} else {
|
||||
viewport.viewportOptions.viewportType = 'volume';
|
||||
}
|
||||
|
||||
// create same amount of display set options as the number of display set UIDs
|
||||
const displaySetOptions = allDisplaySetInstanceUIDs.map((uid, index) => {
|
||||
if (index === 0) {
|
||||
// no colormap for background
|
||||
return {};
|
||||
}
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(uid);
|
||||
return createColormapOverlayDisplaySetOptions(displaySet, 90, customizationService);
|
||||
});
|
||||
|
||||
viewport.displaySetOptions = displaySetOptions;
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure viewport for removing a display set layer
|
||||
*/
|
||||
export function configureViewportForLayerRemoval(params: {
|
||||
viewport: any;
|
||||
displaySetInstanceUID: string;
|
||||
currentDisplaySetUIDs: string[];
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}): any {
|
||||
const { viewport, displaySetInstanceUID, currentDisplaySetUIDs, servicesManager } = params;
|
||||
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
|
||||
|
||||
const { viewportId } = viewport;
|
||||
|
||||
// Filter out the display set to remove
|
||||
viewport.displaySetInstanceUIDs = currentDisplaySetUIDs.filter(
|
||||
uid => uid !== displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!viewport.viewportOptions) {
|
||||
viewport.viewportOptions = {};
|
||||
}
|
||||
|
||||
viewport.viewportOptions.viewportType = 'volume';
|
||||
|
||||
// orientation
|
||||
if (!viewport.viewportOptions.orientation) {
|
||||
viewport.viewportOptions.orientation = cornerstoneViewportService.getOrientation(viewportId);
|
||||
}
|
||||
|
||||
// Recreate the display set options
|
||||
viewport.displaySetOptions = viewport.displaySetInstanceUIDs.map(() => {
|
||||
// For simplicity, we're returning empty options for now
|
||||
// In a more complete implementation, we would need to preserve existing display set options
|
||||
return {};
|
||||
});
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a display set can be added as a layer to the specified viewport
|
||||
*/
|
||||
export function canAddDisplaySetToViewport(params: {
|
||||
viewportId: string;
|
||||
displaySetInstanceUID: string;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}): boolean {
|
||||
const { viewportId, displaySetInstanceUID, servicesManager } = params;
|
||||
const { displaySetService, viewportGridService } = servicesManager.services;
|
||||
|
||||
// Check if the display set exists
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!displaySet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get current display sets in the viewport
|
||||
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
// Check if the display set is already in the viewport
|
||||
if (currentDisplaySetUIDs.includes(displaySetInstanceUID)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in new issue
Block a user