diff --git a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx index 5a01b3525..9436c1fa3 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx @@ -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) => (
- {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}
))} @@ -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 ( { - 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 ( +
+ {label ? ({label}) : null} + {value} +
); +} + /** * Window Level / Center Overlay item */ @@ -423,7 +460,7 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) { return (
W: {windowWidth.toFixed(0)} @@ -484,3 +521,5 @@ CustomizableViewportOverlay.propTypes = { }; export default CustomizableViewportOverlay; + +export { CustomizableViewportOverlay, CornerstoneOverlay }; diff --git a/extensions/cornerstone/src/Viewport/Overlays/utils.ts b/extensions/cornerstone/src/Viewport/Overlays/utils.ts index 54f164add..d8795f3b3 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/utils.ts +++ b/extensions/cornerstone/src/Viewport/Overlays/utils.ts @@ -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('^') diff --git a/extensions/cornerstone/src/getCustomizationModule.ts b/extensions/cornerstone/src/getCustomizationModule.ts index 6952839d8..32da1bb6d 100644 --- a/extensions/cornerstone/src/getCustomizationModule.ts +++ b/extensions/cornerstone/src/getCustomizationModule.ts @@ -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, diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index 9f7153af2..75de9b03d 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -167,7 +167,6 @@ export default function getCustomizationModule({ servicesManager, extensionManag }, { id: 'studyBrowser.sortFunctions', - merge: 'Append', values: [ { label: 'Series Number', diff --git a/platform/app/public/config/customization.js b/platform/app/public/config/customization.js new file mode 100644 index 000000000..39696c9b0 --- /dev/null +++ b/platform/app/public/config/customization.js @@ -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: [], +}; diff --git a/platform/core/src/services/CustomizationService/CustomizationService.ts b/platform/core/src/services/CustomizationService/CustomizationService.ts index 4323bf88e..4dd7d505a 100644 --- a/platform/core/src/services/CustomizationService/CustomizationService.ts +++ b/platform/core/src/services/CustomizationService/CustomizationService.ts @@ -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); diff --git a/platform/core/src/services/ServicesManager.ts b/platform/core/src/services/ServicesManager.ts index 759a965f4..a73ff9a45 100644 --- a/platform/core/src/services/ServicesManager.ts +++ b/platform/core/src/services/ServicesManager.ts @@ -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 { diff --git a/platform/docs/docs/platform/services/ui/customization-service.md b/platform/docs/docs/platform/services/ui/customization-service.md index 7142b57e8..e42d201aa 100644 --- a/platform/docs/docs/platform/services/ui/customization-service.md +++ b/platform/docs/docs/platform/services/ui/customization-service.md @@ -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', - }, - ], - }, - }, - - // ... -} +... ```