From 8e9d7bd2bb3f4504deaec4666fe8a4a1f6cb0924 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 24 Mar 2023 16:45:21 -0400 Subject: [PATCH] feat(findingSite): Allow customizing the measurement panel and show site/finding (#3155) * feat: Allow customizing the measurements panel. Also includes default display of the site/finding values from the DICOM SR object. Update fixes PR review comments - move the recordInteraction to COmmandsManager.run PR fixes fix: Dependency updates to allow right click to be recognized PR fixes * Use newer CSWIL * Make both the base measurements and the tracking measurements show codes * Fix the right click context menu bringing up menu --- .../utils/getFilteredCornerstoneToolState.ts | 8 +++- .../src/utils/hydrateStructuredReport.js | 38 +++++++++++++++- extensions/cornerstone/src/commandsModule.ts | 10 ----- .../src/Panels/PanelMeasurementTable.tsx | 39 ++++++++++++++-- .../PanelMeasurementTableTracking/index.tsx | 44 ++++++++++++++++--- .../findingsContextMenu.ts | 3 +- .../MeasurementService/MeasurementService.ts | 16 ++++--- .../MeasurementTable/MeasurementItem.tsx | 1 + .../MeasurementTable/MeasurementTable.tsx | 23 +++++++++- yarn.lock | 18 +------- 10 files changed, 152 insertions(+), 48 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/utils/getFilteredCornerstoneToolState.ts b/extensions/cornerstone-dicom-sr/src/utils/getFilteredCornerstoneToolState.ts index 9cef86345..18c5b8465 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/getFilteredCornerstoneToolState.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/getFilteredCornerstoneToolState.ts @@ -35,7 +35,7 @@ function getFilteredCornerstoneToolState( ); const toolData = imageIdSpecificToolState[toolType].data; - let finding; + let { finding } = measurementDataI; const findingSites = []; // NOTE -> We use the CORNERSTONEJS coding schemeDesignator which we have @@ -56,6 +56,10 @@ function getFilteredCornerstoneToolState( } } + if (measurementDataI.findingSites) { + findingSites.push(...measurementDataI.findingSites); + } + const measurement = Object.assign({}, annotation, { finding, findingSites, @@ -73,7 +77,7 @@ function getFilteredCornerstoneToolState( for (let i = 0; i < framesOfReference.length; i++) { const frameOfReference = framesOfReference[i]; - const frameOfReferenceAnnotations = annotationManager.getFrameOfReferenceAnnotations( + const frameOfReferenceAnnotations = annotationManager.getAnnotations( frameOfReference ); diff --git a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js index bb1246e5c..346bfc5db 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js +++ b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js @@ -11,6 +11,25 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; +const convertCode = (codingValues, code) => { + if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') return; + const ref = `${code.CodingSchemeDesignator}:${code.CodeValue}`; + const ret = { ...codingValues[ref], ref, ...code, text: code.CodeMeaning }; + return ret; +}; + +const convertSites = (codingValues, sites) => { + if (!sites || !sites.length) return; + const ret = []; + // Do as a loop to convert away from Proxy instances + for (let i = 0; i < sites.length; i++) { + // Deal with irregular conversion from dcmjs + const site = convertCode(codingValues, sites[i][0] || sites[i]); + if (site) ret.push(site); + } + return (ret.length && ret) || undefined; +}; + /** * Hydrates a structured report, for default viewports. * @@ -20,8 +39,16 @@ export default function hydrateStructuredReport( displaySetInstanceUID ) { const dataSource = extensionManager.getActiveDataSource()[0]; - const { measurementService, displaySetService } = servicesManager.services; + const { + measurementService, + displaySetService, + customizationService, + } = servicesManager.services; + const codingValues = customizationService.getCustomization( + 'codingValues', + {} + ); const displaySet = displaySetService.getDisplaySetByUID( displaySetInstanceUID ); @@ -171,6 +198,15 @@ export default function hydrateStructuredReport( CORNERSTONE_3D_TOOLS_SOURCE_VERSION ); annotation.data.label = getLabelFromDCMJSImportedToolData(toolData); + annotation.data.finding = convertCode( + codingValues, + toolData.finding?.[0] + ); + annotation.data.findingSites = convertSites( + codingValues, + toolData.findingSites + ); + annotation.data.site = annotation.data.findingSites?.[0]; const matchingMapping = mappings.find( m => m.annotationType === annotationType diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 55c212446..4d1948de8 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -222,16 +222,6 @@ function commandsModule({ servicesManager, commandsManager }) { updatedMeasurement.findingSites = [code]; } } - // TODO - remove this once measurement items customization is ready - const allCodes = []; - if (textLabel) allCodes.push(textLabel); - if (updatedMeasurement.finding) { - allCodes.push(updatedMeasurement.finding.CodeMeaning); - } - (updatedMeasurement.findingSites || []).forEach(it => - allCodes.push(it.CodeMeaning) - ); - updatedMeasurement.label = allCodes.join(', '); } measurementService.update( updatedMeasurement.uid, diff --git a/extensions/default/src/Panels/PanelMeasurementTable.tsx b/extensions/default/src/Panels/PanelMeasurementTable.tsx index c2314bf08..e00b2d679 100644 --- a/extensions/default/src/Panels/PanelMeasurementTable.tsx +++ b/extensions/default/src/Panels/PanelMeasurementTable.tsx @@ -218,6 +218,7 @@ export default function PanelMeasurementTable({ > { + if (site?.text !== label) siteText.push(site.text); + }); + displayText = [...siteText, ...displayText]; + } + if (finding && finding?.text !== label) { + displayText = [finding.text, ...displayText]; + } return { uid, - label: label || '(empty)', + label, + baseLabel, measurementType: type, - displayText: displayText || [], + displayText, + baseDisplayText, isActive: selected, + finding, + findingSites, }; } diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx index 8c7d3df03..4006e016d 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx @@ -129,7 +129,8 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { setMeasurementsUpdated(Date.now().toString()); if (evt === added) { debounce(() => { - measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight; + measurementsPanelRef.current.scrollTop = + measurementsPanelRef.current.scrollHeight; }, 300)(); } }).unsubscribe @@ -261,6 +262,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { @@ -268,6 +270,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { @@ -326,13 +329,40 @@ function _mapMeasurementToDisplay(measurement, types, displaySetService) { ); } - const { displayText } = measurement; + const { + displayText: baseDisplayText, + uid, + label: baseLabel, + type, + selected, + findingSites, + finding, + } = measurement; + + const firstSite = findingSites?.[0]; + const label = baseLabel || finding?.text || firstSite?.text || '(empty)'; + let displayText = baseDisplayText || []; + if (findingSites) { + const siteText = []; + findingSites.forEach(site => { + if (site?.text !== label) siteText.push(site.text); + }); + displayText = [...siteText, ...displayText]; + } + if (finding && finding?.text !== label) { + displayText = [finding.text, ...displayText]; + } + return { - uid: measurement.uid, - label: measurement.label || '(empty)', - measurementType: measurement.type, - displayText: displayText || [], - isActive: measurement.selected, + uid, + label, + baseLabel, + measurementType: type, + displayText, + baseDisplayText, + isActive: selected, + finding, + findingSites, }; } diff --git a/extensions/test-extension/src/custom-context-menu/findingsContextMenu.ts b/extensions/test-extension/src/custom-context-menu/findingsContextMenu.ts index 21dc5b621..ebdd32eef 100644 --- a/extensions/test-extension/src/custom-context-menu/findingsContextMenu.ts +++ b/extensions/test-extension/src/custom-context-menu/findingsContextMenu.ts @@ -52,6 +52,7 @@ const findingsContextMenu = { { id: 'orientationSelectionSubMenu', + selector: ({ nearbyToolData }) => false, items: [ { customizationType: '@ohif/contextMenuAnnotationCode', @@ -66,7 +67,7 @@ const findingsContextMenu = { { id: 'findingSelectionSubMenu', - selector: ({ nearbyToolData }) => !!nearbyToolData, + selector: ({ nearbyToolData }) => false, items: [ { customizationType: '@ohif/contextMenuAnnotationCode', diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index e734e5488..3a89d2094 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -459,7 +459,12 @@ class MeasurementService extends PubSubService { log.warn(`Measurement ID not found. Generating UID: ${internalUID}`); } + const annotationData = data.annotation.data; + const newMeasurement = { + finding: annotationData.finding, + findingSites: annotationData.findingSites, + site: annotationData.findingSites?.[0], ...measurement, modifiedTimestamp: Math.floor(Date.now() / 1000), uid: internalUID, @@ -472,7 +477,7 @@ class MeasurementService extends PubSubService { measurement: newMeasurement, }); } else { - log.info(`Measurement added.`, newMeasurement); + log.info('Measurement added', newMeasurement); this.measurements[internalUID] = newMeasurement; this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, { source, @@ -553,26 +558,27 @@ class MeasurementService extends PubSubService { ); } + const oldMeasurement = this.measurements[internalUID]; + const newMeasurement = { + ...oldMeasurement, ...measurement, modifiedTimestamp: Math.floor(Date.now() / 1000), uid: internalUID, }; - if (this.measurements[internalUID]) { + if (oldMeasurement) { // TODO: Ultimately, each annotation should have a selected flag right from the soure. // For now, it is just added in OHIF here and in setMeasurementSelected. - newMeasurement.selected = this.measurements[internalUID].selected; this.measurements[internalUID] = newMeasurement; if (isUpdate) { - this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { + this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { source, measurement: newMeasurement, notYetUpdatedAtSource: false, }); } else { log.info('Measurement added.', newMeasurement); - this.measurements[internalUID] = newMeasurement; this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, { source, measurement: newMeasurement, diff --git a/platform/ui/src/components/MeasurementTable/MeasurementItem.tsx b/platform/ui/src/components/MeasurementTable/MeasurementItem.tsx index 46bf2add9..f66b9d85e 100644 --- a/platform/ui/src/components/MeasurementTable/MeasurementItem.tsx +++ b/platform/ui/src/components/MeasurementTable/MeasurementItem.tsx @@ -11,6 +11,7 @@ const MeasurementItem = ({ isActive, onClick, onEdit, + item, }) => { const [isHovering, setIsHovering] = useState(false); diff --git a/platform/ui/src/components/MeasurementTable/MeasurementTable.tsx b/platform/ui/src/components/MeasurementTable/MeasurementTable.tsx index b04809baa..441298cdb 100644 --- a/platform/ui/src/components/MeasurementTable/MeasurementTable.tsx +++ b/platform/ui/src/components/MeasurementTable/MeasurementTable.tsx @@ -1,13 +1,31 @@ import React from 'react'; +import { ServicesManager } from '@ohif/core'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import MeasurementItem from './MeasurementItem'; -const MeasurementTable = ({ data, title, onClick, onEdit }) => { +const MeasurementTable = ({ + data, + title, + onClick, + onEdit, + servicesManager, +}) => { + servicesManager = servicesManager as ServicesManager; + const { customizationService } = servicesManager.services; const { t } = useTranslation('MeasurementTable'); const amount = data.length; + const itemCustomization = customizationService.getCustomization( + 'MeasurementItem', + { + content: MeasurementItem, + contentProps: {}, + } + ); + const CustomMeasurementItem = itemCustomization.content; + return (
@@ -19,13 +37,14 @@ const MeasurementTable = ({ data, title, onClick, onEdit }) => {
{data.length !== 0 && data.map((measurementItem, index) => ( - diff --git a/yarn.lock b/yarn.lock index 0e7b213ac..154f218fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8519,23 +8519,7 @@ cornerstone-math@^0.1.9: resolved "https://registry.npmjs.org/cornerstone-math/-/cornerstone-math-0.1.10.tgz#a3f99db64d73c5adee61ae0d570128eca1682d07" integrity sha512-23XSAyP7t70ANvhFyqwvva+zFd1bQ2d5GL7tg9qKE932WmImjA2Y9tiy5n0iTtnf51W/78Png8Lia2o4dCdJaQ== -cornerstone-wado-image-loader@^4.10.0: - version "4.10.2" - resolved "https://registry.npmjs.org/cornerstone-wado-image-loader/-/cornerstone-wado-image-loader-4.10.2.tgz#139956654324fd2b01fe5b4900d0f4ed8c52633d" - integrity sha512-qj9dThELqYCm3jAZfg9qnUl8d76gngOl55kYJabY5lh/dFeVIxno/hYxy3ydE7RtG2c/TUGXb+EMUl0CJSqKBQ== - dependencies: - "@babel/eslint-parser" "^7.19.1" - "@cornerstonejs/codec-charls" "^1.2.3" - "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" - "@cornerstonejs/codec-openjpeg" "^1.2.2" - "@cornerstonejs/codec-openjph" "^2.4.2" - coverage-istanbul-loader "^3.0.5" - date-format "^4.0.14" - dicom-parser "^1.8.9" - pako "^2.0.4" - uuid "^9.0.0" - -cornerstone-wado-image-loader@^4.10.2: +cornerstone-wado-image-loader@^4.10.0, cornerstone-wado-image-loader@^4.10.2: version "4.10.2" resolved "https://registry.yarnpkg.com/cornerstone-wado-image-loader/-/cornerstone-wado-image-loader-4.10.2.tgz#139956654324fd2b01fe5b4900d0f4ed8c52633d" integrity sha512-qj9dThELqYCm3jAZfg9qnUl8d76gngOl55kYJabY5lh/dFeVIxno/hYxy3ydE7RtG2c/TUGXb+EMUl0CJSqKBQ==