import { hotkeys, classes } from '@ohif/core'; import toolbarButtons from './toolbarButtons.js'; import { id } from './id.js'; import initToolGroups from './initToolGroups.js'; import setCrosshairsConfiguration from './utils/setCrosshairsConfiguration.js'; import setFusionActiveVolume from './utils/setFusionActiveVolume.js'; import i18n from 'i18next'; const { MetadataProvider } = classes; const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', thumbnailList: '@ohif/extension-default.panelModule.seriesList', }; const cs3d = { viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', segPanel: '@ohif/extension-cornerstone.panelModule.panelSegmentationNoHeader', measurements: '@ohif/extension-cornerstone.panelModule.measurements', }; const tmtv = { hangingProtocol: '@ohif/extension-tmtv.hangingProtocolModule.ptCT', petSUV: '@ohif/extension-tmtv.panelModule.petSUV', tmtv: '@ohif/extension-tmtv.panelModule.tmtv', }; const extensionDependencies = { // Can derive the versions at least process.env.from npm_package_version '@ohif/extension-default': '^3.0.0', '@ohif/extension-cornerstone': '^3.0.0', '@ohif/extension-cornerstone-dicom-seg': '^3.0.0', '@ohif/extension-tmtv': '^3.0.0', }; const unsubscriptions = []; function modeFactory({ modeConfiguration }) { return { // TODO: We're using this as a route segment // We should not be. id, routeName: 'tmtv', displayName: i18n.t('Modes:Total Metabolic Tumor Volume'), /** * Lifecycle hooks */ onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => { const { toolbarService, toolGroupService, customizationService, hangingProtocolService, displaySetService, } = servicesManager.services; const utilityModule = extensionManager.getModuleEntry( '@ohif/extension-cornerstone.utilityModule.tools' ); const { toolNames, Enums } = utilityModule.exports; // Init Default and SR ToolGroups initToolGroups(toolNames, Enums, toolGroupService, commandsManager, null, servicesManager); const { unsubscribe } = toolGroupService.subscribe( toolGroupService.EVENTS.VIEWPORT_ADDED, () => { // For fusion toolGroup we need to add the volumeIds for the crosshairs // since in the fusion viewport we don't want both PT and CT to render MIP // when slabThickness is modified const { displaySetMatchDetails } = hangingProtocolService.getMatchDetails(); setCrosshairsConfiguration( displaySetMatchDetails, toolNames, toolGroupService, displaySetService ); setFusionActiveVolume( displaySetMatchDetails, toolNames, toolGroupService, displaySetService ); } ); unsubscriptions.push(unsubscribe); toolbarService.addButtons(toolbarButtons); toolbarService.createButtonSection('primary', [ 'MeasurementTools', 'Zoom', 'WindowLevel', 'Crosshairs', 'Pan', ]); toolbarService.createButtonSection('ROIThresholdToolbox', [ 'RectangleROIStartEndThreshold', 'BrushTools', ]); customizationService.addModeCustomizations([ { id: 'PanelSegmentation.tableMode', mode: 'expanded', }, { id: 'PanelSegmentation.onSegmentationAdd', onSegmentationAdd: () => { commandsManager.run('createNewLabelmapFromPT'); }, }, { id: 'PanelSegmentation.readableText', // remove following if you are not interested in that stats readableText: { lesionStats: 'Lesion Statistics', minValue: 'Minimum Value', maxValue: 'Maximum Value', meanValue: 'Mean Value', volume: 'Volume', suvPeak: 'SUV Peak', suvMax: 'Maximum SUV', suvMaxIJK: 'SUV Max IJK', lesionGlyoclysisStats: 'Lesion Glycolysis', }, }, ]); // For the hanging protocol we need to decide on the window level // based on whether the SUV is corrected or not, hence we can't hard // code the window level in the hanging protocol but we add a custom // attribute to the hanging protocol that will be used to get the // window level based on the metadata hangingProtocolService.addCustomAttribute( 'getPTVOIRange', 'get PT VOI based on corrected or not', props => { const ptDisplaySet = props.find(imageSet => imageSet.Modality === 'PT'); if (!ptDisplaySet) { return; } const { imageId } = ptDisplaySet.images[0]; const imageIdScalingFactor = MetadataProvider.get('scalingModule', imageId); const isSUVAvailable = imageIdScalingFactor && imageIdScalingFactor.suvbw; if (isSUVAvailable) { return { windowWidth: 5, windowCenter: 2.5, }; } return; } ); }, onModeExit: ({ servicesManager }: withAppTypes) => { const { toolGroupService, syncGroupService, segmentationService, cornerstoneViewportService, uiDialogService, uiModalService, } = servicesManager.services; unsubscriptions.forEach(unsubscribe => unsubscribe()); uiDialogService.dismissAll(); uiModalService.hide(); toolGroupService.destroy(); syncGroupService.destroy(); segmentationService.destroy(); cornerstoneViewportService.destroy(); }, validationTags: { study: [], series: [], }, isValidMode: ({ modalities, study }) => { const modalities_list = modalities.split('\\'); const invalidModalities = ['SM']; const isValid = modalities_list.includes('CT') && study.mrn !== 'M1' && modalities_list.includes('PT') && !invalidModalities.some(modality => modalities_list.includes(modality)) && // This is study is a 4D study with PT and CT and not a 3D study for the tmtv // mode, until we have a better way to identify 4D studies we will use the // StudyInstanceUID to identify the study // Todo: when we add the 4D mode which comes with a mechanism to identify // 4D studies we can use that study.studyInstanceUid !== '1.3.6.1.4.1.12842.1.1.14.3.20220915.105557.468.2963630849'; // there should be both CT and PT modalities and the modality should not be SM return { valid: isValid, description: 'The mode requires both PT and CT series in the study', }; }, routes: [ { path: 'tmtv', /*init: ({ servicesManager, extensionManager }) => { //defaultViewerRouteInit },*/ layoutTemplate: () => { return { id: ohif.layout, props: { leftPanels: [ohif.thumbnailList], leftPanelClosed: true, rightPanels: [tmtv.tmtv, tmtv.petSUV], viewports: [ { namespace: cs3d.viewport, displaySetsToDisplay: [ohif.sopClassHandler], }, ], }, }; }, }, ], extensions: extensionDependencies, hangingProtocol: tmtv.hangingProtocol, sopClassHandlers: [ohif.sopClassHandler], hotkeys: [...hotkeys.defaults.hotkeyBindings], ...modeConfiguration, }; } const mode = { id, modeFactory, extensionDependencies, }; export default mode;