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:
Michael Andersen 2024-06-21 16:22:35 -04:00 committed by GitHub
parent d5d821464a
commit aef00e91d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 518 additions and 267 deletions

View File

@ -39,11 +39,82 @@ interface OverlayItemProps {
} }
const OverlayItemComponents = { const OverlayItemComponents = {
'ohif.overlayItem': OverlayItem,
'ohif.overlayItem.windowLevel': VOIOverlayItem, 'ohif.overlayItem.windowLevel': VOIOverlayItem,
'ohif.overlayItem.zoomLevel': ZoomOverlayItem, 'ohif.overlayItem.zoomLevel': ZoomOverlayItem,
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem, '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 * Customizable Viewport Overlay
*/ */
@ -60,32 +131,33 @@ function CustomizableViewportOverlay({
viewportId: string; viewportId: string;
servicesManager: AppTypes.ServicesManager; servicesManager: AppTypes.ServicesManager;
}) { }) {
const { cornerstoneViewportService, customizationService, toolGroupService } = const { cornerstoneViewportService, customizationService, toolGroupService, displaySetService } =
servicesManager.services; servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null }); const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1); const [scale, setScale] = useState(1);
const { imageIndex } = imageSliceData; const { imageIndex } = imageSliceData;
const topLeftCustomization = customizationService.getModeCustomization( // The new customization is 'cornerstoneOverlay', with an append or replace
'cornerstoneOverlayTopLeft' // on the individual items rather than defining individual items.
); const cornerstoneOverlay = customizationService.getCustomization('@ohif/cornerstoneOverlay');
const topRightCustomization = customizationService.getModeCustomization(
'cornerstoneOverlayTopRight' // Historical usage defined the overlays as separate items due to lack of
); // append functionality. This code enables the historical usage, but
const bottomLeftCustomization = customizationService.getModeCustomization( // the recommended functionality is to append to the default values in
'cornerstoneOverlayBottomLeft' // cornerstoneOverlay rather than defining individual items.
); const topLeftCustomization = customizationService.getCustomization(
const bottomRightCustomization = customizationService.getModeCustomization( 'cornerstoneOverlayTopLeft'
'cornerstoneOverlayBottomRight' ) || 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( const instanceNumber = useMemo(
() => () =>
@ -95,6 +167,23 @@ function CustomizableViewportOverlay({
[viewportData, viewportId, imageIndex, cornerstoneViewportService] [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 * Updating the VOI when the viewport changes its voi
*/ */
@ -150,8 +239,9 @@ function CustomizableViewportOverlay({
}, [viewportId, viewportData, cornerstoneViewportService, element]); }, [viewportId, viewportData, cornerstoneViewportService, element]);
const _renderOverlayItem = useCallback( const _renderOverlayItem = useCallback(
item => { (item, props) => {
const overlayItemProps = { const overlayItemProps = {
...props,
element, element,
viewportData, viewportData,
imageSliceData, imageSliceData,
@ -164,10 +254,6 @@ function CustomizableViewportOverlay({
formatTime: formatDICOMTime, formatTime: formatDICOMTime,
formatNumberPrecision, formatNumberPrecision,
}, },
instance: instances ? instances[item?.instanceIndex] : null,
voi,
scale,
instanceNumber,
}; };
if (!item) { if (!item) {
@ -182,8 +268,8 @@ function CustomizableViewportOverlay({
} else { } else {
const renderItem = customizationService.transform(item); const renderItem = customizationService.transform(item);
if (typeof renderItem.content === 'function') { if (typeof renderItem.contentF === 'function') {
return renderItem.content(overlayItemProps); return renderItem.contentF(overlayItemProps);
} }
} }
}, },
@ -194,7 +280,7 @@ function CustomizableViewportOverlay({
viewportId, viewportId,
servicesManager, servicesManager,
customizationService, customizationService,
instances, displaySetProps,
voi, voi,
scale, scale,
instanceNumber, instanceNumber,
@ -202,20 +288,26 @@ function CustomizableViewportOverlay({
); );
const getContent = useCallback( const getContent = useCallback(
(customization, defaultItems, keyPrefix) => { (customization, keyPrefix) => {
const items = customization?.items ?? defaultItems; if (!customization?.items) {
return null;
}
const { items } = customization;
const props = {
...displaySetProps,
formatters: { formatDate: formatDICOMDate },
voi,
scale,
instanceNumber,
viewportId,
toolGroupService,
};
return ( return (
<> <>
{items.map((item, index) => ( {items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}> <div key={`${keyPrefix}_${index}`}>
{item?.condition {(!item?.condition || item.condition(props)) && _renderOverlayItem(item, props) || null}
? item.condition({
instance: instances ? instances[item?.instanceIndex] : null,
formatters: { formatDate: formatDICOMDate },
})
? _renderOverlayItem(item)
: null
: _renderOverlayItem(item)}
</div> </div>
))} ))}
</> </>
@ -224,104 +316,30 @@ function CustomizableViewportOverlay({
[_renderOverlayItem] [_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 ( return (
<ViewportOverlay <ViewportOverlay
topLeft={ topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
/** topRight={getContent(topRightCustomization, 'topRightOverlayItem')}
* Inline default overlay items for a more standard expansion bottomLeft={getContent(bottomLeftCustomization, 'bottomLeftOverlayItem')}
*/ bottomRight={getContent(bottomRightCustomization, 'bottomRightOverlayItem')}
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'
)}
/> />
); );
} }
function _getViewportInstances(viewportData) { /**
const imageIds = []; * Gets an array of display sets for the given viewport, based on the viewport data.
if (viewportData.viewportType === Enums.ViewportType.STACK) { * Returns null if none found.
imageIds.push(viewportData.data[0].imageIds[0]); */
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) { function getDisplaySets(viewportData, displaySetService) {
const volumes = viewportData.data; if (!viewportData?.data?.length) {
volumes.forEach(volume => { return null;
if (!volume?.imageIds || volume.imageIds.length === 0) {
return;
}
imageIds.push(volume.imageIds[0]);
});
} }
const instances = []; const displaySets = viewportData.data.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID)).filter(it => !!it);
if (!displaySets.length) {
imageIds.forEach(imageId => { return null;
const instance = metaData.get('instance', imageId) || {}; }
instances.push(instance); return displaySets;
});
return instances;
} }
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => { const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
@ -364,6 +382,7 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
return parseInt(instanceNumber); return parseInt(instanceNumber);
} }
// Since volume viewports can be in any view direction, they can render // 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 // 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 // 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 * Window Level / Center Overlay item
*/ */
@ -423,7 +460,7 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
return ( return (
<div <div
className="overlay-item flex flex-row" 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="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 shrink-0">{windowWidth.toFixed(0)}</span> <span className="ml-1 mr-2 shrink-0">{windowWidth.toFixed(0)}</span>
@ -484,3 +521,5 @@ CustomizableViewportOverlay.propTypes = {
}; };
export default CustomizableViewportOverlay; export default CustomizableViewportOverlay;
export { CustomizableViewportOverlay, CornerstoneOverlay };

View File

@ -62,6 +62,12 @@ export function formatPN(name) {
if (!name) { if (!name) {
return ''; return '';
} }
if (typeof name === 'object') {
name = name.Alphabetic;
if (!name) {
return '';
}
}
const cleaned = name const cleaned = name
.split('^') .split('^')

View File

@ -4,6 +4,7 @@ import DicomUpload from './components/DicomUpload/DicomUpload';
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets'; import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
import { colormaps } from './utils/colormaps'; import { colormaps } from './utils/colormaps';
import { CONSTANTS } from '@cornerstonejs/core'; import { CONSTANTS } from '@cornerstonejs/core';
import { CornerstoneOverlay } from './Viewport/Overlays/CustomizableViewportOverlay';
const DefaultColormap = 'Grayscale'; const DefaultColormap = 'Grayscale';
const { VIEWPORT_PRESETS } = CONSTANTS; const { VIEWPORT_PRESETS } = CONSTANTS;
@ -47,6 +48,7 @@ function getCustomizationModule() {
{ {
name: 'default', name: 'default',
value: [ value: [
CornerstoneOverlay,
{ {
id: 'cornerstone.overlayViewportTools', id: 'cornerstone.overlayViewportTools',
tools, tools,

View File

@ -167,7 +167,6 @@ export default function getCustomizationModule({ servicesManager, extensionManag
}, },
{ {
id: 'studyBrowser.sortFunctions', id: 'studyBrowser.sortFunctions',
merge: 'Append',
values: [ values: [
{ {
label: 'Series Number', label: 'Series Number',

View 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: [],
};

View File

@ -318,9 +318,15 @@ export default class CustomizationService extends PubSubService {
} }
public setGlobalCustomization(id: string, value: Customization, merge = MergeEnum.Replace): void { 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( this.globalCustomizations.set(
id, id,
this.mergeValue(this.globalCustomizations.get(id), value, merge) this.mergeValue(sourceCustomization, value, value.merge ?? merge)
); );
this.transformedCustomizations.clear(); this.transformedCustomizations.clear();
this._broadcastGlobalCustomizationModified(); this._broadcastGlobalCustomizationModified();
@ -445,7 +451,7 @@ function findPosition(key, value, newList) {
const { length: len } = newList; const { length: len } = newList;
if (isNumeric) { if (isNumeric) {
if (newList[(numVal + len) % len]) { if (newList[numVal < 0 ? numVal + len : numVal]) {
return { isMerge: true, position: (numVal + len) % len }; return { isMerge: true, position: (numVal + len) % len };
} }
const absPosition = Math.ceil(numVal < 0 ? len + numVal : numVal); const absPosition = Math.ceil(numVal < 0 ? len + numVal : numVal);

View File

@ -52,7 +52,7 @@ export default class ServicesManager {
extensionManager: this._extensionManager, extensionManager: this._extensionManager,
}); });
if (service.altName) { if (service.altName) {
console.log('Registering old name', service.altName); // TODO - remove this registration
this.services[service.altName] = this.services[service.name]; this.services[service.altName] = this.services[service.name];
} }
} else { } else {

View File

@ -401,145 +401,68 @@ customizationService: [
Below is the full example configuration of the customizable viewport overlay and the screenshot of the result overlay. 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 ```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 = { window.config = {
// ...
customizationService: { // This shows how to append to the customization data
cornerstoneOverlayTopLeft: { customizationService: [
id: 'cornerstoneOverlayTopLeft', {
items: [ id: '@ohif/cornerstoneOverlay',
{ // Append recursively, rather than replacing
id: 'WindowLevel', merge: 'Append',
customizationType: 'ohif.overlayItem.windowLevel', 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" /> <img src="../../../assets/img/customizable-overlay.png" />