diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 6955e5257fe..ffe940b700a 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -35,7 +35,7 @@ "cornerstone-math": "0.1.9", "cornerstone-tools": "6.0.2", "cornerstone-wado-image-loader": "4.0.4", - "dcmjs": "0.16.1", + "dcmjs": "0.18.8", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", @@ -45,8 +45,8 @@ }, "dependencies": { "@babel/runtime": "7.7.6", - "lodash.merge": "^4.6.2", "lodash.debounce": "4.0.8", + "lodash.merge": "^4.6.2", "react-cornerstone-viewport": "4.1.2" } } diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index 01171e71cd6..b2f3125226d 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -30,6 +30,7 @@ const TOOL_TYPES_WITH_CONTEXT_MENU = [ 'SRBidirectional', 'SRArrowAnnotate', 'SREllipticalRoi', + 'SRRectangleRoi', ]; const _refreshViewports = () => @@ -319,6 +320,8 @@ const _initMeasurementService = (MeasurementService, DisplaySetService) => { Length, Bidirectional, EllipticalRoi, + RectangleRoi, + FreehandRoi, ArrowAnnotate, } = measurementServiceMappingsFactory(MeasurementService, DisplaySetService); const csToolsVer4MeasurementSource = MeasurementService.createSource( @@ -351,6 +354,22 @@ const _initMeasurementService = (MeasurementService, DisplaySetService) => { EllipticalRoi.toMeasurement ); + MeasurementService.addMapping( + csToolsVer4MeasurementSource, + 'RectangleRoi', + RectangleRoi.matchingCriteria, + RectangleRoi.toAnnotation, + RectangleRoi.toMeasurement + ); + + MeasurementService.addMapping( + csToolsVer4MeasurementSource, + 'FreehandRoi', + FreehandRoi.matchingCriteria, + FreehandRoi.toAnnotation, + FreehandRoi.toMeasurement + ); + MeasurementService.addMapping( csToolsVer4MeasurementSource, 'ArrowAnnotate', @@ -520,6 +539,8 @@ const _connectMeasurementServiceToTools = ( const TOOL_TYPE_TO_VALUE_TYPE = { Length: POLYLINE, EllipticalRoi: ELLIPSE, + RectangleRoi: POLYLINE, + FreehandRoi: POLYLINE, Bidirectional: BIDIRECTIONAL, ArrowAnnotate: POINT, }; @@ -527,6 +548,8 @@ const _connectMeasurementServiceToTools = ( const VALUE_TYPE_TO_TOOL_TYPE = { [POLYLINE]: 'Length', [ELLIPSE]: 'EllipticalRoi', + [POLYLINE]: 'RectangleRoi', + [POLYLINE]: 'FreehandRoi', [BIDIRECTIONAL]: 'Bidirectional', [POINT]: 'ArrowAnnotate', }; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/FreehandRoi.js b/extensions/cornerstone/src/utils/measurementServiceMappings/FreehandRoi.js new file mode 100644 index 00000000000..7bf092b7cc3 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/FreehandRoi.js @@ -0,0 +1,100 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import getHandlesFromPoints from './utils/getHandlesFromPoints'; + +const FreehandRoi = { + toAnnotation: (measurement, definition) => {}, + toMeasurement: ( + csToolsAnnotation, + DisplaySetService, + getValueTypeFromToolType + ) => { + const { element, measurementData } = csToolsAnnotation; + const tool = + csToolsAnnotation.toolType || + csToolsAnnotation.toolName || + measurementData.toolType; + + const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType(tool)) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + + const { cachedStats, handles } = measurementData; + + const { start, end } = handles; + + const halfXLength = Math.abs(start.x - end.x) / 2; + const halfYLength = Math.abs(start.y - end.y) / 2; + + const points = []; + const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }; + + // To store similar to SR. + if (halfXLength > halfYLength) { + // X-axis major + // Major axis + points.push({ x: center.x - halfXLength, y: center.y }); + points.push({ x: center.x + halfXLength, y: center.y }); + // Minor axis + points.push({ x: center.x, y: center.y - halfYLength }); + points.push({ x: center.x, y: center.y + halfYLength }); + } else { + // Y-axis major + // Major axis + points.push({ x: center.x, y: center.y - halfYLength }); + points.push({ x: center.x, y: center.y + halfYLength }); + // Minor axis + points.push({ x: center.x - halfXLength, y: center.y }); + points.push({ x: center.x + halfXLength, y: center.y }); + } + + let meanSUV; + let stdDevSUV; + + if ( + cachedStats && + cachedStats.meanStdDevSUV && + cachedStats.meanStdDevSUV.mean !== 0 + ) { + const { meanStdDevSUV } = cachedStats; + + meanSUV = meanStdDevSUV.mean; + stdDevSUV = meanStdDevSUV.stdDev; + } + + return { + id: measurementData.id, + SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: measurementData.label, + description: measurementData.description, + unit: measurementData.unit, + area: cachedStats && cachedStats.area, + mean: cachedStats && cachedStats.mean, + stdDev: cachedStats && cachedStats.stdDev, + meanSUV, + stdDevSUV, + type: getValueTypeFromToolType(tool), + points, + }; + }, +}; + +export default FreehandRoi; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleRoi.js b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleRoi.js new file mode 100644 index 00000000000..7998936cb02 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleRoi.js @@ -0,0 +1,116 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getHandlesFromPoints from './utils/getHandlesFromPoints'; +import getPointsFromHandles from './utils/getPointsFromHandles'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const RectangleRoi = { + toAnnotation: (measurement, definition) => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsAnnotation, + DisplaySetService, + getValueTypeFromToolType + ) => { + const { element, measurementData } = csToolsAnnotation; + const tool = + csToolsAnnotation.toolType || + csToolsAnnotation.toolName || + measurementData.toolType; + + const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType(tool)) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + + return { + id: measurementData.id, + SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: measurementData.label, + description: measurementData.description, + unit: measurementData.unit, + length: measurementData.length, + type: getValueTypeFromToolType(tool), + points: getPointsFromHandles(measurementData.handles), + }; + }, +}; + +export default RectangleRoi; + +/** + * { + "data": [ + { + "visible": true, + "active": false, + "invalidated": false, + "handles": { + "start": { + "x": 191.15890083632019, + "y": 108.51135005973717, + "highlight": true, + "active": false + }, + "end": { + "x": 254.77658303464756, + "y": 150.71923536439667, + "highlight": true, + "active": false, + "moving": false + }, + "initialRotation": 0, + "textBox": { + "active": false, + "hasMoved": false, + "movesIndependently": false, + "drawnIndependently": true, + "allowedOutsideImage": true, + "hasBoundingBox": true, + "x": 254.77658303464756, + "y": 129.61529271206692, + "boundingBox": { + "width": 149.6158905029297, + "height": 65, + "left": 625, + "top": 179.39062500000003 + } + } + }, + "uuid": "71509b93-1c6f-4acb-88ba-b67a4debc8a3", + "cachedStats": { + "area": 1364.6373162386578, + "count": 2752, + "mean": -483.00981104651163, + "variance": 169503.0307903713, + "stdDev": 411.7074577784222, + "min": -1024, + "max": 1386 + }, + "unit": "HU" + } + ] + } + */ diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js index a1ca5e83e98..f1de1bf2fc2 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js @@ -1 +1,8 @@ -export default ['Length', 'EllipticalRoi', 'Bidirectional', 'ArrowAnnotate']; +export default [ + 'Length', + 'EllipticalRoi', + 'RectangleRoi', + 'FreehandRoi', + 'Bidirectional', + 'ArrowAnnotate', +]; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js index 069267907fa..8e7129a106f 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js @@ -2,6 +2,8 @@ import Length from './Length'; import Bidirectional from './Bidirectional'; import ArrowAnnotate from './ArrowAnnotate'; import EllipticalRoi from './EllipticalRoi'; +import RectangleRoi from './RectangleRoi'; +import FreehandRoi from './FreehandRoi'; const measurementServiceMappingsFactory = ( MeasurementService, @@ -23,12 +25,14 @@ const measurementServiceMappingsFactory = ( BIDIRECTIONAL, } = MeasurementService.VALUE_TYPES; - // TODO -> I get why this was attemped, but its not nearly flexible enough. + // TODO -> I get why this was attempted, but its not nearly flexible enough. // A single measurement may have an ellipse + a bidirectional measurement, for instances. // You can't define a bidirectional tool as a single type.. const TOOL_TYPE_TO_VALUE_TYPE = { Length: POLYLINE, EllipticalRoi: ELLIPSE, + RectangleRoi: POLYLINE, + FreehandRoi: POLYLINE, Bidirectional: BIDIRECTIONAL, ArrowAnnotate: POINT, }; @@ -88,6 +92,55 @@ const measurementServiceMappingsFactory = ( }, ], }, + FreehandRoi: { + toAnnotation: FreehandRoi.toAnnotation, + toMeasurement: csToolsAnnotation => + FreehandRoi.toMeasurement( + csToolsAnnotation, + DisplaySetService, + _getValueTypeFromToolType + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + properties: ['stdDev'], + // attributes: [ + // { + // codeValue: '386136009', + // codeMeaning: 'Standard Deviation', + // codingSchemeDesignator: 'SCT', + // }, + // ], + }, + ], + }, + RectangleRoi: { + toAnnotation: RectangleRoi.toAnnotation, + toMeasurement: csToolsAnnotation => + RectangleRoi.toMeasurement( + csToolsAnnotation, + DisplaySetService, + _getValueTypeFromToolType + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], + }, EllipticalRoi: { toAnnotation: EllipticalRoi.toAnnotation, toMeasurement: csToolsAnnotation => diff --git a/extensions/default/package.json b/extensions/default/package.json index 047daac4243..d08d9585aea 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -29,7 +29,7 @@ "peerDependencies": { "@ohif/core": "^0.50.0", "@ohif/i18n": "^0.52.8", - "dcmjs": "0.16.1", + "dcmjs": "0.18.8", "dicomweb-client": "^0.6.0", "prop-types": "^15.6.2", "react": "^16.13.1", diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js index 431fca8aecd..30f3a152079 100644 --- a/extensions/default/src/DicomLocalDataSource/index.js +++ b/extensions/default/src/DicomLocalDataSource/index.js @@ -128,7 +128,7 @@ function createDicomLocalApi(dicomLocalConfig) { const study = DicomMetadataStore.getStudy(StudyInstanceUID, madeInClient) // Series metadata already added via local upload - DicomMetadataStore._broadcastEvent(EVENTS.SERIES_ADDED, { + DicomMetadataStore.publish(EVENTS.SERIES_ADDED, { StudyInstanceUID, madeInClient, }) @@ -152,7 +152,7 @@ function createDicomLocalApi(dicomLocalConfig) { }) }) - DicomMetadataStore._broadcastEvent(EVENTS.INSTANCES_ADDED, { + DicomMetadataStore.publish(EVENTS.INSTANCES_ADDED, { StudyInstanceUID, SeriesInstanceUID, madeInClient, diff --git a/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js b/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js index 542267abefb..66e5522eed0 100644 --- a/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js +++ b/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js @@ -1,35 +1,39 @@ -export default function (wadoRoot) { +/** + * Rejects a given series latest structured report. + * + * @param {string} wadoRoot + * @returns + */ +export default function(wadoRoot) { return { - series: (StudyInstanceUID, SeriesInstanceUID) => { - return new Promise((resolve, reject) => { - // Reject because of Quality. (Seems the most sensible out of the options) - const CodeValueAndCodeSchemeDesignator = `113001%5EDCM`; - - const url = `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/reject/${CodeValueAndCodeSchemeDesignator}`; - - const xhr = new XMLHttpRequest(); - xhr.open('POST', url, true); - - //Send the proper header information along with the request - // TODO -> Auth when we re-add authorization. - - console.log(xhr); - - xhr.onreadystatechange = function () { - //Call a function when the state changes. - if (xhr.readyState == 4) { - switch (xhr.status) { - case 204: - resolve(xhr.responseText); - - break; - case 404: - reject('Your dataSource does not support reject functionality'); + series: (StudyInstanceUID, SeriesInstanceUID) => + new Promise((resolve, reject) => { + try { + /** Reject because of Quality. (Seems the most sensible out of the options) */ + const CodeValueAndCodeSchemeDesignator = `113001%5EDCM`; + const url = `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/reject/${CodeValueAndCodeSchemeDesignator}`; + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + switch (xhr.status) { + case 204: + resolve(xhr.responseText); + break; + case 404: + reject( + 'Your dataSource does not support reject functionality' + ); + } } - } - }; - xhr.send(); - }); - }, + if (xhr.readyState === XMLHttpRequest.DONE) { + resolve(); + } + }; + xhr.send(); + } catch (error) { + reject(error); + } + }), }; } diff --git a/extensions/dicom-sr/package.json b/extensions/dicom-sr/package.json index f3d6390589b..24a763835bd 100644 --- a/extensions/dicom-sr/package.json +++ b/extensions/dicom-sr/package.json @@ -33,9 +33,9 @@ "@ohif/ui": "^0.50.0", "cornerstone-core": "^2.6.0", "cornerstone-math": "^0.1.9", + "dcmjs": "0.18.8", "cornerstone-tools": "6.0.2", "cornerstone-wado-image-loader": "4.0.4", - "dcmjs": "0.16.1", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/extensions/dicom-sr/src/constants/scoordTypes.js b/extensions/dicom-sr/src/constants/scoordTypes.js index 9120d71d380..35057e409d4 100644 --- a/extensions/dicom-sr/src/constants/scoordTypes.js +++ b/extensions/dicom-sr/src/constants/scoordTypes.js @@ -4,4 +4,5 @@ export default { POLYLINE: 'POLYLINE', CIRCLE: 'CIRCLE', ELLIPSE: 'ELLIPSE', + POLYGON: 'POLYGON', }; diff --git a/extensions/dicom-sr/src/getSopClassHandlerModule.js b/extensions/dicom-sr/src/getSopClassHandlerModule.js index cc877c409e4..6672989ce67 100644 --- a/extensions/dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/dicom-sr/src/getSopClassHandlerModule.js @@ -1,7 +1,9 @@ -import { SOPClassHandlerName, SOPClassHandlerId } from './id'; import { utils, classes } from '@ohif/core'; + +/** Internal imports */ import addMeasurement from './utils/addMeasurement'; import isRehydratable from './utils/isRehydratable'; +import { SOPClassHandlerName, SOPClassHandlerId } from './id'; const { ImageSet } = classes; @@ -11,9 +13,10 @@ const { ImageSet } = classes; // Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport. const sopClassUids = [ - '1.2.840.10008.5.1.4.1.1.88.11', //BASIC_TEXT_SR: - '1.2.840.10008.5.1.4.1.1.88.22', //ENHANCED_SR: - '1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR: + '1.2.840.10008.5.1.4.1.1.88.11', // BASIC_TEXT_SR + '1.2.840.10008.5.1.4.1.1.88.22', // ENHANCED_SR + '1.2.840.10008.5.1.4.1.1.88.33', // COMPREHENSIVE_SR + '1.2.840.10008.5.1.4.1.1.88.34', // COMPREHENSIVE_3D_SR ]; const CodeNameCodeSequenceValues = { @@ -27,6 +30,7 @@ const CodeNameCodeSequenceValues = { Finding: '121071', FindingSite: 'G-C0E3', // SRT CornerstoneFreeText: 'CORNERSTONEFREETEXT', // CST4 + Score: '246262008', }; const CodingSchemeDesignators = { @@ -36,6 +40,7 @@ const CodingSchemeDesignators = { const RELATIONSHIP_TYPE = { INFERRED_FROM: 'INFERRED FROM', + SELECTED_FROM: 'SELECTED FROM', }; const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT'; @@ -114,7 +119,7 @@ function _load(displaySet, servicesManager, extensionManager) { const { ContentSequence } = displaySet.instance; displaySet.referencedImages = _getReferencedImagesList(ContentSequence); - displaySet.measurements = _getMeasurements(ContentSequence); + displaySet.measurements = _getMeasurements(ContentSequence, displaySet); const mappings = MeasurementService.getSourceMappings( 'CornerstoneTools', @@ -125,12 +130,23 @@ function _load(displaySet, servicesManager, extensionManager) { displaySet.isRehydratable = isRehydratable(displaySet, mappings); displaySet.isLoaded = true; + /** + * Handler that publishes event to notify that + * this display set has fully loaded. + */ + const onMeasurementsLoadedHandler = () => { + DisplaySetService.publish(DisplaySetService.EVENTS.DISPLAY_SET_LOADED, { + displaySet, + }); + }; + // Check currently added displaySets and add measurements if the sources exist. DisplaySetService.activeDisplaySets.forEach(activeDisplaySet => { _checkIfCanAddMeasurementsToDisplaySet( displaySet, activeDisplaySet, - dataSource + dataSource, + onMeasurementsLoadedHandler ); }); @@ -145,103 +161,76 @@ function _load(displaySet, servicesManager, extensionManager) { _checkIfCanAddMeasurementsToDisplaySet( displaySet, newDisplaySet, - dataSource + dataSource, + onMeasurementsLoadedHandler ); }); } ); + + return displaySet; } function _checkIfCanAddMeasurementsToDisplaySet( srDisplaySet, newDisplaySet, - dataSource + dataSource, + onDone ) { - let unloadedMeasurements = srDisplaySet.measurements.filter( - measurement => measurement.loaded === false - ); - - if (unloadedMeasurements.length === 0) { - // All already loaded! - return; - } + let measurements = srDisplaySet.measurements; + /** + * Look for image sets. + * This also filters out _this_ displaySet, as it is not an image set. + */ if (!newDisplaySet instanceof ImageSet) { - // This also filters out _this_ displaySet, as it is not an ImageSet. return; } const { sopClassUids, images } = newDisplaySet; - // Check if any have the newDisplaySet is the correct SOPClass. - unloadedMeasurements = unloadedMeasurements.filter(measurement => - measurement.coords.some(coord => - sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID) - ) - ); + /** + * Filter measurements that references the correct sop class. + */ + measurements = measurements.filter(measurement => { + return measurement.coords.some(coord => { + return sopClassUids.includes( + coord.ReferencedSOPSequence.ReferencedSOPClassUID + ); + }); + }); - if (unloadedMeasurements.length === 0) { - // New displaySet isn't the correct SOPClass, so can't contain the referenced images. + /** + * New display set doesn't have measurements that references the correct sop class. + */ + if (measurements.length === 0) { return; } - const SOPInstanceUIDs = []; + const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet); + const SOPInstanceUIDs = images.map(i => i.SOPInstanceUID); - unloadedMeasurements.forEach(measurement => { + measurements.forEach(measurement => { const { coords } = measurement; coords.forEach(coord => { - const SOPInstanceUID = - coord.ReferencedSOPSequence.ReferencedSOPInstanceUID; - - if (!SOPInstanceUIDs.includes(SOPInstanceUID)) { - SOPInstanceUIDs.push(SOPInstanceUID); + const imageIndex = SOPInstanceUIDs.findIndex( + SOPInstanceUID => + SOPInstanceUID === + coord.ReferencedSOPSequence.ReferencedSOPInstanceUID + ); + if (imageIndex > -1) { + const imageId = imageIds[imageIndex]; + addMeasurement( + measurement, + imageId, + newDisplaySet.displaySetInstanceUID + ); } }); }); - const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet( - newDisplaySet - ); - - for (let i = 0; i < images.length; i++) { - if (!unloadedMeasurements.length) { - // All measurements loaded. - break; - } - - const image = images[i]; - const { SOPInstanceUID } = image; - if (SOPInstanceUIDs.includes(SOPInstanceUID)) { - const imageId = imageIdsForDisplaySet[i]; - - for (let j = unloadedMeasurements.length - 1; j >= 0; j--) { - const measurement = unloadedMeasurements[j]; - if (_measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID)) { - addMeasurement( - measurement, - imageId, - newDisplaySet.displaySetInstanceUID - ); - - unloadedMeasurements.splice(j, 1); - } - } - } - } -} - -function _measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID) { - const { coords } = measurement; - - for (let j = 0; j < coords.length; j++) { - const coord = coords[j]; - const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence; - - if (ReferencedSOPInstanceUID === SOPInstanceUID) { - return true; - } - } + onDone(); } function getSopClassHandlerModule({ servicesManager, extensionManager }) { @@ -262,7 +251,7 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) { ]; } -function _getMeasurements(ImagingMeasurementReportContentSequence) { +function _getMeasurements(ImagingMeasurementReportContentSequence, displaySet) { const ImagingMeasurements = ImagingMeasurementReportContentSequence.find( item => item.ConceptNameCodeSequence.CodeValue === @@ -290,7 +279,10 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) { trackingUniqueIdentifier ]; - const measurement = _processMeasurement(mergedContentSequence); + const measurement = _processMeasurement( + mergedContentSequence, + displaySet + ); if (measurement) { measurements.push(measurement); @@ -353,25 +345,31 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers( return mergedContentSequencesByTrackingUniqueIdentifiers; } -function _processMeasurement(mergedContentSequence) { +function _processMeasurement(mergedContentSequence, displaySet) { if ( mergedContentSequence.some( group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D' ) ) { - return _processTID1410Measurement(mergedContentSequence); + return _processTID1410Measurement(mergedContentSequence, displaySet); } return _processNonGeometricallyDefinedMeasurement(mergedContentSequence); } -function _processTID1410Measurement(mergedContentSequence) { +/** + * TID 1410 Planar ROI Measurements and Qualitative Evaluations. + * + * @param {*} mergedContentSequence + * @returns + */ +function _processTID1410Measurement(mergedContentSequence, displaySet) { // Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level, // And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D // TODO -> Look at RelationshipType => Contains means const graphicItem = mergedContentSequence.find( - group => group.ValueType === 'SCOORD' + group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D' ); const UIDREFContentItem = mergedContentSequence.find( @@ -398,13 +396,37 @@ function _processTID1410Measurement(mergedContentSequence) { const measurement = { loaded: false, labels: [], - coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)], + coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem, displaySet)], TrackingUniqueIdentifier: UIDREFContentItem.UID, TrackingIdentifier: TrackingIdentifierContentItem.TextValue, }; NUMContentItems.forEach(item => { - const { ConceptNameCodeSequence, MeasuredValueSequence } = item; + const { + ConceptNameCodeSequence, + ContentSequence, + MeasuredValueSequence, + } = item; + + if ( + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.Score + ) { + ContentSequence.forEach(item => { + if ( + [ + RELATIONSHIP_TYPE.SELECTED_FROM, + RELATIONSHIP_TYPE.INFERRED_FROM, + ].includes(item.RelationshipType) + ) { + if (item.ReferencedSOPSequence) { + measurement.coords.forEach(coord => { + coord.ReferencedSOPSequence = item.ReferencedSOPSequence; + }); + } + } + }); + } if (MeasuredValueSequence) { measurement.labels.push( @@ -496,7 +518,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { const { ValueType } = ContentSequence; - if (!ValueType === 'SCOORD') { + if (!ValueType === 'SCOORD' && !ValueType === 'SCOORD3D') { console.warn( `Graphic ${ValueType} not currently supported, skipping annotation.` ); @@ -523,28 +545,33 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { return measurement; } -function _getCoordsFromSCOORDOrSCOORD3D(item) { - const { ValueType, RelationshipType, GraphicType, GraphicData } = item; - - if (RelationshipType !== RELATIONSHIP_TYPE.INFERRED_FROM) { - console.warn( - `Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM."` - ); +function _getCoordsFromSCOORDOrSCOORD3D(graphicItem, displaySet) { + const { ValueType, RelationshipType, GraphicType, GraphicData } = graphicItem; - return; - } + // if (RelationshipType !== RELATIONSHIP_TYPE.INFERRED_FROM) { + // console.warn( + // `Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM."` + // ); + // return; + // } const coords = { ValueType, GraphicType, GraphicData }; // ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM' if (ValueType === 'SCOORD') { - const { ReferencedSOPSequence } = item.ContentSequence; - + const { ReferencedSOPSequence } = graphicItem.ContentSequence; coords.ReferencedSOPSequence = ReferencedSOPSequence; } else if (ValueType === 'SCOORD3D') { - const { ReferencedFrameOfReferenceSequence } = item.ContentSequence; + if (graphicItem.ReferencedFrameOfReferenceUID) { + // todo + } - coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence; + if (graphicItem.ContentSequence) { + const { + ReferencedFrameOfReferenceSequence, + } = graphicItem.ContentSequence; + coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence; + } } return coords; @@ -569,12 +596,18 @@ function _getLabelFromMeasuredValueSequence( } function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { + const referencedImages = []; + const ImageLibrary = ImagingMeasurementReportContentSequence.find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary ); + if (!ImageLibrary.ContentSequence) { + return referencedImages; + } + const ImageLibraryGroup = _getSequenceAsArray( ImageLibrary.ContentSequence ).find( @@ -583,8 +616,6 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { CodeNameCodeSequenceValues.ImageLibraryGroup ); - const referencedImages = []; - _getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => { const { ReferencedSOPSequence } = item; const { diff --git a/extensions/dicom-sr/src/index.js b/extensions/dicom-sr/src/index.js index f366f8b34b1..c3bd8bd0455 100644 --- a/extensions/dicom-sr/src/index.js +++ b/extensions/dicom-sr/src/index.js @@ -1,8 +1,11 @@ import React from 'react'; + +/** Internal imports */ import getSopClassHandlerModule from './getSopClassHandlerModule'; import onModeEnter from './onModeEnter'; import id from './id.js'; import init from './init'; +import getToolAlias from './tools/utils/getToolAlias'; const Component = React.lazy(() => { return import( @@ -72,7 +75,7 @@ export default { } // Set same tool or alt tool - const toolAlias = _getToolAlias(toolName); + const toolAlias = getToolAlias(toolName); cornerstoneTools.setToolActiveForElement(element, toolAlias, { mouseButtonMask: 1, @@ -88,24 +91,3 @@ export default { getSopClassHandlerModule, onModeEnter, }; - -function _getToolAlias(toolName) { - let toolAlias = toolName; - - switch (toolName) { - case 'Length': - toolAlias = 'SRLength'; - break; - case 'Bidirectional': - toolAlias = 'SRBidirectional'; - break; - case 'ArrowAnnotate': - toolAlias = 'SRArrowAnnotate'; - break; - case 'EllipticalRoi': - toolAlias = 'SREllipticalRoi'; - break; - } - - return toolAlias; -} diff --git a/extensions/dicom-sr/src/init.js b/extensions/dicom-sr/src/init.js index d11b2088de5..3c53146ec1e 100644 --- a/extensions/dicom-sr/src/init.js +++ b/extensions/dicom-sr/src/init.js @@ -1,7 +1,8 @@ import cornerstoneTools from 'cornerstone-tools'; -import dicomSRModule from './tools/modules/dicomSRModule'; -import id from './id'; +/** Internal imports */ +import id from './id'; +import dicomSRModule from './tools/modules/dicomSRModule'; import TOOL_NAMES from './constants/toolNames'; const defaultConfig = { @@ -14,9 +15,9 @@ const defaultConfig = { * @param {object} configuration */ export default function init({ configuration = {} }) { - const conifg = Object.assign({}, defaultConfig, configuration); + const config = Object.assign({}, defaultConfig, configuration); - TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = conifg.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL; + TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = config.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL; cornerstoneTools.register('module', id, dicomSRModule); } diff --git a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js index 6e6a19c08a7..3608c8f1a1c 100644 --- a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js +++ b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js @@ -1,11 +1,12 @@ import { importInternal, getToolState, toolColors } from 'cornerstone-tools'; import { pixelToCanvas } from 'cornerstone-core'; +/** Internal imports */ import TOOL_NAMES from '../constants/toolNames'; import SCOORD_TYPES from '../constants/scoordTypes'; import id from '../id'; -// Cornerstone 3rd party dev kit imports +/** Cornerstone 3rd party dev kit imports */ const draw = importInternal('drawing/draw'); const drawJoinedLines = importInternal('drawing/drawJoinedLines'); const drawCircle = importInternal('drawing/drawCircle'); @@ -18,6 +19,14 @@ const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox'); /** * @class DICOMSRDisplayTool - Renders DICOMSR data in a read only manner (i.e. as an overlay). + * + * This is a generic render tool. + * + * A single tool that, given some schema, can render + * POINT, MULTIPOINT, POLYLINE, CIRCLE, and ELLIPSE + * value types for a given imageId. + * + * * @extends cornerstoneTools.BaseTool */ export default class DICOMSRDisplayTool extends BaseTool { @@ -93,6 +102,7 @@ export default class DICOMSRDisplayTool extends BaseTool { options ); break; + case SCOORD_TYPES.POLYGON: case SCOORD_TYPES.POLYLINE: this.renderPolyLine( renderableDataForGraphicType, @@ -193,7 +203,6 @@ export default class DICOMSRDisplayTool extends BaseTool { renderPolyLine(renderableData, eventData, options) { const { element } = eventData; const context = getNewContext(eventData.canvasContext.canvas); - renderableData.forEach(points => { draw(context, context => { drawJoinedLines(context, element, points[0], points, options); @@ -336,6 +345,7 @@ function _getTextBoxAnchorPointsForRenderableData(renderableData, eventData) { break; case SCOORD_TYPES.MULTIPOINT: case SCOORD_TYPES.POLYLINE: + case SCOORD_TYPES.POLYGON: renderableDataForGraphicType.forEach(points => { anchorPoints = [...anchorPoints, ...points]; }); diff --git a/extensions/dicom-sr/src/tools/initSRTools.js b/extensions/dicom-sr/src/tools/initSRTools.js new file mode 100644 index 00000000000..57eba840a77 --- /dev/null +++ b/extensions/dicom-sr/src/tools/initSRTools.js @@ -0,0 +1,109 @@ +import cornerstoneTools from 'cornerstone-tools'; +import DICOMSRDisplayTool from './DICOMSRDisplayTool'; +import TOOL_NAMES from '../constants/toolNames'; +import getToolAlias from './utils/getToolAlias'; + +/** + * Initialize SR cornerstone tools. + * + * @param {*} targetElement + * @param {*} ToolBarService + */ +const initSRTools = (targetElement, ToolBarService) => { + const primaryToolId = ToolBarService.state.primaryToolId; + const toolAlias = getToolAlias(primaryToolId); // These are 1:1 for built-in only + + // ~~ MAGIC + cornerstoneTools.addToolForElement(targetElement, DICOMSRDisplayTool); + cornerstoneTools.setToolEnabledForElement( + targetElement, + TOOL_NAMES.DICOM_SR_DISPLAY_TOOL + ); + + // ~~ Variants + cornerstoneTools.addToolForElement( + targetElement, + cornerstoneTools.LengthTool, + { + name: 'SRLength', + configuration: { + renderDashed: true, + }, + } + ); + cornerstoneTools.addToolForElement( + targetElement, + cornerstoneTools.ArrowAnnotateTool, + { + name: 'SRArrowAnnotate', + configuration: { + renderDashed: true, + }, + } + ); + cornerstoneTools.addToolForElement( + targetElement, + cornerstoneTools.BidirectionalTool, + { + name: 'SRBidirectional', + configuration: { + renderDashed: true, + }, + } + ); + cornerstoneTools.addToolForElement( + targetElement, + cornerstoneTools.EllipticalRoiTool, + { + name: 'SREllipticalRoi', + configuration: { + renderDashed: true, + }, + } + ); + cornerstoneTools.addToolForElement( + targetElement, + cornerstoneTools.RectangleRoiTool, + { + name: 'SRRectangleRoi', + configuration: { + renderDashed: true, + }, + } + ); + cornerstoneTools.addToolForElement( + targetElement, + cornerstoneTools.FreehandRoiTool, + { + name: 'SRFreehandRoi', + configuration: { + renderDashed: true, + }, + } + ); + + // ~~ Business as usual + cornerstoneTools.setToolActiveForElement(targetElement, 'PanMultiTouch', { + pointers: 2, + }); + cornerstoneTools.setToolActiveForElement(targetElement, 'ZoomTouchPinch', {}); + + // TODO: Add always dashed tool alternative aliases + // TODO: or same name... alternative config? + cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, { + mouseButtonMask: 1, + }); + cornerstoneTools.setToolActiveForElement(targetElement, 'Pan', { + mouseButtonMask: 4, + }); + cornerstoneTools.setToolActiveForElement(targetElement, 'Zoom', { + mouseButtonMask: 2, + }); + cornerstoneTools.setToolActiveForElement( + targetElement, + 'StackScrollMouseWheel', + {} + ); +}; + +export default initSRTools; diff --git a/extensions/dicom-sr/src/tools/utils/getToolAlias.js b/extensions/dicom-sr/src/tools/utils/getToolAlias.js new file mode 100644 index 00000000000..5460b70ca7a --- /dev/null +++ b/extensions/dicom-sr/src/tools/utils/getToolAlias.js @@ -0,0 +1,32 @@ +/** + * Get cornerstone tool alias. + * + * @param {string} toolName + * @returns tool alias + */ +export default function getToolAlias(toolName) { + let toolAlias = toolName; + + switch (toolName) { + case 'Length': + toolAlias = 'SRLength'; + break; + case 'Bidirectional': + toolAlias = 'SRBidirectional'; + break; + case 'ArrowAnnotate': + toolAlias = 'SRArrowAnnotate'; + break; + case 'EllipticalRoi': + toolAlias = 'SREllipticalRoi'; + break; + case 'FreehandRoi': + toolAlias = 'SRFreehandRoi'; + break; + case 'RectangleRoi': + toolAlias = 'SRRectangleRoi'; + break; + } + + return toolAlias; +} diff --git a/extensions/dicom-sr/src/utils/addMeasurement.js b/extensions/dicom-sr/src/utils/addMeasurement.js index 8d592e7f82c..7b19b12a744 100644 --- a/extensions/dicom-sr/src/utils/addMeasurement.js +++ b/extensions/dicom-sr/src/utils/addMeasurement.js @@ -1,12 +1,20 @@ import cornerstoneTools from 'cornerstone-tools'; import cornerstoneMath from 'cornerstone-math'; -import cornerstone from 'cornerstone-core'; + +/** Internal imports */ import TOOL_NAMES from '../constants/toolNames'; import SCOORD_TYPES from '../constants/scoordTypes'; const globalImageIdSpecificToolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager; +/** + * Add a measurement to a display set. + * + * @param {*} measurement + * @param {*} imageId + * @param {*} displaySetInstanceUID + */ export default function addMeasurement( measurement, imageId, @@ -23,14 +31,14 @@ export default function addMeasurement( }; measurement.coords.forEach(coord => { - const { GraphicType, GraphicData } = coord; + const { GraphicType, GraphicData, ValueType } = coord; if (measurementData.renderableData[GraphicType] === undefined) { measurementData.renderableData[GraphicType] = []; } measurementData.renderableData[GraphicType].push( - _getRenderableData(GraphicType, GraphicData) + _getRenderableData(GraphicType, GraphicData, ValueType) ); }); @@ -62,10 +70,11 @@ export default function addMeasurement( // It'd be super werid if it didn't anyway as a SCOORD. measurement.ReferencedSOPInstanceUID = measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID; - delete measurement.coords; + + return measurement; } -function _getRenderableData(GraphicType, GraphicData) { +function _getRenderableData(GraphicType, GraphicData, ValueType) { let renderableData; switch (GraphicType) { @@ -74,8 +83,30 @@ function _getRenderableData(GraphicType, GraphicData) { case SCOORD_TYPES.POLYLINE: renderableData = []; - for (let i = 0; i < GraphicData.length; i += 2) { - renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] }); + if (ValueType === 'SCOORD3D') { + for (let i = 0; i < GraphicData.length; i += 3) { + renderableData.push({ + x: GraphicData[i], + y: GraphicData[i + 1], + z: GraphicData[i + 2], + }); + } + } else { + for (let i = 0; i < GraphicData.length; i += 2) { + renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] }); + } + } + + break; + case SCOORD_TYPES.POLYGON: + renderableData = []; + + for (let i = 0; i < GraphicData.length; i += 3) { + renderableData.push({ + x: GraphicData[i], + y: GraphicData[i + 1], + z: GraphicData[i + 2], + }); } break; case SCOORD_TYPES.CIRCLE: diff --git a/extensions/dicom-sr/src/viewports/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/viewports/OHIFCornerstoneSRViewport.js index de7c8278540..edec28beed1 100644 --- a/extensions/dicom-sr/src/viewports/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/viewports/OHIFCornerstoneSRViewport.js @@ -3,28 +3,26 @@ import PropTypes from 'prop-types'; import cornerstoneTools from 'cornerstone-tools'; import cornerstone from 'cornerstone-core'; import CornerstoneViewport from 'react-cornerstone-viewport'; -import OHIF, { DicomMetadataStore, utils } from '@ohif/core'; -import DICOMSRDisplayTool from './../tools/DICOMSRDisplayTool'; -import ViewportOverlay from './ViewportOverlay'; +import OHIF, { utils } from '@ohif/core'; import { Notification, ViewportActionBar, useViewportGrid, useViewportDialog, } from '@ohif/ui'; -import TOOL_NAMES from './../constants/toolNames'; -import { adapters } from 'dcmjs'; + +/** Internal imports */ +import ViewportOverlay from './ViewportOverlay'; import id from './../id'; +import initSRTools from '../tools/initSRTools'; const { formatDate } = utils; -const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex'); -const globalImageIdSpecificToolStateManager = - cornerstoneTools.globalImageIdSpecificToolStateManager; - -const { StackManager, guid } = OHIF.utils; - +const { StackManager } = OHIF.utils; const MEASUREMENT_TRACKING_EXTENSION_ID = 'org.ohif.measurement-tracking'; +/** Cornerstone 3rd party dev kit imports */ +const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex'); + function OHIFCornerstoneSRViewport({ children, dataSource, @@ -33,11 +31,7 @@ function OHIFCornerstoneSRViewport({ servicesManager, extensionManager, }) { - const { - DisplaySetService, - MeasurementService, - ToolBarService, - } = servicesManager.services; + const { DisplaySetService, ToolBarService } = servicesManager.services; const [viewportGrid, viewportGridService] = useViewportGrid(); const [viewportDialogState, viewportDialogApi] = useViewportDialog(); const [measurementSelected, setMeasurementSelected] = useState(0); @@ -48,6 +42,9 @@ function OHIFCornerstoneSRViewport({ const [isHydrated, setIsHydrated] = useState(displaySet.isHydrated); const { viewports, activeViewportIndex } = viewportGrid; + /** + * Empty SR viewport if display set removed. + */ useEffect(() => { const onDisplaySetsRemovedSubscription = DisplaySetService.subscribe( DisplaySetService.EVENTS.DISPLAY_SETS_REMOVED, @@ -69,12 +66,15 @@ function OHIFCornerstoneSRViewport({ }; }, []); - // Optional hook into tracking extension, if present. + /** + * Optional hook into tracking extension, if present. + */ let trackedMeasurements; let sendTrackedMeasurementsEvent; - // TODO: this is a hook that fails if we register/de-register - // + /** + * TODO: this is a hook that fails if we register/de-register. + */ if ( extensionManager.registeredExtensionIds.includes( MEASUREMENT_TRACKING_EXTENSION_ID @@ -92,120 +92,34 @@ function OHIFCornerstoneSRViewport({ ] = useTrackedMeasurements(); } - // Locked if tracking any series + /** + * Locked if tracking any series + */ let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0; useEffect(() => { isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0; }, [trackedMeasurements]); - function _getToolAlias() { - const primaryToolId = ToolBarService.state.primaryToolId; - let toolAlias = primaryToolId; - - switch (primaryToolId) { - case 'Length': - toolAlias = 'SRLength'; - break; - case 'Bidirectional': - toolAlias = 'SRBidirectional'; - break; - case 'ArrowAnnotate': - toolAlias = 'SRArrowAnnotate'; - break; - case 'EllipticalRoi': - toolAlias = 'SREllipticalRoi'; - break; - } - - return toolAlias; - } - const onElementEnabled = evt => { const eventData = evt.detail; const targetElement = eventData.element; - const toolAlias = _getToolAlias(); // These are 1:1 for built-in only - - // ~~ MAGIC - cornerstoneTools.addToolForElement(targetElement, DICOMSRDisplayTool); - cornerstoneTools.setToolEnabledForElement( - targetElement, - TOOL_NAMES.DICOM_SR_DISPLAY_TOOL - ); - - // ~~ Variants - cornerstoneTools.addToolForElement( - targetElement, - cornerstoneTools.LengthTool, - { - name: 'SRLength', - configuration: { - renderDashed: true, - }, - } - ); - cornerstoneTools.addToolForElement( - targetElement, - cornerstoneTools.ArrowAnnotateTool, - { - name: 'SRArrowAnnotate', - configuration: { - renderDashed: true, - }, - } - ); - cornerstoneTools.addToolForElement( - targetElement, - cornerstoneTools.BidirectionalTool, - { - name: 'SRBidirectional', - configuration: { - renderDashed: true, - }, - } - ); - cornerstoneTools.addToolForElement( - targetElement, - cornerstoneTools.EllipticalRoiTool, - { - name: 'SREllipticalRoi', - configuration: { - renderDashed: true, - }, - } - ); - // ~~ Business as usual - cornerstoneTools.setToolActiveForElement(targetElement, 'PanMultiTouch', { - pointers: 2, - }); - cornerstoneTools.setToolActiveForElement( - targetElement, - 'ZoomTouchPinch', - {} - ); + /** + * Initialize SR cornerstone tools. + */ + initSRTools(targetElement, ToolBarService); - // TODO: Add always dashed tool alternative aliases - // TODO: or same name... alternative config? - cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, { - mouseButtonMask: 1, - }); - cornerstoneTools.setToolActiveForElement(targetElement, 'Pan', { - mouseButtonMask: 4, - }); - cornerstoneTools.setToolActiveForElement(targetElement, 'Zoom', { - mouseButtonMask: 2, - }); - cornerstoneTools.setToolActiveForElement( + setTrackingUniqueIdentifiersForElement( targetElement, - 'StackScrollMouseWheel', - {} + displaySet, + measurementSelected ); - - setTrackingUniqueIdentifiersForElement(targetElement); setElement(targetElement); - // TODO: Enabled Element appears to be incorrect here, it should be called - // 'element' since it is the DOM element, not the enabledElement object + /** + * TODO: Enabled Element appears to be incorrect here, it should be called + * 'element' since it is the DOM element, not the enabledElement object + */ const OHIFCornerstoneEnabledElementEvent = new CustomEvent( 'ohif-cornerstone-enabled-element-event', { @@ -220,6 +134,9 @@ function OHIFCornerstoneSRViewport({ document.dispatchEvent(OHIFCornerstoneEnabledElementEvent); }; + /** + * Loads display set if not loaded yet. + */ useEffect(() => { if (!displaySet.isLoaded) { displaySet.load(); @@ -227,7 +144,19 @@ function OHIFCornerstoneSRViewport({ setIsHydrated(displaySet.isHydrated); }, [displaySet]); - const setTrackingUniqueIdentifiersForElement = useCallback(targetElement => { + /** + * Update SR module tracking identifiers based on + * currently active display set. + * + * @param {*} targetElement + * @param {*} displaySet + * @param {*} measurementSelected + */ + const setTrackingUniqueIdentifiersForElement = ( + targetElement, + displaySet, + measurementSelected + ) => { const { measurements } = displaySet; const srModule = cornerstoneTools.getModule(id); @@ -237,15 +166,38 @@ function OHIFCornerstoneSRViewport({ measurements.map(measurement => measurement.TrackingUniqueIdentifier), measurementSelected ); - }); + }; + /** + * Updates measurements count. + */ useEffect(() => { const numMeasurements = displaySet.measurements.length; - setMeasurementCount(numMeasurements); }, [dataSource, displaySet]); - const updateViewport = useCallback(newMeasurementSelected => { + /** + * Update SR viewport. + * + * @param {*} displaySet + * @param {*} element + * @param {*} dataSource + * @param {*} newMeasurementSelected + * @returns + */ + const updateViewport = ( + displaySet, + element, + dataSource, + newMeasurementSelected + ) => { + if ( + !displaySet.measurements || + !displaySet.measurements.filter(m => m.loaded === true).length > 0 + ) { + return; + } + const { StudyInstanceUID, displaySetInstanceUID, @@ -278,29 +230,42 @@ function OHIFCornerstoneSRViewport({ cornerstone.updateImage(element); } }); - }); + }; - useEffect( - () => { - if (element !== null) { - setTrackingUniqueIdentifiersForElement(element); + useEffect(() => { + if (element !== null) { + setTrackingUniqueIdentifiersForElement( + element, + displaySet, + measurementSelected + ); + } + }, [dataSource, displaySet]); + + /** + * Updates the SR viewport if data source, + * element or display set changes. + */ + useEffect(() => { + updateViewport(displaySet, element, dataSource, measurementSelected); + + const onDisplaySetLoadedHandler = ({ + displaySet: { displaySetInstanceUID }, + }) => { + if (displaySet.displaySetInstanceUID === displaySetInstanceUID) { + updateViewport(displaySet, element, dataSource, measurementSelected); } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [dataSource, displaySet] - ); + }; - useEffect( - () => { - updateViewport(measurementSelected); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [dataSource, displaySet, element] - ); + const subscription = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SET_LOADED, + onDisplaySetLoadedHandler + ); - const firstViewportIndexWithMatchingDisplaySetUid = viewports.findIndex( - vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID - ); + return () => { + subscription.unsubscribe(); + }; + }, [dataSource, displaySet, element]); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ let childrenWithProps = null; @@ -331,6 +296,10 @@ function OHIFCornerstoneSRViewport({ const { Modality } = displaySet; + if (![]) { + return; + } + const { PatientID, PatientName, @@ -340,12 +309,15 @@ function OHIFCornerstoneSRViewport({ ManufacturerModelName, StudyDate, SeriesDescription, - SeriesInstanceUID, SpacingBetweenSlices, SeriesNumber, - displaySetInstanceUID, } = activeDisplaySetData; + /** + * Measurement change event handler. + * + * @param {*} direction + */ const onMeasurementChange = direction => { let newMeasurementSelected = measurementSelected; @@ -363,7 +335,7 @@ function OHIFCornerstoneSRViewport({ } } - updateViewport(newMeasurementSelected); + updateViewport(displaySet, element, dataSource, measurementSelected); }; const label = viewports.length > 1 ? _viewportLabels[viewportIndex] : ''; @@ -371,45 +343,49 @@ function OHIFCornerstoneSRViewport({ // TODO -> disabled double click for now: onDoubleClick={_onDoubleClick} return ( <> - { - evt.stopPropagation(); - evt.preventDefault(); - }} - onPillClick={() => { - sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', { - displaySetInstanceUID: displaySet.displaySetInstanceUID, - viewportIndex, - }); - }} - onSeriesChange={onMeasurementChange} - studyData={{ - label, - useAltStyling: true, - isTracked: false, - isLocked, - isRehydratable: displaySet.isRehydratable, - isHydrated, - studyDate: formatDate(StudyDate), - currentSeries: SeriesNumber, - seriesDescription: SeriesDescription, - modality: Modality, - patientInformation: { - patientName: PatientName - ? OHIF.utils.formatPN(PatientName.Alphabetic) - : '', - patientSex: PatientSex || '', - patientAge: PatientAge || '', - MRN: PatientID || '', - thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '', - spacing: - SpacingBetweenSlices !== undefined - ? `${SpacingBetweenSlices.toFixed(2)}mm` + {activeDisplaySetData && Object.keys(activeDisplaySetData).length > 0 && ( + { + evt.stopPropagation(); + evt.preventDefault(); + }} + onPillClick={() => { + sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', { + displaySetInstanceUID: displaySet.displaySetInstanceUID, + viewportIndex, + }); + }} + onSeriesChange={onMeasurementChange} + studyData={{ + label, + useAltStyling: true, + isTracked: false, + isLocked, + isRehydratable: displaySet.isRehydratable, + isHydrated, + studyDate: formatDate(StudyDate), + currentSeries: SeriesNumber, + seriesDescription: SeriesDescription, + modality: Modality, + patientInformation: { + patientName: PatientName + ? OHIF.utils.formatPN(PatientName.Alphabetic) + : '', + patientSex: PatientSex || '', + patientAge: PatientAge || '', + MRN: PatientID || '', + thickness: SliceThickness + ? `${Number(SliceThickness).toFixed(2)}mm` : '', - scanner: ManufacturerModelName || '', - }, - }} - /> + spacing: + SpacingBetweenSlices !== undefined + ? `${SpacingBetweenSlices.toFixed(2)}mm` + : '', + scanner: ManufacturerModelName || '', + }, + }} + /> + )}
m.loaded === true); const measurement = measurements[measurementSelected]; const stack = _getCornerstoneStack( diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 322a2ce5e8b..7a84a2449c2 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -29,9 +29,9 @@ "peerDependencies": { "@ohif/core": "^0.50.0", "classnames": "^2.2.6", + "dcmjs": "0.18.8", "cornerstone-core": "^2.6.0", "cornerstone-tools": "6.0.2", - "dcmjs": "0.16.1", "prop-types": "^15.6.2", "react": "^16.13.1", "react-cornerstone-viewport": "^4.1.2", diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js index 2baceb80a0a..9ca9cf15cc0 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js @@ -1,12 +1,11 @@ -import cornerstoneTools from 'cornerstone-tools'; import OHIF, { DicomMetadataStore } from '@ohif/core'; +import { adapters } from 'dcmjs'; + +/** Internal imports */ import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData'; import getCornerstoneToolStateToMeasurementSchema from './getCornerstoneToolStateToMeasurementSchema'; -import { adapters } from 'dcmjs'; const { guid } = OHIF.utils; -const globalImageIdSpecificToolStateManager = - cornerstoneTools.globalImageIdSpecificToolStateManager; /** * diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getCornerstoneToolStateToMeasurementSchema.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getCornerstoneToolStateToMeasurementSchema.js index c548a79292e..6fcee1f8e01 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getCornerstoneToolStateToMeasurementSchema.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getCornerstoneToolStateToMeasurementSchema.js @@ -22,6 +22,8 @@ export default function getCornerstoneToolStateToMeasurementSchema( const TOOL_TYPE_TO_VALUE_TYPE = { Length: POLYLINE, EllipticalRoi: ELLIPSE, + RectangleRoi: POLYLINE, + FreehandRoi: POLYLINE, Bidirectional: BIDIRECTIONAL, ArrowAnnotate: POINT, }; @@ -63,6 +65,28 @@ export default function getCornerstoneToolStateToMeasurementSchema( DisplaySetService, _getValueTypeFromToolType ); + case 'RectangleRoi': + return measurementData => + RectangleRoi( + measurementData, + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + DisplaySetService, + _getValueTypeFromToolType + ); + case 'FreehandRoi': + return measurementData => + FreehandRoi( + measurementData, + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + DisplaySetService, + _getValueTypeFromToolType + ); case 'ArrowAnnotate': return measurementData => ArrowAnnotate( diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js index f377b375209..da5938bafb2 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js @@ -1,3 +1,4 @@ +/** Internal imports */ import hydrateStructuredReport from './_hydrateStructuredReport.js'; const RESPONSE = { diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index 03f3e84e0d7..014a82347ad 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -460,7 +460,6 @@ function _mapDisplaySets( onClose: () => UIDialogService.dismiss({ id: 'ds-reject-sr' }), onShow: () => { const yesButton = document.querySelector('.reject-yes-button'); - yesButton.focus(); }, onSubmit: async ({ action }) => { diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index e0bec512414..352c2082afa 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -26,6 +26,8 @@ const { ArrowAnnotateTool, BidirectionalTool, EllipticalRoiTool, + RectangleRoiTool, + FreehandRoiTool, LengthTool, } = cornerstoneTools; @@ -82,6 +84,8 @@ function TrackedCornerstoneViewport(props) { tool instanceof ArrowAnnotateTool || tool instanceof BidirectionalTool || tool instanceof EllipticalRoiTool || + tool instanceof FreehandRoiTool || + tool instanceof RectangleRoiTool || tool instanceof LengthTool ) { const configuration = tool.configuration; diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index 87d148707ad..21b24e176c4 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -278,6 +278,9 @@ export default [ { toolName: 'RectangleRoi' }, 'Rectangle' ), + _createToolButton('Freehand', 'tool-move', 'Freehand', undefined, { + toolName: 'FreehandRoi', + }), ], }, }, diff --git a/platform/core/package.json b/platform/core/package.json index f6b9dc62e14..efffe8001d5 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -39,14 +39,16 @@ }, "dependencies": { "@babel/runtime": "7.7.6", - "dcmjs": "0.16.1", + "dcmjs": "0.18.8", "dicomweb-client": "^0.6.0", "isomorphic-base64": "^1.0.2", "lodash.merge": "^4.6.1", "lodash.clonedeep": "^4.5.0", "moment": "^2.24.0", - "query-string": "^6.14.0", + "mousetrap": "^1.6.3", "object-hash": "2.1.1", + "query-string": "^6.14.0", + "cornerstone-math": "0.1.9", "validate.js": "^0.12.0" }, "devDependencies": { diff --git a/platform/core/src/classes/MetadataProvider.js b/platform/core/src/classes/MetadataProvider.js index f214d1918b2..a1189cdd7b0 100644 --- a/platform/core/src/classes/MetadataProvider.js +++ b/platform/core/src/classes/MetadataProvider.js @@ -4,6 +4,7 @@ import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacin import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData'; import fetchOverlayData from '../utils/metadataProvider/fetchOverlayData'; import DicomMetadataStore from '../services/DicomMetadataStore'; +import { validNumber } from '../utils'; class MetadataProvider { constructor() { @@ -472,20 +473,6 @@ class MetadataProvider { const metadataProvider = new MetadataProvider(); -/** - * Returns the values as an array of javascript numbers - * - * @param element - The javascript object for the specified element in the metadata - * @returns {*} - */ -const validNumber = val => { - if (Array.isArray(val)) { - return val.map(v => (v !== undefined ? Number(v) : v)); - } else { - return val !== undefined ? Number(val) : val; - } -}; - export default metadataProvider; const WADO_IMAGE_LOADER_TAGS = { diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js index 26f2884dadf..738b72815bd 100644 --- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js @@ -129,7 +129,7 @@ const BaseImplementation = { // This is because the mode needs to listen to instances that are added to build up its active displaySets. // It will see there are cached displaySets and end early if this Series has already been fired in this // Mode session for some reason. - this._broadcastEvent(EVENTS.INSTANCES_ADDED, { + this.publish(EVENTS.INSTANCES_ADDED, { StudyInstanceUID, SeriesInstanceUID, madeInClient, @@ -149,7 +149,7 @@ const BaseImplementation = { study.setSeriesMetadata(SeriesInstanceUID, series); }); - this._broadcastEvent(EVENTS.SERIES_ADDED, { + this.publish(EVENTS.SERIES_ADDED, { StudyInstanceUID, madeInClient, }); diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.js b/platform/core/src/services/DisplaySetService/DisplaySetService.js index 9481559adec..d9f7e46c831 100644 --- a/platform/core/src/services/DisplaySetService/DisplaySetService.js +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.js @@ -78,8 +78,8 @@ export default class DisplaySetService { displaySetCache.splice(displaySetCacheIndex, 1); activeDisplaySets.splice(activeDisplaySetsIndex, 1); - this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets); - this._broadcastEvent(EVENTS.DISPLAY_SETS_REMOVED, { + this.publish(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets); + this.publish(EVENTS.DISPLAY_SETS_REMOVED, { displaySetInstanceUIDs: [displaySetInstanceUID], }); } @@ -141,8 +141,8 @@ export default class DisplaySetService { // TODO: This is tricky. How do we know we're not resetting to the same/existing DSs? // TODO: This is likely run anytime we touch DicomMetadataStore. How do we prevent uneccessary broadcasts? if (displaySetsAdded && displaySetsAdded.length) { - this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets); - this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, { + this.publish(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets); + this.publish(EVENTS.DISPLAY_SETS_ADDED, { displaySetsAdded, options, }); diff --git a/platform/core/src/services/DisplaySetService/EVENTS.js b/platform/core/src/services/DisplaySetService/EVENTS.js index 44e07d1f650..e8c5ced1110 100644 --- a/platform/core/src/services/DisplaySetService/EVENTS.js +++ b/platform/core/src/services/DisplaySetService/EVENTS.js @@ -2,6 +2,7 @@ const EVENTS = { DISPLAY_SETS_ADDED: 'event::displaySetService:displaySetsAdded', DISPLAY_SETS_CHANGED: 'event::displaySetService:displaySetsChanged', DISPLAY_SETS_REMOVED: 'event::displaySetService:displaySetsRemoved', + DISPLAY_SET_LOADED: 'event::displaySetService:displaySetLoaded', }; export default EVENTS; diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js index c4abff9fc92..4c91d29cbe6 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.js +++ b/platform/core/src/services/MeasurementService/MeasurementService.js @@ -1,3 +1,4 @@ +/** Internal imports */ import log from '../../log'; import guid from '../../utils/guid'; import pubSubServiceInterface from '../_shared/pubSubServiceInterface'; @@ -296,7 +297,7 @@ class MeasurementService { this.measurements[id] = updatedMeasurement; - this._broadcastEvent( + this.publish( // Add an internal flag to say the measurement has not yet been updated at source. this.EVENTS.MEASUREMENT_UPDATED, { @@ -384,14 +385,14 @@ class MeasurementService { newMeasurement ); this.measurements[internalId] = newMeasurement; - this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { + this.publish(this.EVENTS.MEASUREMENT_UPDATED, { source, measurement: newMeasurement, }); } else { log.info(`Measurement added.`, newMeasurement); this.measurements[internalId] = newMeasurement; - this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, { + this.publish(this.EVENTS.RAW_MEASUREMENT_ADDED, { source, measurement: newMeasurement, data, @@ -470,7 +471,7 @@ class MeasurementService { newMeasurement ); this.measurements[internalId] = newMeasurement; - this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { + this.publish(this.EVENTS.MEASUREMENT_UPDATED, { source, measurement: newMeasurement, notYetUpdatedAtSource: false, @@ -478,7 +479,7 @@ class MeasurementService { } else { log.info('Measurement added.', newMeasurement); this.measurements[internalId] = newMeasurement; - this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, { + this.publish(this.EVENTS.MEASUREMENT_ADDED, { source, measurement: newMeasurement, }); @@ -501,7 +502,7 @@ class MeasurementService { } delete this.measurements[id]; - this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, { + this.publish(this.EVENTS.MEASUREMENT_REMOVED, { source, measurement: id, // This is weird :shrug: }); @@ -510,7 +511,7 @@ class MeasurementService { clearMeasurements() { this.measurements = {}; this._jumpToMeasurementCache = {}; - this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED); + this.publish(this.EVENTS.MEASUREMENTS_CLEARED); } jumpToMeasurement(viewportIndex, id) { @@ -561,7 +562,7 @@ class MeasurementService { */ clear() { this.measurements = {}; - this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED); + this.publish(this.EVENTS.MEASUREMENTS_CLEARED); } /** @@ -581,10 +582,27 @@ class MeasurementService { /* Criteria Matching */ return sourceMappingsByDefinition.find(({ matchingCriteria }) => { - return ( + if (matchingCriteria.type !== measurement.type) { + return false; + } + + if ( + matchingCriteria.properties && + matchingCriteria.properties.every(name => + measurement.hasOwnProperty(name) + ) + ) { + return true; + } + + if ( measurement.points && measurement.points.length === matchingCriteria.points - ); + ) { + return true; + } + + return false; }); } diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index e14045e21bb..6a30615593a 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -104,7 +104,7 @@ export default class ToolBarService { this.state.groups[groupId] = itemId; } - this._broadcastEvent(this.EVENTS.TOOL_BAR_STATE_MODIFIED, {}); + this.publish(this.EVENTS.TOOL_BAR_STATE_MODIFIED, {}); } getButtons() { @@ -118,7 +118,7 @@ export default class ToolBarService { setButton(id, button) { if (this.buttons[id]) { this.buttons[id] = merge(this.buttons[id], button); - this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { + this.publish(this.EVENTS.TOOL_BAR_MODIFIED, { buttons: this.buttons, button: this.buttons[id], buttonSections: this.buttonSections, @@ -128,7 +128,7 @@ export default class ToolBarService { setButtons(buttons) { this.buttons = buttons; - this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { + this.publish(this.EVENTS.TOOL_BAR_MODIFIED, { buttons: this.buttons, buttonSections: this.buttonSections, }); @@ -159,7 +159,7 @@ export default class ToolBarService { // Props check important for validation here... this.buttonSections[key] = buttons; - this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {}); + this.publish(this.EVENTS.TOOL_BAR_MODIFIED, {}); } /** @@ -199,7 +199,7 @@ export default class ToolBarService { } }); - this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {}); + this.publish(this.EVENTS.TOOL_BAR_MODIFIED, {}); } /** diff --git a/platform/core/src/services/_shared/pubSubServiceInterface.js b/platform/core/src/services/_shared/pubSubServiceInterface.js index 9a22a3ccb32..9fdba83c512 100644 --- a/platform/core/src/services/_shared/pubSubServiceInterface.js +++ b/platform/core/src/services/_shared/pubSubServiceInterface.js @@ -7,7 +7,7 @@ import guid from '../../utils/guid'; */ export default { subscribe, - _broadcastEvent, + publish, _unsubscribe, _isValidEvent, }; @@ -76,7 +76,7 @@ function _isValidEvent(eventName) { * @param {func} callbackProps - Properties to pass callback * @return void */ -function _broadcastEvent(eventName, callbackProps) { +function publish(eventName, callbackProps) { const hasListeners = Object.keys(this.listeners).length > 0; const hasCallbacks = Array.isArray(this.listeners[eventName]); diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index baed6ace70f..135f9380313 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -20,6 +20,7 @@ import resolveObjectPath from './resolveObjectPath'; import hierarchicalListUtils from './hierarchicalListUtils'; import progressTrackingUtils from './progressTrackingUtils'; import isLowPriorityModality from './isLowPriorityModality'; +import validNumber from './validNumber'; // Commented out unused functionality. // Need to implement new mechanism for dervived displaySets using the displaySetManager. @@ -47,6 +48,7 @@ const utils = { hierarchicalListUtils, progressTrackingUtils, isLowPriorityModality, + validNumber, }; export { @@ -70,6 +72,7 @@ export { hierarchicalListUtils, progressTrackingUtils, isLowPriorityModality, + validNumber, }; export default utils; diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index 791681acdc4..4a5390263b6 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -14,6 +14,7 @@ describe('Top level exports', () => { 'StackManager', 'formatDate', 'formatPN', + 'validNumber', //'loadAndCacheDerivedDisplaySets', 'DicomLoaderService', 'urlUtil', diff --git a/platform/core/src/utils/validNumber.js b/platform/core/src/utils/validNumber.js new file mode 100644 index 00000000000..db6178bc7b3 --- /dev/null +++ b/platform/core/src/utils/validNumber.js @@ -0,0 +1,15 @@ +/** + * Validate a number + * + * @param {number} val + * @returns {boolean} boolean indicating wether the number is a valid number + */ +const validNumber = val => { + if (Array.isArray(val)) { + return val.map(v => (v !== undefined ? Number(v) : v)); + } else { + return val !== undefined ? Number(val) : val; + } +}; + +export default validNumber; diff --git a/platform/docs/docs/README.md b/platform/docs/docs/README.md index c7b5c7f8c7e..7a776807115 100644 --- a/platform/docs/docs/README.md +++ b/platform/docs/docs/README.md @@ -55,12 +55,13 @@ To cite the OHIF Viewer in an academic publication, please cite > _Open Health Imaging Foundation Viewer: An Extensible Open-Source Framework > for Building Web-Based Imaging Applications to Support Cancer Research_ > -> Erik Ziegler, Trinity Urban, Danny Brown, James Petts, Steve D. Pieper, Rob -> Lewis, Chris Hafey, and Gordon J. Harris _JCO Clinical Cancer Informatics_, no. 4 (2020), 336-345, DOI: +> Erik Ziegler, Trinity Urban, Danny Brown, James Petts, Steve D. Pieper, Rob +> Lewis, Chris Hafey, and Gordon J. Harris _JCO Clinical Cancer Informatics_, +> no. 4 (2020), 336-345, DOI: > [10.1200/CCI.19.00131](https://www.doi.org/10.1200/CCI.19.00131) -This article is freely available on Pubmed Central: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7259879/ - +This article is freely available on Pubmed Central: +https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7259879/ or, for Lesion Tracker of OHIF v1, please cite: @@ -68,13 +69,13 @@ or, for Lesion Tracker of OHIF v1, please cite: > Imaging Research and Clinical Trials_ > > Trinity Urban, Erik Ziegler, Rob Lewis, Chris Hafey, Cheryl Sadow, Annick D. -> Van den Abbeele and Gordon J. Harris _Cancer Research_, November 1 2017 (77) (21) e119-e122 DOI: +> Van den Abbeele and Gordon J. Harris _Cancer Research_, November 1 2017 (77) +> (21) e119-e122 DOI: > [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334) This article is freely available on Pubmed Central. https://pubmed.ncbi.nlm.nih.gov/29092955/ - **Note:** If you use or find this repository helpful, please take the time to star this repository on Github. This is an easy way for us to assess adoption and it can help us obtain future funding for the project. diff --git a/platform/docs/docusaurus.config.js b/platform/docs/docusaurus.config.js index 4a8cf9a20d6..f3bec2f0974 100644 --- a/platform/docs/docusaurus.config.js +++ b/platform/docs/docusaurus.config.js @@ -56,11 +56,11 @@ const isI18nStaging = process.env.I18N_STAGING === 'true'; defaultLocale: 'en', locales: isDeployPreview ? // Deploy preview: keep it fast! - ['en'] + ['en'] : isI18nStaging - ? // Staging locales: https://docusaurus-i18n-staging.netlify.app/ + ? // Staging locales: https://docusaurus-i18n-staging.netlify.app/ ['en'] - : // Production locales + : // Production locales ['en'], }, onBrokenLinks: 'warn', @@ -191,7 +191,7 @@ const isI18nStaging = process.env.I18N_STAGING === 'true'; ], presets: [ [ - "@docusaurus/preset-classic", + '@docusaurus/preset-classic', { debug: true, // force debug plugin usage docs: { diff --git a/platform/ui/src/components/Dialog/Dialog.jsx b/platform/ui/src/components/Dialog/Dialog.jsx index d1b61500894..5d25ba48b9f 100644 --- a/platform/ui/src/components/Dialog/Dialog.jsx +++ b/platform/ui/src/components/Dialog/Dialog.jsx @@ -30,10 +30,8 @@ const Dialog = ({ const width = 'w-full'; useEffect(() => { - if (onShow) { - onShow(); - } - }, [onShow]); + onShow(); + }, []); return (
@@ -55,11 +53,14 @@ const Dialog = ({ ); }; +const noop = () => {}; + Dialog.propTypes = { title: PropTypes.string, text: PropTypes.string, onClose: PropTypes.func, noCloseButton: PropTypes.bool, + onShow: PropTypes.func, header: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), body: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), footer: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), @@ -80,6 +81,8 @@ Dialog.defaultProps = { footer: Footer, body: Body, value: {}, + onShow: noop, + onClose: noop }; export default Dialog; diff --git a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx index 1a6e249526e..c5211c2cd17 100644 --- a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx +++ b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx @@ -86,7 +86,7 @@ ThumbnailNoImage.propTypes = { /** Must match the "type" a dropTarget expects */ type: PropTypes.string.isRequired, }), - description: PropTypes.string.isRequired, + description: PropTypes.string, modality: PropTypes.string.isRequired, /* Tooltip message to display when modality text is hovered */ modalityTooltip: PropTypes.string.isRequired, diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 7e8666186a5..72934a374f4 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -59,9 +59,9 @@ "classnames": "^2.2.6", "core-js": "^3.16.1", "cornerstone-math": "^0.1.9", + "dcmjs": "0.18.8", "cornerstone-tools": "6.0.2", "cornerstone-wado-image-loader": "4.0.4", - "dcmjs": "0.16.1", "dicom-parser": "^1.8.9", "dotenv-webpack": "^1.7.0", "hammerjs": "^2.0.8", diff --git a/yarn.lock b/yarn.lock index b183fe3f56a..632dc2ec33a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6685,13 +6685,15 @@ dayjs@^1.9.3: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.6.tgz#288b2aa82f2d8418a6c9d4df5898c0737ad02a63" integrity sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw== -dcmjs@0.16.1: - version "0.16.1" - resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.16.1.tgz#76bc61cbdf2c58a2f54990080729ca2d7ee19543" - integrity sha512-t6vsKi5QXzwX1fwnHY2hdU99FfyzmK8aO0OSBDH6XvJNrBp2A6HpoVWbucybOy8eStIPDqs4v0FGP/m39cTCRA== +dcmjs@0.18.8: + version "0.18.8" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.18.8.tgz#fc8e6af03ace9050da49f4108334aa3d0a3bdf3d" + integrity sha512-Z3nNEmzkWdpHQPgjozGqk8XcuhP/hXP1C64Wtg9V+VO9eEc8eNFJFJ6If62f1cuOh0FaRwHqxBT7OzhOAUpFTA== dependencies: "@babel/polyfill" "^7.8.3" "@babel/runtime" "^7.8.4" + gl-matrix "^3.1.0" + lodash.clonedeep "^4.5.0" loglevelnext "^3.0.1" ndarray "^1.0.19" @@ -8803,6 +8805,11 @@ github-slugger@^1.3.0: dependencies: emoji-regex ">=6.0.0 <=6.1.1" +gl-matrix@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b" + integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA== + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" @@ -12075,7 +12082,7 @@ moo-color@^1.0.2: dependencies: color-name "^1.1.4" -mousetrap@^1.6.5: +mousetrap@^1.6.3, mousetrap@^1.6.5: version "1.6.5" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==