Merge pull request #1811 from OHIF/feat/OHIF-177

[OHIF-177] - First pass at SR STOW with Arrow Annotate, Ellipse, Length and Bidirectional.
This commit is contained in:
Danny Brown 2020-06-25 16:24:46 -04:00 committed by GitHub
commit 1a065855f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 230 additions and 29 deletions

View File

@ -34,7 +34,7 @@
"cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.16.0",
"dcmjs": "^0.12.3",
"dcmjs": "0.14.0",
"cornerstone-wado-image-loader": "^3.1.2",
"dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8",

View File

@ -1,5 +1,4 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
import getPointsFromHandles from './utils/getPointsFromHandles';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';

View File

@ -32,7 +32,7 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"webpack": "^4.0.0",
"dcmjs": "^0.12.4"
"dcmjs": "0.14.0"
},
"dependencies": {
"@babel/runtime": "7.7.6"

View File

@ -12,9 +12,14 @@ import getImageId from './utils/getImageId';
import * as dcmjs from 'dcmjs';
import { retrieveStudyMetadata } from './retrieveStudyMetadata.js';
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary;
const { urlUtil } = utils;
const VERSION_NAME = 'OHIF-0.1';
const SR_TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
/**
*
* @param {string} name - Data source name
@ -128,6 +133,32 @@ function createDicomWebApi(dicomWebConfig) {
},
},
},
store: {
dicom: async dataset => {
const meta = {
FileMetaInformationVersion:
dataset._meta.FileMetaInformationVersion.Value,
MediaStorageSOPClassUID: dataset.SOPClassUID,
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
TransferSyntaxUID: SR_TRANSFER_SYNTAX_UID,
ImplementationClassUID: DicomMetaDictionary.uid(),
ImplementationVersionName: VERSION_NAME,
};
const denaturalized = denaturalizeDataset(meta);
const dicomDict = new DicomDict(denaturalized);
dicomDict.dict = denaturalizeDataset(dataset);
const part10Buffer = dicomDict.write();
const options = {
datasets: [part10Buffer],
};
await wadoDicomWebClient.storeInstances(options);
},
},
retrieveSeriesMetadata: async ({ StudyInstanceUID } = {}) => {
if (!StudyInstanceUID) {
throw new Error(

View File

@ -20,7 +20,7 @@ function WrappedPanelStudyBrowser({
}) {
// TODO: This should be made available a different way; route should have
// already determined our datasource
const dataSource = extensionManager.getDataSources('dicomweb')[0];
const dataSource = extensionManager.getDataSources()[0];
const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind(
null,
dataSource

View File

@ -28,7 +28,7 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dcmjs": "^0.12.3",
"dcmjs": "0.14.0",
"prop-types": "^15.6.2",
"react": "^16.11.0",
"react-dom": "^16.11.0"

View File

@ -31,7 +31,7 @@
"@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "4.16.0",
"dcmjs": "^0.12.3",
"dcmjs": "0.14.0",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"

View File

@ -31,7 +31,7 @@
"@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "4.15.1",
"dcmjs": "^0.12.2",
"dcmjs": "0.14.0",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"

View File

@ -34,7 +34,7 @@
"cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.16.0",
"dcmjs": "^0.12.3",
"dcmjs": "0.14.0",
"cornerstone-wado-image-loader": "^3.1.2",
"dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8",

View File

@ -28,11 +28,12 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dcmjs": "^0.12.4",
"dcmjs": "0.14.0",
"prop-types": "^15.6.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"webpack": "^4.0.0"
"webpack": "^4.0.0",
"cornerstone-tools": "4.15.1"
},
"dependencies": {
"@babel/runtime": "7.7.6",

View File

@ -2,10 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui';
function ActionButtons() {
function ActionButtons({ onExportClick, onCreateReportClick }) {
return (
<React.Fragment>
<ButtonGroup onClick={() => alert('Export')}>
<ButtonGroup onClick={onExportClick}>
<Button
className="px-2 py-2 text-base text-white bg-black border-primary-main"
size="initial"
@ -26,7 +26,7 @@ function ActionButtons() {
variant="outlined"
size="initial"
color="inherit"
onClick={() => alert('Create Report')}
onClick={onCreateReportClick}
>
Create Report
</Button>
@ -34,4 +34,14 @@ function ActionButtons() {
);
}
ActionButtons.propTypes = {
onExportClick: PropTypes.func,
onCreateReportClick: PropTypes.func,
};
ActionButtons.defaultProps = {
onExportClick: () => alert('Export'),
onCreateReportClick: () => alert('Create Report'),
};
export default ActionButtons;

View File

@ -1,10 +1,13 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { StudySummary, MeasurementTable } from '@ohif/ui';
import { DicomMetadataStore } from '@ohif/core';
import { DicomMetadataStore, DICOMSR } from '@ohif/core';
import { useDebounce } from '@hooks';
import ActionButtons from './ActionButtons';
import { useTrackedMeasurements } from '../../getContextModule';
import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import dcmjs from 'dcmjs';
const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
key: undefined, //
@ -13,7 +16,7 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
description: undefined, // 'CHEST/ABD/PELVIS W CONTRAST',
};
function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(
Date.now().toString()
);
@ -102,6 +105,35 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
const activeMeasurementItem = 0;
const onExportClick = () => {
const measurements = MeasurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
m =>
trackedStudy === m.referenceStudyUID &&
trackedSeries.includes(m.referenceSeriesUID)
);
// TODO -> local download.
DICOMSR.downloadReport(trackedMeasurements, dataSource);
};
const onCreateReportClick = () => {
const measurements = MeasurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
m =>
trackedStudy === m.referenceStudyUID &&
trackedSeries.includes(m.referenceSeriesUID)
);
const dataSources = extensionManager.getDataSources();
// TODO -> Eventually deal with multiple dataSources.
// Would need some way of saying which one is the "push" dataSource
const dataSource = dataSources[0];
DICOMSR.storeMeasurements(trackedMeasurements, dataSource);
};
return (
<>
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
@ -121,7 +153,10 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
/>
</div>
<div className="flex justify-center p-4">
<ActionButtons />
<ActionButtons
onExportClick={onExportClick}
onCreateReportClick={onCreateReportClick}
/>
</div>
</>
);

View File

@ -33,7 +33,7 @@
"@ohif/ui": "^2.0.0",
"cornerstone-core": "^2.3.0",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.12.2",
"dcmjs": "0.14.0",
"dicom-parser": "^1.8.3",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",

View File

@ -38,7 +38,7 @@
"dependencies": {
"@babel/runtime": "7.7.6",
"ajv": "^6.10.0",
"dcmjs": "^0.12.4",
"dcmjs": "0.14.0",
"dicomweb-client": "^0.6.0",
"immer": "6.0.2",
"isomorphic-base64": "^1.0.2",

View File

@ -6,6 +6,10 @@ import {
stowSRFromMeasurements,
} from './handleStructuredReport';
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
import cornerstoneTools from 'cornerstone-tools';
import dcmjs from 'dcmjs';
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
/**
*
@ -47,7 +51,7 @@ const retrieveMeasurements = server => {
* @param {serverType} server
* @returns {Object} With message to be displayed on success
*/
const storeMeasurements = async (measurementData, filter, server) => {
const storeMeasurementsOld = async (measurementData, filter, server) => {
log.info('[DICOMSR] storeMeasurements');
if (!server || server.type !== 'dicomWeb') {
@ -78,4 +82,112 @@ const storeMeasurements = async (measurementData, filter, server) => {
}
};
export { retrieveMeasurements, storeMeasurements };
const downloadReport = measurementData => {
const srDataset = generateReport(measurementData);
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary.
var objectUrl = URL.createObjectURL(reportBlob);
window.location.assign(objectUrl);
};
const generateReport = measurementData => {
const ids = measurementData.map(md => md.id);
const filteredToolState = _getFilteredCornerstoneToolState(ids);
const report = MeasurementReport.generateReport(
filteredToolState,
cornerstone.metaData
);
return report.dataset;
};
const storeMeasurements = async (measurementData, dataSource) => {
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
// But it is good enough for now whilst we only have cornerstone as a datasource.
log.info('[DICOMSR] storeMeasurements');
if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!');
return Promise.reject({});
}
const naturalizedReport = generateReport(measurementData);
const { StudyInstanceUID } = naturalizedReport;
try {
await dataSource.store.dicom(naturalizedReport);
if (StudyInstanceUID) {
studies.deleteStudyMetadataPromise(StudyInstanceUID);
}
return {
message: 'Measurements saved successfully',
};
} catch (error) {
log.error(
`[DICOMSR] Error while saving the measurements: ${error.message}`
);
throw new Error('Error while saving the measurements.');
}
};
function _getFilteredCornerstoneToolState(uidFilter) {
const globalToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
const filteredToolState = {};
function addToFilteredToolState(imageId, toolType, toolDataI) {
if (!filteredToolState[imageId]) {
filteredToolState[imageId] = {};
}
const imageIdSpecificToolState = filteredToolState[imageId];
if (!imageIdSpecificToolState[toolType]) {
imageIdSpecificToolState[toolType] = {
data: [],
};
}
const toolData = imageIdSpecificToolState[toolType].data;
toolData.push(toolDataI);
}
const uids = uidFilter.slice();
const imageIds = Object.keys(globalToolState);
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const imageIdSpecificToolState = globalToolState[imageId];
const toolTypes = Object.keys(imageIdSpecificToolState);
for (let j = 0; j < toolTypes.length; j++) {
const toolType = toolTypes[j];
const toolData = imageIdSpecificToolState[toolType].data;
if (toolData) {
for (let k = 0; k < toolData.length; k++) {
const toolDataK = toolData[k];
const uidIndex = uids.findIndex(uid => uid === toolDataK.id);
if (uidIndex !== -1) {
addToFilteredToolState(imageId, toolType, toolDataK);
uids.splice(uidIndex, 1);
if (!uids.length) {
return filteredToolState;
}
}
}
}
}
}
return filteredToolState;
}
export { retrieveMeasurements, storeMeasurements, downloadReport };

View File

@ -1,9 +1,14 @@
import { retrieveMeasurements, storeMeasurements } from './dataExchange';
import {
retrieveMeasurements,
storeMeasurements,
downloadReport,
} from './dataExchange';
import isToolSupported from './utils/isToolSupported';
const DICOMSR = {
retrieveMeasurements,
storeMeasurements,
downloadReport,
isToolSupported,
};

View File

@ -15,6 +15,7 @@ import { DicomMetadataStore } from '@ohif/core';
function create({
query,
retrieve,
store,
retrieveSeriesMetadata,
getImageIdsForDisplaySet,
}) {
@ -44,13 +45,20 @@ function create({
series: {},
};
const defaultStore = {
dicom: async naturalizedDataset => {
throw new Error(
'store.dicom(naturalizedDicom, StudyInstanceUID) not implemented for dataSource.'
);
},
};
return {
query: query || defaultQuery,
retrieve: retrieve || defaultRetrieve,
store: store || defaultStore,
getImageIdsForDisplaySet,
retrieveSeriesMetadata,
// then go get all series level metadata.
// Store this in the DICOM MetadataStore.
};
}

View File

@ -69,7 +69,7 @@
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.16.0",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "^0.12.2",
"dcmjs": "0.14.0",
"dicom-parser": "^1.8.3",
"dicomweb-client": "^0.4.4",
"dotenv-webpack": "^1.7.0",

View File

@ -7353,10 +7353,10 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dcmjs@^0.12.2, dcmjs@^0.12.4:
version "0.12.4"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.12.4.tgz#82c24abdc357ea5281b78eb2cae8b781f7392aa3"
integrity sha512-N1ZsXqZIysirqdytb7h572TyIjmxpvCjrzdjtQsuPN8gC2EpxsUHQ598CPzaJBpBy9i1kfKuq4h2Jwt99cr/QQ==
dcmjs@0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.0.tgz#0dc6cb2d15ddcff759bc9002f2a9704537735d1c"
integrity sha512-VL/Ibxe5RDsc5j5SEv3aEqdlKuBXz81/bBuW59Or0cos9vgK3XnVl3rr0ct6DWXJGK8vGIUMBOguyd/NRvlN0w==
dependencies:
"@babel/polyfill" "^7.8.3"
"@babel/runtime" "^7.8.4"