diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContainer.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContainer.tsx
new file mode 100644
index 000000000..461eba1ac
--- /dev/null
+++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContainer.tsx
@@ -0,0 +1,78 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { OHIFCornerstoneSRContentItem } from './OHIFCornerstoneSRContentItem';
+
+export function OHIFCornerstoneSRContainer(props) {
+ const { container, nodeIndexesTree = [0], containerNumberedTree = [1] } = props;
+ const { ContinuityOfContent, ConceptNameCodeSequence } = container;
+ const { CodeMeaning } = ConceptNameCodeSequence ?? {};
+ let childContainerIndex = 1;
+ const contentItems = container.ContentSequence?.map((contentItem, i) => {
+ const { ValueType } = contentItem;
+ const childNodeLevel = [...nodeIndexesTree, i];
+ const key = childNodeLevel.join('.');
+
+ let Component;
+ let componentProps;
+
+ if (ValueType === 'CONTAINER') {
+ const childContainerNumberedTree = [...containerNumberedTree, childContainerIndex++];
+
+ Component = OHIFCornerstoneSRContainer;
+ componentProps = {
+ container: contentItem,
+ nodeIndexesTree: childNodeLevel,
+ containerNumberedTree: childContainerNumberedTree,
+ };
+ } else {
+ Component = OHIFCornerstoneSRContentItem;
+ componentProps = {
+ contentItem,
+ nodeIndexesTree: childNodeLevel,
+ continuityOfContent: ContinuityOfContent,
+ };
+ }
+
+ return (
+
+ );
+ });
+
+ return (
+
+
+ {containerNumberedTree.join('.')}.
+ {CodeMeaning}
+
+
{contentItems}
+
+ );
+}
+
+OHIFCornerstoneSRContainer.propTypes = {
+ /**
+ * A tree node that may contain another container or one or more content items
+ * (text, code, uidref, pname, etc.)
+ */
+ container: PropTypes.object,
+ /**
+ * A 0-based index list
+ */
+ nodeIndexesTree: PropTypes.arrayOf(PropTypes.number),
+ /**
+ * A 1-based index list that represents a container in a multi-level numbered
+ * list (tree).
+ *
+ * Example:
+ * 1. History
+ * 1.1. Chief Complaint
+ * 1.2. Present Illness
+ * 1.3. Past History
+ * 1.4. Family History
+ * 2. Findings
+ * */
+ containerNumberedTree: PropTypes.arrayOf(PropTypes.number),
+};
diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContentItem.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContentItem.tsx
new file mode 100644
index 000000000..b12aafb7b
--- /dev/null
+++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContentItem.tsx
@@ -0,0 +1,63 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import CodeNameCodeSequenceValues from '../constants/CodeNameCodeSequenceValues';
+import formatContentItemValue from '../utils/formatContentItem';
+
+const EMPTY_TAG_VALUE = '[empty]';
+
+function OHIFCornerstoneSRContentItem(props) {
+ const { contentItem, nodeIndexesTree, continuityOfContent } = props;
+ const { ConceptNameCodeSequence } = contentItem;
+ const { CodeValue, CodeMeaning } = ConceptNameCodeSequence;
+ const isChildFirstNode = nodeIndexesTree[nodeIndexesTree.length - 1] === 0;
+ const formattedValue = formatContentItemValue(contentItem) ?? EMPTY_TAG_VALUE;
+ const startWithAlphaNumCharRegEx = /^[a-zA-Z0-9]/;
+ const isContinuous = continuityOfContent === 'CONTINUOUS';
+ const isFinding = CodeValue === CodeNameCodeSequenceValues.Finding;
+ const addExtraSpace =
+ isContinuous && !isChildFirstNode && startWithAlphaNumCharRegEx.test(formattedValue?.[0]);
+
+ // Collapse sequences of white space preserving newline characters
+ let className = 'whitespace-pre-line';
+
+ if (CodeValue === CodeNameCodeSequenceValues.Finding) {
+ // Preserve spaces because it is common to see tabular text in a
+ // "Findings" ConceptNameCodeSequence
+ className = 'whitespace-pre-wrap';
+ }
+
+ if (isContinuous) {
+ return (
+ <>
+
+ {addExtraSpace ? ' ' : ''}
+ {formattedValue}
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
{CodeMeaning}:
+ {isFinding ? (
+
{formattedValue}
+ ) : (
+
{formattedValue}
+ )}
+
+ >
+ );
+}
+
+OHIFCornerstoneSRContentItem.propTypes = {
+ contentItem: PropTypes.object,
+ nodeIndexesTree: PropTypes.arrayOf(PropTypes.number),
+ continuityOfContent: PropTypes.string,
+};
+
+export { OHIFCornerstoneSRContentItem };
diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx
similarity index 97%
rename from extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx
rename to extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx
index 6ceb7787c..e6f50fe22 100644
--- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx
+++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx
@@ -14,7 +14,7 @@ const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
-function OHIFCornerstoneSRViewport(props: withAppTypes) {
+function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
@@ -278,14 +278,14 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
*/
useEffect(() => {
const updateSR = async () => {
- if (!srDisplaySet.isLoaded) {
- await srDisplaySet.load();
- }
- if (!element || !srDisplaySet.isLoaded) {
- return;
- }
- setTrackingIdentifiers(measurementSelected);
- }
+ if (!srDisplaySet.isLoaded) {
+ await srDisplaySet.load();
+ }
+ if (!element || !srDisplaySet.isLoaded) {
+ return;
+ }
+ setTrackingIdentifiers(measurementSelected);
+ };
updateSR();
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
@@ -367,7 +367,7 @@ function OHIFCornerstoneSRViewport(props: withAppTypes) {
);
}
-OHIFCornerstoneSRViewport.propTypes = {
+OHIFCornerstoneSRMeasurementViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
@@ -508,4 +508,4 @@ function _getStatusComponent({
);
}
-export default OHIFCornerstoneSRViewport;
+export default OHIFCornerstoneSRMeasurementViewport;
diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRTextViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRTextViewport.tsx
new file mode 100644
index 000000000..78d4929b7
--- /dev/null
+++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRTextViewport.tsx
@@ -0,0 +1,32 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { ExtensionManager } from '@ohif/core';
+import { OHIFCornerstoneSRContainer } from './OHIFCornerstoneSRContainer';
+
+function OHIFCornerstoneSRTextViewport(props: withAppTypes) {
+ const { displaySets } = props;
+ const displaySet = displaySets[0];
+ const instance = displaySet.instances[0];
+
+ return (
+
+
+ {/* The root level is always a container */}
+
+
+
+ );
+}
+
+OHIFCornerstoneSRTextViewport.propTypes = {
+ displaySets: PropTypes.arrayOf(PropTypes.object),
+ viewportId: PropTypes.string.isRequired,
+ dataSource: PropTypes.object,
+ children: PropTypes.node,
+ viewportLabel: PropTypes.string,
+ viewportOptions: PropTypes.object,
+ servicesManager: PropTypes.object.isRequired,
+ extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
+};
+
+export default OHIFCornerstoneSRTextViewport;
diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRViewport.tsx
new file mode 100644
index 000000000..3bbb73a5b
--- /dev/null
+++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRViewport.tsx
@@ -0,0 +1,30 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { ExtensionManager } from '@ohif/core';
+
+import OHIFCornerstoneSRMeasurementViewport from './OHIFCornerstoneSRMeasurementViewport';
+import OHIFCornerstoneSRTextViewport from './OHIFCornerstoneSRTextViewport';
+
+function OHIFCornerstoneSRViewport(props: withAppTypes) {
+ const { displaySets } = props;
+ const { isImagingMeasurementReport } = displaySets[0];
+
+ if (isImagingMeasurementReport) {
+ return ;
+ }
+
+ return ;
+}
+
+OHIFCornerstoneSRViewport.propTypes = {
+ displaySets: PropTypes.arrayOf(PropTypes.object),
+ viewportId: PropTypes.string.isRequired,
+ dataSource: PropTypes.object,
+ children: PropTypes.node,
+ viewportLabel: PropTypes.string,
+ viewportOptions: PropTypes.object,
+ servicesManager: PropTypes.object.isRequired,
+ extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
+};
+
+export default OHIFCornerstoneSRViewport;
diff --git a/extensions/cornerstone-dicom-sr/src/constants/CodeNameCodeSequenceValues.ts b/extensions/cornerstone-dicom-sr/src/constants/CodeNameCodeSequenceValues.ts
new file mode 100644
index 000000000..cba848cbe
--- /dev/null
+++ b/extensions/cornerstone-dicom-sr/src/constants/CodeNameCodeSequenceValues.ts
@@ -0,0 +1,18 @@
+import { adaptersSR } from '@cornerstonejs/adapters';
+
+const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
+
+const CodeNameCodeSequenceValues = {
+ ImagingMeasurementReport: '126000',
+ ImageLibrary: '111028',
+ ImagingMeasurements: '126010',
+ MeasurementGroup: '125007',
+ ImageLibraryGroup: '126200',
+ TrackingUniqueIdentifier: '112040',
+ TrackingIdentifier: '112039',
+ Finding: '121071',
+ FindingSite: 'G-C0E3', // SRT
+ CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
+};
+
+export { CodeNameCodeSequenceValues as default };
diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts
index 73789049f..9c035faa2 100644
--- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts
+++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts
@@ -3,6 +3,7 @@ import { utils, classes, DisplaySetService, Types } from '@ohif/core';
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
import isRehydratable from './utils/isRehydratable';
import { adaptersSR } from '@cornerstonejs/adapters';
+import CodeNameCodeSequenceValues from './constants/CodeNameCodeSequenceValues';
type InstanceMetadata = Types.InstanceMetadata;
@@ -16,10 +17,11 @@ const { ImageSet, MetadataProvider: metadataProvider } = classes;
// Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport.
const sopClassUids = [
- '1.2.840.10008.5.1.4.1.1.88.11', //BASIC_TEXT_SR:
- '1.2.840.10008.5.1.4.1.1.88.22', //ENHANCED_SR:
- '1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
- '1.2.840.10008.5.1.4.1.1.88.34', //COMPREHENSIVE_3D_SR:
+ '1.2.840.10008.5.1.4.1.1.88.11', // BASIC TEXT SR
+ '1.2.840.10008.5.1.4.1.1.88.22', // ENHANCED SR
+ '1.2.840.10008.5.1.4.1.1.88.33', // COMPREHENSIVE SR
+ '1.2.840.10008.5.1.4.1.1.88.34', // Comprehensive 3D SR
+ // '1.2.840.10008.5.1.4.1.1.88.50', // Mammography CAD SR
];
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
@@ -34,19 +36,6 @@ const validateSameStudyUID = (uid: string, instances): void => {
});
};
-const CodeNameCodeSequenceValues = {
- ImagingMeasurementReport: '126000',
- ImageLibrary: '111028',
- ImagingMeasurements: '126010',
- MeasurementGroup: '125007',
- ImageLibraryGroup: '126200',
- TrackingUniqueIdentifier: '112040',
- TrackingIdentifier: '112039',
- Finding: '121071',
- FindingSite: 'G-C0E3', // SRT
- CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, //
-};
-
const CodingSchemeDesignators = {
SRT: 'SRT',
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
@@ -113,22 +102,10 @@ function _getDisplaySetsFromSeries(
} = instance;
validateSameStudyUID(instance.StudyInstanceUID, instances);
- if (
- !ConceptNameCodeSequence ||
- ConceptNameCodeSequence.CodeValue !== CodeNameCodeSequenceValues.ImagingMeasurementReport
- ) {
- servicesManager.services.uiNotificationService.show({
- title: 'DICOM SR',
- message:
- 'OHIF only supports TID1500 Imaging Measurement Report Structured Reports. The SR you’re trying to view is not supported.',
- type: 'warning',
- duration: 6000,
- });
- return [];
- }
+ const isImagingMeasurementReport =
+ ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurementReport;
const displaySet = {
- //plugin: id,
Modality: 'SR',
displaySetInstanceUID: utils.guid(),
SeriesDescription,
@@ -144,6 +121,7 @@ function _getDisplaySetsFromSeries(
measurements: null,
isDerivedDisplaySet: true,
isLoaded: false,
+ isImagingMeasurementReport,
sopClassUids,
instance,
addInstances,
@@ -158,7 +136,6 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte
const { displaySetService, measurementService } = servicesManager.services;
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
-
const { ContentSequence } = displaySet.instance;
async function retrieveBulkData(obj, parentObj = null, key = null) {
@@ -185,8 +162,13 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte
await retrieveBulkData(ContentSequence);
}
- displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
- displaySet.measurements = _getMeasurements(ContentSequence);
+ if (displaySet.isImagingMeasurementReport) {
+ displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
+ displaySet.measurements = _getMeasurements(ContentSequence);
+ } else {
+ displaySet.referencedImages = [];
+ displaySet.measurements = [];
+ }
const mappings = measurementService.getSourceMappings(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
@@ -371,6 +353,10 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements
);
+ if (!ImagingMeasurements) {
+ return [];
+ }
+
const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup
);
@@ -625,6 +611,10 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
);
+ if (!ImageLibrary) {
+ return [];
+ }
+
const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
);
diff --git a/extensions/cornerstone-dicom-sr/src/index.tsx b/extensions/cornerstone-dicom-sr/src/index.tsx
index b0b5e7abb..e479307d2 100644
--- a/extensions/cornerstone-dicom-sr/src/index.tsx
+++ b/extensions/cornerstone-dicom-sr/src/index.tsx
@@ -10,7 +10,7 @@ import hydrateStructuredReport from './utils/hydrateStructuredReport';
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
const Component = React.lazy(() => {
- return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport');
+ return import(/* webpackPrefetch: true */ './components/OHIFCornerstoneSRViewport');
});
const OHIFCornerstoneSRViewport = props => {
diff --git a/extensions/cornerstone-dicom-sr/src/utils/formatContentItem.ts b/extensions/cornerstone-dicom-sr/src/utils/formatContentItem.ts
new file mode 100644
index 000000000..144607755
--- /dev/null
+++ b/extensions/cornerstone-dicom-sr/src/utils/formatContentItem.ts
@@ -0,0 +1,67 @@
+import { utils } from '@ohif/core';
+
+/**
+ * Formatters used to format each of the content items (SR "nodes") which can be
+ * text, code, UID ref, number, person name, date, time and date time. Each
+ * formatter must be a function with the following signature:
+ *
+ * [VALUE_TYPE]: (contentItem) => string
+ *
+ */
+const contentItemFormatters = {
+ TEXT: contentItem => contentItem.TextValue,
+ CODE: contentItem => contentItem.ConceptCodeSequence?.[0]?.CodeMeaning,
+ UIDREF: contentItem => contentItem.UID,
+ NUM: contentItem => {
+ const measuredValue = contentItem.MeasuredValueSequence?.[0];
+
+ if (!measuredValue) {
+ return;
+ }
+
+ const { NumericValue, MeasurementUnitsCodeSequence } = measuredValue;
+ const { CodeValue } = MeasurementUnitsCodeSequence;
+
+ return `${NumericValue} ${CodeValue}`;
+ },
+ PNAME: contentItem => {
+ const personName = contentItem.PersonName?.[0]?.Alphabetic;
+ return personName ? utils.formatPN(personName) : undefined;
+ },
+ DATE: contentItem => {
+ const { Date } = contentItem;
+ return Date ? utils.formatDate(Date) : undefined;
+ },
+ TIME: contentItem => {
+ const { Time } = contentItem;
+ return Time ? utils.formatTime(Time) : undefined;
+ },
+ DATETIME: contentItem => {
+ const { DateTime } = contentItem;
+
+ if (typeof DateTime !== 'string') {
+ return;
+ }
+
+ // 14 characters because it should be something like 20180614113714
+ if (DateTime.length < 14) {
+ return DateTime;
+ }
+
+ const dicomDate = DateTime.substring(0, 8);
+ const dicomTime = DateTime.substring(8, 14);
+ const formattedDate = utils.formatDate(dicomDate);
+ const formattedTime = utils.formatTime(dicomTime);
+
+ return `${formattedDate} ${formattedTime}`;
+ },
+};
+
+function formatContentItemValue(contentItem) {
+ const { ValueType } = contentItem;
+ const fnFormat = contentItemFormatters[ValueType];
+
+ return fnFormat ? fnFormat(contentItem) : `[${ValueType} is not supported]`;
+}
+
+export { formatContentItemValue as default, formatContentItemValue };
diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts
index 0da0a69cc..08362cafe 100644
--- a/modes/longitudinal/src/index.ts
+++ b/modes/longitudinal/src/index.ts
@@ -7,7 +7,7 @@ import moreTools from './moreTools';
// Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it.
-const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG', 'RTSTRUCT'];
+const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SEG', 'RTSTRUCT'];
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
@@ -169,7 +169,7 @@ function modeFactory({ modeConfiguration }) {
valid: !!modalities_list.filter(modality => NON_IMAGE_MODALITIES.indexOf(modality) === -1)
.length,
description:
- 'The mode does not support studies that ONLY include the following modalities: SM, ECG, SR, SEG, RTSTRUCT',
+ 'The mode does not support studies that ONLY include the following modalities: SM, ECG, SEG, RTSTRUCT',
};
},
routes: [
diff --git a/platform/core/src/utils/formatTime.ts b/platform/core/src/utils/formatTime.ts
new file mode 100644
index 000000000..5d46ee047
--- /dev/null
+++ b/platform/core/src/utils/formatTime.ts
@@ -0,0 +1,12 @@
+import moment from 'moment';
+
+/**
+ * Format time in HHmmss.SSS format (24h time) into HH:mm:ss
+ *
+ * @param time - Time to be formatted
+ * @param format - Desired time format
+ * @returns Formatted time
+ */
+export default function formatTime(time: string, format = 'HH:mm:ss') {
+ return moment(time, 'HH:mm:ss').format(format);
+}
diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js
index 9ac5e103f..f72714b47 100644
--- a/platform/core/src/utils/index.test.js
+++ b/platform/core/src/utils/index.test.js
@@ -24,6 +24,7 @@ describe('Top level exports', () => {
'b64toBlob',
'createStudyBrowserTabs',
'formatDate',
+ 'formatTime',
'formatPN',
'generateAcceptHeader',
'isEqualWithin',
diff --git a/platform/core/src/utils/index.ts b/platform/core/src/utils/index.ts
index a22f8a7ef..63518961c 100644
--- a/platform/core/src/utils/index.ts
+++ b/platform/core/src/utils/index.ts
@@ -13,6 +13,7 @@ import hotkeys from './hotkeys';
import Queue from './Queue';
import isDicomUid from './isDicomUid';
import formatDate from './formatDate';
+import formatTime from './formatTime';
import formatPN from './formatPN';
import generateAcceptHeader from './generateAcceptHeader';
import resolveObjectPath from './resolveObjectPath';
@@ -56,6 +57,7 @@ const utils = {
seriesSortCriteria,
writeScript,
formatDate,
+ formatTime,
formatPN,
b64toBlob,
urlUtil,