IDC2797: improving UI for SR qualitative measurements and bug fixes (#2832)

This commit is contained in:
Davide Punzo 2022-06-29 04:24:35 +02:00 committed by GitHub
parent 5d6cf2a609
commit 1bcb0acf9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 241 additions and 80 deletions

View File

@ -237,7 +237,7 @@ class OHIFCornerstoneViewport extends Component {
} }
}; };
const warningsOverlay = props => { const overlay = props => {
const displaySet = this.props.viewportData.displaySet; const displaySet = this.props.viewportData.displaySet;
let filteredSRLabels; let filteredSRLabels;
if (displaySet.SRLabels && displaySet.SRLabels.length !== 0) { if (displaySet.SRLabels && displaySet.SRLabels.length !== 0) {
@ -264,7 +264,7 @@ class OHIFCornerstoneViewport extends Component {
imageIdIndex={currentImageIdIndex} imageIdIndex={currentImageIdIndex}
onNewImageDebounced={newImageHandler} onNewImageDebounced={newImageHandler}
onNewImageDebounceTime={300} onNewImageDebounceTime={300}
viewportOverlayComponent={warningsOverlay} viewportOverlayComponent={overlay}
stackPrefetch={this.props.stackPrefetch} stackPrefetch={this.props.stackPrefetch}
isStackPrefetchEnabled={this.props.isStackPrefetchEnabled} isStackPrefetchEnabled={this.props.isStackPrefetchEnabled}
// ~~ Connected (From REDUX) // ~~ Connected (From REDUX)

View File

@ -35,7 +35,7 @@
} }
.OHIFCornerstoneViewportOverlay .bottom-left3 { .OHIFCornerstoneViewportOverlay .bottom-left3 {
bottom: 110px; bottom: 110px;
left: 100px; left: -20px;
} }
.OHIFCornerstoneViewportOverlay .bottom-right { .OHIFCornerstoneViewportOverlay .bottom-right {
bottom: 20px; bottom: 20px;

View File

@ -18,19 +18,13 @@ import { Tooltip } from '@ohif/ui/src/components/tooltip';
import { OverlayTrigger } from '@ohif/ui/src/components/overlayTrigger'; import { OverlayTrigger } from '@ohif/ui/src/components/overlayTrigger';
const Button = styled.button` const Button = styled.button`
background-color: #3f51b5;
color: white; color: white;
padding: 5px 15px; padding: 2px 7px;
border-radius: 10px; border-radius: 10px;
outline: 0; outline: 0;
text-transform: lowercase; text-transform: none;
margin: 5px 5px; margin: 2px 2px;
cursor: pointer; cursor: pointer;
box-shadow: 0px 2px 2px lightgray;
transition: ease background-color 250ms;
&:hover {
background-color: #283593;
}
&:disabled { &:disabled {
cursor: default; cursor: default;
opacity: 0.9; opacity: 0.9;
@ -152,10 +146,14 @@ class OHIFCornerstoneViewportOverlay extends PureComponent {
const SRLabelsOn = SRLabels && SRLabels.length !== 0 ? true : false; const SRLabelsOn = SRLabels && SRLabels.length !== 0 ? true : false;
/**/
const getSRLabelsContent = SRLabels => { const getSRLabelsContent = SRLabels => {
if (Array.isArray(SRLabels)) { if (Array.isArray(SRLabels)) {
const listedSRLabels = SRLabels.map((SRLabel, index) => { const listedSRLabels = SRLabels.map((SRLabel, index) => {
const color = SRLabel.labels.color;
return ( return (
SRLabel.labels.visible && (
<OverlayTrigger <OverlayTrigger
key={index} key={index}
placement="top" placement="top"
@ -165,7 +163,10 @@ class OHIFCornerstoneViewportOverlay extends PureComponent {
className="in tooltip-warning" className="in tooltip-warning"
id="tooltip-top" id="tooltip-top"
> >
<div className="warningTitle"> Designators </div> <div className="warningTitle">
{' '}
Coding scheme designators{' '}
</div>
<div className="warningContent"> <div className="warningContent">
{SRLabel.labels.labelCodingSchemeDesignator + {SRLabel.labels.labelCodingSchemeDesignator +
' : ' + ' : ' +
@ -175,11 +176,18 @@ class OHIFCornerstoneViewportOverlay extends PureComponent {
} }
> >
<div style={{ display: 'inline-block' }}> <div style={{ display: 'inline-block' }}>
<Button disabled={true} key={index}> <Button
style={{
backgroundColor: color,
}}
disabled={true}
key={index}
>
{SRLabel.labels.label + ' : ' + SRLabel.labels.value} {SRLabel.labels.label + ' : ' + SRLabel.labels.value}
</Button> </Button>
</div> </div>
</OverlayTrigger> </OverlayTrigger>
)
); );
}); });

View File

@ -76,13 +76,13 @@ const additionalFindings = [
const currentCollections = [ const currentCollections = [
{ {
selectorAction: () => { }, selectorAction: () => {},
maxMeasurements: 3, maxMeasurements: 3,
groupName: 'Measurements', groupName: 'Measurements',
measurements: measurements, measurements: measurements,
}, },
{ {
selectorAction: () => { }, selectorAction: () => {},
groupName: 'Additional Findings', groupName: 'Additional Findings',
measurements: additionalFindings, measurements: additionalFindings,
}, },
@ -90,13 +90,13 @@ const currentCollections = [
const comparisonColletions = [ const comparisonColletions = [
{ {
selectorAction: () => { }, selectorAction: () => {},
maxMeasurements: 3, maxMeasurements: 3,
groupName: 'Measurements', groupName: 'Measurements',
measurements: measurements, measurements: measurements,
}, },
{ {
selectorAction: () => { }, selectorAction: () => {},
groupName: 'Additional Findings', groupName: 'Additional Findings',
measurements: additionalFindings, measurements: additionalFindings,
}, },
@ -128,7 +128,7 @@ const comparisonTimepoints = [
{ {
key: 'Comparison', key: 'Comparison',
date: '15-Jun-18', date: '15-Jun-18',
} },
]; ];
const MeasurementComparisonTable = () => { const MeasurementComparisonTable = () => {
@ -138,8 +138,8 @@ const MeasurementComparisonTable = () => {
timepoints={comparisonTimepoints} timepoints={comparisonTimepoints}
overallWarnings={overallWarnings} overallWarnings={overallWarnings}
measurementCollection={comparisonCollections} measurementCollection={comparisonCollections}
onRelabelClick={() => { }} onRelabelClick={() => {}}
onEditDescriptionClick={() => { }} onEditDescriptionClick={() => {}}
/> />
<div className="generate-report"> <div className="generate-report">
<button className="btn btn-primary">Generate Report</button> <button className="btn btn-primary">Generate Report</button>

View File

@ -128,9 +128,9 @@ const checkIfCanAddMeasurementsToDisplaySet = (
const imageIds = images.map(i => i.getImageId()); const imageIds = images.map(i => i.getImageId());
const SOPInstanceUIDs = images.map(i => i.SOPInstanceUID); const SOPInstanceUIDs = images.map(i => i.SOPInstanceUID);
imageDisplaySet.SRLabels = []; imageDisplaySet.SRLabels = [];
const colors = new Map();
measurements.forEach(measurement => { measurements.forEach(measurement => {
const { coords } = measurement; const { coords } = measurement;
coords.forEach((coord, index) => { coords.forEach((coord, index) => {
if (coord.ReferencedSOPSequence !== undefined) { if (coord.ReferencedSOPSequence !== undefined) {
const imageIndex = SOPInstanceUIDs.findIndex( const imageIndex = SOPInstanceUIDs.findIndex(
@ -143,13 +143,35 @@ const checkIfCanAddMeasurementsToDisplaySet = (
const imageMetadata = images[imageIndex].getData().metadata; const imageMetadata = images[imageIndex].getData().metadata;
if (coord.GraphicType === 'TEXT') { if (coord.GraphicType === 'TEXT') {
const key =
measurement.labels[index].label + measurement.labels[index].value;
let color = colors.get(key);
if (!color) {
// random dark color
color =
'hsla(' + Math.floor(Math.random() * 360) + ', 70%, 30%, 1)';
colors.set(key, color);
}
measurement.labels[index].color = color;
measurement.isSRText = true;
measurement.labels[index].visible = true;
imageDisplaySet.SRLabels.push({ imageDisplaySet.SRLabels.push({
ReferencedSOPInstanceUID: ReferencedSOPInstanceUID:
coord.ReferencedSOPSequence.ReferencedSOPInstanceUID, coord.ReferencedSOPSequence.ReferencedSOPInstanceUID,
labels: measurement.labels[index], labels: measurement.labels[index],
}); });
}
if (index === 0) {
addMeasurement(
measurement,
imageId,
imageMetadata,
imageDisplaySet.displaySetInstanceUID
);
}
} else {
addMeasurement( addMeasurement(
measurement, measurement,
imageId, imageId,
@ -158,6 +180,7 @@ const checkIfCanAddMeasurementsToDisplaySet = (
); );
} }
} }
}
}); });
}); });
}; };

View File

@ -26,8 +26,10 @@ export default function addMeasurement(
const measurementData = { const measurementData = {
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier, TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
TrackingIdentifier: measurement.TrackingIdentifier,
renderableData: {}, renderableData: {},
labels: measurement.labels, labels: measurement.labels,
isSRText: measurement.isSRText,
}; };
measurement.coords.forEach(coord => { measurement.coords.forEach(coord => {

View File

@ -6,7 +6,8 @@ export const dicomSRDisplayTool = {
options: { options: {
measurementTable: { measurementTable: {
displayFunction: data => { displayFunction: data => {
return `(SR) ${data.lesionNamingNumber || return `(SR) ${data.TrackingIdentifier ||
data.lesionNamingNumber ||
data.measurementNumber || data.measurementNumber ||
data.text || data.text ||
''}`; ''}`;

View File

@ -9,6 +9,14 @@ import { TableListItem } from './../tableList/TableListItem.js';
import './MeasurementTableItem.styl'; import './MeasurementTableItem.styl';
const ColoredCircle = ({ color }) => {
return <div className="item-color" style={{ backgroundColor: color }}></div>;
};
ColoredCircle.propTypes = {
color: PropTypes.string.isRequired,
};
class MeasurementTableItem extends Component { class MeasurementTableItem extends Component {
static propTypes = { static propTypes = {
measurementData: PropTypes.object.isRequired, measurementData: PropTypes.object.isRequired,
@ -21,6 +29,14 @@ class MeasurementTableItem extends Component {
t: PropTypes.func, t: PropTypes.func,
}; };
constructor(props) {
super(props);
this.state = {
collapsed: true,
visible: true,
};
}
render() { render() {
const { const {
warningTitle = '', warningTitle = '',
@ -93,8 +109,93 @@ class MeasurementTableItem extends Component {
actionButtons.push(deleteButton); actionButtons.push(deleteButton);
} }
if (
this.props.measurementData.isSRText === true &&
this.props.measurementData.labels &&
this.props.measurementData.labels.length > 0
) {
return (
<React.Fragment>
<TableListItem
key={this.props.measurementData.measurementNumber}
itemKey={this.props.measurementData.measurementNumber}
itemClass={`measurementItem ${this.props.itemClass} ${hasWarningClass}`}
itemIndex={this.props.itemIndex}
onItemClick={this.onItemClick}
>
<div>
<div className="measurementLocation">
{this.props.t(this.props.measurementData.label, {
keySeparator: '>',
nsSeparator: '|',
})}
</div>
</div>
<div className="icons">
<div className="displayTexts">{this.getDataDisplayText()}</div>
<Icon
className={`eye-icon`}
name={this.state.visible ? 'eye' : 'eye-closed'}
width="20px"
height="20px"
onClick={() => {
this.props.measurementData.labels.forEach(label => {
label.visible = !this.state.visible;
});
this.setState({
visible: !this.state.visible,
});
}}
/>
<Icon
className={`angle-double-${
this.state.collapsed ? 'down' : 'up'
}`}
name={`angle-double-${this.state.collapsed ? 'down' : 'up'}`}
width="20px"
height="20px"
onClick={() => {
this.setState({
collapsed: !this.state.collapsed,
});
}}
/>
</div>
</TableListItem>
{this.state.collapsed &&
this.props.measurementData.labels.map((SRLabel, index) => {
return ( return (
<TableListItem <TableListItem
key={index}
itemKey={index}
itemMeta={<ColoredCircle color={SRLabel.color} />}
itemMetaClass="item-color-section"
onItemClick={this.onItemClick}
>
<div>
<div className="icons">
<span>{SRLabel.label + ' : ' + SRLabel.value}</span>
<Icon
className={`eye-icon`}
name={SRLabel.visible ? 'eye' : 'eye-closed'}
width="20px"
height="20px"
onClick={() => {
SRLabel.visible = !SRLabel.visible;
}}
/>
</div>
</div>
</TableListItem>
);
})}
</React.Fragment>
);
} else {
return (
<TableListItem
key={this.props.measurementData.measurementNumber}
itemKey={this.props.measurementData.measurementNumber} itemKey={this.props.measurementData.measurementNumber}
itemClass={`measurementItem ${this.props.itemClass} ${hasWarningClass}`} itemClass={`measurementItem ${this.props.itemClass} ${hasWarningClass}`}
itemIndex={this.props.itemIndex} itemIndex={this.props.itemIndex}
@ -114,6 +215,7 @@ class MeasurementTableItem extends Component {
</div> </div>
</TableListItem> </TableListItem>
); );
}
}; };
onItemClick = event => { onItemClick = event => {

View File

@ -1,4 +1,16 @@
.measurementItem .measurementItem
.item-color-section
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.item-color
margin-top: 8px;
border-radius: 100%;
width: 20px;
height: 20px;
.rowActions .rowActions
margin-left: -1px; margin-left: -1px;
background-color: var(--ui-gray-darker) background-color: var(--ui-gray-darker)
@ -63,3 +75,16 @@
width: 22px; width: 22px;
height: 20px; height: 20px;
pointer-events: inherit; pointer-events: inherit;
.icons
display: flex;
align-items: center;
border-radius: 5px;
.icons .eye-icon
margin-left: 100px;
margin-right: -5px;
.icons .angle-double-down
.icons .angle-double-up
margin-left: 15px;
margin-right: -5px;

View File

@ -12,16 +12,13 @@ export class TableListItem extends Component {
itemIndex: PropTypes.number, itemIndex: PropTypes.number,
itemMeta: PropTypes.node, itemMeta: PropTypes.node,
itemMetaClass: PropTypes.string, itemMetaClass: PropTypes.string,
itemKey: PropTypes.oneOfType([ itemKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
PropTypes.string,
PropTypes.number,
]),
onItemClick: PropTypes.func.isRequired, onItemClick: PropTypes.func.isRequired,
}; };
static defaultProps = { static defaultProps = {
itemMeta: null, itemMeta: null,
itemMetaClass: '' itemMetaClass: '',
}; };
render() { render() {

View File

@ -122,6 +122,7 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
Object.keys(toolCollections).forEach(toolId => { Object.keys(toolCollections).forEach(toolId => {
const toolMeasurements = toolCollections[toolId]; const toolMeasurements = toolCollections[toolId];
const tool = tools.find(tool => tool.id === toolId); const tool = tools.find(tool => tool.id === toolId);
const { displayFunction } = tool.options.measurementTable; const { displayFunction } = tool.options.measurementTable;
@ -131,12 +132,13 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
Object.keys(groupedMeasurements).forEach(groupedMeasurementsIndex => { Object.keys(groupedMeasurements).forEach(groupedMeasurementsIndex => {
const measurementNumberList = const measurementNumberList =
groupedMeasurements[groupedMeasurementsIndex]; groupedMeasurements[groupedMeasurementsIndex];
const measurementData = measurementNumberList[0]; const measurementData = measurementNumberList[0];
const { const {
measurementNumber, measurementNumber,
lesionNamingNumber, lesionNamingNumber,
toolType, toolType,
isReadOnly isReadOnly,
} = measurementData; } = measurementData;
const measurementId = measurementData._id; const measurementId = measurementData._id;
@ -150,6 +152,8 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
const tableMeasurement = { const tableMeasurement = {
itemNumber: lesionNamingNumber, itemNumber: lesionNamingNumber,
label: getMeasurementText(measurementData), label: getMeasurementText(measurementData),
labels: measurementData.labels,
isSRText: measurementData.isSRText,
measurementId, measurementId,
measurementNumber, measurementNumber,
lesionNamingNumber, lesionNamingNumber,

View File

@ -661,10 +661,9 @@ const _mapStudiesToThumbnails = function(studies, activeDisplaySetInstanceUID) {
let altImageText; let altImageText;
if (displaySet.Modality && displaySet.Modality === 'SEG') { if (displaySet.Modality && displaySet.Modality === 'SEG') {
// TODO: We want to replace this with a thumbnail showing
// the segmentation map on the image, but this is easier
// and better than what we have right now.
altImageText = 'SEG'; altImageText = 'SEG';
} else if (displaySet.Modality && displaySet.Modality === 'SR') {
altImageText = 'SR';
} else if (displaySet.images && displaySet.images.length) { } else if (displaySet.images && displaySet.images.length) {
const imageIndex = Math.floor(displaySet.images.length / 2); const imageIndex = Math.floor(displaySet.images.length / 2);
imageId = displaySet.images[imageIndex].getImageId(); imageId = displaySet.images[imageIndex].getImageId();