feat: 🎸 DICOM SR STOW on MeasurementAPI (#954)

* feat: 🎸 DICOM SR STOW on MeasurementAPI

Closes: #758

* feat: 🎸 Adding the dataExchance from DICOM SR

* feat: 🎸 Fix some issues and get the retrieve measurements working

* feat: 🎸 Preventing measurement tool helpers to broke because the data is not Number (it comes as string from dcmjs)

* feat: 🎸 Adding a button to start testing SATE functionality

* feat: 🎸Small fixes into the code

* feat: 🎸 MAke sure we get latest SR in the study, even on lazyLoad mode

* Revert "feat: 🎸 Preventing measurement tool helpers to broke because the data is not Number (it comes as string from dcmjs)"

This reverts commit 4b3a1efb5810487d2319826fd59c03d779882b5e.

* feat: 🎸 Make sure after sync we update the viewports and also that annotation tools are set to passive, so it can be drawn in the viewports

* feat: 🎸 Remove console logs and start using OHIF.log

* feat: 🎸 Refactor measurement table to use Snackbar service of messages and make sure we only show save button if server type is dicomWeb

* feat: 🎸 Refactor to remove mocked wadoRoot and use server

* Fix to expose error once we can't save the SR

* Update DCMJS into latest dev version

* Update dcmjs with latest fixes

* Fix PR callouts

* Logging error on MeasurementReport creation

* Preventing from breaking on non supported tools and also warning that some toolType is not sopported

* Small error message update

* Expose better the errors and when we have unsopported tools into the measurements

* Renaming variable

* Expose dcmjs tool support check

* Change the way we warn the unsupportedTools

* Adding style to the button

* Small change on button style

* Fix git conflicts

* Fix after merge with latest master

* Fix after merge with master, btn was not clickable

* Fixing callouts

* Adding util functions into files and refactor with JSDocs

* Refactoring code, adding some basic JSDocs

* Bump dcmjs with latest version and fixes

* JSDocs and small refactors

* yarn lock file after dcmjs update

* Remove unused parameters

* Registering a plain copy of server into measurementAPI

* Small refactor

* Start changing from toolType to toolName

* Rename a function to get instanceMetadata

* Adding error if server is not DICOMWeb on retrieve

* Move snackbar outside of UI component and use UINotificationService

* Refactor and JSDocs

* Small fixes on JSDocs (style)

* Remove unused import

* Improve JSDocs on MeasurementApi

* Remove refactor leftover

* Improve JSDocs and propTypes

* JSDocs and small refactors
This commit is contained in:
Gustavo André Lelis 2019-12-11 13:14:01 -03:00 committed by Danny Brown
parent 5cda5159da
commit ebe1af8d4f
25 changed files with 772 additions and 133 deletions

View File

@ -34,7 +34,7 @@
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0", "cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -123,6 +123,16 @@ export default function init({ servicesManager, configuration }) {
csTools.addTool(tool, props); csTools.addTool(tool, props);
}); });
// TODO -> We need a better way to do this with maybe global tool state setting all tools passive.
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
tools.forEach(tool => {
if (tool.prototype instanceof BaseAnnotationTool) {
// BaseAnnotationTool would likely come from csTools lib exports
const toolName = new tool().name;
csTools.setToolPassive(toolName); // there may be a better place to determine name; may not be on uninstantiated class
}
});
csTools.setToolActive('Pan', { mouseButtonMask: 4 }); csTools.setToolActive('Pan', { mouseButtonMask: 4 });
csTools.setToolActive('Zoom', { mouseButtonMask: 2 }); csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 }); csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });

View File

@ -29,7 +29,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6" "react-dom": "^16.8.6"

View File

@ -34,7 +34,7 @@
"@ohif/ui": "^0.50.0", "@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"i18next": "^17.0.3", "i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",
@ -56,7 +56,6 @@
"@ohif/ui": "^1.0.1", "@ohif/ui": "^1.0.1",
"cornerstone-tools": "^4.8.0", "cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"gh-pages": "^2.0.1", "gh-pages": "^2.0.1",
"i18next": "^17.0.3", "i18next": "^17.0.3",

View File

