From 74c14622715acb36ec95141250742afc3511c0b1 Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Thu, 25 Jun 2020 17:21:03 +0100 Subject: [PATCH] Working local download, STOW to PACS an view in SR Viewport. --- extensions/cornerstone/package.json | 2 +- .../ArrowAnnotate.js | 1 - extensions/default/package.json | 2 +- .../default/src/DicomWebDataSource/index.js | 33 ++++- .../src/Panels/WrappedPanelStudyBrowser.jsx | 2 +- extensions/dicom-html/package.json | 2 +- extensions/dicom-rt/package.json | 2 +- extensions/dicom-segmentation/package.json | 2 +- extensions/dicom-sr/package.json | 2 +- extensions/measurement-tracking/package.json | 5 +- .../ActionButtons.jsx | 16 ++- .../PanelMeasurementTableTracking/index.js | 43 ++++++- extensions/vtk/package.json | 2 +- platform/core/package.json | 2 +- platform/core/src/DICOMSR/dataExchange.js | 116 +++++++++++++++++- platform/core/src/DICOMSR/index.js | 7 +- .../core/src/DataSources/IWebApiDataSource.js | 12 +- platform/viewer/package.json | 2 +- yarn.lock | 8 +- 19 files changed, 232 insertions(+), 29 deletions(-) diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 4dd657d40..b09d30d55 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -34,7 +34,7 @@ "cornerstone-core": "^2.3.0", "cornerstone-math": "^0.1.8", "cornerstone-tools": "4.15.1", - "dcmjs": "^0.12.3", + "dcmjs": "0.14.0", "cornerstone-wado-image-loader": "^3.1.2", "dicom-parser": "^1.8.3", "hammerjs": "^2.0.8", diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js index b1d9f2f44..7d2c1c3b2 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js @@ -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'; diff --git a/extensions/default/package.json b/extensions/default/package.json index e9633cdc1..09342a611 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -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" diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index c832ee4a9..33bce395d 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -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( diff --git a/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx b/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx index c3bb27d01..e79deea68 100644 --- a/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx @@ -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 diff --git a/extensions/dicom-html/package.json b/extensions/dicom-html/package.json index d76e6fa11..eca2b16a3 100644 --- a/extensions/dicom-html/package.json +++ b/extensions/dicom-html/package.json @@ -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" diff --git a/extensions/dicom-rt/package.json b/extensions/dicom-rt/package.json index fb67f9acf..55ce43430 100644 --- a/extensions/dicom-rt/package.json +++ b/extensions/dicom-rt/package.json @@ -31,7 +31,7 @@ "@ohif/core": "^0.50.0", "cornerstone-core": "^2.2.8", "cornerstone-tools": "4.15.1", - "dcmjs": "^0.12.3", + "dcmjs": "0.14.0", "prop-types": "^15.6.2", "react": "^16.8.6", "react-dom": "^16.8.6" diff --git a/extensions/dicom-segmentation/package.json b/extensions/dicom-segmentation/package.json index ede02d636..e9214776f 100644 --- a/extensions/dicom-segmentation/package.json +++ b/extensions/dicom-segmentation/package.json @@ -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" diff --git a/extensions/dicom-sr/package.json b/extensions/dicom-sr/package.json index 6b14984c9..c8d6e6839 100644 --- a/extensions/dicom-sr/package.json +++ b/extensions/dicom-sr/package.json @@ -34,7 +34,7 @@ "cornerstone-core": "^2.3.0", "cornerstone-math": "^0.1.8", "cornerstone-tools": "4.15.1", - "dcmjs": "^0.12.3", + "dcmjs": "0.14.0", "cornerstone-wado-image-loader": "^3.1.2", "dicom-parser": "^1.8.3", "hammerjs": "^2.0.8", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 7d08ecdbd..86bad62c0 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -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", diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx index 0e3e8a3b6..6d239cf81 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx @@ -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 ( - alert('Export')}> + @@ -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; diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index edfd44aa1..a37240549 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -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,37 @@ 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) + ); + + console.log(servicesManager); + console.log(extensionManager); + + 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 ( <>
@@ -121,7 +155,10 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) { />
- +
); diff --git a/extensions/vtk/package.json b/extensions/vtk/package.json index 422d61c6f..bbe03c172 100644 --- a/extensions/vtk/package.json +++ b/extensions/vtk/package.json @@ -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", diff --git a/platform/core/package.json b/platform/core/package.json index 83320ad2f..608077d64 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -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", diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js index aab594da3..dac463a12 100644 --- a/platform/core/src/DICOMSR/dataExchange.js +++ b/platform/core/src/DICOMSR/dataExchange.js @@ -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 }; diff --git a/platform/core/src/DICOMSR/index.js b/platform/core/src/DICOMSR/index.js index 786202642..759f26cb1 100644 --- a/platform/core/src/DICOMSR/index.js +++ b/platform/core/src/DICOMSR/index.js @@ -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, }; diff --git a/platform/core/src/DataSources/IWebApiDataSource.js b/platform/core/src/DataSources/IWebApiDataSource.js index e10914e9e..10285d364 100644 --- a/platform/core/src/DataSources/IWebApiDataSource.js +++ b/platform/core/src/DataSources/IWebApiDataSource.js @@ -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. }; } diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 52c3ff3b7..de94e0357 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -69,7 +69,7 @@ "cornerstone-math": "^0.1.8", "cornerstone-tools": "4.15.1", "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", diff --git a/yarn.lock b/yarn.lock index eb88b95ac..71ee64509 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"