|
|
|
|
@@ -9,7 +9,7 @@ import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } fro
|
|
|
|
|
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
|
|
|
|
|
|
|
|
|
import './CustomizableViewportOverlay.css';
|
|
|
|
|
import { studyDataForOverlayItem } from './studyDataForOverlayItem';
|
|
|
|
|
import { studyDataForOverlayItem, instanceDataForMriOverlayItem } from './studyDataForOverlayItem';
|
|
|
|
|
|
|
|
|
|
const EPSILON = 1e-4;
|
|
|
|
|
|
|
|
|
|
@@ -58,10 +58,10 @@ const studyDateItem = {
|
|
|
|
|
title: 'Study date and time',
|
|
|
|
|
condition: ({ referenceInstance, studyApiData }) =>
|
|
|
|
|
referenceInstance?.StudyDate || studyApiData?.studyTime,
|
|
|
|
|
contentF: ({ referenceInstance, formatters: { formatDate }, studyApiData }) => {
|
|
|
|
|
contentF: ({ referenceInstance, formatters: { formatDate, formatTime }, studyApiData }) => {
|
|
|
|
|
const date = referenceInstance?.StudyDate ? formatDate(referenceInstance.StudyDate) : '';
|
|
|
|
|
const time = studyApiData?.studyTime ? studyApiData.studyTime : '';
|
|
|
|
|
return `${date} ${time}`;
|
|
|
|
|
const time = studyApiData?.studyTime ? formatTime(studyApiData.studyTime) : '';
|
|
|
|
|
return `${date} ${time}`.trim();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -125,12 +125,230 @@ const accessionNumberItem = {
|
|
|
|
|
contentF: ({ studyApiData }) => studyApiData.accessionNumber,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Tambahan untuk Overlay MRI di Top Left
|
|
|
|
|
const sliceThicknessItem = {
|
|
|
|
|
id: 'SliceThickness',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Slice Thickness: ',
|
|
|
|
|
title: 'Slice Thickness',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.sliceThickness,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.sliceThickness} mm`,
|
|
|
|
|
};
|
|
|
|
|
const sliceLocationItem = {
|
|
|
|
|
id: 'SliceLocation',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Slice Loc: ',
|
|
|
|
|
title: 'Slice Location',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.sliceLocation,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.sliceLocation} mm`,
|
|
|
|
|
};
|
|
|
|
|
const sliceSpacingItem = {
|
|
|
|
|
id: 'SliceSpacing',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Slice Spacing: ',
|
|
|
|
|
title: 'Slice Spacing',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.spacingBetweenSlices,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.spacingBetweenSlices} mm`,
|
|
|
|
|
};
|
|
|
|
|
const phaseFieldOfViewItem = {
|
|
|
|
|
id: 'PhaseFOV',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: '% Phase FOV: ',
|
|
|
|
|
title: 'Percent Phase FOV',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.percentPhaseFieldOfView,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.percentPhaseFieldOfView} %`,
|
|
|
|
|
};
|
|
|
|
|
const phaseFieldOfViewDimensionsItem = {
|
|
|
|
|
id: 'FOVDimensions',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'FOV Dim: ',
|
|
|
|
|
title: 'Field of View Dimensions',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.fieldOfViewDimensions,
|
|
|
|
|
contentF: ({ mriInstanceData }) => mriInstanceData?.fieldOfViewDimensions.join(' x '),
|
|
|
|
|
};
|
|
|
|
|
const acquisitionMatrixItem = {
|
|
|
|
|
id: 'AcquisitionMatrix',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Acq Matrix: ',
|
|
|
|
|
title: 'Acquisition Matrix',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.acquisitionMatrix,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.acquisitionMatrix.join(' x ')}`,
|
|
|
|
|
};
|
|
|
|
|
const imageMatrixItem = {
|
|
|
|
|
id: 'ImageMatrix',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Image Matrix (RxC): ',
|
|
|
|
|
title: 'Image Matrix',
|
|
|
|
|
condition: ({ mriInstanceData }) =>
|
|
|
|
|
mriInstanceData?.imageMatrixRows && mriInstanceData?.imageMatrixColumns,
|
|
|
|
|
contentF: ({ mriInstanceData }) =>
|
|
|
|
|
`${mriInstanceData.imageMatrixRows} x ${mriInstanceData.imageMatrixColumns}`,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mriTopLeftItems = {
|
|
|
|
|
id: 'cornerstoneOverlayTopLeft',
|
|
|
|
|
items: [
|
|
|
|
|
sliceThicknessItem,
|
|
|
|
|
sliceLocationItem,
|
|
|
|
|
sliceSpacingItem,
|
|
|
|
|
phaseFieldOfViewItem,
|
|
|
|
|
phaseFieldOfViewDimensionsItem,
|
|
|
|
|
acquisitionMatrixItem,
|
|
|
|
|
imageMatrixItem,
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Tambahan bottom left overlay MRI
|
|
|
|
|
const scanningSequenceItem = {
|
|
|
|
|
id: 'MRIScanningSequence',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Scanning Seq: ',
|
|
|
|
|
title: 'MRI Scanning Sequence',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.scanningSequence,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.scanningSequence} `,
|
|
|
|
|
};
|
|
|
|
|
const repetitionEchoInversionTimesItem = {
|
|
|
|
|
id: 'MRIRepetitionEchoInversionTimes',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'TR/TE/TI: ',
|
|
|
|
|
title: 'MRI Repetition/Echo/Inversion Times',
|
|
|
|
|
condition: ({ mriInstanceData }) =>
|
|
|
|
|
mriInstanceData?.repetitionTime || mriInstanceData?.echoTime || mriInstanceData?.inversionTime,
|
|
|
|
|
contentF: ({ mriInstanceData }) => {
|
|
|
|
|
const tr = mriInstanceData?.repetitionTime || '-';
|
|
|
|
|
const te = mriInstanceData?.echoTime || '-';
|
|
|
|
|
const ti = mriInstanceData?.inversionTime || '-';
|
|
|
|
|
return `${tr}/${te}/${ti} ms`.replace(/\/+/g, ' / ').trim(); // Clean up extra slashes
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
const receiverCoilItem = {
|
|
|
|
|
id: 'MRIReceiverCoil',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Receiver Coil: ',
|
|
|
|
|
title: 'MRI Receiver Coil',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.receiveCoilName,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.receiveCoilName} `,
|
|
|
|
|
};
|
|
|
|
|
const mriScanModeItem = {
|
|
|
|
|
id: 'MRIScanMode',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Scan Mode: ',
|
|
|
|
|
title: 'MRI Scan Mode',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.mrAcquisitionType,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.mrAcquisitionType} `,
|
|
|
|
|
};
|
|
|
|
|
const phaseEncodingDirectionItem = {
|
|
|
|
|
id: 'MRIPhaseEncodingDirection',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Phase Enc Direction: ',
|
|
|
|
|
title: 'MRI Phase Encoding Direction',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.phaseEncodingDirection,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.phaseEncodingDirection} `,
|
|
|
|
|
};
|
|
|
|
|
const numOfAveragesItem = {
|
|
|
|
|
id: 'MRINumOfAverages',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'NEX: ',
|
|
|
|
|
title: 'MRI Number of Averages',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.numOfAverages,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.numOfAverages} `,
|
|
|
|
|
};
|
|
|
|
|
const echoTrainLengthItem = {
|
|
|
|
|
id: 'MRIEchoTrainLength',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'ETL: ',
|
|
|
|
|
title: 'MRI Echo Train Length',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.echoTrainLength,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.echoTrainLength} `,
|
|
|
|
|
};
|
|
|
|
|
const flipAngleItem = {
|
|
|
|
|
id: 'MRIFlipAngle',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Flip Angle: ',
|
|
|
|
|
title: 'MRI Flip Angle',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.flipAngle,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.flipAngle} °`,
|
|
|
|
|
};
|
|
|
|
|
const pixelBandwidthItem = {
|
|
|
|
|
id: 'MRIPixelBandwidth',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Pixel Bandwidth: ',
|
|
|
|
|
title: 'MRI Pixel Bandwidth',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.pixelBandwidth,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.pixelBandwidth} Hz`,
|
|
|
|
|
};
|
|
|
|
|
const acquisitionTimeItem = {
|
|
|
|
|
id: 'MRIAcquisitionTime',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Acq Time: ',
|
|
|
|
|
title: 'MRI Acquisition Time',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.acquisitionTime,
|
|
|
|
|
contentF: ({ referenceInstance, formatters: { formatTime }, mriInstanceData }) => {
|
|
|
|
|
const time = mriInstanceData?.acquisitionTime
|
|
|
|
|
? formatTime(mriInstanceData.acquisitionTime)
|
|
|
|
|
: '';
|
|
|
|
|
return `${time}`.trim();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
const acquisitionDurationTotalItem = {
|
|
|
|
|
id: 'MRIAcquisitionDurationTotal',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Total Acq Dur: ',
|
|
|
|
|
title: 'MRI Total Duration Acquisition',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.acquisitionDurationTotal,
|
|
|
|
|
contentF: ({ mriInstanceData }) => mriInstanceData.acquisitionDurationTotal,
|
|
|
|
|
};
|
|
|
|
|
const acquisitionDurationPerFrameItem = {
|
|
|
|
|
id: 'MRIAcquisitionDurationPerFrame',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Frame Acq Dur: ',
|
|
|
|
|
title: 'MRI Duration Acquisition Per Frame',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.acquisitionDurationPerFrame,
|
|
|
|
|
contentF: ({ mriInstanceData }) => mriInstanceData.acquisitionDurationPerFrame,
|
|
|
|
|
};
|
|
|
|
|
const parallelImagingItem = {
|
|
|
|
|
id: 'MRIParallelImaging',
|
|
|
|
|
customizationType: 'ohif.overlayItem',
|
|
|
|
|
label: 'Parallel Img: ',
|
|
|
|
|
title: 'MRI Parallel Imaging',
|
|
|
|
|
condition: ({ mriInstanceData }) => mriInstanceData?.parallelAcquisitionTechnique,
|
|
|
|
|
contentF: ({ mriInstanceData }) => `${mriInstanceData.parallelAcquisitionTechnique} `,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mriBottomLeftItems = {
|
|
|
|
|
id: 'cornerstoneOverlayBottomLeft',
|
|
|
|
|
items: [
|
|
|
|
|
scanningSequenceItem,
|
|
|
|
|
repetitionEchoInversionTimesItem,
|
|
|
|
|
receiverCoilItem,
|
|
|
|
|
mriScanModeItem,
|
|
|
|
|
phaseEncodingDirectionItem,
|
|
|
|
|
numOfAveragesItem,
|
|
|
|
|
echoTrainLengthItem,
|
|
|
|
|
flipAngleItem,
|
|
|
|
|
pixelBandwidthItem,
|
|
|
|
|
acquisitionDurationTotalItem,
|
|
|
|
|
acquisitionDurationPerFrameItem,
|
|
|
|
|
parallelImagingItem,
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const topLeftItems = {
|
|
|
|
|
id: 'cornerstoneOverlayTopLeft',
|
|
|
|
|
items: [patientNameItem, mrnItem, sexAndAgeItem],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
|
|
|
|
|
const topRightItems = {
|
|
|
|
|
id: 'cornerstoneOverlayTopRight',
|
|
|
|
|
items: [
|
|
|
|
|
patientNameItem,
|
|
|
|
|
mrnItem,
|
|
|
|
|
sexAndAgeItem,
|
|
|
|
|
accessionNumberItem,
|
|
|
|
|
studyDescriptionItem,
|
|
|
|
|
studyDateItem,
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const bottomLeftItems = {
|
|
|
|
|
id: 'cornerstoneOverlayBottomLeft',
|
|
|
|
|
@@ -160,9 +378,9 @@ const bottomRightItems = {
|
|
|
|
|
*/
|
|
|
|
|
const CornerstoneOverlay = {
|
|
|
|
|
id: '@ohif/cornerstoneOverlay',
|
|
|
|
|
topLeftItems,
|
|
|
|
|
mriTopLeftItems,
|
|
|
|
|
topRightItems,
|
|
|
|
|
bottomLeftItems,
|
|
|
|
|
mriBottomLeftItems,
|
|
|
|
|
bottomRightItems,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -192,23 +410,6 @@ function CustomizableViewportOverlay({
|
|
|
|
|
// on the individual items rather than defining individual items.
|
|
|
|
|
const cornerstoneOverlay = customizationService.getCustomization('@ohif/cornerstoneOverlay');
|
|
|
|
|
|
|
|
|
|
// Historical usage defined the overlays as separate items due to lack of
|
|
|
|
|
// 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 instanceNumber = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
viewportData
|
|
|
|
|
@@ -236,8 +437,41 @@ function CustomizableViewportOverlay({
|
|
|
|
|
|
|
|
|
|
// FEAT: Edit overlays item [2025-09-04]
|
|
|
|
|
const studyInstanceUID = displaySetProps?.referenceInstance?.StudyInstanceUID;
|
|
|
|
|
const seriesInstanceUID = displaySetProps?.referenceInstance?.SeriesInstanceUID;
|
|
|
|
|
const sopInstanceUID = displaySetProps?.instance?.SOPInstanceUID;
|
|
|
|
|
const { studyData: studyApiData, loading, error } = studyDataForOverlayItem(studyInstanceUID);
|
|
|
|
|
|
|
|
|
|
console.debug('Reference Instance:', displaySetProps?.referenceInstance);
|
|
|
|
|
console.debug('Instance:', displaySetProps?.instance);
|
|
|
|
|
|
|
|
|
|
// Modality untuk menentukan kustomisasi overlay
|
|
|
|
|
const modality = displaySetProps?.referenceInstance?.Modality;
|
|
|
|
|
|
|
|
|
|
const { instanceData: mriInstanceData } = instanceDataForMriOverlayItem(
|
|
|
|
|
modality === 'MR' ? studyInstanceUID : null,
|
|
|
|
|
modality === 'MR' ? seriesInstanceUID : null,
|
|
|
|
|
modality === 'MR' ? sopInstanceUID : null
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Historical usage defined the overlays as separate items due to lack of
|
|
|
|
|
// 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') ||
|
|
|
|
|
(modality === 'MR' ? cornerstoneOverlay?.mriTopLeftItems : cornerstoneOverlay?.topLeftItems);
|
|
|
|
|
const topRightCustomization =
|
|
|
|
|
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
|
|
|
|
|
cornerstoneOverlay?.topRightItems;
|
|
|
|
|
const bottomLeftCustomization =
|
|
|
|
|
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
|
|
|
|
|
(modality === 'MR'
|
|
|
|
|
? cornerstoneOverlay?.mriBottomLeftItems
|
|
|
|
|
: cornerstoneOverlay?.bottomLeftItems);
|
|
|
|
|
const bottomRightCustomization =
|
|
|
|
|
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
|
|
|
|
|
cornerstoneOverlay?.bottomRightItems;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Updating the VOI when the viewport changes its voi
|
|
|
|
|
*/
|
|
|
|
|
@@ -357,6 +591,7 @@ function CustomizableViewportOverlay({
|
|
|
|
|
viewportId,
|
|
|
|
|
toolGroupService,
|
|
|
|
|
studyApiData, // Pass the API data to the overlay items
|
|
|
|
|
mriInstanceData,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
@@ -370,7 +605,7 @@ function CustomizableViewportOverlay({
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
[_renderOverlayItem, displaySetProps, studyApiData]
|
|
|
|
|
[_renderOverlayItem, displaySetProps, studyApiData, mriInstanceData]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|