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',
- },
- ],
- },
- },
-
- // ...
-}
+...
```