ohif-viewer/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx
Alireza 83ea228d86
feat: Add DICOM SEG support and MPR, referenceLines and StackSync and more (#3015)
* feat: add initial sop class handler for SEG

* feat: move segmentation service

* update segmentation service methods

* fix: viewport data structure

* feat: Add initial render for the DICOM SEG for each displaySet

* fix: various wrong architectural dependencies between services

* feat: initial separate SEG display in each viewport

* feat: refactore viewport action bar

* initial work for SEG hydration

* fix: various bugs regarding drag and dropping different viewports

* fix: rendering issues for multiple seg displaysets

* fix: bugs for thumbnail and hydration

* feat: fix the initial segment color

* update after rebase

* feat: initial design for the segmentation group table

* feat: initial new design for the segmentation panel

* feat: segmentation panel

* feat: make segmentation appear on all related viewports

* fix: segmentation load bug based on functional groups

* initial work for segmentation crosshairs

* fix: various stylings and functionality

* fix: various stylings for the seg panel

* fix: overflow styles

* feat: add more ui components

* feat: added segmentation config

* feat: add jump to segment

* feat: add jump to segment for DICOM SEG viewport

* fix: bugs after rebase

* feat: jump in segmentation viewport

* fix: mpr support for seg

* feat: add more icons

* feat: new icons

* feat: add new side panel

* feat: add segmentation config

* feat: add loading indicator to ohif

* feat: make hanging protocols follow matching rules for viewports

* feat: enhance drag and drop to be hanging protocol aware

* fix: mpr restore previous layout

* fix: crosshairs toggle

* fix: add auth headers to the dicom loader via dicomwebclient

* fix: bug for crosshairs toggle in mpr

* fix: seg viewport reusing old toolGroup

* feat: add loading animation with lottie

* fix: various bugs for Segmentation hydration in mpr

* feat: change outline alpha to outline opacity

* feat: loading indicator for seg viewport

* fix: various segmentation group styles

* fix: local mode for seg

* feat: add animation for the highlight

* fix: loading indicatro to show segment indices

* feat: enhance modality drop down ui

* fix: panels

* fix: download form

* fix: layout shift in loading indicator

* fix: loading indicator to have correct values

* fix: update software number

* fix: image jump between MPR and default

* fix: segmentation cleanup and cine service cleanup

* fix: segmentation toolgroup clena up

* fix: highlight interval should not trigger again

* fix: issue with multiframe sorting

* rename: change onSeriesChange to onArrowsClick

* fix: buttons for tmtv and layout shift

* fix: various bugs wrt crosshairs

* fix: crosshairs re init on reset camera

* fix: middle slice calculation different from cs middle reset camera

* fix: reset camera should reset viewport camera

* fix: loading segments in MPR mode

* fix: tmtv hp back to before

* fix: layout shift

* feat: orientation markers for volume viewport

* fix: capture for volume viewports

* fix: memory leak for back to worklist

* fix: loading bar bg color

* fix: side panel for only one panel

* fix: react select style in production

* fix: various styling for segmentation groups

* fix: various ui styles

* feat: add new segmentation config styles

* fix: hover state for segmentation item

* fix: side panel layout shift

* temp add panels

* try to fix scrollbar for thimbnail

* fix: scroll not appearing for side panels

* feat: changed styles for scrollbar

* feat: add cpu fallback warning

* fix: webworker destroy

* fix: select styles

* fix: orientation marker color and position

* fix: capture screenshot for volume viewports

* fix: hide segment visibility on mpr

* fix: reloading an already loaded seg displayset

* feat: add is equal

* fix: mpr jump to measurements

* apply review comments

* fix: optimization for the segmentation load

* fix: remove unnecessary context menu and hp service reset

* fix: segmentation service wrt brush settings

* feat: initial work for the stack synchronization

* fix: add validateDisplaySetSelectorsForNewDisplaySets to make sure drag and drop can follow requirements

* fix: various react proptypes

* fix: thumbnails for the tmtv and change default props to default params

* feat: make showing loading indicator configurable and add docs

* bump versions and add more docs

* fix demo

* update cornerstone versions

* apply review comments

* feat: add reference lines

* fix build

* fix unit tests

* add reference lines icon

* fix: e2e tests

* fix static wado config

* docs: add segmentation service docs

* fix: docker build

* fix: keep camera and bump versions

* Try re-enabling minification to fix deploy previews

Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
2022-11-16 14:19:52 -05:00

327 lines
9.9 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
StudySummary,
MeasurementTable,
Dialog,
Input,
useViewportGrid,
} from '@ohif/ui';
import { DicomMetadataStore, utils } from '@ohif/core';
import { useDebounce } from '@hooks';
import ActionButtons from './ActionButtons';
import { useTrackedMeasurements } from '../../getContextModule';
const { downloadCSVReport } = utils;
const { formatDate } = utils;
const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
key: undefined, //
date: '', // '07-Sep-2010',
modality: '', // 'CT',
description: '', // 'CHEST/ABD/PELVIS W CONTRAST',
};
function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
const [viewportGrid, viewportGridService] = useViewportGrid();
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(
Date.now().toString()
);
const debouncedMeasurementChangeTimestamp = useDebounce(
measurementChangeTimestamp,
200
);
const {
MeasurementService,
UIDialogService,
DisplaySetService,
} = servicesManager.services;
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
] = useTrackedMeasurements();
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
const [displayStudySummary, setDisplayStudySummary] = useState(
DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
);
const [displayMeasurements, setDisplayMeasurements] = useState([]);
useEffect(() => {
const measurements = MeasurementService.getMeasurements();
const filteredMeasurements = measurements.filter(
m =>
trackedStudy === m.referenceStudyUID &&
trackedSeries.includes(m.referenceSeriesUID)
);
const mappedMeasurements = filteredMeasurements.map(m =>
_mapMeasurementToDisplay(
m,
MeasurementService.VALUE_TYPES,
DisplaySetService
)
);
setDisplayMeasurements(mappedMeasurements);
// eslint-ignore-next-line
}, [
MeasurementService,
trackedStudy,
trackedSeries,
debouncedMeasurementChangeTimestamp,
]);
const updateDisplayStudySummary = async () => {
if (trackedMeasurements.matches('tracking')) {
const StudyInstanceUID = trackedStudy;
const studyMeta = DicomMetadataStore.getStudy(StudyInstanceUID);
const instanceMeta = studyMeta.series[0].instances[0];
const { StudyDate, StudyDescription } = instanceMeta;
const modalities = new Set();
studyMeta.series.forEach(series => {
if (trackedSeries.includes(series.SeriesInstanceUID)) {
modalities.add(series.instances[0].Modality);
}
});
const modality = Array.from(modalities).join('/');
if (displayStudySummary.key !== StudyInstanceUID) {
setDisplayStudySummary({
key: StudyInstanceUID,
date: StudyDate, // TODO: Format: '07-Sep-2010'
modality,
description: StudyDescription,
});
}
} else if (trackedStudy === '' || trackedStudy === undefined) {
setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE);
}
};
// ~~ DisplayStudySummary
useEffect(() => {
updateDisplayStudySummary();
}, [
displayStudySummary.key,
trackedMeasurements,
trackedStudy,
updateDisplayStudySummary,
]);
// TODO: Better way to consolidated, debounce, check on change?
// Are we exposing the right API for measurementService?
// This watches for ALL MeasurementService changes. It updates a timestamp,
// which is debounced. After a brief period of inactivity, this triggers
// a re-render where we grab up-to-date measurements
useEffect(() => {
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
const subscriptions = [];
[added, addedRaw, updated, removed, cleared].forEach(evt => {
subscriptions.push(
MeasurementService.subscribe(evt, () => {
setMeasurementsUpdated(Date.now().toString());
}).unsubscribe
);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, [MeasurementService, sendTrackedMeasurementsEvent]);
async function exportReport() {
const measurements = MeasurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
m =>
trackedStudy === m.referenceStudyUID &&
trackedSeries.includes(m.referenceSeriesUID)
);
downloadCSVReport(trackedMeasurements, MeasurementService);
}
const jumpToImage = ({ uid, isActive }) => {
MeasurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, uid);
onMeasurementItemClickHandler({ uid, isActive });
};
const onMeasurementItemEditHandler = ({ uid, isActive }) => {
const measurement = MeasurementService.getMeasurement(uid);
jumpToImage({ uid, isActive });
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save': {
MeasurementService.update(
uid,
{
...measurement,
...value,
},
true
);
}
}
UIDialogService.dismiss({ id: 'enter-annotation' });
};
UIDialogService.create({
id: 'enter-annotation',
centralize: true,
isDraggable: false,
showOverlay: true,
content: Dialog,
contentProps: {
title: 'Enter your annotation',
noCloseButton: true,
value: { label: measurement.label || '' },
body: ({ value, setValue }) => {
const onChangeHandler = event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
};
const onKeyPressHandler = event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
}
};
return (
<div className="p-4 bg-primary-dark">
<Input
autoFocus
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"
value={value.label}
onChange={onChangeHandler}
onKeyPress={onKeyPressHandler}
/>
</div>
);
},
actions: [
// temp: swap button types until colors are updated
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' },
],
onSubmit: onSubmitHandler,
},
});
};
const onMeasurementItemClickHandler = ({ uid, isActive }) => {
if (!isActive) {
const measurements = [...displayMeasurements];
const measurement = measurements.find(m => m.uid === uid);
measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true));
measurement.isActive = true;
setDisplayMeasurements(measurements);
}
};
const displayMeasurementsWithoutFindings = displayMeasurements.filter(
dm => dm.measurementType !== MeasurementService.VALUE_TYPES.POINT
);
const additionalFindings = displayMeasurements.filter(
dm => dm.measurementType === MeasurementService.VALUE_TYPES.POINT
);
return (
<>
<div
className="overflow-x-hidden overflow-y-auto invisible-scrollbar"
data-cy={'trackedMeasurements-panel'}
>
{displayStudySummary.key && (
<StudySummary
date={formatDate(displayStudySummary.date)}
modality={displayStudySummary.modality}
description={displayStudySummary.description}
/>
)}
<MeasurementTable
title="Measurements"
data={displayMeasurementsWithoutFindings}
onClick={jumpToImage}
onEdit={onMeasurementItemEditHandler}
/>
{additionalFindings.length !== 0 && (
<MeasurementTable
title="Additional Findings"
data={additionalFindings}
onClick={jumpToImage}
onEdit={onMeasurementItemEditHandler}
/>
)}
</div>
<div className="flex justify-center p-4">
<ActionButtons
onExportClick={exportReport}
onCreateReportClick={() => {
sendTrackedMeasurementsEvent('SAVE_REPORT', {
viewportIndex: viewportGrid.activeViewportIndex,
isBackupSave: true,
});
}}
/>
</div>
</>
);
}
PanelMeasurementTableTracking.propTypes = {
servicesManager: PropTypes.shape({
services: PropTypes.shape({
MeasurementService: PropTypes.shape({
getMeasurements: PropTypes.func.isRequired,
VALUE_TYPES: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
};
// TODO: This could be a MeasurementService mapper
function _mapMeasurementToDisplay(measurement, types, DisplaySetService) {
const { referenceStudyUID, referenceSeriesUID, SOPInstanceUID } = measurement;
// TODO: We don't deal with multiframe well yet, would need to update
// This in OHIF-312 when we add FrameIndex to measurements.
const instance = DicomMetadataStore.getInstance(
referenceStudyUID,
referenceSeriesUID,
SOPInstanceUID
);
const displaySets = DisplaySetService.getDisplaySetsForSeries(
referenceSeriesUID
);
if (!displaySets[0] || !displaySets[0].images) {
throw new Error(
'The tracked measurements panel should only be tracking "stack" displaySets.'
);
}
const { displayText } = measurement;
return {
uid: measurement.uid,
label: measurement.label || '(empty)',
measurementType: measurement.type,
displayText: displayText || [],
isActive: false, // activeMeasurementItem === i + 1,
};
}
export default PanelMeasurementTableTracking;