@ -1,4 +1,5 @@
import cornerstoneTools from 'cornerstone-tools'; import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import log from '../../log'; import log from '../../log';
import getLabel from '../lib/getLabel'; import getLabel from '../lib/getLabel';
import getDescription from '../lib/getDescription'; import getDescription from '../lib/getDescription';
@ -7,13 +8,32 @@ import guid from '../../utils/guid';
import studyMetadataManager from '../../utils/studyMetadataManager'; import studyMetadataManager from '../../utils/studyMetadataManager';
import { measurementApiDefaultConfig } from './../configuration.js'; import { measurementApiDefaultConfig } from './../configuration.js';
const configuration = { const configuration = {
...measurementApiDefaultConfig, ...measurementApiDefaultConfig
}; };
export default class MeasurementApi { export default class MeasurementApi {
static Instance; static Instance;
/**
* Set configuration: It should merge default configuration with any new one
*
* @static
* @param {Object} config
* @param {Object} config.server
* @param {string} config.server.type - The server type
* @param {string} config.server.wadoRoot - The server wado URL root
* @param {Array} config.measurementTools
* @param {string} config.measurementTools[].id - The tool group id
* @param {string} config.measurementTools[].name - The tool group name
* @param {Array} config.measurementTools[].childTools - The child tool's configuration
* @param {Object} config.dataExchange
* @param {Function} config.dataExchange.store - Function that store measurement data
* @param {Function} config.dataExchange.retrieve - Function that retrieves measurement data
*
* @memberof MeasurementApi
*/
static setConfiguration(config) { static setConfiguration(config) {
Object.assign(configuration, config); Object.assign(configuration, config);
} }
@ -203,13 +223,14 @@ export default class MeasurementApi {
retrieveMeasurements(patientId, timepointIds) { retrieveMeasurements(patientId, timepointIds) {
const retrievalFn = configuration.dataExchange.retrieve; const retrievalFn = configuration.dataExchange.retrieve;
const { server } = configuration;
if (typeof retrievalFn !== 'function') { if (typeof retrievalFn !== 'function') {
log.error('Measurement retrieval function has not been configured.'); log.error('Measurement retrieval function has not been configured.');
return; return;
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
retrievalFn(patientId, timepointIds).then(measurementData => { retrievalFn(server).then(measurementData => {
if (measurementData) { if (measurementData) {
log.info('Measurement data retrieval'); log.info('Measurement data retrieval');
log.info(measurementData); log.info(measurementData);
@ -230,6 +251,10 @@ export default class MeasurementApi {
// Synchronize the new tool data // Synchronize the new tool data
this.syncMeasurementsAndToolData(); this.syncMeasurementsAndToolData();
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
// Let others know that the measurements are updated // Let others know that the measurements are updated
this.onMeasurementsUpdated(); this.onMeasurementsUpdated();
}, reject); }, reject);
@ -237,6 +262,7 @@ export default class MeasurementApi {
} }
storeMeasurements(timepointId) { storeMeasurements(timepointId) {
const { server } = configuration;
const storeFn = configuration.dataExchange.store; const storeFn = configuration.dataExchange.store;
if (typeof storeFn !== 'function') { if (typeof storeFn !== 'function') {
log.error('Measurement store function has not been configured.'); log.error('Measurement store function has not been configured.');
@ -278,8 +304,9 @@ export default class MeasurementApi {
}; };
log.info('Saving Measurements for timepoints:', timepoints); log.info('Saving Measurements for timepoints:', timepoints);
return storeFn(measurementData, filter).then(() => { return storeFn(measurementData, filter, server).then(result => {
log.info('Measurement storage completed'); log.info('Measurement storage completed');
return result;
}); });
} }

View File

@ -22,8 +22,9 @@ class MeasurementTable extends Component {
onDeleteClick: PropTypes.func, onDeleteClick: PropTypes.func,
onEditDescriptionClick: PropTypes.func, onEditDescriptionClick: PropTypes.func,
selectedMeasurementNumber: PropTypes.number, selectedMeasurementNumber: PropTypes.number,
overwallWarnings: PropTypes.object,
t: PropTypes.func, t: PropTypes.func,
saveFunction: PropTypes.func,
onSaveComplete: PropTypes.func,
}; };
static defaultProps = { static defaultProps = {
@ -38,8 +39,9 @@ class MeasurementTable extends Component {
}; };
render() { render() {
const hasOverallWarnings = const { overallWarnings, saveFunction, t } = this.props;
this.props.overallWarnings.warningList.length > 0; const hasOverallWarnings = overallWarnings.warningList.length > 0;
return ( return (
<div className="measurementTable"> <div className="measurementTable">
<div className="measurementTableHeader"> <div className="measurementTableHeader">
@ -55,7 +57,7 @@ class MeasurementTable extends Component {
style={{}} style={{}}
> >
<div className="warningTitle"> <div className="warningTitle">
{this.props.t('Criteria nonconformities')} {t('Criteria nonconformities')}
</div> </div>
<div className="warningContent"> <div className="warningContent">
{this.getWarningContent()} {this.getWarningContent()}
@ -75,10 +77,42 @@ class MeasurementTable extends Component {
<ScrollableArea> <ScrollableArea>
<div>{this.getMeasurementsGroups()}</div> <div>{this.getMeasurementsGroups()}</div>
</ScrollableArea> </ScrollableArea>
<div className="measurementTableFooter">
{saveFunction && (
<button onClick={this.saveFunction} className="saveBtn">
<Icon name="save" width="14px" height="14px" />
Save measurements
</button>
)}
</div>
</div> </div>
); );
} }
saveFunction = async event => {
const { saveFunction, onSaveComplete } = this.props;
if (saveFunction) {
try {
const result = await saveFunction();
if (onSaveComplete) {
onSaveComplete({
title: 'STOW SR',
message: result.message,
type: 'success',
});
}
} catch (error) {
if (onSaveComplete) {
onSaveComplete({
title: 'STOW SR',
message: error.message,
type: 'error',
});
}
}
}
};
getMeasurementsGroups = () => { getMeasurementsGroups = () => {
return this.props.measurementCollection.map((measureGroup, index) => { return this.props.measurementCollection.map((measureGroup, index) => {
return ( return (
@ -157,7 +191,7 @@ class MeasurementTable extends Component {
}; };
getWarningContent = () => { getWarningContent = () => {
const { warningList = '' } = this.props.overwallWarnings; const { warningList = '' } = this.props.overallWarnings;
if (Array.isArray(warningList)) { if (Array.isArray(warningList)) {
const listedWarnings = warningList.map((warn, index) => { const listedWarnings = warningList.map((warn, index) => {

View File

@ -1,92 +1,117 @@
.measurementTable .measurementTable
background-color: var(--primary-background-color) background-color: var(--primary-background-color)
height: 100%; height: 100%
width: 100%; width: 100%
display: flex; display: flex
flex-direction: column; flex-direction: column
.maxMeasurements .maxMeasurements
border-radius: 3px; border-radius: 3px
color: var(--primary-background-color); color: var(--primary-background-color)
font-size: 12px; font-size: 12px
font-weight: 500; font-weight: 500
height: 19px; height: 19px
line-height: 17px; line-height: 17px
margin-top: 22px; margin-top: 22px
margin-right: 15px; margin-right: 15px
padding: 2px 6px 0; padding: 2px 6px 0
text-transform: uppercase; text-transform: uppercase
transition: all 0.3s ease; transition: all 0.3s ease
background-color: var(--default-color); background-color: var(--default-color)
.measurementTableHeader .measurementTableHeader
padding-left: 45px; padding-left: 45px
.measurementTableHeaderItem .measurementTableHeaderItem
display: inline-block; display: inline-block
margin-top: 9px; margin-top: 9px
margin-left: 9px; margin-left: 9px
padding-left: 9px; padding-left: 9px
width: 90px; width: 90px
font-size: 14px; font-size: 14px
line-height: 20px; line-height: 20px
border-left: 1px solid var(--text-secondary-color); border-left: 1px solid var(--text-secondary-color)
color: var(--text-primary-color); color: var(--text-primary-color)
.timepointLabel .timepointLabel
color: var(--text-secondary-color); color: var(--text-secondary-color)
font-size: 12px; font-size: 12px
.warning-status .warning-status
display: block; display: block
position: absolute; position: absolute
width: 50px; width: 50px
height: 40px; height: 40px
padding-left: 7px; padding-left: 7px
left: 15px; left: 15px
top: 7px; top: 7px
.warning-border .warning-border
display: block; display: block
border: 2px solid #e29e4a; border: 2px solid #e29e4a
border-radius: 36px; border-radius: 36px
padding: 6px 7px; padding: 6px 7px
width: 40px; width: 40px
height: 40px; height: 40px
svg svg
color: #e29e4a; color: #e29e4a
width: 22px; width: 22px
height: 20px; height: 20px
pointer-events: inherit; pointer-events: inherit
.measurementTableFooter
padding: 10px
color: #fff
text-align: center
z-index: 1
.saveBtn
border: 1px solid var(--ui-border-color-active)
border-radius: 25px
background-color: var(--active-color)
color: var(--ui-gray-dark)
line-height: 25px
padding: 0 20px
outline: none
cursor: pointer
&:hover, &:active
background-color: var(--ui-sky-blue)
svg
margin-right: 4px
position: relative
top: 2px
.tooltip-warning .tooltip-warning
&.in &.in
opacity 1; opacity 1
.tooltip-inner .tooltip-inner
padding: 0 padding: 0
.warningTitle .warningTitle
text-align: left text-align: left
background-color: var(--ui-gray-darkest); background-color: var(--ui-gray-darkest)
padding: 7px 15px; padding: 7px 15px
border-top-left-radius: 7px; border-top-left-radius: 7px
border-top-right-radius: 7px; border-top-right-radius: 7px
.warningContent .warningContent
text-align center text-align center
background-color: var(--ui-gray); background-color: var(--ui-gray)
border-bottom-left-radius: 7px; border-bottom-left-radius: 7px
border-bottom-right-radius: 7px; border-bottom-right-radius: 7px
ol ol
margin-top: auto; margin-top: auto
padding-inline-start: 20px; padding-inline-start: 20px
padding-top: 10px; padding-top: 10px
padding-bottom: 10px; padding-bottom: 10px
padding-left: 25px; padding-left: 25px
text-align: left; text-align: left
li li
padding-right: 10px; padding-right: 10px

View File

@ -62,6 +62,7 @@ import powerOff from './icons/power-off.svg';
import reset from './icons/reset.svg'; import reset from './icons/reset.svg';
import rotate from './icons/rotate.svg'; import rotate from './icons/rotate.svg';
import rotateRight from './icons/rotate-right.svg'; import rotateRight from './icons/rotate-right.svg';
import saveRegular from './icons/save-regular.svg';
import search from './icons/search.svg'; import search from './icons/search.svg';
import searchPlus from './icons/search-plus.svg'; import searchPlus from './icons/search-plus.svg';
import softTissue from './icons/soft-tissue.svg'; import softTissue from './icons/soft-tissue.svg';
@ -164,6 +165,7 @@ const ICONS = {
'soft-tissue': softTissue, 'soft-tissue': softTissue,
lung, lung,
liver, liver,
save: saveRegular,
}; };
/** /**

View File

@ -0,0 +1,11 @@
<svg
aria-hidden="true"
focusable="false"
data-icon="save"
role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<path
fill="currentColor"
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM272 80v80H144V80h128zm122 352H54a6 6 0 0 1-6-6V86a6 6 0 0 1 6-6h42v104c0 13.255 10.745 24 24 24h176c13.255 0 24-10.745 24-24V83.882l78.243 78.243a6 6 0 0 1 1.757 4.243V426a6 6 0 0 1-6 6zM224 232c-48.523 0-88 39.477-88 88s39.477 88 88 88 88-39.477 88-88-39.477-88-88-88zm0 128c-22.056 0-40-17.944-40-40s17.944-40 40-40 40 17.944 40 40-17.944 40-40 40z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 738 B

View File

@ -60,7 +60,7 @@
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0", "cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0", "cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1", "dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"dicomweb-client": "^0.4.4", "dicomweb-client": "^0.4.4",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",

View File

@ -3,12 +3,20 @@ import { MeasurementTable } from '@ohif/ui';
import OHIF from '@ohif/core'; import OHIF from '@ohif/core';
import moment from 'moment'; import moment from 'moment';
import cornerstone from 'cornerstone-core'; import cornerstone from 'cornerstone-core';
import DICOMSR from './../../lib/DICOMSR';
import jumpToRowItem from './jumpToRowItem.js'; import jumpToRowItem from './jumpToRowItem.js';
const { setViewportSpecificData } = OHIF.redux.actions; const { setViewportSpecificData } = OHIF.redux.actions;
const { MeasurementApi } = OHIF.measurements; const { MeasurementApi } = OHIF.measurements;
/**
* Takes a list of objects and a property and return the list grouped by the property
*
* @param {Array} list - The objects to be grouped by
* @param {string} props - The property to group the objects
* @returns {Object}
*/
function groupBy(list, props) { function groupBy(list, props) {
return list.reduce((a, b) => { return list.reduce((a, b) => {
(a[b[props]] = a[b[props]] || []).push(b); (a[b[props]] = a[b[props]] || []).push(b);
@ -16,16 +24,29 @@ function groupBy(list, props) {
}, {}); }, {});
} }
function getAllTools() { /**
const config = OHIF.measurements.MeasurementApi.getConfiguration(); * Takes a list of tools grouped and return all tools separately
*
* @param {Array} [toolGroups=[]] - The grouped tools
* @returns {Array} - The list of all tools on all groups
*/
function getAllTools(toolGroups = []) {
let tools = []; let tools = [];
config.measurementTools.forEach( toolGroups.forEach(
toolGroup => (tools = tools.concat(toolGroup.childTools)) toolGroup => (tools = tools.concat(toolGroup.childTools))
); );
return tools; return tools;
} }
/**
* Takes measurementData and build the measurement text to be used into the table
*
* @param {Object} [measurementData={}]
* @param {string} measurementData.location - The measurement location
* @param {string} measurementData.description - The measurement description
* @returns {string}
*/
function getMeasurementText(measurementData = {}) { function getMeasurementText(measurementData = {}) {
const defaultText = '...'; const defaultText = '...';
const { location = '', description = '' } = measurementData; const { location = '', description = '' } = measurementData;
@ -34,6 +55,14 @@ function getMeasurementText(measurementData = {}) {
return result || defaultText; return result || defaultText;
} }
/**
* Takes a list of measurements grouped by measurement numbers and return each measurement data by available timepoint
*
* @param {Array} measurementNumberList - The list of measurements
* @param {Array} timepoints - The list of available timepoints
* @param {Function} displayFunction - The function that builds the display text by each tool
* @returns
*/
function getDataForEachMeasurementNumber( function getDataForEachMeasurementNumber(
measurementNumberList, measurementNumberList,
timepoints, timepoints,
@ -56,10 +85,35 @@ function getDataForEachMeasurementNumber(
return data; return data;
} }
/**
* Take a measurement toolName and return if any warnings
*
* @param {string} toolName - The tool name
* @returns {string}
*/
function getWarningsForMeasurement(toolName) {
const isToolSupported = DICOMSR.isToolSupported(toolName);
return {
hasWarnings: !isToolSupported,
warningTitle: isToolSupported ? '' : 'Unsupported Tool',
warningList: isToolSupported
? []
: [`${toolName} cannot be persisted at this time`],
};
}
/**
* Take measurements from MeasurementAPI structure and convert into a measurementTable structure
*
* @param {Object} toolCollections - The list of all measurement grouped by groupTool and toolName
* @param {Array} timepoints - The list of available timepoints
* @returns
*/
function convertMeasurementsToTableData(toolCollections, timepoints) { function convertMeasurementsToTableData(toolCollections, timepoints) {
const config = OHIF.measurements.MeasurementApi.getConfiguration(); const config = OHIF.measurements.MeasurementApi.getConfiguration();
const toolGroups = config.measurementTools; const toolGroups = config.measurementTools;
const tools = getAllTools(); const tools = getAllTools(toolGroups);
const tableMeasurements = toolGroups.map(toolGroup => { const tableMeasurements = toolGroups.map(toolGroup => {
return { return {
@ -88,6 +142,12 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
} = measurementData; } = measurementData;
const measurementId = measurementData._id; const measurementId = measurementData._id;
const {
hasWarnings,
warningTitle,
warningList,
} = getWarningsForMeasurement(toolType);
//check if all measurements with same measurementNumber will have same LABEL //check if all measurements with same measurementNumber will have same LABEL
const tableMeasurement = { const tableMeasurement = {
itemNumber: lesionNamingNumber, itemNumber: lesionNamingNumber,
@ -96,10 +156,10 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
measurementNumber, measurementNumber,
lesionNamingNumber, lesionNamingNumber,
toolType, toolType,
hasWarnings: false, //TODO hasWarnings,
warningTitle: '', //TODO warningTitle,
warningList,
isSplitLesion: false, //TODO isSplitLesion: false, //TODO
warningList: [], //TODO
data: getDataForEachMeasurementNumber( data: getDataForEachMeasurementNumber(
measurementNumberList, measurementNumberList,
timepoints, timepoints,
@ -126,6 +186,13 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
return tableMeasurements; return tableMeasurements;
} }
/**
* Take a list of available timepoints and return a list header information for each timepoint
*
* @param {Array} timepoints - The list of available timepoints
* @param {string} timepoints[].latestDate - The date of the last study taken on the timepoint
* @returns {{label: string, key: string, date: string}[]}
*/
function convertTimepointsToTableData(timepoints) { function convertTimepointsToTableData(timepoints) {
if (!timepoints || !timepoints.length) { if (!timepoints || !timepoints.length) {
return []; return [];
@ -140,8 +207,28 @@ function convertTimepointsToTableData(timepoints) {
]; ];
} }
/**
* Takes server type and return a function or undefined
*
* @param {string} serverType - The server type
* @returns {undefined|Function}
*/
function getSaveFunction(serverType) {
if (serverType === 'dicomWeb') {
return () => {
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
const promise = measurementApi.storeMeasurements();
return promise;
};
}
}
const mapStateToProps = state => { const mapStateToProps = state => {
const { timepoints, measurements } = state.timepointManager; const { timepointManager, servers } = state;
const { timepoints, measurements } = timepointManager;
const activeServer = servers.servers.find(a => a.active === true);
const saveFunction = getSaveFunction(activeServer.type);
return { return {
timepoints: convertTimepointsToTableData(timepoints), timepoints: convertTimepointsToTableData(timepoints),
measurementCollection: convertMeasurementsToTableData( measurementCollection: convertMeasurementsToTableData(
@ -150,6 +237,7 @@ const mapStateToProps = state => {
), ),
timepointManager: state.timepointManager, timepointManager: state.timepointManager,
viewports: state.viewports, viewports: state.viewports,
saveFunction,
}; };
}; };
@ -265,10 +353,15 @@ const mapDispatchToProps = (dispatch, ownProps) => {
}; };
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => { const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
const { timepoints, saveFunction, measurementCollection } = propsFromState;
const { onSaveComplete, selectedMeasurementNumber } = ownProps;
return { return {
timepoints: propsFromState.timepoints, timepoints,
measurementCollection: propsFromState.measurementCollection, saveFunction,
selectedMeasurementNumber: ownProps.selectedMeasurementNumber, measurementCollection,
onSaveComplete,
selectedMeasurementNumber,
...propsFromDispatch, ...propsFromDispatch,
onItemClick: (event, measurementData) => { onItemClick: (event, measurementData) => {
// TODO: Add timepointId to .data for measurementData? // TODO: Add timepointId to .data for measurementData?

View File

@ -11,8 +11,9 @@ export default {
preRegistration({ servicesManager, commandsManager, configuration = {} }) { preRegistration({ servicesManager, commandsManager, configuration = {} }) {
init({ servicesManager, commandsManager, configuration }); init({ servicesManager, commandsManager, configuration });
}, },
getPanelModule({ servicesManager, commandsManager }) { getPanelModule({ servicesManager, commandsManager }) {
const { UILabellingFlowService } = servicesManager.services; const { UILabellingFlowService, UINotificationService } = servicesManager.services;
const ExtendedConnectedMeasurementTable = () => ( const ExtendedConnectedMeasurementTable = () => (
<ConnectedMeasurementTable <ConnectedMeasurementTable
onRelabel={tool => { onRelabel={tool => {
@ -38,6 +39,11 @@ export default {
}); });
} }
}} }}
onSaveComplete={message => {
if (UINotificationService) {
UINotificationService.show(message);
}
}}
/> />
); );
return { return {

View File

@ -1,13 +1,20 @@
import { connect } from "react-redux"; import { connect } from 'react-redux';
import Viewer from "./Viewer.js"; import Viewer from './Viewer.js';
import OHIF from "@ohif/core"; import OHIF from '@ohif/core';
const { setTimepoints, setMeasurements } = OHIF.redux.actions; const { setTimepoints, setMeasurements } = OHIF.redux.actions;
const mapStateToProps = (state, ownProps) => { const getActiveServer = servers => {
const isActive = a => a.active === true;
return servers.servers.find(isActive);
};
const mapStateToProps = state => {
const { viewports, servers } = state;
return { return {
viewports: state.viewports.viewportSpecificData, viewports: viewports.viewportSpecificData,
activeViewportIndex: state.viewports.activeViewportIndex activeViewportIndex: viewports.activeViewportIndex,
activeServer: getActiveServer(servers),
}; };
}; };
@ -18,7 +25,7 @@ const mapDispatchToProps = dispatch => {
}, },
onMeasurementsUpdated: measurements => { onMeasurementsUpdated: measurements => {
dispatch(setMeasurements(measurements)); dispatch(setMeasurements(measurements));
} },
}; };
}; };

View File

@ -11,6 +11,7 @@ import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js'; import ConnectedViewerMain from './ConnectedViewerMain.js';
import SidePanel from './../components/SidePanel.js'; import SidePanel from './../components/SidePanel.js';
import { extensionManager } from './../App.js'; import { extensionManager } from './../App.js';
import DICOMSR from '../lib/DICOMSR';
// Contexts // Contexts
import WhiteLabellingContext from '../context/WhiteLabellingContext.js'; import WhiteLabellingContext from '../context/WhiteLabellingContext.js';
@ -57,21 +58,31 @@ class Viewer extends Component {
static propTypes = { static propTypes = {
studies: PropTypes.array, studies: PropTypes.array,
studyInstanceUids: PropTypes.array, studyInstanceUids: PropTypes.array,
activeServer: PropTypes.shape({
type: PropTypes.string,
wadoRoot: PropTypes.string,
}),
onTimepointsUpdated: PropTypes.func, onTimepointsUpdated: PropTypes.func,
onMeasurementsUpdated: PropTypes.func, onMeasurementsUpdated: PropTypes.func,
// window.store.getState().viewports.viewportSpecificData // window.store.getState().viewports.viewportSpecificData
viewports: PropTypes.object.isRequired, viewports: PropTypes.object.isRequired,
// window.store.getState().viewports.activeViewportIndex // window.store.getState().viewports.activeViewportIndex
activeViewportIndex: PropTypes.number.isRequired, activeViewportIndex: PropTypes.number.isRequired,
isStudyLoaded: PropTypes.bool,
}; };
constructor(props) { constructor(props) {
super(props); super(props);
const { activeServer } = this.props;
const server = Object.assign({}, activeServer);
OHIF.measurements.MeasurementApi.setConfiguration({ OHIF.measurements.MeasurementApi.setConfiguration({
dataExchange: { dataExchange: {
retrieve: this.retrieveMeasurements, retrieve: DICOMSR.retrieveMeasurements,
store: this.storeMeasurements, store: DICOMSR.storeMeasurements,
}, },
server,
}); });
OHIF.measurements.TimepointApi.setConfiguration({ OHIF.measurements.TimepointApi.setConfiguration({
@ -93,18 +104,6 @@ class Viewer extends Component {
thumbnails: [], thumbnails: [],
}; };
retrieveMeasurements = (patientId, timepointIds) => {
OHIF.log.info('retrieveMeasurements');
// TODO: Retrieve the measurements from the latest available SR
return Promise.resolve();
};
storeMeasurements = (measurementData, timepointIds) => {
OHIF.log.info('storeMeasurements');
// TODO: Store the measurements into a new SR sent to the active server
return Promise.resolve();
};
retrieveTimepoints = filter => { retrieveTimepoints = filter => {
OHIF.log.info('retrieveTimepoints'); OHIF.log.info('retrieveTimepoints');
@ -171,7 +170,7 @@ class Viewer extends Component {
}; };
componentDidMount() { componentDidMount() {
const { studies } = this.props; const { studies, isStudyLoaded } = this.props;
const { TimepointApi, MeasurementApi } = OHIF.measurements; const { TimepointApi, MeasurementApi } = OHIF.measurements;
const currentTimepointId = 'TimepointId'; const currentTimepointId = 'TimepointId';
@ -191,8 +190,11 @@ class Viewer extends Component {
const patientId = studies[0] && studies[0].patientId; const patientId = studies[0] && studies[0].patientId;
timepointApi.retrieveTimepoints({ patientId }); timepointApi.retrieveTimepoints({ patientId });
measurementApi.retrieveMeasurements(patientId, [currentTimepointId]); if (isStudyLoaded) {
this.measurementApi.retrieveMeasurements(patientId, [
currentTimepointId,
]);
}
this.setState({ this.setState({
thumbnails: _mapStudiesToThumbnails(studies), thumbnails: _mapStudiesToThumbnails(studies),
}); });
@ -200,18 +202,19 @@ class Viewer extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.studies !== prevProps.studies) { const { studies, isStudyLoaded } = this.props;
const { studies } = this.props; if (studies !== prevProps.studies) {
const patientId = studies[0] && studies[0].patientId;
const currentTimepointId = this.currentTimepointId;
this.timepointApi.retrieveTimepoints({ patientId });
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
this.setState({ this.setState({
thumbnails: _mapStudiesToThumbnails(studies), thumbnails: _mapStudiesToThumbnails(studies),
}); });
} }
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
const patientId = studies[0] && studies[0].patientId;
const { currentTimepointId } = this;
this.timepointApi.retrieveTimepoints({ patientId });
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
}
} }
render() { render() {
@ -294,11 +297,11 @@ class Viewer extends Component {
activeIndex={this.props.activeViewportIndex} activeIndex={this.props.activeViewportIndex}
/> />
) : ( ) : (
<ConnectedStudyBrowser <ConnectedStudyBrowser
studies={this.state.thumbnails} studies={this.state.thumbnails}
studyMetadata={this.props.studies} studyMetadata={this.props.studies}
/> />
)} )}
</SidePanel> </SidePanel>
{/* MAIN */} {/* MAIN */}
@ -334,7 +337,7 @@ export default Viewer;
* @param {Study[]} studies * @param {Study[]} studies
* @param {DisplaySet[]} studies[].displaySets * @param {DisplaySet[]} studies[].displaySets
*/ */
const _mapStudiesToThumbnails = function(studies) { const _mapStudiesToThumbnails = function (studies) {
return studies.map(study => { return studies.map(study => {
const { studyInstanceUid } = study; const { studyInstanceUid } = study;

View File

@ -178,6 +178,7 @@ function ViewerRetrieveStudyData({
// hooks // hooks
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [studies, setStudies] = useState([]); const [studies, setStudies] = useState([]);
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
const snackbarContext = useSnackbarContext(); const snackbarContext = useSnackbarContext();
const { appConfig = {} } = useContext(AppContext); const { appConfig = {} } = useContext(AppContext);
const { filterQueryParam: isFilterStrategy = false } = appConfig; const { filterQueryParam: isFilterStrategy = false } = appConfig;
@ -218,6 +219,7 @@ function ViewerRetrieveStudyData({
); );
setStudies([...studies, study]); setStudies([...studies, study]);
setIsStudyLoaded(true);
}; };
/** /**
@ -332,7 +334,11 @@ function ViewerRetrieveStudyData({
} }
return ( return (
<ConnectedViewer studies={studies} studyInstanceUids={studyInstanceUids} /> <ConnectedViewer
studies={studies}
isStudyLoaded={isStudyLoaded}
studyInstanceUids={studyInstanceUids}
/>
); );
} }

View File

@ -0,0 +1,73 @@
import { log, studies, utils } from '@ohif/core';
import { retrieveMeasurementFromSR, stowSRFromMeasurements } from './handleStructuredReport';
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
/**
*
* @typedef serverType
* @property {string} type - type of the server
* @property {string} wadoRoot - server wado root url
*
*/
/**
* Function to be registered into MeasurementAPI to retrieve measurements from DICOM Structured Reports
*
* @param {serverType} server
* @returns {Promise} Should resolve with OHIF measurementData object
*/
const retrieveMeasurements = server => {
log.info('[DICOMSR] retrieveMeasurements');
if (!server || server.type !== 'dicomWeb') {
log.error('[DICOMSR] DicomWeb server is required!');
return Promise.reject({});
}
const serverUrl = server.wadoRoot;
const studies = utils.studyMetadataManager.all();
const latestSeries = findMostRecentStructuredReport(studies);
if (!latestSeries) return Promise.resolve({});
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl);
};
/**
* Function to be registered into MeasurementAPI to store measurements into DICOM Structured Reports
*
* @param {Object} measurementData - OHIF measurementData object
* @param {Object} filter
* @param {serverType} server
* @returns {Object} With message to be displayed on success
*/
const storeMeasurements = async (measurementData, filter, server) => {
log.info('[DICOMSR] storeMeasurements');
if (!server || server.type !== 'dicomWeb') {
log.error('[DICOMSR] DicomWeb server is required!');
return Promise.reject({});
}
const serverUrl = server.wadoRoot;
const firstMeasurementKey = Object.keys(measurementData)[0];
const firstMeasurement = measurementData[firstMeasurementKey][0];
const studyInstanceUid =
firstMeasurement && firstMeasurement.studyInstanceUid;
try {
await stowSRFromMeasurements(measurementData, serverUrl);
if (studyInstanceUid) {
studies.deleteStudyMetadataPromise(studyInstanceUid);
}
return {
message: 'Measurements were saved with success',
};
} catch (error) {
log.error(`[DICOMSR] Error while saving the measurements: ${error.message}`);
throw new Error('Error while saving the measurements.');
}
};
export { retrieveMeasurements, storeMeasurements };

View File

@ -0,0 +1,84 @@
import * as dcmjs from 'dcmjs';
import { DICOMWeb } from '@ohif/core';
import parseDicomStructuredReport from './parseDicomStructuredReport';
import parseMeasurementsData from './parseMeasurementsData';
import getAllDisplaySets from './utils/getAllDisplaySets';
import { api } from 'dicomweb-client';
const VERSION_NAME = 'dcmjs-0.0';
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
/**
* Function to retrieve measurements from DICOM Structured Reports coming from determined server
*
* @param {Array} series - List of all series metaData loaded
* @param {Array} studies - List of all studies metaData loaded
* @param {string} serverUrl - Server URL to be used on request
* @returns {Object} MeasurementData
*/
const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
const config = {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
};
const dicomWeb = new api.DICOMwebClient(config);
const instance = series.getFirstInstance();
const options = {
studyInstanceUID: instance.getStudyInstanceUID(),
seriesInstanceUID: instance.getSeriesInstanceUID(),
sopInstanceUID: instance.getSOPInstanceUID(),
};
const part10SRArrayBuffer = await dicomWeb.retrieveInstance(options);
const displaySets = getAllDisplaySets(studies);
const measurementsData = parseDicomStructuredReport(part10SRArrayBuffer, displaySets);
return measurementsData;
};
/**
* Function to store measurements to DICOM Structured Reports in determined server
*
* @param {Object} measurements - OHIF measurementData object
* @param {string} serverUrl - Server URL to be used on request
* @returns {Promise}
*/
const stowSRFromMeasurements = async (measurements, serverUrl) => {
const { dataset } = parseMeasurementsData(
measurements
);
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
const meta = {
FileMetaInformationVersion:
dataset._meta.FileMetaInformationVersion.Value,
MediaStorageSOPClassUID: dataset.SOPClassUID,
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
TransferSyntaxUID: TRANSFER_SYNTAX_UID,
ImplementationClassUID: DicomMetaDictionary.uid(),
ImplementationVersionName: VERSION_NAME,
};
const denaturalized = DicomMetaDictionary.denaturalizeDataset(meta);
const dicomDict = new DicomDict(denaturalized);
dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(dataset);
const part10Buffer = dicomDict.write();
const config = {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
datasets: [part10Buffer],
};
await dicomWeb.storeInstances(options);
};
export { retrieveMeasurementFromSR, stowSRFromMeasurements };

View File

@ -0,0 +1,10 @@
import { retrieveMeasurements, storeMeasurements } from './dataExchange';
import isToolSupported from './utils/isToolSupported';
const DICOMSR = {
retrieveMeasurements,
storeMeasurements,
isToolSupported,
};
export default DICOMSR;

View File

@ -0,0 +1,82 @@
import * as dcmjs from 'dcmjs';
import findInstanceMetadataBySopInstanceUid from './utils/findInstanceMetadataBySopInstanceUid';
/**
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
* measurementData format is a viewer specific format to be stored into the redux and consumed by other components
* (e.g. measurement table)
*
* @param {ArrayBuffer} part10SRArrayBuffer
* @param {Array} displaySets
* @returns
*/
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
// Get the dicom data as an Object
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
const storedMeasurementByToolType = MeasurementReport.generateToolState(
dataset
);
const measurementData = {};
let measurementNumber = 0;
Object.keys(storedMeasurementByToolType).forEach(toolName => {
const measurements = storedMeasurementByToolType[toolName];
measurementData[toolName] = [];
measurements.forEach(measurement => {
const instanceMetadata = findInstanceMetadataBySopInstanceUid(
displaySets,
measurement.sopInstanceUid
);
const { _study: study, _series: series } = instanceMetadata;
const { studyInstanceUid, patientId } = study;
const { seriesInstanceUid } = series;
const { sopInstanceUid, frameIndex } = measurement;
const imagePath = getImagePath(studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex);
const imageId = instanceMetadata.getImageId();
if (!imageId) {
return;
}
// TODO: We need the currentTimepointID set into the viewer
const currentTimepointId = 'TimepointId';
const toolData = Object.assign({}, measurement, {
imageId,
imagePath,
seriesInstanceUid,
studyInstanceUid,
patientId,
measurementNumber: ++measurementNumber,
timepointId: currentTimepointId,
toolType: toolName,
_id: imageId + measurementNumber,
});
measurementData[toolName].push(toolData);
});
});
return measurementData;
};
/**
* Function to create imagePath with all imageData related
*
* @param {string} studyInstanceUid
* @param {string} seriesInstanceUid
* @param {string} sopInstanceUid
* @param {string} frameIndex
* @returns
*/
const getImagePath = (studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex) => {
return [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join('_');
}
export default parseDicomStructuredReport;

View File

@ -0,0 +1,55 @@
import * as dcmjs from 'dcmjs';
import { log, measurements } from '@ohif/core';
import cornerstone from 'cornerstone-core';
import isToolSupported from './utils/isToolSupported';
/**
* Function to parse OHIF viewer measurementData into a dcmjs MeasurementReport
*
* @param {Object} measurementsData - OHIF measurementData object
* @returns {Object} Dataset: measurement report from dcmjs
*/
const parseMeasurementsData = measurementsData => {
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
const { getImageIdForImagePath } = measurements;
const toolState = {};
const unsupportedTools = [];
Object.keys(measurementsData).forEach(measurementType => {
const annotations = measurementsData[measurementType];
annotations.forEach(annotation => {
const { toolType, imagePath } = annotation;
if (isToolSupported(toolType)) {
const imageId = getImageIdForImagePath(imagePath);
toolState[imageId] = toolState[imageId] || {};
toolState[imageId][toolType] = toolState[imageId][toolType] || {
data: [],
};
toolState[imageId][toolType].data.push(annotation);
} else {
unsupportedTools.push(toolType);
}
});
});
if (unsupportedTools.length > 0) {
log.warn(
`[DICOMSR] Tooltypes not supported: ${unsupportedTools.join(', ')}`
);
}
const report = MeasurementReport.generateReport(
toolState,
cornerstone.metaData
);
return {
dataset: report.dataset,
};
};
export default parseMeasurementsData;

View File

@ -0,0 +1,22 @@
/**
* Should Find the requested instance metadata into the displaySets and return
*
* @param {Array} displaySets - List of displaySets
* @param {string} sopInstanceUid - sopInstanceUID to look for
* @returns {Object} - instance metadata found
*/
const findInstanceMetadataBySopInstanceUid = (displaySets, sopInstanceUid) => {
let instanceFound;
displaySets.find(displaySet => {
if (!displaySet.images) return false;
instanceFound = displaySet.images.find(instanceMetadata => instanceMetadata._sopInstanceUID === sopInstanceUid);
return !!instanceFound;
});
return instanceFound;
};
export default findInstanceMetadataBySopInstanceUid;

View File

@ -0,0 +1,57 @@
/**
* Should find the most recent Structured Report metadata
*
* @param {Array} studies
* @returns {Object} Series
*/
const findMostRecentStructuredReport = (studies) => {
let mostRecentStructuredReport;
studies.forEach(study => {
const allSeries = study.getSeries ? study.getSeries() : [];
allSeries.forEach(series => {
if (isStructuredReportSeries(series)) {
if (!mostRecentStructuredReport || compareSeriesDate(series, mostRecentStructuredReport)) {
mostRecentStructuredReport = series;
}
}
});
});
return mostRecentStructuredReport;
};
/**
* Checks if series sopClassUID matches with the supported Structured Reports sopClassUID
*
* @param {Object} series - Series metadata
* @returns {boolean}
*/
const isStructuredReportSeries = (series) => {
const supportedSopClassUIDs = [
'1.2.840.10008.5.1.4.1.1.88.22',
'1.2.840.10008.5.1.4.1.1.11.1',
];
const firstInstance = series.getFirstInstance();
const sopClassUid = firstInstance._instance.sopClassUid;
return supportedSopClassUIDs.includes(sopClassUid);
}
/**
* Checkes if series1 is newer than series2
*
* @param {Object} series1 - Series Metadata 1
* @param {Object} series2 - Series Metadata 2
* @returns {boolean} true/false if series1 is newer than series2
*/
const compareSeriesDate = (series1, series2) => {
return series1._data.seriesDate > series2._data.seriesDate ||
(series1._data.seriesDate === series2._data.seriesDate &&
series1._data.seriesTime > series2._data.seriesTime);
}
export default findMostRecentStructuredReport;

View File

@ -0,0 +1,19 @@
/**
* Retrieve a list of all displaySets of all studies
*
* @param {Object} studies - List of studies loaded into the viewer
* @returns {Object} List of DisplaySets
*/
const getAllDisplaySets = (studies) => {
let allDisplaySets = [];
studies.forEach(study => {
if (study.getDisplaySets) {
allDisplaySets = allDisplaySets.concat(study.getDisplaySets());
}
});
return allDisplaySets;
};
export default getAllDisplaySets;

View File

@ -0,0 +1,14 @@
import * as dcmjs from 'dcmjs';
/**
* Checks if dcmjs has support to determined tool
*
* @param {string} toolName
* @returns {boolean}
*/
const isToolSupported = toolName => {
const adapter = dcmjs.adapters.Cornerstone;
return !!adapter[toolName];
};
export default isToolSupported;

View File

@ -876,7 +876,7 @@
core-js "^2.6.5" core-js "^2.6.5"
regenerator-runtime "^0.13.2" regenerator-runtime "^0.13.2"
"@babel/polyfill@^7.0.0", "@babel/polyfill@^7.2.5": "@babel/polyfill@^7.0.0", "@babel/polyfill@^7.6.0":
version "7.6.0" version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc" resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc"
integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw== integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw==
@ -6535,14 +6535,14 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dcmjs@^0.6.1: dcmjs@^0.8.2:
version "0.6.1" version "0.8.2"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.6.1.tgz#453f764bca2cd8e3430126ba8385ef4c77071bcd" resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.8.2.tgz#e1107273764de0a3052244b79d999fd870e58650"
integrity sha512-sJ0cRVZY02GSaAW5S9JPcv8+7iDtgrM0L/niYWAvu2piSa4bFSXwKi+t4XevAfkm+mF0sXZtaD15bw+/oiCYkw== integrity sha512-HEd1Rq12qxZXVXr5B4xgl/foCWaXxioO5v42FLgiUjKrbQR6iKIVcZsmE+3/AvAurcHiQFfW5vk5K4AY7eAp5A==
dependencies: dependencies:
"@babel/polyfill" "^7.2.5" "@babel/polyfill" "^7.6.0"
"@babel/runtime" "^7.3.1" "@babel/runtime" "^7.6.3"
loglevelnext "^3.0.0" loglevelnext "^3.0.1"
ndarray "^1.0.18" ndarray "^1.0.18"
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.1, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.1, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
@ -12373,7 +12373,7 @@ loglevelnext@^1.0.1, loglevelnext@^1.0.2:
es6-symbol "^3.1.1" es6-symbol "^3.1.1"
object.assign "^4.1.0" object.assign "^4.1.0"
loglevelnext@^3.0.0: loglevelnext@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04" resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04"
integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA== integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA==