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:
authored and GitHub committed 2024-06-21 16:22:35 -04:00
1 parent d5d821464a
commit aef00e91d6
8 files changed
+518 -267

No files matched your search

+276
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: [],
};
@@ -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" />