Files
ohif-viewer/modes/tmtv/src/index.ts
2025-05-27 11:05:07 +07:00

246 lines
7.7 KiB
TypeScript

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;