fix(CustomViewportOverlay): pass accurate data to Custom Viewport Functions (#4224)
Co-authored-by: Michael Andersen <Michael.Andersen@rmp.uhn.ca> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
parent
d5d821464a
commit
aef00e91d6
@ -39,11 +39,82 @@ interface OverlayItemProps {
|
||||
}
|
||||
|
||||
const OverlayItemComponents = {
|
||||
'ohif.overlayItem': OverlayItem,
|
||||
'ohif.overlayItem.windowLevel': VOIOverlayItem,
|
||||
'ohif.overlayItem.zoomLevel': ZoomOverlayItem,
|
||||
'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
|
||||
*/
|
||||
@ -60,32 +131,33 @@ function CustomizableViewportOverlay({
|
||||
viewportId: string;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}) {
|
||||
const { cornerstoneViewportService, customizationService, toolGroupService } =
|
||||
const { cornerstoneViewportService, customizationService, toolGroupService, displaySetService } =
|
||||
servicesManager.services;
|
||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||
const [scale, setScale] = useState(1);
|
||||
const { imageIndex } = imageSliceData;
|
||||
|
||||
const topLeftCustomization = customizationService.getModeCustomization(
|
||||
'cornerstoneOverlayTopLeft'
|
||||
);
|
||||
const topRightCustomization = customizationService.getModeCustomization(
|
||||
'cornerstoneOverlayTopRight'
|
||||
);
|
||||
const bottomLeftCustomization = customizationService.getModeCustomization(
|
||||
'cornerstoneOverlayBottomLeft'
|
||||
);
|
||||
const bottomRightCustomization = customizationService.getModeCustomization(
|
||||
'cornerstoneOverlayBottomRight'
|
||||
);
|
||||
// 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 instances = useMemo(() => {
|
||||
if (viewportData != null) {
|
||||
return _getViewportInstances(viewportData);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [viewportData, imageIndex]);
|
||||
|
||||
const instanceNumber = useMemo(
|
||||
() =>
|
||||
@ -95,6 +167,23 @@ function CustomizableViewportOverlay({
|
||||
[viewportData, viewportId, imageIndex, cornerstoneViewportService]
|
||||
);
|
||||
|
||||
const displaySetProps = useMemo(() => {
|
||||
const displaySets = getDisplaySets(viewportData, displaySetService);
|
||||
if (!displaySets) {
|
||||
return null;
|
||||
}
|
||||
const [displaySet] = displaySets;
|
||||
const { instances, instance: referenceInstance } = displaySet;
|
||||
return {
|
||||
displaySets,
|
||||
displaySet,
|
||||
instance: instances[imageIndex],
|
||||
instances,
|
||||
referenceInstance,
|
||||
};
|
||||
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
|
||||
|
||||
|
||||
/**
|
||||
* Updating the VOI when the viewport changes its voi
|
||||
*/
|
||||
@ -150,8 +239,9 @@ function CustomizableViewportOverlay({
|
||||
}, [viewportId, viewportData, cornerstoneViewportService, element]);
|
||||
|
||||
const _renderOverlayItem = useCallback(
|
||||
item => {
|
||||
(item, props) => {
|
||||
const overlayItemProps = {
|
||||
...props,
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
@ -164,10 +254,6 @@ function CustomizableViewportOverlay({
|
||||
formatTime: formatDICOMTime,
|
||||
formatNumberPrecision,
|
||||
},
|
||||
instance: instances ? instances[item?.instanceIndex] : null,
|
||||
voi,
|
||||
scale,
|
||||
instanceNumber,
|
||||
};
|
||||
|
||||
if (!item) {
|
||||
@ -182,8 +268,8 @@ function CustomizableViewportOverlay({
|
||||
} else {
|
||||
const renderItem = customizationService.transform(item);
|
||||
|
||||
if (typeof renderItem.content === 'function') {
|
||||
return renderItem.content(overlayItemProps);
|
||||
if (typeof renderItem.contentF === 'function') {
|
||||
return renderItem.contentF(overlayItemProps);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -194,7 +280,7 @@ function CustomizableViewportOverlay({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
customizationService,
|
||||
instances,
|
||||
displaySetProps,
|
||||
voi,
|
||||
scale,
|
||||
instanceNumber,
|
||||
@ -202,20 +288,26 @@ function CustomizableViewportOverlay({
|
||||
);
|
||||
|
||||
const getContent = useCallback(
|
||||
(customization, defaultItems, keyPrefix) => {
|
||||
const items = customization?.items ?? defaultItems;
|
||||
(customization, keyPrefix) => {
|
||||
if (!customization?.items) {
|
||||
return null;
|
||||
}
|
||||
const { items } = customization;
|
||||
const props = {
|
||||
...displaySetProps,
|
||||
formatters: { formatDate: formatDICOMDate },
|
||||
voi,
|
||||
scale,
|
||||
instanceNumber,
|
||||
viewportId,
|
||||
toolGroupService,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item, index) => (
|
||||
<div key={`${keyPrefix}_${index}`}>
|
||||
{item?.condition
|
||||
? item.condition({
|
||||
instance: instances ? instances[item?.instanceIndex] : null,
|
||||
formatters: { formatDate: formatDICOMDate },
|
||||
})
|
||||
? _renderOverlayItem(item)
|
||||
: null
|
||||
: _renderOverlayItem(item)}
|
||||
{(!item?.condition || item.condition(props)) && _renderOverlayItem(item, props) || null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@ -224,104 +316,30 @@ function CustomizableViewportOverlay({
|
||||
[_renderOverlayItem]
|
||||
);
|
||||
|
||||
const studyDateItem = {
|
||||
id: 'StudyDate',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Study date',
|
||||
condition: ({ instance }) => instance && instance.StudyDate,
|
||||
contentF: ({ instance, formatters: { formatDate } }) => formatDate(instance.StudyDate),
|
||||
};
|
||||
|
||||
const seriesDescriptionItem = {
|
||||
id: 'SeriesDescription',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Series description',
|
||||
attribute: 'SeriesDescription',
|
||||
condition: ({ instance }) => {
|
||||
return instance && instance.SeriesDescription;
|
||||
},
|
||||
};
|
||||
|
||||
const topLeftItems = instances
|
||||
? instances
|
||||
.map((instance, index) => {
|
||||
return [
|
||||
{
|
||||
...studyDateItem,
|
||||
instanceIndex: index,
|
||||
},
|
||||
{
|
||||
...seriesDescriptionItem,
|
||||
instanceIndex: index,
|
||||
},
|
||||
];
|
||||
})
|
||||
.flat()
|
||||
: [];
|
||||
|
||||
return (
|
||||
<ViewportOverlay
|
||||
topLeft={
|
||||
/**
|
||||
* Inline default overlay items for a more standard expansion
|
||||
*/
|
||||
getContent(topLeftCustomization, [...topLeftItems], 'topLeftOverlayItem')
|
||||
}
|
||||
topRight={getContent(topRightCustomization, [], 'topRightOverlayItem')}
|
||||
bottomLeft={getContent(
|
||||
bottomLeftCustomization,
|
||||
[
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
customizationType: 'ohif.overlayItem.windowLevel',
|
||||
},
|
||||
{
|
||||
id: 'ZoomLevel',
|
||||
customizationType: 'ohif.overlayItem.zoomLevel',
|
||||
condition: () => {
|
||||
const activeToolName = toolGroupService.getActiveToolForViewport(viewportId);
|
||||
return activeToolName === 'Zoom';
|
||||
},
|
||||
},
|
||||
],
|
||||
'bottomLeftOverlayItem'
|
||||
)}
|
||||
bottomRight={getContent(
|
||||
bottomRightCustomization,
|
||||
[
|
||||
{
|
||||
id: 'InstanceNumber',
|
||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
],
|
||||
'bottomRightOverlayItem'
|
||||
)}
|
||||
topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
|
||||
topRight={getContent(topRightCustomization, 'topRightOverlayItem')}
|
||||
bottomLeft={getContent(bottomLeftCustomization, 'bottomLeftOverlayItem')}
|
||||
bottomRight={getContent(bottomRightCustomization, 'bottomRightOverlayItem')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function _getViewportInstances(viewportData) {
|
||||
const imageIds = [];
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
imageIds.push(viewportData.data[0].imageIds[0]);
|
||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
const volumes = viewportData.data;
|
||||
volumes.forEach(volume => {
|
||||
if (!volume?.imageIds || volume.imageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
imageIds.push(volume.imageIds[0]);
|
||||
});
|
||||
/**
|
||||
* Gets an array of display sets for the given viewport, based on the viewport data.
|
||||
* Returns null if none found.
|
||||
*/
|
||||
function getDisplaySets(viewportData, displaySetService) {
|
||||
if (!viewportData?.data?.length) {
|
||||
return null;
|
||||
}
|
||||
const instances = [];
|
||||
|
||||
imageIds.forEach(imageId => {
|
||||
const instance = metaData.get('instance', imageId) || {};
|
||||
instances.push(instance);
|
||||
});
|
||||
return instances;
|
||||
const displaySets = viewportData.data.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID)).filter(it => !!it);
|
||||
if (!displaySets.length) {
|
||||
return null;
|
||||
}
|
||||
return displaySets;
|
||||
}
|
||||
|
||||
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
|
||||
@ -364,6 +382,7 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||
return parseInt(instanceNumber);
|
||||
}
|
||||
|
||||
|
||||
// Since volume viewports can be in any view direction, they can render
|
||||
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
|
||||
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
|
||||
@ -411,6 +430,24 @@ function _getInstanceNumberFromVolume(
|
||||
}
|
||||
}
|
||||
|
||||
function OverlayItem(props) {
|
||||
const { instance, customization = {} } = props;
|
||||
const { color, attribute, title, label, background } = customization;
|
||||
const value = customization.contentF?.(props, customization) ?? instance?.[attribute];
|
||||
if (value === undefined || value === null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color, background }}
|
||||
title={title}
|
||||
>
|
||||
{label ? (<span className="mr-1 shrink-0">{label}</span>) : null}
|
||||
<span className="ml-1 mr-2 shrink-0">{value}</span>
|
||||
</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Window Level / Center Overlay item
|
||||
*/
|
||||
@ -423,7 +460,7 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
style={{ color: customization?.color }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">W:</span>
|
||||
<span className="ml-1 mr-2 shrink-0">{windowWidth.toFixed(0)}</span>
|
||||
@ -484,3 +521,5 @@ CustomizableViewportOverlay.propTypes = {
|
||||
};
|
||||
|
||||
export default CustomizableViewportOverlay;
|
||||
|
||||
export { CustomizableViewportOverlay, CornerstoneOverlay };
|
||||
|
||||
@ -62,6 +62,12 @@ export function formatPN(name) {
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
if (typeof name === 'object') {
|
||||
name = name.Alphabetic;
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const cleaned = name
|
||||
.split('^')
|
||||
|
||||
@ -4,6 +4,7 @@ import DicomUpload from './components/DicomUpload/DicomUpload';
|
||||
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;
|
||||
@ -47,6 +48,7 @@ function getCustomizationModule() {
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
CornerstoneOverlay,
|
||||
{
|
||||
id: 'cornerstone.overlayViewportTools',
|
||||
tools,
|
||||
|
||||
@ -167,7 +167,6 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
||||
},
|
||||
{
|
||||
id: 'studyBrowser.sortFunctions',
|
||||
merge: 'Append',
|
||||
values: [
|
||||
{
|
||||
label: 'Series Number',
|
||||
|
||||
276
platform/app/public/config/customization.js
Normal file
276
platform/app/public/config/customization.js
Normal file
@ -0,0 +1,276 @@
|
||||
/** @type {AppTypes.Config} */
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
extensions: [],
|
||||
modes: ['@ohif/mode-test'],
|
||||
showStudyList: true,
|
||||
// below flag is for performance reasons, but it might not work for all servers
|
||||
maxNumberOfWebWorkers: 3,
|
||||
showWarningMessageForCrossOrigin: false,
|
||||
showCPUFallbackMessage: false,
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
// filterQueryParam: false,
|
||||
|
||||
// Add some customizations to the default e2e datasource
|
||||
customizationService: [
|
||||
'@ohif/extension-default.customizationModule.datasources',
|
||||
'@ohif/extension-default.customizationModule.helloPage',
|
||||
|
||||
{
|
||||
id: '@ohif/cornerstoneOverlay',
|
||||
// Append recursively, rather than replacing
|
||||
merge: 'Append',
|
||||
topRightItems: {
|
||||
id: 'cornerstoneOverlayTopRight',
|
||||
items: [
|
||||
{
|
||||
id: 'PatientNameOverlay',
|
||||
// Note below that here we are using the customization prototype of
|
||||
// `ohif.overlayItem` which was registered to the customization module in
|
||||
// `ohif/extension-default` extension.
|
||||
customizationType: 'ohif.overlayItem',
|
||||
// the following props are passed to the `ohif.overlayItem` prototype
|
||||
// which is used to render the overlay item based on the label, color,
|
||||
// conditions, etc.
|
||||
attribute: 'PatientName',
|
||||
label: 'PN:',
|
||||
title: 'Patient Name',
|
||||
color: 'yellow',
|
||||
condition: ({ instance }) => instance?.PatientName,
|
||||
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||
formatPN(instance.PatientName) +
|
||||
(instance.PatientSex ? ' (' + instance.PatientSex + ')' : ''),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
topLeftItems: {
|
||||
items: {
|
||||
// Note the -10000 means -10000 + length of existing list, which is
|
||||
// much before the start of hte list, so put the new value at the start.
|
||||
'-10000':
|
||||
{
|
||||
id: 'Species',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Species:',
|
||||
color: 'red',
|
||||
background: 'green',
|
||||
condition: ({ instance }) =>
|
||||
instance?.PatientSpeciesDescription,
|
||||
contentF: ({ instance }) =>
|
||||
instance.PatientSpeciesDescription +
|
||||
'/' +
|
||||
instance.PatientBreedDescription,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
defaultDataSourceName: 'e2e',
|
||||
investigationalUseDialog: {
|
||||
option: 'never',
|
||||
},
|
||||
dataSources: [
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'e2e',
|
||||
configuration: {
|
||||
friendlyName: 'StaticWado test data',
|
||||
// The most important field to set for static WADO
|
||||
staticWado: true,
|
||||
name: 'StaticWADO',
|
||||
wadoUriRoot: '/viewer-testdata',
|
||||
qidoRoot: '/viewer-testdata',
|
||||
wadoRoot: '/viewer-testdata',
|
||||
qidoSupportsIncludeField: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
singlepart: 'video,thumbnail,pdf',
|
||||
omitQuotationForMultipartRequest: true,
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'local5000',
|
||||
configuration: {
|
||||
friendlyName: 'Static WADO Local Data',
|
||||
name: 'DCM4CHEE',
|
||||
qidoRoot: 'http://localhost:5000/dicomweb',
|
||||
wadoRoot: 'http://localhost:5000/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: true,
|
||||
supportsStow: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
singlepart: 'video',
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'docker',
|
||||
configuration: {
|
||||
friendlyName: 'Static WADO Docker Data',
|
||||
name: 'DCM4CHEE',
|
||||
qidoRoot: 'http://localhost:25080/dicomweb',
|
||||
wadoRoot: 'http://localhost:25080/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: true,
|
||||
supportsStow: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
singlepart: 'bulkdata,video,pdf',
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'ohif',
|
||||
configuration: {
|
||||
friendlyName: 'AWS S3 Static wado server',
|
||||
name: 'aws',
|
||||
wadoUriRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
|
||||
qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
|
||||
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
singlepart: 'video,pdf',
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'ohif2',
|
||||
configuration: {
|
||||
friendlyName: 'AWS S3 Static wado secondary server',
|
||||
name: 'aws',
|
||||
wadoUriRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb',
|
||||
qidoRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb',
|
||||
wadoRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
singlepart: 'bulkdata,video',
|
||||
// whether the data source should use retrieveBulkData to grab metadata,
|
||||
// and in case of relative path, what would it be relative to, options
|
||||
// are in the series level or study level (some servers like series some study)
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
omitQuotationForMultipartRequest: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'ohif3',
|
||||
configuration: {
|
||||
friendlyName: 'AWS S3 Static wado secondary server',
|
||||
name: 'aws',
|
||||
wadoUriRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb',
|
||||
qidoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb',
|
||||
wadoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
singlepart: 'bulkdata,video',
|
||||
// whether the data source should use retrieveBulkData to grab metadata,
|
||||
// and in case of relative path, what would it be relative to, options
|
||||
// are in the series level or study level (some servers like series some study)
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
omitQuotationForMultipartRequest: true,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
friendlyName: 'StaticWado default data',
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'dicomweb',
|
||||
configuration: {
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot: '/dicomweb',
|
||||
qidoRoot: '/dicomweb',
|
||||
wadoRoot: '/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomjson',
|
||||
sourceName: 'dicomjson',
|
||||
configuration: {
|
||||
friendlyName: 'dicom json',
|
||||
name: 'json',
|
||||
},
|
||||
},
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal',
|
||||
sourceName: 'dicomlocal',
|
||||
configuration: {
|
||||
friendlyName: 'dicom local',
|
||||
},
|
||||
},
|
||||
],
|
||||
httpErrorHandler: error => {
|
||||
// This is 429 when rejected from the public idc sandbox too often.
|
||||
console.warn(error.status);
|
||||
|
||||
// Could use services manager here to bring up a dialog/modal if needed.
|
||||
console.warn('test, navigate to https://ohif.org/');
|
||||
},
|
||||
hotkeys: [],
|
||||
};
|
||||
@ -318,9 +318,15 @@ export default class CustomizationService extends PubSubService {
|
||||
}
|
||||
|
||||
public setGlobalCustomization(id: string, value: Customization, merge = MergeEnum.Replace): void {
|
||||
const defaultCustomization = this.defaultCustomizations.get(id);
|
||||
const globCustomization = this.globalCustomizations.get(id);
|
||||
const sourceCustomization =
|
||||
(globCustomization && cloneDeepWith(globCustomization, cloneCustomizer)) ||
|
||||
defaultCustomization ||
|
||||
{};
|
||||
this.globalCustomizations.set(
|
||||
id,
|
||||
this.mergeValue(this.globalCustomizations.get(id), value, merge)
|
||||
this.mergeValue(sourceCustomization, value, value.merge ?? merge)
|
||||
);
|
||||
this.transformedCustomizations.clear();
|
||||
this._broadcastGlobalCustomizationModified();
|
||||
@ -445,7 +451,7 @@ function findPosition(key, value, newList) {
|
||||
const { length: len } = newList;
|
||||
|
||||
if (isNumeric) {
|
||||
if (newList[(numVal + len) % len]) {
|
||||
if (newList[numVal < 0 ? numVal + len : numVal]) {
|
||||
return { isMerge: true, position: (numVal + len) % len };
|
||||
}
|
||||
const absPosition = Math.ceil(numVal < 0 ? len + numVal : numVal);
|
||||
|
||||
@ -52,7 +52,7 @@ export default class ServicesManager {
|
||||
extensionManager: this._extensionManager,
|
||||
});
|
||||
if (service.altName) {
|
||||
console.log('Registering old name', service.altName);
|
||||
// TODO - remove this registration
|
||||
this.services[service.altName] = this.services[service.name];
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -401,145 +401,68 @@ customizationService: [
|
||||
|
||||
Below is the full example configuration of the customizable viewport overlay and the screenshot of the result overlay.
|
||||
|
||||
There are working examples that can be run with:
|
||||
```
|
||||
set APP_CONFIG=config/customization.js
|
||||
yarn dev
|
||||
```
|
||||
|
||||
```javascript
|
||||
// this is one of the configuration files in `platform/app/public/config/*.js`
|
||||
// this is part of customization.js, an example customization dataset
|
||||
window.config = {
|
||||
// ...
|
||||
|
||||
customizationService: {
|
||||
cornerstoneOverlayTopLeft: {
|
||||
id: 'cornerstoneOverlayTopLeft',
|
||||
items: [
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
customizationType: 'ohif.overlayItem.windowLevel',
|
||||
// This shows how to append to the customization data
|
||||
customizationService: [
|
||||
{
|
||||
id: '@ohif/cornerstoneOverlay',
|
||||
// Append recursively, rather than replacing
|
||||
merge: 'Append',
|
||||
topRightItems: {
|
||||
id: 'cornerstoneOverlayTopRight',
|
||||
items: [
|
||||
{
|
||||
id: 'PatientNameOverlay',
|
||||
// Note below that here we are using the customization prototype of
|
||||
// `ohif.overlayItem` which was registered to the customization module in
|
||||
// `ohif/extension-default` extension.
|
||||
customizationType: 'ohif.overlayItem',
|
||||
// the following props are passed to the `ohif.overlayItem` prototype
|
||||
// which is used to render the overlay item based on the label, color,
|
||||
// conditions, etc.
|
||||
attribute: 'PatientName',
|
||||
label: 'PN:',
|
||||
title: 'Patient Name',
|
||||
color: 'yellow',
|
||||
condition: ({ instance }) => instance?.PatientName,
|
||||
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||
formatPN(instance.PatientName) +
|
||||
(instance.PatientSex ? ' (' + instance.PatientSex + ')' : ''),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
topLeftItems: {
|
||||
items: {
|
||||
// Note the -10000 means -10000 + length of existing list, which is
|
||||
// much before the start of hte list, so put the new value at the start.
|
||||
'-10000':
|
||||
{
|
||||
id: 'Species',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Species:',
|
||||
color: 'red',
|
||||
background: 'green',
|
||||
condition: ({ instance }) =>
|
||||
instance?.PatientSpeciesDescription,
|
||||
contentF: ({ instance }) =>
|
||||
instance.PatientSpeciesDescription +
|
||||
'/' +
|
||||
instance.PatientBreedDescription,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'PatientName',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
color: 'green',
|
||||
background: 'white',
|
||||
condition: ({ instance }) =>
|
||||
instance && instance.PatientName && instance.PatientName.Alphabetic,
|
||||
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||
formatPN(instance.PatientName.Alphabetic) +
|
||||
' ' +
|
||||
(instance.PatientSex ? '(' + instance.PatientSex + ')' : ''),
|
||||
},
|
||||
{
|
||||
id: 'Species',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Species:',
|
||||
condition: ({ instance }) =>
|
||||
instance && instance.PatientSpeciesDescription,
|
||||
contentF: ({ instance }) =>
|
||||
instance.PatientSpeciesDescription +
|
||||
'/' +
|
||||
instance.PatientBreedDescription,
|
||||
},
|
||||
{
|
||||
id: 'PID',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'PID:',
|
||||
title: 'Patient PID',
|
||||
condition: ({ instance }) => instance && instance.PatientID,
|
||||
contentF: ({ instance }) => instance.PatientID,
|
||||
},
|
||||
{
|
||||
id: 'PatientBirthDate',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'DOB:',
|
||||
title: "Patient's Date of birth",
|
||||
condition: ({ instance }) => instance && instance.PatientBirthDate,
|
||||
contentF: ({ instance }) => instance.PatientBirthDate,
|
||||
},
|
||||
{
|
||||
id: 'OtherPid',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Other PID:',
|
||||
title: 'Other Patient IDs',
|
||||
condition: ({ instance }) => instance && instance.OtherPatientIDs,
|
||||
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||
formatPN(instance.OtherPatientIDs),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
cornerstoneOverlayTopRight: {
|
||||
id: 'cornerstoneOverlayTopRight',
|
||||
|
||||
items: [
|
||||
{
|
||||
id: 'InstanceNmber',
|
||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
{
|
||||
id: 'StudyDescription',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: ({ instance }) =>
|
||||
instance &&
|
||||
instance.StudyDescription &&
|
||||
`Study Description: ${instance.StudyDescription}`,
|
||||
condition: ({ instance }) => instance && instance.StudyDescription,
|
||||
contentF: ({ instance }) => instance.StudyDescription,
|
||||
},
|
||||
{
|
||||
id: 'StudyDate',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Study date',
|
||||
condition: ({ instance }) => instance && instance.StudyDate,
|
||||
contentF: ({ instance, formatters: { formatDate } }) =>
|
||||
formatDate(instance.StudyDate),
|
||||
},
|
||||
{
|
||||
id: 'StudyTime',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Study time',
|
||||
condition: ({ instance }) => instance && instance.StudyTime,
|
||||
contentF: ({ instance, formatters: { formatTime } }) =>
|
||||
formatTime(instance.StudyTime),
|
||||
},
|
||||
],
|
||||
},
|
||||
cornerstoneOverlayBottomLeft: {
|
||||
id: 'cornerstoneOverlayBottomLeft',
|
||||
|
||||
items: [
|
||||
{
|
||||
id: 'SeriesNumber',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Ser:',
|
||||
title: 'Series Number',
|
||||
condition: ({ instance }) => instance && instance.SeriesNumber,
|
||||
contentF: ({ instance }) => instance.SeriesNumber,
|
||||
},
|
||||
{
|
||||
id: 'SliceLocation',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Loc:',
|
||||
title: 'Slice Location',
|
||||
condition: ({ instance }) => instance && instance.SliceLocation,
|
||||
contentF: ({ instance, formatters: { formatNumberPrecision } }) =>
|
||||
formatNumberPrecision(instance.SliceLocation, 2) + ' mm',
|
||||
},
|
||||
{
|
||||
id: 'SliceThickness',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Thick:',
|
||||
title: 'Slice Thickness',
|
||||
condition: ({ instance }) => instance && instance.SliceThickness,
|
||||
contentF: ({ instance, formatters: { formatNumberPrecision } }) =>
|
||||
formatNumberPrecision(instance.SliceThickness, 2) + ' mm',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// ...
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
<img src="../../../assets/img/customizable-overlay.png" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user