fix(bugs): enhancements and bug fixes (#4036)
This commit is contained in:
1 parent
41a8d105f1
commit
e80fc6f477
58 files changed
+768
-148
No files matched your search
@@ -66,6 +66,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,6 +63,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const Header = ({ title, tooltip }) => (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Tooltip
|
||||
content={<div className="text-white">{tooltip}</div>}
|
||||
position="bottom"
|
||||
position="bottom-left"
|
||||
tight={true}
|
||||
tooltipBoxClassName="max-w-xs"
|
||||
>
|
||||
|
||||
@@ -68,6 +68,11 @@ export function Colormap({
|
||||
key: index,
|
||||
style: {
|
||||
minWidth: `calc(100% / ${displaySets.length})`,
|
||||
fontSize: '0.8rem',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
||||
}, [displaySets]);
|
||||
|
||||
@@ -60,7 +60,11 @@ export function VolumeLighting({
|
||||
min={0}
|
||||
type="range"
|
||||
step={0.1}
|
||||
style={{ background: calculateBackground(ambient) }}
|
||||
style={{
|
||||
background: calculateBackground(ambient),
|
||||
'--thumb-inner-color': '#5acce6',
|
||||
'--thumb-outer-color': '#090c29',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -84,7 +88,11 @@ export function VolumeLighting({
|
||||
min={0}
|
||||
type="range"
|
||||
step={0.1}
|
||||
style={{ background: calculateBackground(diffuse) }}
|
||||
style={{
|
||||
background: calculateBackground(diffuse),
|
||||
'--thumb-inner-color': '#5acce6',
|
||||
'--thumb-outer-color': '#090c29',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -109,7 +117,11 @@ export function VolumeLighting({
|
||||
min={0}
|
||||
type="range"
|
||||
step={0.1}
|
||||
style={{ background: calculateBackground(specular) }}
|
||||
style={{
|
||||
background: calculateBackground(specular),
|
||||
'--thumb-inner-color': '#5acce6',
|
||||
'--thumb-outer-color': '#090c29',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -25,10 +25,10 @@ export function VolumeRenderingOptions({
|
||||
commandsManager={commandsManager}
|
||||
serviceManager={serviceManager}
|
||||
/>
|
||||
<div className="all-in-one-menu-item flex w-full justify-start">
|
||||
<div className="all-in-one-menu-item flex !h-[20px] w-full justify-start">
|
||||
<div className="text-aqua-pale text-[13px]">LIGHTING</div>
|
||||
</div>
|
||||
<AllInOneMenu.DividerItem />
|
||||
<div className="bg-primary-dark mt-1 mb-1 h-[2px] w-full"></div>
|
||||
<div className="all-in-one-menu-item flex w-full justify-center">
|
||||
<VolumeShade
|
||||
commandsManager={commandsManager}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export function VolumeRenderingPresets({
|
||||
commandsManager,
|
||||
},
|
||||
containerDimensions: 'h-[543px] w-[460px]',
|
||||
contentDimensions: 'h-[493px] w-[460px] pl-[12px]',
|
||||
contentDimensions: 'h-[493px] w-[460px] pl-[12px] pr-[12px]',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+5
-1
@@ -60,7 +60,11 @@ export function VolumeRenderingQuality({
|
||||
type="range"
|
||||
step={step}
|
||||
onChange={e => onChange(parseInt(e.target.value, 10))}
|
||||
style={{ background: calculateBackground((quality - min) / (max - min)) }}
|
||||
style={{
|
||||
background: calculateBackground((quality - min) / (max - min)),
|
||||
'--thumb-inner-color': '#5acce6',
|
||||
'--thumb-outer-color': '#090c29',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -82,6 +82,8 @@ export function VolumeShift({
|
||||
step={step}
|
||||
style={{
|
||||
background: calculateBackground((shift - minShift) / (maxShift - minShift)),
|
||||
'--thumb-inner-color': '#5acce6',
|
||||
'--thumb-outer-color': '#090c29',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type WindowLevelProps = {
|
||||
viewportId: string;
|
||||
presets: Record<string, Array<WindowLevelPreset>>;
|
||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
||||
commandsManager: CommandsManager;
|
||||
};
|
||||
|
||||
@@ -23,25 +23,34 @@ export function WindowLevel({
|
||||
commandName: 'setViewportWindowLevel',
|
||||
commandOptions: {
|
||||
...props,
|
||||
viewportId,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
});
|
||||
},
|
||||
[commandsManager]
|
||||
[commandsManager, viewportId]
|
||||
);
|
||||
|
||||
return (
|
||||
<AllInOneMenu.ItemPanel>
|
||||
<AllInOneMenu.HeaderItem>
|
||||
{t('Modality Presets', { modality: Object.keys(presets)[0] })}
|
||||
</AllInOneMenu.HeaderItem>
|
||||
{Object.values(presets)[0].map((preset, index) => (
|
||||
<AllInOneMenu.Item
|
||||
key={index}
|
||||
label={preset.description}
|
||||
secondaryLabel={`${preset.window} / ${preset.level}`}
|
||||
onClick={() => onSetWindowLevel({ ...preset, viewportId })}
|
||||
></AllInOneMenu.Item>
|
||||
{presets.map((modalityPresets, modalityIndex) => (
|
||||
<React.Fragment key={modalityIndex}>
|
||||
{Object.entries(modalityPresets).map(([modality, presetsArray]) => (
|
||||
<React.Fragment key={modality}>
|
||||
<AllInOneMenu.HeaderItem>
|
||||
{t('Modality Presets', { modality })}
|
||||
</AllInOneMenu.HeaderItem>
|
||||
{presetsArray.map((preset, index) => (
|
||||
<AllInOneMenu.Item
|
||||
key={`${modality}-${index}`}
|
||||
label={preset.description}
|
||||
secondaryLabel={`${preset.window} / ${preset.level}`}
|
||||
onClick={() => onSetWindowLevel(preset)}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</AllInOneMenu.ItemPanel>
|
||||
);
|
||||
|
||||
+5
-7
@@ -19,7 +19,7 @@ import { utilities } from '@cornerstonejs/core';
|
||||
export type WindowLevelActionMenuProps = {
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
presets: Record<string, Array<WindowLevelPreset>>;
|
||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
||||
verticalDirection: AllInOneMenu.VerticalDirection;
|
||||
horizontalDirection: AllInOneMenu.HorizontalDirection;
|
||||
commandsManager: CommandsManager;
|
||||
@@ -128,10 +128,8 @@ export function WindowLevelActionMenu({
|
||||
iconClassName={classNames(
|
||||
// Visible on hover and for the active viewport
|
||||
activeViewportId === viewportId ? 'visible' : 'invisible group-hover:visible',
|
||||
'flex shrink-0 cursor-pointer rounded active:text-white',
|
||||
isLight
|
||||
? 'text-aqua-pale hover:bg-secondary-dark'
|
||||
: 'text-primary-light hover:bg-secondary-light/60'
|
||||
'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light',
|
||||
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60'
|
||||
)}
|
||||
menuStyle={{ maxHeight: vpHeight - 32, minWidth: 218 }}
|
||||
onVisibilityChange={() => {
|
||||
@@ -166,10 +164,10 @@ export function WindowLevelActionMenu({
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
|
||||
{presets && !is3DVolume && (
|
||||
{presets && presets.length > 0 && !is3DVolume && (
|
||||
<AllInOneMenu.SubMenu
|
||||
key="windowLevelPresets"
|
||||
itemLabel={t('Modality Window Presets', { modality: Object.keys(presets)[0] })}
|
||||
itemLabel={t('Modality Window Presets')}
|
||||
itemIcon="viewport-window-level"
|
||||
>
|
||||
<WindowLevel
|
||||
|
||||
+3
-5
@@ -24,13 +24,11 @@ export function getWindowLevelActionMenu({
|
||||
return { [displaySet.Modality]: presets[displaySet.Modality] };
|
||||
});
|
||||
|
||||
const hasMenu = displaySetPresets.length > 0;
|
||||
|
||||
return hasMenu ? (
|
||||
return (
|
||||
<WindowLevelActionMenu
|
||||
viewportId={viewportId}
|
||||
element={element}
|
||||
presets={displaySetPresets[0]}
|
||||
presets={displaySetPresets}
|
||||
verticalDirection={verticalDirection}
|
||||
horizontalDirection={horizontalDirection}
|
||||
commandsManager={commandsManager}
|
||||
@@ -40,5 +38,5 @@ export function getWindowLevelActionMenu({
|
||||
volumeRenderingPresets={volumeRenderingPresets}
|
||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||
/>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
@@ -43,8 +43,8 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
return {
|
||||
disabled: false,
|
||||
className: isPrimaryActive
|
||||
? '!text-black bg-primary-light'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light',
|
||||
? '!text-black bg-primary-light rounded'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light rounded',
|
||||
// Todo: isActive right now is used for nested buttons where the primary
|
||||
// button needs to be fully rounded (vs partial rounded) when active
|
||||
// otherwise it does not have any other use
|
||||
|
||||
@@ -77,8 +77,11 @@ export const fourUp = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
// ToDo: choose appropriate preset
|
||||
displayPreset: 'CT-Bone',
|
||||
displayPreset: {
|
||||
CT: 'CT-Bone',
|
||||
MR: 'MR-Default',
|
||||
default: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -77,8 +77,11 @@ export const main3D = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
// ToDo: choose appropriate preset
|
||||
displayPreset: 'CT-Bone',
|
||||
displayPreset: {
|
||||
CT: 'CT-Bone',
|
||||
MR: 'MR-Default',
|
||||
default: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -84,7 +84,11 @@ export const mprAnd3DVolumeViewport = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
displayPreset: 'CT-Bone',
|
||||
displayPreset: {
|
||||
CT: 'CT-Bone',
|
||||
MR: 'MR-Default',
|
||||
default: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -51,8 +51,11 @@ export const only3D = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
// ToDo: choose appropriate preset
|
||||
displayPreset: 'CT-Bone',
|
||||
displayPreset: {
|
||||
CT: 'CT-Bone',
|
||||
MR: 'MR-Default',
|
||||
default: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -77,8 +77,11 @@ export const primary3D = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
// ToDo: choose appropriate preset
|
||||
displayPreset: 'CT-Bone',
|
||||
displayPreset: {
|
||||
CT: 'CT-Bone',
|
||||
MR: 'MR-Default',
|
||||
default: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -160,7 +160,7 @@ export default async function init({
|
||||
const labelmapRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(labelmapRepresentation, {
|
||||
fillAlpha: 0.3,
|
||||
fillAlpha: 0.5,
|
||||
fillAlphaInactive: 0.2,
|
||||
outlineOpacity: 1,
|
||||
outlineOpacityInactive: 0.65,
|
||||
|
||||
@@ -33,6 +33,8 @@ const initMeasurementService = (
|
||||
PlanarFreehandROI,
|
||||
SplineROI,
|
||||
LivewireContour,
|
||||
Probe,
|
||||
UltrasoundDirectional,
|
||||
} = measurementServiceMappingsFactory(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
@@ -157,6 +159,22 @@ const initMeasurementService = (
|
||||
LivewireContour.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'Probe',
|
||||
Probe.matchingCriteria,
|
||||
Probe.toAnnotation,
|
||||
Probe.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'UltrasoundDirectionalTool',
|
||||
UltrasoundDirectional.matchingCriteria,
|
||||
UltrasoundDirectional.toAnnotation,
|
||||
UltrasoundDirectional.toMeasurement
|
||||
);
|
||||
|
||||
return csTools3DVer1MeasurementSource;
|
||||
};
|
||||
|
||||
|
||||
@@ -721,10 +721,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
|
||||
public async setVolumesForViewport(viewport, volumeInputArray, presentations) {
|
||||
const { displaySetService, toolGroupService } = this.servicesManager.services;
|
||||
const { displaySetService, toolGroupService, viewportGridService } =
|
||||
this.servicesManager.services;
|
||||
|
||||
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewport.id);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
|
||||
const displaySetModality = displaySet?.Modality;
|
||||
|
||||
// Todo: use presentations states
|
||||
const volumesProperties = volumeInputArray.map((volumeInput, index) => {
|
||||
@@ -750,7 +754,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
|
||||
if (displayPreset !== undefined) {
|
||||
properties.preset = displayPreset;
|
||||
properties.preset = displayPreset[displaySetModality] || displayPreset.default;
|
||||
}
|
||||
|
||||
return { properties, volumeId };
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
const colormaps = [
|
||||
{
|
||||
ColorSpace: 'RGB',
|
||||
Name: 'Grayscale',
|
||||
name: 'Grayscale',
|
||||
NanColor: [1, 0, 0],
|
||||
RGBPoints: [0, 0, 0, 0, 1, 1, 1, 1],
|
||||
description: 'Grayscale',
|
||||
},
|
||||
{
|
||||
ColorSpace: 'RGB',
|
||||
Name: 'X Ray',
|
||||
name: 'X Ray',
|
||||
NanColor: [1, 0, 0],
|
||||
RGBPoints: [0, 1, 1, 1, 1, 0, 0, 0],
|
||||
description: 'X Ray',
|
||||
},
|
||||
{
|
||||
ColorSpace: 'RGB',
|
||||
Name: 'hsv',
|
||||
@@ -1579,22 +1595,6 @@ const colormaps = [
|
||||
],
|
||||
description: 'Siemens',
|
||||
},
|
||||
{
|
||||
ColorSpace: 'RGB',
|
||||
Name: 'X Ray',
|
||||
name: 'X Ray',
|
||||
NanColor: [1, 0, 0],
|
||||
RGBPoints: [0, 1, 1, 1, 1, 0, 0, 0],
|
||||
description: 'X Ray',
|
||||
},
|
||||
{
|
||||
ColorSpace: 'RGB',
|
||||
Name: 'Grayscale',
|
||||
name: 'Grayscale',
|
||||
NanColor: [1, 0, 0],
|
||||
RGBPoints: [0, 0, 0, 0, 1, 1, 1, 1],
|
||||
description: 'Grayscale',
|
||||
},
|
||||
];
|
||||
|
||||
export { colormaps };
|
||||
@@ -0,0 +1,187 @@
|
||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||
import { getDisplayUnit } from './utils';
|
||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const Probe = {
|
||||
toAnnotation: measurement => {},
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (
|
||||
csToolsEventDetail,
|
||||
displaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType,
|
||||
customizationService
|
||||
) => {
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
if (!metadata || !data) {
|
||||
console.warn('Length tool: Missing metadata or data');
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
|
||||
getSOPInstanceAttributes(referencedImageId);
|
||||
|
||||
let displaySet;
|
||||
|
||||
if (SOPInstanceUID) {
|
||||
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
} else {
|
||||
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
const { points } = data.handles;
|
||||
|
||||
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
|
||||
|
||||
const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
|
||||
const getReport = () =>
|
||||
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
|
||||
|
||||
return {
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
frameNumber: mappedAnnotations?.[0]?.frameNumber || 1,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: data.label,
|
||||
displayText: displayText,
|
||||
data: data.cachedStats,
|
||||
type: getValueTypeFromToolType(toolName),
|
||||
getReport,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
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];
|
||||
|
||||
if (!referencedImageId) {
|
||||
throw new Error('Non-acquisition plane measurement mapping not supported');
|
||||
}
|
||||
|
||||
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
|
||||
getSOPInstanceAttributes(referencedImageId);
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const { SeriesNumber } = displaySet;
|
||||
const { value } = targetStats;
|
||||
const unit = 'HU';
|
||||
|
||||
annotations.push({
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesNumber,
|
||||
frameNumber,
|
||||
unit,
|
||||
value,
|
||||
});
|
||||
});
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
/*
|
||||
This function is used to convert the measurement data to a format that is
|
||||
suitable for the report generation (e.g. for the csv report). The report
|
||||
returns a list of columns and corresponding values.
|
||||
*/
|
||||
function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) {
|
||||
const columns = [];
|
||||
const values = [];
|
||||
|
||||
// Add Type
|
||||
columns.push('AnnotationType');
|
||||
values.push('Cornerstone:Probe');
|
||||
|
||||
mappedAnnotations.forEach(annotation => {
|
||||
const { value, unit } = annotation;
|
||||
columns.push(`Probe (${unit})`);
|
||||
values.push(value);
|
||||
});
|
||||
|
||||
if (FrameOfReferenceUID) {
|
||||
columns.push('FrameOfReferenceUID');
|
||||
values.push(FrameOfReferenceUID);
|
||||
}
|
||||
|
||||
if (points) {
|
||||
columns.push('points');
|
||||
values.push(points.map(p => p.join(' ')).join(';'));
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
values,
|
||||
};
|
||||
}
|
||||
|
||||
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
if (!mappedAnnotations || !mappedAnnotations.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const displayText = [];
|
||||
|
||||
const { value, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
InstanceNumber = instance.InstanceNumber;
|
||||
}
|
||||
|
||||
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||
if (value === undefined) {
|
||||
return displayText;
|
||||
}
|
||||
const roundedValue = utils.roundNumber(value, 2);
|
||||
displayText.push(
|
||||
`${roundedValue} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})`
|
||||
);
|
||||
|
||||
return displayText;
|
||||
}
|
||||
|
||||
export default Probe;
|
||||
@@ -0,0 +1,206 @@
|
||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||
import { getDisplayUnit } from './utils';
|
||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const UltrasoundDirectional = {
|
||||
toAnnotation: measurement => {},
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (
|
||||
csToolsEventDetail,
|
||||
displaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType,
|
||||
customizationService
|
||||
) => {
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
if (!metadata || !data) {
|
||||
console.warn('Length tool: Missing metadata or data');
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
|
||||
getSOPInstanceAttributes(referencedImageId);
|
||||
|
||||
let displaySet;
|
||||
|
||||
if (SOPInstanceUID) {
|
||||
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
} else {
|
||||
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
const { points } = data.handles;
|
||||
|
||||
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
|
||||
|
||||
const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
|
||||
const getReport = () =>
|
||||
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
|
||||
|
||||
return {
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
frameNumber: mappedAnnotations?.[0]?.frameNumber || 1,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: data.label,
|
||||
displayText: displayText,
|
||||
data: data.cachedStats,
|
||||
type: getValueTypeFromToolType(toolName),
|
||||
getReport,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
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];
|
||||
|
||||
if (!referencedImageId) {
|
||||
throw new Error('Non-acquisition plane measurement mapping not supported');
|
||||
}
|
||||
|
||||
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
|
||||
getSOPInstanceAttributes(referencedImageId);
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const { SeriesNumber } = displaySet;
|
||||
const { xValues, yValues, units, isUnitless, isHorizontal } = targetStats;
|
||||
|
||||
annotations.push({
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesNumber,
|
||||
frameNumber,
|
||||
xValues,
|
||||
yValues,
|
||||
units,
|
||||
isUnitless,
|
||||
isHorizontal,
|
||||
});
|
||||
});
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
/*
|
||||
This function is used to convert the measurement data to a format that is
|
||||
suitable for the report generation (e.g. for the csv report). The report
|
||||
returns a list of columns and corresponding values.
|
||||
*/
|
||||
function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) {
|
||||
const columns = [];
|
||||
const values = [];
|
||||
|
||||
// Add Type
|
||||
columns.push('AnnotationType');
|
||||
values.push('Cornerstone:UltrasoundDirectional');
|
||||
|
||||
mappedAnnotations.forEach(annotation => {
|
||||
const { xValues, yValues, units, isUnitless } = annotation;
|
||||
if (isUnitless) {
|
||||
columns.push('Length' + units[0]);
|
||||
values.push(utils.roundNumber(xValues[0], 2));
|
||||
} else {
|
||||
const dist1 = Math.abs(xValues[1] - xValues[0]);
|
||||
const dist2 = Math.abs(yValues[1] - yValues[0]);
|
||||
columns.push('Time' + units[0]);
|
||||
values.push(utils.roundNumber(dist1, 2));
|
||||
columns.push('Length' + units[1]);
|
||||
values.push(utils.roundNumber(dist2, 2));
|
||||
}
|
||||
});
|
||||
|
||||
if (FrameOfReferenceUID) {
|
||||
columns.push('FrameOfReferenceUID');
|
||||
values.push(FrameOfReferenceUID);
|
||||
}
|
||||
|
||||
if (points) {
|
||||
columns.push('points');
|
||||
values.push(points.map(p => p.join(' ')).join(';'));
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
values,
|
||||
};
|
||||
}
|
||||
|
||||
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
if (!mappedAnnotations || !mappedAnnotations.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const displayText = [];
|
||||
|
||||
const { xValues, yValues, units, isUnitless, SeriesNumber, SOPInstanceUID, frameNumber } =
|
||||
mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
InstanceNumber = instance.InstanceNumber;
|
||||
}
|
||||
|
||||
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||
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}`);
|
||||
} 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}`);
|
||||
}
|
||||
|
||||
return displayText;
|
||||
}
|
||||
|
||||
export default UltrasoundDirectional;
|
||||
+2
@@ -11,4 +11,6 @@ export default [
|
||||
'PlanarFreehandROI',
|
||||
'SplineROI',
|
||||
'LivewireContour',
|
||||
'Probe',
|
||||
'UltrasoundDirectionalTool',
|
||||
];
|
||||
+39
@@ -10,6 +10,8 @@ import PlanarFreehandROI from './PlanarFreehandROI';
|
||||
import RectangleROI from './RectangleROI';
|
||||
import SplineROI from './SplineROI';
|
||||
import LivewireContour from './LivewireContour';
|
||||
import Probe from './Probe';
|
||||
import UltrasoundDirectional from './UltrasoundDirectional';
|
||||
|
||||
const measurementServiceMappingsFactory = (
|
||||
measurementService: MeasurementService,
|
||||
@@ -44,6 +46,8 @@ const measurementServiceMappingsFactory = (
|
||||
Angle: ANGLE,
|
||||
SplineROI: POLYLINE,
|
||||
LivewireContour: POLYLINE,
|
||||
Probe: POINT,
|
||||
UltrasoundDirectional: POLYLINE,
|
||||
};
|
||||
|
||||
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
|
||||
@@ -211,6 +215,24 @@ const measurementServiceMappingsFactory = (
|
||||
],
|
||||
},
|
||||
|
||||
Probe: {
|
||||
toAnnotation: Probe.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
Probe.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
_getValueTypeFromToolType,
|
||||
customizationService
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: MeasurementService.VALUE_TYPES.POINT,
|
||||
points: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
CobbAngle: {
|
||||
toAnnotation: CobbAngle.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
@@ -244,6 +266,23 @@ const measurementServiceMappingsFactory = (
|
||||
},
|
||||
],
|
||||
},
|
||||
UltrasoundDirectional: {
|
||||
toAnnotation: UltrasoundDirectional.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
UltrasoundDirectional.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
_getValueTypeFromToolType,
|
||||
customizationService
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
|
||||
points: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return factories;
|
||||
|
||||
@@ -53,6 +53,7 @@ function ViewerHeader({ hotkeysManager, extensionManager, servicesManager, appCo
|
||||
content: AboutModal,
|
||||
title: t('AboutModal:About OHIF Viewer'),
|
||||
contentProps: { versionNumber, commitHash },
|
||||
containerDimensions: 'max-w-4xl max-h-4xl',
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -114,7 +115,7 @@ function ViewerHeader({ hotkeysManager, extensionManager, servicesManager, appCo
|
||||
appConfig={appConfig}
|
||||
>
|
||||
<ErrorBoundary context="Primary Toolbar">
|
||||
<div className="relative flex justify-center">
|
||||
<div className="relative flex justify-center gap-[4px]">
|
||||
<Toolbar servicesManager={servicesManager} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -445,6 +445,7 @@ const commandsModule = ({
|
||||
displaySetInstanceUID,
|
||||
onClose: UIModalService.hide,
|
||||
},
|
||||
containerDimensions: 'max-w-4xl max-h-4xl',
|
||||
title: 'DICOM Tag Browser',
|
||||
});
|
||||
},
|
||||
|
||||
+6
@@ -74,6 +74,12 @@ function _askTrackMeasurements(uiViewportDialogService, viewportId) {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
const action = actions.find(action => action.value === RESPONSE.HYDRATE_REPORT);
|
||||
onSubmit(action.value);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+60
-12
@@ -329,6 +329,65 @@ function PanelStudyBrowserTracking({
|
||||
}
|
||||
}, [expandedStudyInstanceUIDs, jumpToDisplaySet, tabs]);
|
||||
|
||||
const onClickUntrack = displaySetInstanceUID => {
|
||||
const onConfirm = () => {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
sendTrackedMeasurementsEvent('UNTRACK_SERIES', {
|
||||
SeriesInstanceUID: displaySet.SeriesInstanceUID,
|
||||
});
|
||||
const measurements = measurementService.getMeasurements();
|
||||
measurements.forEach(m => {
|
||||
if (m.referenceSeriesUID === displaySet.SeriesInstanceUID) {
|
||||
measurementService.remove(m.uid);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
uiDialogService.create({
|
||||
id: 'untrack-series',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Untrack Series',
|
||||
body: () => (
|
||||
<div className="bg-primary-dark p-4 text-white">
|
||||
<p>Are you sure you want to untrack this series?</p>
|
||||
<p className="mt-2">
|
||||
This action cannot be undone and will delete all your existing measurements.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
id: 'cancel',
|
||||
text: 'Cancel',
|
||||
type: ButtonEnums.type.secondary,
|
||||
},
|
||||
{
|
||||
id: 'yes',
|
||||
text: 'Yes',
|
||||
type: ButtonEnums.type.primary,
|
||||
classes: ['untrack-yes-button'],
|
||||
},
|
||||
],
|
||||
onClose: () => uiDialogService.dismiss({ id: 'untrack-series' }),
|
||||
onSubmit: async ({ action }) => {
|
||||
switch (action.id) {
|
||||
case 'yes':
|
||||
onConfirm();
|
||||
uiDialogService.dismiss({ id: 'untrack-series' });
|
||||
break;
|
||||
case 'cancel':
|
||||
uiDialogService.dismiss({ id: 'untrack-series' });
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StudyBrowser
|
||||
tabs={tabs}
|
||||
@@ -340,18 +399,7 @@ function PanelStudyBrowserTracking({
|
||||
setActiveTabName(clickedTabName);
|
||||
}}
|
||||
onClickUntrack={displaySetInstanceUID => {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
// TODO: shift this somewhere else where we're centralizing this logic?
|
||||
// Potentially a helper from displaySetInstanceUID to this
|
||||
sendTrackedMeasurementsEvent('UNTRACK_SERIES', {
|
||||
SeriesInstanceUID: displaySet.SeriesInstanceUID,
|
||||
});
|
||||
const measurements = measurementService.getMeasurements();
|
||||
measurements.forEach(m => {
|
||||
if (m.referenceSeriesUID === displaySet.SeriesInstanceUID) {
|
||||
measurementService.remove(m.uid);
|
||||
}
|
||||
});
|
||||
onClickUntrack(displaySetInstanceUID);
|
||||
}}
|
||||
onClickThumbnail={() => {}}
|
||||
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
||||
}
|
||||
return (
|
||||
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
|
||||
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
|
||||
<div className="flex min-h-0 flex-1 flex-col bg-black text-[13px] font-[300]">
|
||||
<PanelSection title={t('Patient Information')}>
|
||||
<div className="flex flex-col">
|
||||
<div className="bg-primary-dark flex flex-col gap-4 p-2">
|
||||
|
||||
+2
-2
@@ -77,7 +77,7 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-1 flex flex-col">
|
||||
<div className="mt-2 mb-10 flex flex-col">
|
||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||
{tmtvValue !== null ? (
|
||||
<div className="bg-secondary-dark mt-1 flex items-baseline justify-between px-2 py-1">
|
||||
@@ -87,7 +87,7 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
||||
<div className="text-white">{`${tmtvValue} mL`}</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-1 flex justify-center">
|
||||
<div className="mt-2 flex justify-center">
|
||||
<ActionButtons
|
||||
actions={actions}
|
||||
t={t}
|
||||
|
||||
Reference in new issue
Block a user