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
This commit is contained in:
Bill Wallace 2023-03-24 16:45:21 -04:00 committed by GitHub
parent dd22e37e4c
commit 8e9d7bd2bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 48 deletions

View File

@ -35,7 +35,7 @@ function getFilteredCornerstoneToolState(
); );
const toolData = imageIdSpecificToolState[toolType].data; const toolData = imageIdSpecificToolState[toolType].data;
let finding; let { finding } = measurementDataI;
const findingSites = []; const findingSites = [];
// NOTE -> We use the CORNERSTONEJS coding schemeDesignator which we have // 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, { const measurement = Object.assign({}, annotation, {
finding, finding,
findingSites, findingSites,
@ -73,7 +77,7 @@ function getFilteredCornerstoneToolState(
for (let i = 0; i < framesOfReference.length; i++) { for (let i = 0; i < framesOfReference.length; i++) {
const frameOfReference = framesOfReference[i]; const frameOfReference = framesOfReference[i];
const frameOfReferenceAnnotations = annotationManager.getFrameOfReferenceAnnotations( const frameOfReferenceAnnotations = annotationManager.getAnnotations(
frameOfReference frameOfReference
); );

View File

@ -11,6 +11,25 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; 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. * Hydrates a structured report, for default viewports.
* *
@ -20,8 +39,16 @@ export default function hydrateStructuredReport(
displaySetInstanceUID displaySetInstanceUID
) { ) {
const dataSource = extensionManager.getActiveDataSource()[0]; 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( const displaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID displaySetInstanceUID
); );
@ -171,6 +198,15 @@ export default function hydrateStructuredReport(
CORNERSTONE_3D_TOOLS_SOURCE_VERSION CORNERSTONE_3D_TOOLS_SOURCE_VERSION
); );
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData); 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( const matchingMapping = mappings.find(
m => m.annotationType === annotationType m => m.annotationType === annotationType

View File

@ -222,16 +222,6 @@ function commandsModule({ servicesManager, commandsManager }) {
updatedMeasurement.findingSites = [code]; 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( measurementService.update(
updatedMeasurement.uid, updatedMeasurement.uid,

View File

@ -218,6 +218,7 @@ export default function PanelMeasurementTable({
> >
<MeasurementTable <MeasurementTable
title="Measurements" title="Measurements"
servicesManager={servicesManager}
data={displayMeasurements} data={displayMeasurements}
onClick={jumpToImage} onClick={jumpToImage}
onEdit={onMeasurementItemEditHandler} onEdit={onMeasurementItemEditHandler}
@ -248,14 +249,46 @@ function _getMappedMeasurements(measurementService) {
return mappedMeasurements; return mappedMeasurements;
} }
/**
* Map the measurements to the display text.
* Adds finding and site inforamtion to the displayText and/or label,
* and provides as 'displayText' and 'label', while providing the original
* values as baseDisplayText and baseLabel
*/
function _mapMeasurementToDisplay(measurement, index, types) { function _mapMeasurementToDisplay(measurement, index, types) {
const { displayText, uid, label, type, selected } = 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 { return {
uid, uid,
label: label || '(empty)', label,
baseLabel,
measurementType: type, measurementType: type,
displayText: displayText || [], displayText,
baseDisplayText,
isActive: selected, isActive: selected,
finding,
findingSites,
}; };
} }

View File

@ -129,7 +129,8 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
setMeasurementsUpdated(Date.now().toString()); setMeasurementsUpdated(Date.now().toString());
if (evt === added) { if (evt === added) {
debounce(() => { debounce(() => {
measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight; measurementsPanelRef.current.scrollTop =
measurementsPanelRef.current.scrollHeight;
}, 300)(); }, 300)();
} }
}).unsubscribe }).unsubscribe
@ -261,6 +262,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
<MeasurementTable <MeasurementTable
title="Measurements" title="Measurements"
data={displayMeasurementsWithoutFindings} data={displayMeasurementsWithoutFindings}
servicesManager={servicesManager}
onClick={jumpToImage} onClick={jumpToImage}
onEdit={onMeasurementItemEditHandler} onEdit={onMeasurementItemEditHandler}
/> />
@ -268,6 +270,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
<MeasurementTable <MeasurementTable
title="Additional Findings" title="Additional Findings"
data={additionalFindings} data={additionalFindings}
servicesManager={servicesManager}
onClick={jumpToImage} onClick={jumpToImage}
onEdit={onMeasurementItemEditHandler} onEdit={onMeasurementItemEditHandler}
/> />
@ -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 { return {
uid: measurement.uid, uid,
label: measurement.label || '(empty)', label,
measurementType: measurement.type, baseLabel,
displayText: displayText || [], measurementType: type,
isActive: measurement.selected, displayText,
baseDisplayText,
isActive: selected,
finding,
findingSites,
}; };
} }

View File

@ -52,6 +52,7 @@ const findingsContextMenu = {
{ {
id: 'orientationSelectionSubMenu', id: 'orientationSelectionSubMenu',
selector: ({ nearbyToolData }) => false,
items: [ items: [
{ {
customizationType: '@ohif/contextMenuAnnotationCode', customizationType: '@ohif/contextMenuAnnotationCode',
@ -66,7 +67,7 @@ const findingsContextMenu = {
{ {
id: 'findingSelectionSubMenu', id: 'findingSelectionSubMenu',
selector: ({ nearbyToolData }) => !!nearbyToolData, selector: ({ nearbyToolData }) => false,
items: [ items: [
{ {
customizationType: '@ohif/contextMenuAnnotationCode', customizationType: '@ohif/contextMenuAnnotationCode',

View File

@ -459,7 +459,12 @@ class MeasurementService extends PubSubService {
log.warn(`Measurement ID not found. Generating UID: ${internalUID}`); log.warn(`Measurement ID not found. Generating UID: ${internalUID}`);
} }
const annotationData = data.annotation.data;
const newMeasurement = { const newMeasurement = {
finding: annotationData.finding,
findingSites: annotationData.findingSites,
site: annotationData.findingSites?.[0],
...measurement, ...measurement,
modifiedTimestamp: Math.floor(Date.now() / 1000), modifiedTimestamp: Math.floor(Date.now() / 1000),
uid: internalUID, uid: internalUID,
@ -472,7 +477,7 @@ class MeasurementService extends PubSubService {
measurement: newMeasurement, measurement: newMeasurement,
}); });
} else { } else {
log.info(`Measurement added.`, newMeasurement); log.info('Measurement added', newMeasurement);
this.measurements[internalUID] = newMeasurement; this.measurements[internalUID] = newMeasurement;
this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, { this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
source, source,
@ -553,26 +558,27 @@ class MeasurementService extends PubSubService {
); );
} }
const oldMeasurement = this.measurements[internalUID];
const newMeasurement = { const newMeasurement = {
...oldMeasurement,
...measurement, ...measurement,
modifiedTimestamp: Math.floor(Date.now() / 1000), modifiedTimestamp: Math.floor(Date.now() / 1000),
uid: internalUID, uid: internalUID,
}; };
if (this.measurements[internalUID]) { if (oldMeasurement) {
// TODO: Ultimately, each annotation should have a selected flag right from the soure. // 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. // For now, it is just added in OHIF here and in setMeasurementSelected.
newMeasurement.selected = this.measurements[internalUID].selected;
this.measurements[internalUID] = newMeasurement; this.measurements[internalUID] = newMeasurement;
if (isUpdate) { if (isUpdate) {
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
source, source,
measurement: newMeasurement, measurement: newMeasurement,
notYetUpdatedAtSource: false, notYetUpdatedAtSource: false,
}); });
} else { } else {
log.info('Measurement added.', newMeasurement); log.info('Measurement added.', newMeasurement);
this.measurements[internalUID] = newMeasurement;
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, { this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
source, source,
measurement: newMeasurement, measurement: newMeasurement,

View File

@ -11,6 +11,7 @@ const MeasurementItem = ({
isActive, isActive,
onClick, onClick,
onEdit, onEdit,
item,
}) => { }) => {
const [isHovering, setIsHovering] = useState(false); const [isHovering, setIsHovering] = useState(false);

View File

@ -1,13 +1,31 @@
import React from 'react'; import React from 'react';
import { ServicesManager } from '@ohif/core';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MeasurementItem from './MeasurementItem'; 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 { t } = useTranslation('MeasurementTable');
const amount = data.length; const amount = data.length;
const itemCustomization = customizationService.getCustomization(
'MeasurementItem',
{
content: MeasurementItem,
contentProps: {},
}
);
const CustomMeasurementItem = itemCustomization.content;
return ( return (
<div> <div>
<div className="flex justify-between px-2 py-1 bg-secondary-main"> <div className="flex justify-between px-2 py-1 bg-secondary-main">
@ -19,13 +37,14 @@ const MeasurementTable = ({ data, title, onClick, onEdit }) => {
<div className="overflow-hidden ohif-scrollbar max-h-112"> <div className="overflow-hidden ohif-scrollbar max-h-112">
{data.length !== 0 && {data.length !== 0 &&
data.map((measurementItem, index) => ( data.map((measurementItem, index) => (
<MeasurementItem <CustomMeasurementItem
key={measurementItem.uid} key={measurementItem.uid}
uid={measurementItem.uid} uid={measurementItem.uid}
index={index + 1} index={index + 1}
label={measurementItem.label} label={measurementItem.label}
isActive={measurementItem.isActive} isActive={measurementItem.isActive}
displayText={measurementItem.displayText} displayText={measurementItem.displayText}
item={measurementItem}
onClick={onClick} onClick={onClick}
onEdit={onEdit} onEdit={onEdit}
/> />

View File

@ -8519,23 +8519,7 @@ cornerstone-math@^0.1.9:
resolved "https://registry.npmjs.org/cornerstone-math/-/cornerstone-math-0.1.10.tgz#a3f99db64d73c5adee61ae0d570128eca1682d07" resolved "https://registry.npmjs.org/cornerstone-math/-/cornerstone-math-0.1.10.tgz#a3f99db64d73c5adee61ae0d570128eca1682d07"
integrity sha512-23XSAyP7t70ANvhFyqwvva+zFd1bQ2d5GL7tg9qKE932WmImjA2Y9tiy5n0iTtnf51W/78Png8Lia2o4dCdJaQ== integrity sha512-23XSAyP7t70ANvhFyqwvva+zFd1bQ2d5GL7tg9qKE932WmImjA2Y9tiy5n0iTtnf51W/78Png8Lia2o4dCdJaQ==
cornerstone-wado-image-loader@^4.10.0: cornerstone-wado-image-loader@^4.10.0, cornerstone-wado-image-loader@^4.10.2:
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:
version "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" 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== integrity sha512-qj9dThELqYCm3jAZfg9qnUl8d76gngOl55kYJabY5lh/dFeVIxno/hYxy3ydE7RtG2c/TUGXb+EMUl0CJSqKBQ==