feat(customization): new customization service api (#4688)

This commit is contained in:
Alireza authored and GitHub committed 2025-01-23 14:24:13 -05:00
1 parent fab52808cb
commit 55ad8efbab
124 files changed
+4261 -2517

No files matched your search

@@ -1,5 +1,5 @@
function createRTToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
const tools = customizationService.getCustomization('cornerstone.overlayViewportTools');
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
@@ -1,5 +1,5 @@
function createSEGToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
const tools = customizationService.getCustomization('cornerstone.overlayViewportTools');
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
@@ -118,12 +118,11 @@ const commandsModule = (props: withAppTypes) => {
throw new Error('Invalid report, no content');
}
const onBeforeDicomStore =
customizationService.getModeCustomization('onBeforeDicomStore')?.value;
const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore');
let dicomDict;
if (typeof onBeforeDicomStore === 'function') {
dicomDict = onBeforeDicomStore({ measurementData, naturalizedReport });
dicomDict = onBeforeDicomStore({ dicomDict, measurementData, naturalizedReport });
}
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
@@ -151,7 +150,7 @@ const commandsModule = (props: withAppTypes) => {
*/
loadSRMeasurements: ({ displaySetInstanceUID }) => {
const { SeriesInstanceUIDs } = hydrateStructuredReport(
{ servicesManager, extensionManager },
{ servicesManager, extensionManager, commandsManager },
displaySetInstanceUID
);
@@ -263,9 +263,9 @@ function _checkIfCanAddMeasurementsToDisplaySet(
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j];
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
const onBeforeSRAddMeasurement = customizationService.getCustomization(
'onBeforeSRAddMeasurement'
)?.value;
);
if (typeof onBeforeSRAddMeasurement === 'function') {
measurement = onBeforeSRAddMeasurement({
@@ -41,22 +41,14 @@ const convertSites = (codingValues, sites) => {
*
*/
export default function hydrateStructuredReport(
{ servicesManager, extensionManager }: withAppTypes,
{ servicesManager, extensionManager, commandsManager }: withAppTypes,
displaySetInstanceUID
) {
const annotationManager = CsAnnotation.state.getAnnotationManager();
const dataSource = extensionManager.getActiveDataSource()[0];
const { measurementService, displaySetService, customizationService } = servicesManager.services;
const codingValues = customizationService.getCustomization('codingValues', {});
const { disableEditing } = customizationService.getCustomization(
'PanelMeasurement.disableEditing',
{
id: 'default.disableEditing',
disableEditing: false,
}
);
const codingValues = customizationService.getCustomization('codingValues');
const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing');
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
@@ -107,8 +99,7 @@ export default function hydrateStructuredReport(
metaData
);
const onBeforeSRHydration =
customizationService.getModeCustomization('onBeforeSRHydration')?.value;
const onBeforeSRHydration = customizationService.getCustomization('onBeforeSRHydration')?.value;
if (typeof onBeforeSRHydration === 'function') {
storedMeasurementByAnnotationType = onBeforeSRHydration({
@@ -230,8 +221,7 @@ export default function hydrateStructuredReport(
});
if (disableEditing) {
const addedAnnotation = annotationManager.getAnnotation(newAnnotationUID);
locking.setAnnotationLocked(addedAnnotation, true);
locking.setAnnotationLocked(newAnnotationUID, true);
}
if (!imageIds.includes(imageId)) {
@@ -5,7 +5,7 @@ function WorkflowPanel({ servicesManager }: { servicesManager: ServicesManager }
const ProgressDropdownWithService =
servicesManager.services.customizationService.getCustomization(
'progressDropdownWithServiceComponent'
).component;
);
return (
<div
@@ -47,80 +47,6 @@ const OverlayItemComponents = {
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
};
const studyDateItem = {
id: 'StudyDate',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Study date',
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
formatDate(referenceInstance.StudyDate),
};
const seriesDescriptionItem = {
id: 'SeriesDescription',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Series description',
condition: ({ referenceInstance }) => {
return referenceInstance && referenceInstance.SeriesDescription;
},
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
};
const topLeftItems = {
id: 'cornerstoneOverlayTopLeft',
items: [studyDateItem, seriesDescriptionItem],
};
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
const bottomLeftItems = {
id: 'cornerstoneOverlayBottomLeft',
items: [
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: props => {
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
return activeToolName === 'Zoom';
},
},
],
};
const bottomRightItems = {
id: 'cornerstoneOverlayBottomRight',
items: [
{
id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
],
};
/**
* The @ohif/cornerstoneOverlay is a default value for a customization
* for the cornerstone overlays. The intent is to allow it to be extended
* without needing to re-write the individual overlays by using the append
* mechanism. Individual attributes can be modified individually without
* affecting the other items by using the append as well, with position
* based replacement.
* This is used as a default in the getCustomizationModule so that it
* is available early for additional customization extensions.
*/
const CornerstoneOverlay = {
id: '@ohif/cornerstoneOverlay',
topLeftItems,
topRightItems,
bottomLeftItems,
bottomRightItems,
};
/**
* Customizable Viewport Overlay
*/
@@ -143,26 +69,18 @@ function CustomizableViewportOverlay({
const [scale, setScale] = useState(1);
const { imageIndex } = imageSliceData;
// The new customization is 'cornerstoneOverlay', with an append or replace
// 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 topLeftCustomization = customizationService.getCustomization('viewportOverlay.topLeft');
const topRightCustomization = customizationService.getCustomization('viewportOverlay.topRight');
const bottomLeftCustomization = customizationService.getCustomization(
'viewportOverlay.bottomLeft'
);
const bottomRightCustomization = customizationService.getCustomization(
'viewportOverlay.bottomRight'
);
const instanceNumber = useMemo(
() =>
@@ -264,8 +182,8 @@ function CustomizableViewportOverlay({
return null;
}
const { customizationType } = item;
const OverlayItemComponent = OverlayItemComponents[customizationType];
const { inheritsFrom } = item;
const OverlayItemComponent = OverlayItemComponents[inheritsFrom];
if (OverlayItemComponent) {
return <OverlayItemComponent {...overlayItemProps} />;
@@ -293,10 +211,6 @@ function CustomizableViewportOverlay({
const getContent = useCallback(
(customization, keyPrefix) => {
if (!customization?.items) {
return null;
}
const { items } = customization;
const props = {
...displaySetProps,
formatters: { formatDate: formatDICOMDate },
@@ -309,7 +223,7 @@ function CustomizableViewportOverlay({
return (
<>
{items.map((item, index) => (
{customization.map((item, index) => (
<div key={`${keyPrefix}_${index}`}>
{((!item?.condition || item.condition(props)) && _renderOverlayItem(item, props)) ||
null}
@@ -533,4 +447,4 @@ CustomizableViewportOverlay.propTypes = {
export default CustomizableViewportOverlay;
export { CustomizableViewportOverlay, CornerstoneOverlay };
export { CustomizableViewportOverlay };
+3 -3
View File
@@ -194,7 +194,7 @@ function commandsModule({
* on the measurement with a response if not cancelled.
*/
setMeasurementLabel: ({ uid }) => {
const labelConfig = customizationService.get('measurementLabels');
const labelConfig = customizationService.getCustomization('measurementLabels');
const measurement = measurementService.getMeasurement(uid);
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(
(val: Map<any, any>) => {
@@ -311,7 +311,7 @@ function commandsModule({
},
renameMeasurement: ({ uid }) => {
const labelConfig = customizationService.get('measurementLabels');
const labelConfig = customizationService.getCustomization('measurementLabels');
const measurement = measurementService.getMeasurement(uid);
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => {
measurementService.update(
@@ -362,7 +362,7 @@ function commandsModule({
viewportGridService.setActiveViewportId(viewportId);
},
arrowTextCallback: ({ callback, data, uid }) => {
const labelConfig = customizationService.get('measurementLabels');
const labelConfig = customizationService.getCustomization('measurementLabels');
callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig);
},
toggleCine: () => {
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { CinePlayer, useCine } from '@ohif/ui';
import { useCine } from '@ohif/ui';
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
import { useAppConfig } from '@state';
@@ -160,8 +160,7 @@ function RenderCinePlayer({
dynamicInfo: dynamicInfoProp,
customizationService,
}) {
const { component: CinePlayerComponent = CinePlayer } =
customizationService.get('cinePlayer') ?? {};
const CinePlayerComponent = customizationService.getCustomization('cinePlayer');
const [dynamicInfo, setDynamicInfo] = useState(dynamicInfoProp);
@@ -2,21 +2,21 @@
// configured via the customization service.
const defaultWindowLevelPresets = {
CT: [
{ description: 'Soft tissue', window: '400', level: '40' },
{ description: 'Lung', window: '1500', level: '-600' },
{ description: 'Liver', window: '150', level: '90' },
{ description: 'Bone', window: '2500', level: '480' },
{ description: 'Brain', window: '80', level: '40' },
{ id: 'ct-soft-tissue', description: 'Soft tissue', window: '400', level: '40' },
{ id: 'ct-lung', description: 'Lung', window: '1500', level: '-600' },
{ id: 'ct-liver', description: 'Liver', window: '150', level: '90' },
{ id: 'ct-bone', description: 'Bone', window: '2500', level: '480' },
{ id: 'ct-brain', description: 'Brain', window: '80', level: '40' },
],
PT: [
{ description: 'Default', window: '5', level: '2.5' },
{ description: 'SUV', window: '0', level: '3' },
{ description: 'SUV', window: '0', level: '5' },
{ description: 'SUV', window: '0', level: '7' },
{ description: 'SUV', window: '0', level: '8' },
{ description: 'SUV', window: '0', level: '10' },
{ description: 'SUV', window: '0', level: '15' },
{ id: 'pt-default', description: 'Default', window: '5', level: '2.5' },
{ id: 'pt-suv-3', description: 'SUV', window: '0', level: '3' },
{ id: 'pt-suv-5', description: 'SUV', window: '0', level: '5' },
{ id: 'pt-suv-7', description: 'SUV', window: '0', level: '7' },
{ id: 'pt-suv-8', description: 'SUV', window: '0', level: '8' },
{ id: 'pt-suv-10', description: 'SUV', window: '0', level: '10' },
{ id: 'pt-suv-15', description: 'SUV', window: '0', level: '15' },
],
};
@@ -16,11 +16,10 @@ export function getWindowLevelActionMenu({
}>): ReactNode {
const { customizationService } = servicesManager.services;
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
const colorbarProperties = customizationService.get('cornerstone.colorbar');
const { volumeRenderingPresets, volumeRenderingQualityRange } = customizationService.get(
'cornerstone.3dVolumeRendering'
);
const presets = customizationService.getCustomization('cornerstone.windowLevelPresets');
const colorbarProperties = customizationService.getCustomization('cornerstone.colorbar');
const { volumeRenderingPresets, volumeRenderingQualityRange } =
customizationService.getCustomization('cornerstone.3dVolumeRendering');
const displaySetPresets = displaySets
.filter(displaySet => presets[displaySet.Modality])
@@ -0,0 +1,13 @@
import { colormaps } from '../utils/colormaps';
const DefaultColormap = 'Grayscale';
export default {
'cornerstone.colorbar': {
width: '16px',
colorbarTickPosition: 'left',
colormaps,
colorbarContainerPosition: 'right',
colorbarInitialColormap: DefaultColormap,
},
};
@@ -0,0 +1,99 @@
export default {
'layoutSelector.advancedPresetGenerator': ({
servicesManager,
}: {
servicesManager: AppTypes.ServicesManager;
}) => {
const _areSelectorsValid = (
hp: AppTypes.HangingProtocol.Protocol,
displaySets: AppTypes.DisplaySet[],
hangingProtocolService: AppTypes.HangingProtocolService
) => {
if (!hp.displaySetSelectors || Object.values(hp.displaySetSelectors).length === 0) {
return true;
}
return hangingProtocolService.areRequiredSelectorsValid(
Object.values(hp.displaySetSelectors),
displaySets[0]
);
};
const generateAdvancedPresets = ({
servicesManager,
}: {
servicesManager: AppTypes.ServicesManager;
}) => {
const { hangingProtocolService, viewportGridService, displaySetService } =
servicesManager.services;
const hangingProtocols = Array.from(hangingProtocolService.protocols.values());
const viewportId = viewportGridService.getActiveViewportId();
if (!viewportId) {
return [];
}
const displaySetInsaneUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetInsaneUIDs) {
return [];
}
const displaySets = displaySetInsaneUIDs.map(uid =>
displaySetService.getDisplaySetByUID(uid)
);
return hangingProtocols
.map(hp => {
if (!hp.isPreset) {
return null;
}
const areValid = _areSelectorsValid(hp, displaySets, hangingProtocolService);
return {
icon: hp.icon,
title: hp.name,
commandOptions: {
protocolId: hp.id,
},
disabled: !areValid,
};
})
.filter(preset => preset !== null);
};
return generateAdvancedPresets({ servicesManager });
},
'layoutSelector.commonPresets': [
{
icon: 'layout-common-1x1',
commandOptions: {
numRows: 1,
numCols: 1,
},
},
{
icon: 'layout-common-1x2',
commandOptions: {
numRows: 1,
numCols: 2,
},
},
{
icon: 'layout-common-2x2',
commandOptions: {
numRows: 2,
numCols: 2,
},
},
{
icon: 'layout-common-2x3',
commandOptions: {
numRows: 2,
numCols: 3,
},
},
],
};
@@ -0,0 +1,124 @@
export default {
'cornerstone.measurements': {
Angle: {
displayText: [],
report: [],
},
CobbAngle: {
displayText: [],
report: [],
},
ArrowAnnotate: {
displayText: [],
report: [],
},
RectangleROi: {
displayText: [],
report: [],
},
CircleROI: {
displayText: [],
report: [],
},
EllipticalROI: {
displayText: [],
report: [],
},
Bidirectional: {
displayText: [],
report: [],
},
Length: {
displayText: [],
report: [],
},
LivewireContour: {
displayText: [],
report: [],
},
SplineROI: {
displayText: [
{
displayName: 'Areas',
value: 'area',
type: 'value',
},
{
value: 'areaUnits',
for: ['area'],
type: 'unit',
},
],
report: [
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
displayName: 'Unit',
value: 'areaUnits',
type: 'value',
},
],
},
PlanarFreehandROI: {
displayTextOpen: [
{
displayName: 'Length',
value: 'length',
type: 'value',
},
],
displayText: [
{
displayName: 'Mean',
value: 'mean',
type: 'value',
},
{
displayName: 'Max',
value: 'max',
type: 'value',
},
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
value: 'pixelValueUnits',
for: ['mean', 'max'],
type: 'unit',
},
{
value: 'areaUnits',
for: ['area'],
type: 'unit',
},
],
report: [
{
displayName: 'Mean',
value: 'mean',
type: 'value',
},
{
displayName: 'Max',
value: 'max',
type: 'value',
},
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
displayName: 'Unit',
value: 'unit',
type: 'value',
},
],
},
},
};
@@ -0,0 +1,16 @@
import { CinePlayer } from '@ohif/ui';
import DicomUpload from '../components/DicomUpload/DicomUpload';
export default {
cinePlayer: CinePlayer,
autoCineModalities: ['OT', 'US'],
'panelMeasurement.disableEditing': false,
onBeforeSRAddMeasurement: ({ measurement, StudyInstanceUID, SeriesInstanceUID }) => {
return measurement;
},
onBeforeDicomStore: ({ dicomDict, measurementData, naturalizedReport }) => {
return dicomDict;
},
dicomUploadComponent: DicomUpload,
codingValues: {},
};
@@ -0,0 +1,95 @@
import React from 'react';
import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
Icons,
} from '@ohif/ui-next';
export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) {
return {
'panelSegmentation.customDropdownMenuContent': ({
activeSegmentation,
onSegmentationAdd,
onSegmentationRemoveFromViewport,
onSegmentationEdit,
onSegmentationDelete,
allowExport,
storeSegmentation,
onSegmentationDownload,
onSegmentationDownloadRTSS,
t,
}) => (
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => onSegmentationAdd(activeSegmentation.segmentationId)}>
<Icons.Add className="text-foreground" />
<span className="pl-2">{t('Create New Segmentation')}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => onSegmentationRemoveFromViewport(activeSegmentation.segmentationId)}
>
<Icons.Series className="text-foreground" />
<span className="pl-2">{t('Remove from Viewport')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onSegmentationEdit(activeSegmentation.segmentationId)}>
<Icons.Rename className="text-foreground" />
<span className="pl-2">{t('Rename')}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={!allowExport}
className="pl-1"
>
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Export & Download')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={() => storeSegmentation(activeSegmentation.segmentationId)}
>
{t('Export DICOM SEG')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onSegmentationDownload(activeSegmentation.segmentationId)}
>
{t('Download DICOM SEG')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onSegmentationDelete(activeSegmentation.segmentationId)}>
<Icons.Delete className="text-red-600" />
<span className="pl-2 text-red-600">{t('Delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
),
'panelSegmentation.disableEditing': false,
'panelSegmentation.showAddSegment': true,
'panelSegmentation.onSegmentationAdd': () => {
const { viewportGridService } = servicesManager.services;
const viewportId = viewportGridService.getState().activeViewportId;
commandsManager.run('createLabelmapForViewport', { viewportId });
},
'panelSegmentation.tableMode': 'collapsed',
'panelSegmentation.readableText': {
lesionStats: 'Lesion Statistics',
minValue: 'Minimum Value',
maxValue: 'Maximum Value',
meanValue: 'Mean Value',
volume: 'Volume (ml)',
suvPeak: 'SUV Peak',
suvMax: 'Maximum SUV',
suvMaxIJK: 'SUV Max IJK',
lesionGlyoclysisStats: 'Lesion Glycolysis',
},
};
}
@@ -0,0 +1,15 @@
export default {
cornerstoneViewportClickCommands: {
doubleClick: ['toggleOneUp'],
button1: ['closeContextMenu'],
button3: [
{
commandName: 'showCornerstoneContextMenu',
commandOptions: {
requireNearbyToolData: true,
menuId: 'measurementsContextMenu',
},
},
],
},
};
@@ -0,0 +1,44 @@
export default {
'viewportOverlay.topLeft': [
{
id: 'StudyDate',
inheritsFrom: 'ohif.overlayItem',
label: '',
title: 'Study date',
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
formatDate(referenceInstance.StudyDate),
},
{
id: 'SeriesDescription',
inheritsFrom: 'ohif.overlayItem',
label: '',
title: 'Series description',
condition: ({ referenceInstance }) => {
return referenceInstance && referenceInstance.SeriesDescription;
},
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
},
],
'viewportOverlay.topRight': [],
'viewportOverlay.bottomLeft': [
{
id: 'WindowLevel',
inheritsFrom: 'ohif.overlayItem.windowLevel',
},
{
id: 'ZoomLevel',
inheritsFrom: 'ohif.overlayItem.zoomLevel',
condition: props => {
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
return activeToolName === 'Zoom';
},
},
],
'viewportOverlay.bottomRight': [
{
id: 'InstanceNumber',
inheritsFrom: 'ohif.overlayItem.instanceNumber',
},
],
};
@@ -0,0 +1,33 @@
import { Enums } from '@cornerstonejs/tools';
import { toolNames } from '../initCornerstoneTools';
export default {
'cornerstone.overlayViewportTools': {
active: [
{
toolName: toolNames.WindowLevel,
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
},
{
toolName: toolNames.Pan,
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
},
{
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
{
toolName: toolNames.StackScroll,
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
],
enabled: [
{
toolName: toolNames.PlanarFreehandContourSegmentation,
configuration: {
displayOnePointAsCrosshairs: true,
},
},
],
},
};
@@ -0,0 +1,14 @@
import { CONSTANTS } from '@cornerstonejs/core';
const { VIEWPORT_PRESETS } = CONSTANTS;
export default {
'cornerstone.3dVolumeRendering': {
volumeRenderingPresets: VIEWPORT_PRESETS,
volumeRenderingQualityRange: {
min: 1,
max: 4,
step: 1,
},
},
};
@@ -0,0 +1,5 @@
import defaultWindowLevelPresets from '../components/WindowLevelActionMenu/defaultWindowLevelPresets';
export default {
'cornerstone.windowLevelPresets': defaultWindowLevelPresets,
};
@@ -1,213 +0,0 @@
import { Enums } from '@cornerstonejs/tools';
import { toolNames } from './initCornerstoneTools';
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
import { colormaps } from './utils/colormaps';
import { CONSTANTS } from '@cornerstonejs/core';
import { CornerstoneOverlay } from './Viewport/Overlays/CustomizableViewportOverlay';
const DefaultColormap = 'Grayscale';
const { VIEWPORT_PRESETS } = CONSTANTS;
const tools = {
active: [
{
toolName: toolNames.WindowLevel,
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
},
{
toolName: toolNames.Pan,
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
},
{
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
{
toolName: toolNames.StackScroll,
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
],
enabled: [
{
toolName: toolNames.PlanarFreehandContourSegmentation,
configuration: {
displayOnePointAsCrosshairs: true,
},
},
],
};
function getCustomizationModule() {
return [
{
name: 'default',
value: [
CornerstoneOverlay,
{
id: 'cornerstone.overlayViewportTools',
tools,
},
{
id: 'cornerstone.windowLevelPresets',
presets: defaultWindowLevelPresets,
},
{
id: 'cornerstone.colorbar',
width: '16px',
colorbarTickPosition: 'left',
colormaps,
colorbarContainerPosition: 'right',
colorbarInitialColormap: DefaultColormap,
},
{
id: 'cornerstone.3dVolumeRendering',
volumeRenderingPresets: VIEWPORT_PRESETS,
volumeRenderingQualityRange: {
min: 1,
max: 4,
step: 1,
},
},
{
id: 'cornerstone.measurements',
Angle: {
displayText: [],
report: [],
},
CobbAngle: {
displayText: [],
report: [],
},
ArrowAnnotate: {
displayText: [],
report: [],
},
RectangleROi: {
displayText: [],
report: [],
},
CircleROI: {
displayText: [],
report: [],
},
EllipticalROI: {
displayText: [],
report: [],
},
Bidirectional: {
displayText: [],
report: [],
},
Length: {
displayText: [],
report: [],
},
LivewireContour: {
displayText: [],
report: [],
},
SplineROI: {
displayText: [
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
value: 'areaUnits',
for: ['area'],
type: 'unit',
},
/**
{
displayName: 'Modality',
value: 'Modality',
type: 'value',
},
*/
],
report: [
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
displayName: 'Unit',
value: 'areaUnits',
type: 'value',
},
],
},
PlanarFreehandROI: {
displayTextOpen: [
{
displayName: 'Length',
value: 'length',
type: 'value',
},
],
displayText: [
{
displayName: 'Mean',
value: 'mean',
type: 'value',
},
{
displayName: 'Max',
value: 'max',
type: 'value',
},
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
value: 'pixelValueUnits',
for: ['mean', 'max' /** 'stdDev **/],
type: 'unit',
},
{
value: 'areaUnits',
for: ['area'],
type: 'unit',
},
/**
{
displayName: 'Std Dev',
value: 'stdDev',
type: 'value',
},
*/
],
report: [
{
displayName: 'Mean',
value: 'mean',
type: 'value',
},
{
displayName: 'Max',
value: 'max',
type: 'value',
},
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
displayName: 'Unit',
value: 'unit',
type: 'value',
},
],
},
},
],
},
];
}
export default getCustomizationModule;
@@ -0,0 +1,32 @@
import viewportOverlayCustomization from './customizations/viewportOverlayCustomization';
import getSegmentationPanelCustomization from './customizations/segmentationPanelCustomization';
import layoutSelectorCustomization from './customizations/layoutSelectorCustomization';
import viewportToolsCustomization from './customizations/viewportToolsCustomization';
import viewportClickCommandsCustomization from './customizations/viewportClickCommandsCustomization';
import measurementsCustomization from './customizations/measurementsCustomization';
import volumeRenderingCustomization from './customizations/volumeRenderingCustomization';
import colorbarCustomization from './customizations/colorbarCustomization';
import windowLevelPresetsCustomization from './customizations/windowLevelPresetsCustomization';
import miscCustomization from './customizations/miscCustomization';
function getCustomizationModule({ commandsManager, servicesManager }) {
return [
{
name: 'default',
value: {
...viewportOverlayCustomization,
...getSegmentationPanelCustomization({ commandsManager, servicesManager }),
...layoutSelectorCustomization,
...viewportToolsCustomization,
...viewportClickCommandsCustomization,
...measurementsCustomization,
...volumeRenderingCustomization,
...colorbarCustomization,
...windowLevelPresetsCustomization,
...miscCustomization,
},
},
];
}
export default getCustomizationModule;
@@ -12,10 +12,7 @@ function mapSegmentationToDisplay(segmentation, customizationService) {
const { label, segments } = segmentation;
// Get the readable text mapping once
const { readableText: readableTextMap } = customizationService.getCustomization(
'PanelSegmentation.readableText',
{}
);
const readableTextMap = customizationService.getCustomization('panelSegmentation.readableText');
// Helper function to recursively map cachedStats to readable display text
function mapStatsToDisplay(stats, indent = 0) {
@@ -7,10 +7,7 @@ function mapSegmentationToDisplay(segmentation, customizationService) {
const { label, segments } = segmentation;
// Get the readable text mapping once
const { readableText: readableTextMap } = customizationService.getCustomization(
'PanelSegmentation.readableText',
{}
);
const readableTextMap = customizationService.getCustomization('panelSegmentation.readableText');
// Helper function to recursively map cachedStats to readable display text
function mapStatsToDisplay(stats, indent = 0) {
+2 -4
View File
@@ -160,11 +160,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
ViewportActionCornersProvider
);
const { syncGroupService } = servicesManager.services;
const { syncGroupService, customizationService } = servicesManager.services;
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
servicesManager.services.customizationService.setGlobalCustomization('dicomUploadComponent', {
component: props => <DicomUpload {...props} />,
});
return init.call(this, props);
},
getToolbarModule,
+5 -24
View File
@@ -5,27 +5,6 @@ import { findNearbyToolData } from './utils/findNearbyToolData';
const cs3DToolsEvents = Enums.Events;
const DEFAULT_CONTEXT_MENU_CLICKS = {
button1: {
commands: [
{
commandName: 'closeContextMenu',
},
],
},
button3: {
commands: [
{
commandName: 'showCornerstoneContextMenu',
commandOptions: {
requireNearbyToolData: true,
menuId: 'measurementsContextMenu',
},
},
],
},
};
/**
* Generates a name, consisting of:
* * alt when the alt key is down
@@ -60,8 +39,10 @@ function initContextMenu({
* defaults on button1 and button2
*/
const cornerstoneViewportHandleEvent = (name, evt) => {
const customizations =
customizationService.get('cornerstoneViewportClickCommands') || DEFAULT_CONTEXT_MENU_CLICKS;
const customizations = customizationService.getCustomization(
'cornerstoneViewportClickCommands'
);
const toRun = customizations[name];
if (!toRun) {
@@ -71,7 +52,7 @@ function initContextMenu({
// only find nearbyToolData if required, for the click (which closes the context menu
// we don't need to find nearbyToolData)
let nearbyToolData = null;
if (toRun.commands.some(command => command.commandOptions?.requireNearbyToolData)) {
if (toRun.some(command => command.commandOptions?.requireNearbyToolData)) {
nearbyToolData = findNearbyToolData(commandsManager, evt);
}
+4 -10
View File
@@ -1,17 +1,10 @@
import { eventTarget, EVENTS } from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/tools';
import { CommandsManager, CustomizationService, Types } from '@ohif/core';
import { CommandsManager, CustomizationService } from '@ohif/core';
import { findNearbyToolData } from './utils/findNearbyToolData';
const cs3DToolsEvents = Enums.Events;
const DEFAULT_DOUBLE_CLICK = {
doubleClick: {
commandName: 'toggleOneUp',
commandOptions: {},
},
};
/**
* Generates a double click event name, consisting of:
* * alt when the alt key is down
@@ -50,8 +43,9 @@ function initDoubleClick({ customizationService, commandsManager }: initDoubleCl
const eventName = getDoubleClickEventName(evt);
// Allows for the customization of the double click on a viewport.
const customizations =
customizationService.get('cornerstoneViewportClickCommands') || DEFAULT_DOUBLE_CLICK;
const customizations = customizationService.getCustomization(
'cornerstoneViewportClickCommands'
);
const toRun = customizations[eventName];
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef } from 'react';
import { utils } from '@ohif/core';
import { MeasurementTable } from '@ohif/ui-next';
import debounce from 'lodash.debounce';
@@ -1,16 +1,5 @@
import React from 'react';
import {
DropdownMenuLabel,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
Icons,
SegmentationTable,
DropdownMenuSubTrigger,
} from '@ohif/ui-next';
import { SegmentationTable } from '@ohif/ui-next';
import { useActiveViewportSegmentationRepresentations } from '../hooks/useActiveViewportSegmentationRepresentations';
import { metaData } from '@cornerstonejs/core';
@@ -19,7 +8,7 @@ export default function PanelSegmentation({
commandsManager,
children,
}: withAppTypes) {
const { customizationService, viewportGridService, displaySetService } = servicesManager.services;
const { customizationService, displaySetService } = servicesManager.services;
const { segmentationsWithRepresentations, disabled } =
useActiveViewportSegmentationRepresentations({
@@ -27,11 +16,6 @@ export default function PanelSegmentation({
});
const handlers = {
onSegmentationAdd: async () => {
const viewportId = viewportGridService.getState().activeViewportId;
commandsManager.run('createLabelmapForViewport', { viewportId });
},
onSegmentationClick: (segmentationId: string) => {
commandsManager.run('setActiveSegmentation', { segmentationId });
},
@@ -125,107 +109,19 @@ export default function PanelSegmentation({
},
};
const { mode: SegmentationTableMode } = customizationService.getCustomization(
'PanelSegmentation.tableMode',
{
id: 'default.segmentationTable.mode',
mode: 'collapsed',
}
const segmentationTableMode = customizationService.getCustomization(
'panelSegmentation.tableMode'
);
// custom onSegmentationAdd if provided
const { onSegmentationAdd } = customizationService.getCustomization(
'PanelSegmentation.onSegmentationAdd',
{
id: 'segmentation.onSegmentationAdd',
onSegmentationAdd: handlers.onSegmentationAdd,
}
const onSegmentationAdd = customizationService.getCustomization(
'panelSegmentation.onSegmentationAdd'
);
const { disableEditing } = customizationService.getCustomization(
'PanelSegmentation.disableEditing',
{
id: 'default.disableEditing',
disableEditing: false,
}
);
const { showAddSegment } = customizationService.getCustomization(
'PanelSegmentation.showAddSegment',
{
id: 'default.showAddSegment',
showAddSegment: true,
}
);
const CustomDropdownMenuContent = customizationService.getCustomComponent(
'PanelSegmentation.CustomDropdownMenuContent',
({
activeSegmentation,
onSegmentationAdd,
onSegmentationRemoveFromViewport,
onSegmentationEdit,
onSegmentationDelete,
allowExport,
storeSegmentation,
onSegmentationDownload,
onSegmentationDownloadRTSS,
t,
}) => {
return (
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => onSegmentationAdd(activeSegmentation.segmentationId)}>
<Icons.Add className="text-foreground" />
<span className="pl-2">{t('Create New Segmentation')}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => onSegmentationRemoveFromViewport(activeSegmentation.segmentationId)}
>
<Icons.Series className="text-foreground" />
<span className="pl-2">{t('Remove from Viewport')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onSegmentationEdit(activeSegmentation.segmentationId)}>
<Icons.Rename className="text-foreground" />
<span className="pl-2">{t('Rename')}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={!allowExport}
className="pl-1"
>
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Export & Download')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={() => storeSegmentation(activeSegmentation.segmentationId)}
>
{t('Export DICOM SEG')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onSegmentationDownload(activeSegmentation.segmentationId)}
>
{t('Download DICOM SEG')}
</DropdownMenuItem>
{/* <DropdownMenuItem
onClick={() => onSegmentationDownloadRTSS(activeSegmentation.segmentationId)}
>
{t('Download DICOM RTSTRUCT')}
</DropdownMenuItem> */}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onSegmentationDelete(activeSegmentation.segmentationId)}>
<Icons.Delete className="text-red-600" />
<span className="pl-2 text-red-600">{t('Delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
);
}
const disableEditing = customizationService.getCustomization('panelSegmentation.disableEditing');
const showAddSegment = customizationService.getCustomization('panelSegmentation.showAddSegment');
const CustomDropdownMenuContent = customizationService.getCustomization(
'panelSegmentation.customDropdownMenuContent'
);
const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
@@ -272,7 +168,7 @@ export default function PanelSegmentation({
<SegmentationTable
disabled={disabled}
data={segmentationsWithRepresentations}
mode={SegmentationTableMode}
mode={segmentationTableMode}
title="Segmentations"
exportOptions={exportOptions}
disableEditing={disableEditing}
@@ -308,7 +204,7 @@ export default function PanelSegmentation({
<SegmentationTable.Config />
<SegmentationTable.AddSegmentationRow />
{SegmentationTableMode === 'collapsed' ? (
{segmentationTableMode === 'collapsed' ? (
<SegmentationTable.Collapsed>
<SegmentationTable.SelectorHeader>
<CustomDropdownMenuContent />
@@ -1,7 +1,5 @@
import { useEffect, useState, memo } from 'react';
const MODALITIES_REQUIRING_CINE_AUTO_MOUNT = ['OT', 'US'];
const ActiveViewportBehavior = memo(
({ servicesManager, viewportId }: withAppTypes<{ viewportId: string }>) => {
const { displaySetService, cineService, viewportGridService, customizationService } =
@@ -41,13 +39,7 @@ const ActiveViewportBehavior = memo(
const modalities = displaySets.map(displaySet => displaySet?.Modality);
const isDynamicVolume = displaySets.some(displaySet => displaySet?.isDynamicVolume);
const { modalities: sourceModalities } = customizationService.getModeCustomization(
'autoCineModalities',
{
id: 'autoCineModalities',
modalities: MODALITIES_REQUIRING_CINE_AUTO_MOUNT,
}
);
const sourceModalities = customizationService.getCustomization('autoCineModalities');
const requiresCine = modalities.some(modality => sourceModalities.includes(modality));
@@ -142,7 +142,7 @@ function getMappedAnnotations(annotation, displaySetService) {
* @returns {object} Report's content.
*/
function getColumnValueReport(annotation, customizationService) {
const { PlanarFreehandROI } = customizationService.get('cornerstone.measurements');
const { PlanarFreehandROI } = customizationService.getCustomization('cornerstone.measurements');
const { report } = PlanarFreehandROI;
const columns = [];
const values = [];
@@ -145,7 +145,7 @@ function getMappedAnnotations(annotation, displaySetService) {
* @returns {object} Report's content.
*/
function getColumnValueReport(annotation, customizationService) {
const { SplineROI } = customizationService.get('cornerstone.measurements');
const { SplineROI } = customizationService.getCustomization('cornerstone.measurements');
const { report } = SplineROI;
const columns = [];
const values = [];
@@ -29,8 +29,9 @@ function DataSourceConfigurationComponent({
return;
}
const { factory: configurationAPIFactory } =
customizationService.get(activeDataSourceDef.configuration.configurationAPI) ?? {};
const { factory: configurationAPIFactory } = customizationService.getCustomization(
activeDataSourceDef.configuration.configurationAPI
) ?? { factory: () => null };
if (!configurationAPIFactory) {
return;
@@ -56,9 +56,9 @@ export default function MoreDropdownMenu(bindProps) {
} = bindProps;
const { customizationService } = servicesManager.services;
const items = customizationService.getCustomization(menuItemsKey)?.value;
const items = customizationService.getCustomization(menuItemsKey);
if (!items) {
if (!items?.length) {
return null;
}
@@ -1,34 +0,0 @@
const defaultContextMenu = {
id: 'measurementsContextMenu',
customizationType: 'ohif.contextMenu',
menus: [
// Get the items from the UI Customization for the menu name (and have a custom name)
{
id: 'forExistingMeasurement',
selector: ({ nearbyToolData }) => !!nearbyToolData,
items: [
{
label: 'Delete measurement',
commands: [
{
commandName: 'deleteMeasurement',
// we only have support for cornerstoneTools context menu since
// they are svg based
context: 'CORNERSTONE',
},
],
},
{
label: 'Add Label',
commands: [
{
commandName: 'setMeasurementLabel',
},
],
},
],
},
],
};
export default defaultContextMenu;
@@ -1,11 +1,5 @@
import ContextMenuController from './ContextMenuController';
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
import defaultContextMenu from './defaultContextMenu';
import * as CustomizableContextMenuTypes from './types';
export {
ContextMenuController,
CustomizableContextMenuTypes,
ContextMenuItemsBuilder,
defaultContextMenu,
};
export { ContextMenuController, CustomizableContextMenuTypes, ContextMenuItemsBuilder };
@@ -45,10 +45,11 @@ export type UIMenuItem = {
*/
export interface MenuItem {
id?: string;
/** The customization type is used to apply preset values to this item
/**
* The customization type is used to apply preset values to this item
* when registered with the customization service.
*/
customizationType?: string;
inheritsFrom?: string;
// The label is the value to show in the menu for this item
label?: string;
@@ -91,7 +92,7 @@ export interface Menu {
/** The customization type is used to apply preset values to this item
* when registered with the customization service.
*/
customizationType?: string;
inheritsFrom?: string;
// Choose whether this menu applies.
selector?: Types.Predicate;
@@ -582,8 +582,11 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
return dicomWebConfigCopy;
},
getStudyInstanceUIDs({ params, query }) {
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
const queryStudyInstanceUIDs = utils.splitComma(query.getAll('StudyInstanceUIDs'));
const paramsStudyInstanceUIDs = params.StudyInstanceUIDs || params.studyInstanceUIDs;
const queryStudyInstanceUIDs = utils.splitComma(
query.getAll('StudyInstanceUIDs').concat(query.getAll('studyInstanceUIDs'))
);
const StudyInstanceUIDs =
(queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) || paramsStudyInstanceUIDs;
@@ -6,7 +6,7 @@ import { utils } from '@ohif/core';
import { useNavigate } from 'react-router-dom';
import { Separator } from '@ohif/ui-next';
import { PanelStudyBrowserHeader } from './PanelStudyBrowserHeader';
import { defaultActionIcons, defaultViewPresets } from './constants';
import { defaultActionIcons } from './constants';
import MoreDropdownMenu from '../../Components/MoreDropdownMenu';
const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils;
@@ -43,7 +43,7 @@ function PanelStudyBrowser({
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
const [viewPresets, setViewPresets] = useState(
customizationService.getCustomization('studyBrowser.viewPresets')?.value || defaultViewPresets
customizationService.getCustomization('studyBrowser.viewPresets')
);
const [actionIcons, setActionIcons] = useState(defaultActionIcons);
@@ -2,87 +2,6 @@ import React, { useEffect, useState, useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton, LayoutPreset } from '@ohif/ui';
const defaultCommonPresets = [
{
icon: 'layout-common-1x1',
commandOptions: {
numRows: 1,
numCols: 1,
},
},
{
icon: 'layout-common-1x2',
commandOptions: {
numRows: 1,
numCols: 2,
},
},
{
icon: 'layout-common-2x2',
commandOptions: {
numRows: 2,
numCols: 2,
},
},
{
icon: 'layout-common-2x3',
commandOptions: {
numRows: 2,
numCols: 3,
},
},
];
const _areSelectorsValid = (hp, displaySets, hangingProtocolService) => {
if (!hp.displaySetSelectors || Object.values(hp.displaySetSelectors).length === 0) {
return true;
}
return hangingProtocolService.areRequiredSelectorsValid(
Object.values(hp.displaySetSelectors),
displaySets[0]
);
};
const generateAdvancedPresets = ({ servicesManager }: withAppTypes) => {
const { hangingProtocolService, viewportGridService, displaySetService } =
servicesManager.services;
const hangingProtocols = Array.from(hangingProtocolService.protocols.values());
const viewportId = viewportGridService.getActiveViewportId();
if (!viewportId) {
return [];
}
const displaySetInsaneUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetInsaneUIDs) {
return [];
}
const displaySets = displaySetInsaneUIDs.map(uid => displaySetService.getDisplaySetByUID(uid));
return hangingProtocols
.map(hp => {
if (!hp.isPreset) {
return null;
}
const areValid = _areSelectorsValid(hp, displaySets, hangingProtocolService);
return {
icon: hp.icon,
title: hp.name,
commandOptions: {
protocolId: hp.id,
},
disabled: !areValid,
};
})
.filter(preset => preset !== null);
};
function ToolbarLayoutSelectorWithServices({
commandsManager,
servicesManager,
@@ -138,9 +57,13 @@ function LayoutSelector({
const dropdownRef = useRef(null);
const { customizationService } = servicesManager.services;
const commonPresets = customizationService.get('commonPresets') || defaultCommonPresets;
const advancedPresets =
customizationService.get('advancedPresets') || generateAdvancedPresets({ servicesManager });
const commonPresets = customizationService.getCustomization('layoutSelector.commonPresets');
const advancedPresetsGenerator = customizationService.getCustomization(
'layoutSelector.advancedPresetGenerator'
);
const advancedPresets = advancedPresetsGenerator({ servicesManager });
const closeOnOutsideClick = event => {
if (isOpen && dropdownRef.current) {
@@ -216,7 +139,7 @@ function LayoutSelector({
</div>
</div>
<div className="bg-primary-dark flex flex-col gap-2.5 border-l-2 border-solid border-black p-2">
<div className="bg-primary-dark flex flex-col gap-2.5 border-l-2 border-solid border-black p-2">
<div className="text-aqua-pale text-xs">Custom</div>
<DropdownContent
rows={rows}
+2 -5
View File
@@ -1,6 +1,6 @@
import { Types, DicomMetadataStore } from '@ohif/core';
import { ContextMenuController, defaultContextMenu } from './CustomizableContextMenu';
import { ContextMenuController } from './CustomizableContextMenu';
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
import reuseCachedLayouts from './utils/reuseCachedLayouts';
import findViewportsByPosition, {
@@ -113,10 +113,7 @@ const commandsModule = ({
const optionsToUse = { ...options };
if (menuCustomizationId) {
Object.assign(
optionsToUse,
customizationService.get(menuCustomizationId, defaultContextMenu)
);
Object.assign(optionsToUse, customizationService.getCustomization(menuCustomizationId));
}
// TODO - make the selectorProps richer by including the study metadata and display set.
@@ -0,0 +1,25 @@
import { CustomizationService } from '@ohif/core';
export default {
'ohif.contextMenu': {
$transform: function (customizationService: CustomizationService) {
/**
* Applies the inheritsFrom to all the menu items.
* This function clones the object and child objects to prevent
* changes to the original customization object.
*/
// Don't modify the children, as those are copied by reference
const clonedObject = { ...this };
clonedObject.menus = this.menus.map(menu => ({ ...menu }));
for (const menu of clonedObject.menus) {
const { items: originalItems } = menu;
menu.items = [];
for (const item of originalItems) {
menu.items.push(customizationService.transform(item));
}
}
return clonedObject;
},
},
};
@@ -0,0 +1,6 @@
export default {
'routes.customRoutes': {
routes: [],
notFoundRoute: null,
},
};
@@ -0,0 +1,19 @@
import DataSourceConfigurationComponent from '../Components/DataSourceConfigurationComponent';
import { GoogleCloudDataSourceConfigurationAPI } from '../DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
export default function getDataSourceConfigurationCustomization({
servicesManager,
extensionManager,
}) {
return {
// the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI
'ohif.dataSourceConfigurationComponent': DataSourceConfigurationComponent.bind(null, {
servicesManager,
extensionManager,
}),
// The factory for creating an instance of a BaseDataSourceConfigurationAPI for Google Cloud Healthcare
'ohif.dataSourceConfigurationAPI.google': (dataSourceName: string) =>
new GoogleCloudDataSourceConfigurationAPI(dataSourceName, servicesManager, extensionManager),
};
}
@@ -0,0 +1,14 @@
import DataSourceSelector from '../Panels/DataSourceSelector';
export default {
'routes.customRoutes': {
routes: {
$push: [
{
path: '/datasources',
children: DataSourceSelector,
},
],
},
},
};
@@ -0,0 +1,22 @@
export default {
measurementsContextMenu: {
inheritsFrom: 'ohif.contextMenu',
menus: [
// Get the items from the UI Customization for the menu name (and have a custom name)
{
id: 'forExistingMeasurement',
selector: ({ nearbyToolData }) => !!nearbyToolData,
items: [
{
label: 'Delete measurement',
commands: 'deleteMeasurement',
},
{
label: 'Add Label',
commands: 'setMeasurementLabel',
},
],
},
],
},
};
@@ -0,0 +1,14 @@
import React from 'react';
export default {
'routes.customRoutes': {
routes: {
$push: [
{
path: '/custom',
children: () => <h1 style={{ color: 'white' }}>Hello Custom Route</h1>,
},
],
},
},
};
@@ -0,0 +1,67 @@
import React from 'react';
import {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuPortal,
DropdownMenuSubContent,
DropdownMenuItem,
Icons,
} from '@ohif/ui-next';
export default {
'ohif.menuContent': function (props) {
const { item: topLevelItem, commandsManager, servicesManager, ...rest } = props;
const content = function (subProps) {
const { item: subItem } = subProps;
// Regular menu item
const isDisabled = subItem.selector && !subItem.selector({ servicesManager });
return (
<DropdownMenuItem
disabled={isDisabled}
onSelect={() => {
commandsManager.runAsync(subItem.commands, {
...subItem.commandOptions,
...rest,
});
}}
className="gap-[6px]"
>
{subItem.iconName && (
<Icons.ByName
name={subItem.iconName}
className="-ml-1"
/>
)}
{subItem.label}
</DropdownMenuItem>
);
};
// If item has sub-items, render a submenu
if (topLevelItem.items) {
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="gap-[6px]">
{topLevelItem.iconName && (
<Icons.ByName
name={topLevelItem.iconName}
className="-ml-1"
/>
)}
{topLevelItem.label}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{topLevelItem.items.map(subItem => content({ ...props, item: subItem }))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
);
}
return content({ ...props, item: topLevelItem });
},
};
@@ -0,0 +1,63 @@
export default {
'studyBrowser.studyMenuItems': {
$push: [
{
id: 'applyHangingProtocol',
label: 'Apply Hanging Protocol',
iconName: 'ViewportViews',
items: [
{
id: 'applyDefaultProtocol',
label: 'Default',
commands: [
'loadStudy',
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: 'default',
},
},
],
},
{
id: 'applyMPRProtocol',
label: '2x2 Grid',
commands: [
'loadStudy',
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid',
},
},
],
},
],
},
{
id: 'showInOtherMonitor',
label: 'Launch On Second Monitor',
iconName: 'DicomTagBrowser',
selector: ({ servicesManager }) => {
const { multiMonitorService } = servicesManager.services;
return multiMonitorService.isMultimonitor;
},
commands: {
commandName: 'multimonitor',
commandOptions: {
hashParams: '&hangingProtocolId=@ohif/mnGrid8',
commands: [
'loadStudy',
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid8',
},
},
],
},
},
},
],
},
};
@@ -0,0 +1,31 @@
import React from 'react';
export default {
'ohif.overlayItem': function (props) {
if (this.condition && !this.condition(props)) {
return null;
}
const { instance } = props;
const value =
instance && this.attribute
? instance[this.attribute]
: this.contentF && typeof this.contentF === 'function'
? this.contentF(props)
: null;
if (!value) {
return null;
}
return (
<span
className="overlay-item flex flex-row"
style={{ color: this.color || undefined }}
title={this.title || ''}
>
{this.label && <span className="mr-1 shrink-0">{this.label}</span>}
<span className="font-light">{value}</span>
</span>
);
},
};
@@ -0,0 +1,5 @@
import { ProgressDropdownWithService } from '../Components/ProgressDropdownWithService';
export default {
progressDropdownWithServiceComponent: ProgressDropdownWithService,
};
@@ -0,0 +1,7 @@
import { utils } from '@ohif/core';
const { sortingCriteria } = utils;
export default {
sortingCriteria: sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
};
@@ -0,0 +1,43 @@
import { utils } from '@ohif/core';
const { formatDate } = utils;
export default {
'studyBrowser.studyMenuItems': [],
'studyBrowser.thumbnailMenuItems': [
{
id: 'tagBrowser',
label: 'Tag Browser',
iconName: 'DicomTagBrowser',
commands: 'openDICOMTagViewer',
},
],
'studyBrowser.sortFunctions': [
{
label: 'Series Number',
sortFunction: (a, b) => {
return a?.SeriesNumber - b?.SeriesNumber;
},
},
{
label: 'Series Date',
sortFunction: (a, b) => {
const dateA = new Date(formatDate(a?.SeriesDate));
const dateB = new Date(formatDate(b?.SeriesDate));
return dateB.getTime() - dateA.getTime();
},
},
],
'studyBrowser.viewPresets': [
{
id: 'list',
iconName: 'ListView',
selected: false,
},
{
id: 'thumbnails',
iconName: 'ThumbnailView',
selected: true,
},
],
'studyBrowser.studyMode': 'all',
};
+26 -317
View File
@@ -1,20 +1,15 @@
import { CustomizationService } from '@ohif/core';
import React from 'react';
import DataSourceSelector from './Panels/DataSourceSelector';
import { ProgressDropdownWithService } from './Components/ProgressDropdownWithService';
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
import { utils } from '@ohif/core';
import {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuPortal,
DropdownMenuSubContent,
DropdownMenuItem,
Icons,
} from '@ohif/ui-next';
const formatDate = utils.formatDate;
import defaultContextMenuCustomization from './customizations/defaultContextMenuCustomization';
import helloPageCustomization from './customizations/helloPageCustomization';
import datasourcesCustomization from './customizations/datasourcesCustomization';
import multimonitorCustomization from './customizations/multimonitorCustomization';
import customRoutesCustomization from './customizations/customRoutesCustomization';
import studyBrowserCustomization from './customizations/studyBrowserCustomization';
import overlayItemCustomization from './customizations/overlayItemCustomization';
import contextMenuCustomization from './customizations/contextMenuCustomization';
import menuContentCustomization from './customizations/menuContentCustomization';
import getDataSourceConfigurationCustomization from './customizations/dataSourceConfigurationCustomization';
import progressDropdownCustomization from './customizations/progressDropdownCustomization';
import sortingCriteriaCustomization from './customizations/sortingCriteriaCustomization';
/**
*
@@ -29,315 +24,29 @@ export default function getCustomizationModule({ servicesManager, extensionManag
return [
{
name: 'helloPage',
merge: 'Append',
value: {
id: 'customRoutes',
routes: [
{
path: '/custom',
children: () => <h1 style={{ color: 'white' }}>Hello Custom Route</h1>,
},
],
},
value: helloPageCustomization,
},
// Example customization to list a set of datasources
{
name: 'datasources',
merge: 'Append',
value: {
id: 'customRoutes',
routes: [
{
path: '/datasources',
children: DataSourceSelector,
},
],
},
value: datasourcesCustomization,
},
{
name: 'multimonitor',
merge: 'Append',
value: {
id: 'studyBrowser.studyMenuItems',
customizationType: 'ohif.menuContent',
value: [
{
id: 'applyHangingProtocol',
label: 'Apply Hanging Protocol',
iconName: 'ViewportViews',
items: [
{
id: 'applyDefaultProtocol',
label: 'Default',
commands: [
'loadStudy',
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: 'default',
},
},
],
},
{
id: 'applyMPRProtocol',
label: '2x2 Grid',
commands: [
'loadStudy',
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid',
},
},
],
},
],
},
{
id: 'showInOtherMonitor',
label: 'Launch On Second Monitor',
iconName: 'DicomTagBrowser',
// we should use evaluator for this, as these are basically toolbar buttons
selector: ({ servicesManager }) => {
const { multiMonitorService } = servicesManager.services;
return multiMonitorService.isMultimonitor;
},
commands: {
commandName: 'multimonitor',
commandOptions: {
hashParams: '&hangingProtocolId=@ohif/mnGrid8',
commands: [
'loadStudy',
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid8',
},
},
],
},
},
},
],
},
value: multimonitorCustomization,
},
{
name: 'default',
value: [
/**
* Customization Component Type definition for overlay items.
* Overlay items are texts (or other components) that will be displayed
* on a Viewport Overlay, which contains the information panels on the
* four corners of a viewport.
*
* @definition of a overlay item using this type
* The value to be displayed is defined by
* - setting DICOM image instance's property to this field,
* - or defining contentF()
*
* {
* id: string - unique id for the overlay item
* customizationType: string - indicates customization type definition to this
* label: string - Label, to be displayed for the item
* title: string - Tooltip, for the item
* color: string - Color of the text
* condition: ({ instance }) => boolean - decides whether to display the overlay item or not
* attribute: string - property name of the DICOM image instance
* contentF: ({ instance, formatters }) => string | component,
* }
*
* @example
* {
* id: 'PatientNameOverlay',
* customizationType: 'ohif.overlayItem',
* label: 'PN:',
* title: 'Patient Name',
* color: 'yellow',
* condition: ({ instance }) => instance && instance.PatientName && instance.PatientName.Alphabetic,
* attribute: 'PatientName',
* contentF: ({ instance, formatters: { formatPN } }) => `${formatPN(instance.PatientName.Alphabetic)} ${(instance.PatientSex ? '(' + instance.PatientSex + ')' : '')}`,
* },
*
* @see CustomizableViewportOverlay
*/
{
id: 'ohif.overlayItem',
content: function (props) {
if (this.condition && !this.condition(props)) {
return null;
}
const { instance } = props;
const value =
instance && this.attribute
? instance[this.attribute]
: this.contentF && typeof this.contentF === 'function'
? this.contentF(props)
: null;
if (!value) {
return null;
}
return (
<span
className="overlay-item flex flex-row"
style={{ color: this.color || undefined }}
title={this.title || ''}
>
{this.label && <span className="mr-1 shrink-0">{this.label}</span>}
<span className="font-light">{value}</span>
</span>
);
},
},
{
id: 'ohif.contextMenu',
/** Applies the customizationType to all the menu items.
* This function clones the object and child objects to prevent
* changes to the original customization object.
*/
transform: function (customizationService: CustomizationService) {
// Don't modify the children, as those are copied by reference
const clonedObject = { ...this };
clonedObject.menus = this.menus.map(menu => ({ ...menu }));
for (const menu of clonedObject.menus) {
const { items: originalItems } = menu;
menu.items = [];
for (const item of originalItems) {
menu.items.push(customizationService.transform(item));
}
}
return clonedObject;
},
},
{
// the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI
id: 'ohif.dataSourceConfigurationComponent',
component: DataSourceConfigurationComponent.bind(null, {
servicesManager,
extensionManager,
}),
},
{
// The factory for creating an instance of a BaseDataSourceConfigurationAPI for Google Cloud Healthcare
id: 'ohif.dataSourceConfigurationAPI.google',
factory: (dataSourceName: string) =>
new GoogleCloudDataSourceConfigurationAPI(
dataSourceName,
servicesManager,
extensionManager
),
},
{
id: 'progressDropdownWithServiceComponent',
component: ProgressDropdownWithService,
},
{
id: 'studyBrowser.sortFunctions',
values: [
{
label: 'Series Number',
sortFunction: (a, b) => {
return a?.SeriesNumber - b?.SeriesNumber;
},
},
{
label: 'Series Date',
sortFunction: (a, b) => {
const dateA = new Date(formatDate(a?.SeriesDate));
const dateB = new Date(formatDate(b?.SeriesDate));
return dateB.getTime() - dateA.getTime();
},
},
],
},
{
id: 'studyBrowser.viewPresets',
// change your default selected preset here
value: [
{
id: 'list',
iconName: 'ListView',
selected: false,
},
{
id: 'thumbnails',
iconName: 'ThumbnailView',
selected: true,
},
],
},
{
id: 'studyBrowser.thumbnailMenuItems',
value: [
{
id: 'tagBrowser',
label: 'Tag Browser',
iconName: 'DicomTagBrowser',
commands: 'openDICOMTagViewer',
},
],
},
{
id: 'ohif.menuContent',
content: function (props) {
const { item, commandsManager, servicesManager, ...rest } = props;
// If item has sub-items, render a submenu
if (item.items) {
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="gap-[6px]">
{item.iconName && (
<Icons.ByName
name={item.iconName}
className="-ml-1"
/>
)}
{item.label}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{item.items.map(subItem => this.content({ ...props, item: subItem }))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
);
}
// Regular menu item
const isDisabled = item.selector && !item.selector({ servicesManager });
return (
<DropdownMenuItem
disabled={isDisabled}
onSelect={() => {
commandsManager.runAsync(item.commands, {
...item.commandOptions,
...rest,
});
}}
className="gap-[6px]"
>
{item.iconName && (
<Icons.ByName
name={item.iconName}
className="-ml-1"
/>
)}
{item.label}
</DropdownMenuItem>
);
},
},
],
value: {
...customRoutesCustomization,
...studyBrowserCustomization,
...overlayItemCustomization,
...contextMenuCustomization,
...menuContentCustomization,
...getDataSourceConfigurationCustomization({ servicesManager, extensionManager }),
...progressDropdownCustomization,
...sortingCriteriaCustomization,
...defaultContextMenuCustomization,
},
},
];
}
@@ -4,7 +4,7 @@ function promptLabelAnnotation({ servicesManager }, ctx, evt) {
const { measurementService, customizationService } = servicesManager.services;
const { viewportId, StudyInstanceUID, SeriesInstanceUID, measurementId } = evt;
return new Promise(async function (resolve) {
const labelConfig = customizationService.get('measurementLabels');
const labelConfig = customizationService.getCustomization('measurementLabels');
const measurement = measurementService.getMeasurement(measurementId);
const value = await showLabelAnnotationPopup(
measurement,
@@ -159,6 +159,7 @@ function TrackedMeasurementsContextProvider(
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
servicesManager,
extensionManager,
commandsManager,
appConfig,
}),
hydrateStructuredReport: hydrateStructuredReport.bind(null, {
@@ -173,11 +174,11 @@ function TrackedMeasurementsContextProvider(
});
machineOptions.guards = Object.assign({}, machineOptions.guards, {
isLabelOnMeasure: (ctx, evt, condMeta) => {
const labelConfig = customizationService.get('measurementLabels');
const labelConfig = customizationService.getCustomization('measurementLabels');
return labelConfig?.labelOnMeasure;
},
isLabelOnMeasureAndShouldKillMachine: (ctx, evt, condMeta) => {
const labelConfig = customizationService.get('measurementLabels');
const labelConfig = customizationService.getCustomization('measurementLabels');
return evt.data && evt.data.userResponse === RESPONSE.NO_NEVER && labelConfig?.labelOnMeasure;
},
});
@@ -1,7 +1,7 @@
import { hydrateStructuredReport as baseHydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr';
function hydrateStructuredReport(
{ servicesManager, extensionManager, appConfig }: withAppTypes,
{ servicesManager, extensionManager, commandsManager, appConfig }: withAppTypes,
ctx,
evt
) {
@@ -11,7 +11,7 @@ function hydrateStructuredReport(
return new Promise((resolve, reject) => {
const hydrationResult = baseHydrateStructuredReport(
{ servicesManager, extensionManager, appConfig },
{ servicesManager, extensionManager, commandsManager, appConfig },
displaySetInstanceUID
);
@@ -11,7 +11,11 @@ const RESPONSE = {
HYDRATE_REPORT: 5,
};
function promptHydrateStructuredReport({ servicesManager, extensionManager, appConfig }, ctx, evt) {
function promptHydrateStructuredReport(
{ servicesManager, extensionManager, commandsManager, appConfig },
ctx,
evt
) {
const { uiViewportDialogService, displaySetService } = servicesManager.services;
const { viewportId, displaySetInstanceUID } = evt;
const srDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
@@ -26,7 +30,7 @@ function promptHydrateStructuredReport({ servicesManager, extensionManager, appC
if (promptResult === RESPONSE.HYDRATE_REPORT) {
console.warn('!! HYDRATING STRUCTURED REPORT');
const hydrationResult = hydrateStructuredReport(
{ servicesManager, extensionManager, appConfig },
{ servicesManager, extensionManager, commandsManager, appConfig },
displaySetInstanceUID
);
@@ -5,7 +5,7 @@ import { Button, Icons } from '@ohif/ui-next';
import { PanelMeasurement, StudySummaryFromMetadata } from '@ohif/extension-cornerstone';
import { useTrackedMeasurements } from '../getContextModule';
const { filterAnd, filterPlanarMeasurement, filterAny, filterMeasurementsBySeriesUID } =
const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } =
utils.MeasurementFilters;
function PanelMeasurementTableTracking({
@@ -21,13 +21,7 @@ function PanelMeasurementTableTracking({
? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries))
: filterPlanarMeasurement;
const { disableEditing } = customizationService.getCustomization(
'PanelMeasurement.disableEditing',
{
id: 'default.disableEditing',
disableEditing: false,
}
);
const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing');
return (
<>
@@ -4,14 +4,15 @@ import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { utils } from '@ohif/core';
import { useImageViewer, Dialog, ButtonEnums } from '@ohif/ui';
import { useViewportGrid, DropdownMenu, DropdownMenuTrigger, Icons, Button } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import { StudyBrowser } from '@ohif/ui-next';
import { useTrackedMeasurements } from '../../getContextModule';
import { Separator } from '@ohif/ui-next';
import { PanelStudyBrowserHeader, MoreDropdownMenu } from '@ohif/extension-default';
import { defaultActionIcons, defaultViewPresets } from './constants';
import { MoreDropdownMenu, PanelStudyBrowserHeader } from '@ohif/extension-default';
import { defaultActionIcons } from './constants';
const { formatDate, createStudyBrowserTabs } = utils;
const thumbnailNoImageModalities = [
'SR',
'SEG',
@@ -46,10 +47,7 @@ export default function PanelStudyBrowserTracking({
customizationService,
} = servicesManager.services;
const navigate = useNavigate();
const { mode: studyMode } = customizationService.getCustomization('PanelStudyBrowser.studyMode', {
id: 'default',
mode: 'all',
});
const studyMode = customizationService.getCustomization('studyBrowser.studyMode');
const { t } = useTranslation('Common');
@@ -73,7 +71,7 @@ export default function PanelStudyBrowserTracking({
const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null);
const [viewPresets, setViewPresets] = useState(
customizationService.getCustomization('studyBrowser.viewPresets')?.value || defaultViewPresets
customizationService.getCustomization('studyBrowser.viewPresets')
);
const [actionIcons, setActionIcons] = useState(defaultActionIcons);
@@ -16,81 +16,79 @@
* See https://dicom.nema.org/medical/dicom/current/output/html/part16.html
* for definitions of SCT and other code values.
*/
const codingValues = {
id: 'codingValues',
export default {
codingValues: {
// Sites
'SCT:69536005': {
text: 'Head',
type: 'site',
style: {
color: 'red',
},
},
'SCT:45048000': {
text: 'Neck',
type: 'site',
style: {
color: 'blue',
},
},
'SCT:818981001': {
text: 'Abdomen',
type: 'site',
style: {
color: 'orange',
},
},
'SCT:816092008': {
text: 'Pelvis',
type: 'site',
style: {
color: 'cyan',
},
},
// Sites
'SCT:69536005': {
text: 'Head',
type: 'site',
style: {
color: 'red',
// Findings
'SCT:371861004': {
text: 'Mild intimal coronary irregularities',
style: {
color: 'green',
},
},
},
'SCT:45048000': {
text: 'Neck',
type: 'site',
style: {
color: 'blue',
'SCT:194983005': {
text: 'Aortic insufficiency',
style: {
color: 'darkred',
},
},
},
'SCT:818981001': {
text: 'Abdomen',
type: 'site',
style: {
color: 'orange',
'SCT:399232001': {
text: '2-chamber',
},
},
'SCT:816092008': {
text: 'Pelvis',
type: 'site',
style: {
color: 'cyan',
'SCT:103340004': {
text: 'SAX',
},
'SCT:91134007': {
text: 'MV',
},
'SCT:122972007': {
text: 'PV',
},
},
// Findings
'SCT:371861004': {
text: 'Mild intimal coronary irregularities',
style: {
color: 'green',
// Orientations
'SCT:24422004': {
text: 'Axial',
color: '#000000',
type: 'orientation',
},
},
'SCT:194983005': {
text: 'Aortic insufficiency',
style: {
color: 'darkred',
'SCT:81654009': {
text: 'Coronal',
color: '#000000',
type: 'orientation',
},
'SCT:30730003': {
text: 'Sagittal',
color: '#000000',
type: 'orientation',
},
},
'SCT:399232001': {
text: '2-chamber',
},
'SCT:103340004': {
text: 'SAX',
},
'SCT:91134007': {
text: 'MV',
},
'SCT:122972007': {
text: 'PV',
},
// Orientations
'SCT:24422004': {
text: 'Axial',
color: '#000000',
type: 'orientation',
},
'SCT:81654009': {
text: 'Coronal',
color: '#000000',
type: 'orientation',
},
'SCT:30730003': {
text: 'Sagittal',
color: '#000000',
type: 'orientation',
},
};
export default codingValues;
@@ -1,26 +1,24 @@
const codeMenuItem = {
id: '@ohif/contextMenuAnnotationCode',
/** Applies the code value setup for this item */
transform: function (customizationService) {
const { code: codeRef } = this;
if (!codeRef) {
throw new Error(`item ${this} has no code ref`);
}
const codingValues = customizationService.get('codingValues');
const code = codingValues[codeRef];
return {
...this,
codeRef,
code: { ref: codeRef, ...code },
label: this.label || code.text || codeRef,
commands: [
{
commandName: 'updateMeasurement',
},
],
};
export default {
'@ohif/contextMenuAnnotationCode': {
/** Applies the code value setup for this item */
$transform: function (customizationService) {
const { code: codeRef } = this;
if (!codeRef) {
throw new Error(`item ${this} has no code ref`);
}
const codingValues = customizationService.getCustomization('codingValues');
const code = codingValues[codeRef];
return {
...this,
codeRef,
code: { ref: codeRef, ...code },
label: this.label || code.text || codeRef,
commands: [
{
commandName: 'updateMeasurement',
},
],
};
},
},
};
export default codeMenuItem;
@@ -1,100 +1,98 @@
const findingsContextMenu = {
id: 'measurementsContextMenu',
customizationType: 'ohif.contextMenu',
menus: [
{
id: 'forExistingMeasurement',
// selector restricts context menu to when there is nearbyToolData
selector: ({ nearbyToolData }) => !!nearbyToolData,
items: [
export default {
measurementsContextMenu: {
$set: {
inheritsFrom: 'ohif.contextMenu',
menus: [
{
customizationType: 'ohif.contextSubMenu',
label: 'Site',
actionType: 'ShowSubMenu',
subMenu: 'siteSelectionSubMenu',
},
{
customizationType: 'ohif.contextSubMenu',
label: 'Finding',
actionType: 'ShowSubMenu',
subMenu: 'findingSelectionSubMenu',
},
{
// customizationType is implicit here in the configuration setup
label: 'Delete Measurement',
commands: [
// selector restricts context menu to when there is nearbyToolData
selector: ({ nearbyToolData }) => !!nearbyToolData,
items: [
{
commandName: 'deleteMeasurement',
label: 'Site',
actionType: 'ShowSubMenu',
subMenu: 'siteSelectionSubMenu',
},
],
},
{
label: 'Add Label',
commands: [
{
commandName: 'setMeasurementLabel',
label: 'Finding',
actionType: 'ShowSubMenu',
subMenu: 'findingSelectionSubMenu',
},
{
// inheritsFrom is implicit here in the configuration setup
label: 'Delete Measurement',
commands: [
{
commandName: 'deleteMeasurement',
},
],
},
{
label: 'Add Label',
commands: [
{
commandName: 'setMeasurementLabel',
},
],
},
// The example below shows how to include a delegating sub-menu,
// Only available on the @ohif/mnGrid hanging protocol
// To demonstrate, select the 3x1 layout from the protocol menu
// and right click on a measurement.
{
label: 'IncludeSubMenu',
selector: ({ protocol }) => protocol?.id === '@ohif/mnGrid',
delegating: true,
subMenu: 'orientationSelectionSubMenu',
},
],
},
// The example below shows how to include a delegating sub-menu,
// Only available on the @ohif/mnGrid hanging protocol
// To demonstrate, select the 3x1 layout from the protocol menu
// and right click on a measurement.
{
label: 'IncludeSubMenu',
selector: ({ protocol }) => protocol?.id === '@ohif/mnGrid',
delegating: true,
subMenu: 'orientationSelectionSubMenu',
id: 'orientationSelectionSubMenu',
selector: ({ nearbyToolData }) => !!nearbyToolData,
items: [
{
inheritsFrom: '@ohif/contextMenuAnnotationCode',
code: 'SCT:24422004',
},
{
inheritsFrom: '@ohif/contextMenuAnnotationCode',
code: 'SCT:81654009',
},
],
},
],
},
{
id: 'orientationSelectionSubMenu',
selector: ({ nearbyToolData }) => false,
items: [
{
customizationType: '@ohif/contextMenuAnnotationCode',
code: 'SCT:24422004',
id: 'findingSelectionSubMenu',
selector: ({ nearbyToolData }) => !!nearbyToolData,
items: [
{
inheritsFrom: '@ohif/contextMenuAnnotationCode',
code: 'SCT:371861004',
},
{
inheritsFrom: '@ohif/contextMenuAnnotationCode',
code: 'SCT:194983005',
},
],
},
{
customizationType: '@ohif/contextMenuAnnotationCode',
code: 'SCT:81654009',
},
],
},
{
id: 'findingSelectionSubMenu',
selector: ({ nearbyToolData }) => false,
items: [
{
customizationType: '@ohif/contextMenuAnnotationCode',
code: 'SCT:371861004',
},
{
customizationType: '@ohif/contextMenuAnnotationCode',
code: 'SCT:194983005',
id: 'siteSelectionSubMenu',
selector: ({ nearbyToolData }) => !!nearbyToolData,
items: [
{
inheritsFrom: '@ohif/contextMenuAnnotationCode',
code: 'SCT:69536005',
},
{
inheritsFrom: '@ohif/contextMenuAnnotationCode',
code: 'SCT:45048000',
},
],
},
],
},
{
id: 'siteSelectionSubMenu',
selector: ({ nearbyToolData }) => !!nearbyToolData,
items: [
{
customizationType: '@ohif/contextMenuAnnotationCode',
code: 'SCT:69536005',
},
{
customizationType: '@ohif/contextMenuAnnotationCode',
code: 'SCT:45048000',
},
],
},
],
},
};
export default findingsContextMenu;
@@ -4,11 +4,17 @@ export default function getCustomizationModule() {
return [
{
name: 'custom-context-menu',
value: [codingValues, contextMenuCodeItem, findingsContextMenu],
value: {
...codingValues,
...contextMenuCodeItem,
...findingsContextMenu,
},
},
{
name: 'contextMenuCodeItem',
value: [contextMenuCodeItem],
value: {
...contextMenuCodeItem,
},
},
];
}
+3 -3
View File
@@ -5,13 +5,12 @@ import { id } from './id';
import hpTestSwitch from './hpTestSwitch';
import getCustomizationModule from './getCustomizationModule';
// import {setViewportZoomPan, storeViewportZoomPan } from './custom-viewport/setViewportZoomPan';
import sameAs from './custom-attribute/sameAs';
import numberOfDisplaySets from './custom-attribute/numberOfDisplaySets';
import maxNumImageFrames from './custom-attribute/maxNumImageFrames';
/**
* The test extension provides additional behaviour for testing various
* The test extension provides additional behavior for testing various
* customizations and settings for OHIF.
*/
const testExtension: Types.Extensions.Extension = {
@@ -20,7 +19,8 @@ const testExtension: Types.Extensions.Extension = {
*/
id,
/** Register additional behaviour:
/**
* Register additional behavior:
* * HP custom attribute seriesDescriptions to retrieve an array of all series descriptions
* * HP custom attribute numberOfDisplaySets to retrieve the number of display sets
* * HP custom attribute numberOfDisplaySetsWithImages to retrieve the number of display sets containing images
@@ -67,7 +67,7 @@ export default function PanelRoiThresholdSegmentation({
<span className="text-base font-bold uppercase tracking-widest text-white">
{'TMTV:'}
</span>
<div className="text-white">{`${tmtvValue.toFixed(3)} mL`}</div>
<div className="text-white">{`${tmtvValue?.toFixed(3)} mL`}</div>
</div>
) : null}
</div>
+7 -1
View File
@@ -420,7 +420,13 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
return;
}
return await workerManager.executeTask('suv-peak-worker', 'calculateTMTV', labelmapProps);
const tmtv = await workerManager.executeTask(
'suv-peak-worker',
'calculateTMTV',
labelmapProps
);
return tmtv;
},
exportTMTVReportCSV: async ({ segmentations, tmtv, config, options }) => {
const segReport = commandsManager.runCommand('getSegmentationCSVReport', {