diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 93a7b1b2f..e1b1a5f3a 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -10,7 +10,10 @@ import { DicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core'; import getImageId from './utils/getImageId'; import * as dcmjs from 'dcmjs'; -import { retrieveStudyMetadata } from './retrieveStudyMetadata.js'; +import { + retrieveStudyMetadata, + deleteStudyMetadataPromise, +} from './retrieveStudyMetadata.js'; const { DicomMetaDictionary, DicomDict } = dcmjs.data; @@ -187,6 +190,7 @@ function createDicomWebApi(dicomWebConfig) { storeInstances(instances); }); }, + deleteStudyMetadataPromise, getImageIdsForDisplaySet(displaySet) { const images = displaySet.images; const imageIds = []; diff --git a/extensions/default/src/Panels/PanelStudyBrowser.jsx b/extensions/default/src/Panels/PanelStudyBrowser.jsx index 221395926..8e813217f 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.jsx @@ -1,6 +1,9 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { StudyBrowser, useImageViewer } from '@ohif/ui'; +import { utils } from '@ohif/core'; + +const { formatDate } = utils; /** * @@ -37,7 +40,7 @@ function PanelStudyBrowser({ const actuallyMappedStudies = mappedStudies.map(qidoStudy => { return { studyInstanceUid: qidoStudy.StudyInstanceUID, - date: qidoStudy.StudyDate, + date: formatDate(qidoStudy.StudyDate), description: qidoStudy.StudyDescription, modalities: qidoStudy.ModalitiesInStudy, numInstances: qidoStudy.NumInstances, @@ -151,11 +154,11 @@ function PanelStudyBrowser({ ); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? // eslint-disable-next-line prettier/prettier - [ - ...expandedStudyInstanceUIDs.filter( - stdyUid => stdyUid !== StudyInstanceUID - ), - ] + [ + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx index ca7bc92c6..898d74651 100644 --- a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx @@ -5,14 +5,6 @@ import { useViewportGrid, } from '@ohif/ui'; -const DEFAULT_LAYOUT = { - type: 'SET_LAYOUT', - payload: { - numCols: 1, - numRows: 1, - }, -}; - function LayoutSelector() { const [isOpen, setIsOpen] = useState(false); const [viewportGridState, viewportGridService] = useViewportGrid(); @@ -33,7 +25,7 @@ function LayoutSelector() { useEffect(() => { /* Reset to default layout when component unmounts */ return () => { - dispatch(DEFAULT_LAYOUT); + viewportGridService.setLayout({ numCols: 1, numRows: 1 }); }; }, []); diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index a228ea220..cc80065be 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -3,13 +3,14 @@ import PropTypes from 'prop-types'; import cornerstoneTools from 'cornerstone-tools'; import cornerstone from 'cornerstone-core'; import CornerstoneViewport from 'react-cornerstone-viewport'; -import OHIF, { DicomMetadataStore } from '@ohif/core'; +import OHIF, { DicomMetadataStore, utils } from '@ohif/core'; import { ViewportActionBar, useViewportGrid } from '@ohif/ui'; import TOOL_NAMES from './constants/toolNames'; import { adapters } from 'dcmjs'; import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema'; import id from './id'; +const { formatDate } = utils; const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex'); const globalImageIdSpecificToolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager; @@ -298,7 +299,59 @@ function OHIFCornerstoneSRViewport({ } }); - const imageIds = []; + if ( + extensionManager.registeredExtensionIds.includes( + MEASUREMENT_TRACKING_EXTENSION_ID + ) + ) { + // Set the series touched as tracked. + const imageIds = []; + + Object.keys(hydratableMeasurementsInSR).forEach(toolType => { + const toolDataForToolType = hydratableMeasurementsInSR[toolType]; + + toolDataForToolType.forEach(data => { + // Add the measurement to toolState + const imageId = sopInstanceUIDToImageId[data.sopInstanceUid]; + + if (!imageIds.includes(imageId)) { + imageIds.push(imageId); + } + }); + }); + + let targetStudyInstanceUID; + const SeriesInstanceUIDs = []; + + for (let i = 0; i < imageIds.length; i++) { + const imageId = imageIds[0]; + const { + SeriesInstanceUID, + StudyInstanceUID, + } = cornerstone.metaData.get('instance', imageId); + + if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) { + SeriesInstanceUIDs.push(SeriesInstanceUID); + } + + if (!targetStudyInstanceUID) { + targetStudyInstanceUID = StudyInstanceUID; + } else if (targetStudyInstanceUID !== StudyInstanceUID) { + console.warn( + 'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.' + ); + } + } + + debugger; + + sendTrackedMeasurementsEvent('SET_TRACKED_SERIES', { + StudyInstanceUID: targetStudyInstanceUID, + SeriesInstanceUIDs, + }); + + debugger; + } Object.keys(hydratableMeasurementsInSR).forEach(toolType => { const toolDataForToolType = hydratableMeasurementsInSR[toolType]; @@ -342,44 +395,6 @@ function OHIFCornerstoneSRViewport({ // Deal with optional extensions - if ( - extensionManager.registeredExtensionIds.includes( - MEASUREMENT_TRACKING_EXTENSION_ID - ) - ) { - // Set the series touched as tracked. - - let targetStudyInstanceUID; - const SeriesInstanceUIDs = []; - - for (let i = 0; i < imageIds.length; i++) { - const imageId = imageIds[0]; - const { - SeriesInstanceUID, - StudyInstanceUID, - } = cornerstone.metaData.get('instance', imageId); - - if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) { - SeriesInstanceUIDs.push(SeriesInstanceUID); - } - - if (!targetStudyInstanceUID) { - targetStudyInstanceUID = StudyInstanceUID; - } else if (targetStudyInstanceUID !== StudyInstanceUID) { - console.warn( - 'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.' - ); - } - } - - debugger; - - sendTrackedMeasurementsEvent('SET_TRACKED_SERIES', { - StudyInstanceUID: targetStudyInstanceUID, - SeriesInstanceUID: SeriesInstanceUIDs[0], - }); - } - debugger; viewportGridService.setDisplaysetForViewport({ @@ -399,7 +414,7 @@ function OHIFCornerstoneSRViewport({ isTracked: false, isLocked: displaySet.isLocked, isHydrated, - studyDate: StudyDate, + studyDate: formatDate(StudyDate), currentSeries: SeriesNumber, seriesDescription: SeriesDescription, modality: Modality, @@ -410,7 +425,7 @@ function OHIFCornerstoneSRViewport({ patientSex: PatientSex || '', patientAge: PatientAge || '', MRN: PatientID || '', - thickness: `${SliceThickness}mm`, + thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '', spacing: PixelSpacing && PixelSpacing.length ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed( diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx index 063995a13..5081deaad 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx @@ -6,6 +6,9 @@ import { machineConfiguration, defaultOptions, } from './measurementTrackingMachine'; +import promptBeginTracking from './promptBeginTracking'; +import promptTrackNewSeries from './promptTrackNewSeries'; +import promptTrackNewStudy from './promptTrackNewStudy'; const TrackedMeasurementsContext = React.createContext(); TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext'; @@ -19,41 +22,20 @@ function TrackedMeasurementsContextProvider( UIViewportDialogService, { children } ) { - function promptUser(message, ctx, evt) { - const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; - - return new Promise(function(resolve, reject) { - /** - * TODO: Will have issues if "SeriesInstanceUID" exists in multiple displaySets? - * - * @param {number} result - -1 | 0 | 1 --> deny | cancel | accept - * @return resolve { userResponse: number, StudyInstanceUID: string, SeriesInstanceUID: string } - */ - const handleSubmit = result => { - UIViewportDialogService.hide(); - resolve({ userResponse: result, StudyInstanceUID, SeriesInstanceUID }); - }; - - UIViewportDialogService.show({ - viewportIndex, - type: 'info', - message, - actions: [ - { type: 'cancel', text: 'No', value: 0 }, - { type: 'secondary', text: 'No, do not ask again', value: -1 }, - { type: 'primary', text: 'Yes', value: 1 }, - ], - onSubmit: handleSubmit, - }); - }); - } - - // Set StateMachine behavior for prompts (invoked services) const machineOptions = Object.assign({}, defaultOptions); machineOptions.services = Object.assign({}, machineOptions.services, { - promptBeginTracking: promptUser.bind(null, 'Start tracking?'), - promptTrackNewStudy: promptUser.bind(null, 'New study?'), - promptTrackNewSeries: promptUser.bind(null, 'New series?'), + promptBeginTracking: promptBeginTracking.bind( + null, + UIViewportDialogService + ), + promptTrackNewSeries: promptTrackNewSeries.bind( + null, + UIViewportDialogService + ), + promptTrackNewStudy: promptTrackNewStudy.bind( + null, + UIViewportDialogService + ), }); diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js index 5ff597b9a..4aa90ffb5 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js @@ -1,5 +1,13 @@ import { assign } from 'xstate'; +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, +}; + const machineConfiguration = { id: 'measurementTracking', initial: 'idle', @@ -18,13 +26,7 @@ const machineConfiguration = { SET_TRACKED_SERIES: [ { target: 'tracking', - actions: ['setTrackedStudyAndSeries'], - cond: 'isNewStudy', - }, - { - target: 'tracking', - actions: ['addTrackedSeries'], - cond: 'isNewSeries', + actions: ['setTrackedStudyAndMultipleSeries'], }, ], }, @@ -36,11 +38,11 @@ const machineConfiguration = { { target: 'tracking', actions: ['setTrackedStudyAndSeries'], - cond: 'promptAccepted', + cond: 'shouldSetStudyAndSeries', }, { target: 'off', - cond: 'promptDeclined', + cond: 'shouldKillMachine', }, { target: 'idle', @@ -76,15 +78,21 @@ const machineConfiguration = { ], }, }, - promptTrackNewStudy: { + promptTrackNewSeries: { invoke: { - src: 'promptTrackNewStudy', + src: 'promptTrackNewSeries', onDone: [ { target: 'tracking', - actions: ['setTrackedStudyAndSeries'], - cond: 'promptAccepted', + actions: ['addTrackedSeries'], + cond: 'shouldAddSeries', }, + { + target: 'tracking', + actions: ['setTrackedStudyAndSeries'], + cond: 'shouldSetStudyAndSeries', + }, + // CREATE_REPORT && CANCEL { target: 'tracking', }, @@ -94,14 +102,14 @@ const machineConfiguration = { }, }, }, - promptTrackNewSeries: { + promptTrackNewStudy: { invoke: { - src: 'promptTrackNewSeries', + src: 'promptTrackNewStudy', onDone: [ { target: 'tracking', - actions: ['addTrackedSeries'], - cond: 'promptAccepted', + actions: ['setTrackedStudyAndSeries'], + cond: 'shouldSetStudyAndSeries', }, { target: 'tracking', @@ -138,6 +146,10 @@ const defaultOptions = { trackedStudy: evt.data.StudyInstanceUID, trackedSeries: [evt.data.SeriesInstanceUID], })), + setTrackedStudyAndMultipleSeries: assign((ctx, evt) => ({ + trackedStudy: evt.StudyInstanceUID, + trackedSeries: [...ctx.trackedSeries, ...evt.SeriesInstanceUIDs], + })), addTrackedSeries: assign((ctx, evt) => ({ trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID], })), @@ -148,9 +160,12 @@ const defaultOptions = { })), }, guards: { - promptAccepted: (ctx, evt) => evt.data && evt.data.userResponse === 1, - promptCanceled: (ctx, evt) => evt.data && evt.data.userResponse === 0, - promptDeclined: (ctx, evt) => evt.data && evt.data.userResponse === -1, + shouldKillMachine: (ctx, evt) => + evt.data && evt.data.userResponse === RESPONSE.NO_NEVER, + shouldAddSeries: (ctx, evt) => + evt.data && evt.data.userResponse === RESPONSE.ADD_SERIES, + shouldSetStudyAndSeries: (ctx, evt) => + evt.data && evt.data.userResponse === RESPONSE.SET_STUDY_AND_SERIES, // Has more than 1, or SeriesInstanceUID is not in list // --> Post removal would have non-empty trackedSeries array hasRemainingTrackedSeries: (ctx, evt) => @@ -162,13 +177,4 @@ const defaultOptions = { }, }; -// const measurementTrackingMachine = Machine( -// machineConfiguration, -// defaultOptions -// ); -// .transition(state, eventArgument).value -// const service = interpret(measurementTrackingMachine).start(); -// .send(event): nextState -// .state (getter) -// .onTransition(state => { state.vale }) export { defaultOptions, machineConfiguration }; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js new file mode 100644 index 000000000..657c24e66 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js @@ -0,0 +1,61 @@ +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, +}; + +function promptUser(UIViewportDialogService, ctx, evt) { + const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; + + return new Promise(async function(resolve, reject) { + let promptResult = await _askTrackMeasurements( + UIViewportDialogService, + viewportIndex + ); + + resolve({ + userResponse: promptResult, + StudyInstanceUID, + SeriesInstanceUID, + }); + }); +} + +function _askTrackMeasurements(UIViewportDialogService, viewportIndex) { + return new Promise(function(resolve, reject) { + const message = 'Track measurements for this series?'; + const actions = [ + { type: 'cancel', text: 'No', value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: 'No, do not ask again', + value: RESPONSE.NO_NEVER, + }, + { + type: 'primary', + text: 'Yes', + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportIndex, + type: 'info', + message, + actions, + onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + }); + }); +} + +export default promptUser; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js new file mode 100644 index 000000000..4a6f7dcc1 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js @@ -0,0 +1,110 @@ +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, +}; + +function promptUser(UIViewportDialogService, ctx, evt) { + const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; + + return new Promise(async function(resolve, reject) { + let promptResult = await _askShouldAddMeasurements( + UIViewportDialogService, + viewportIndex + ); + + if (promptResult === RESPONSE.CREATE_REPORT) { + promptResult = await _askSaveDiscardOrCancel( + UIViewportDialogService, + viewportIndex + ); + } + + // TODO: Hook into @JamesAPetts createReport + if (promptResult === RESPONSE.CREATE_REPORT) { + window.alert('CREATE REPORT'); + } + + resolve({ + userResponse: promptResult, + StudyInstanceUID, + SeriesInstanceUID, + }); + }); +} + +function _askShouldAddMeasurements(UIViewportDialogService, viewportIndex) { + return new Promise(function(resolve, reject) { + const message = + 'Do you want to add this measurement to the existing report?'; + const actions = [ + { type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: 'Create new report', + value: RESPONSE.CREATE_REPORT, + }, + { + type: 'primary', + text: 'Add to existing report', + value: RESPONSE.ADD_SERIES, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportIndex, + type: 'info', + message, + actions, + onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + }); + }); +} + +function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) { + return new Promise(function(resolve, reject) { + const message = + 'You have existing tracked measurements. What would you like to do with your existing tracked measurements?'; + const actions = [ + { type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: 'Save in report', + value: RESPONSE.CREATE_REPORT, + }, + { + type: 'primary', + text: 'Discard', + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportIndex, + type: 'warning', + message, + actions, + onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + }); + }); +} + +export default promptUser; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js new file mode 100644 index 000000000..0f8842a31 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js @@ -0,0 +1,104 @@ +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, +}; + +function promptUser(UIViewportDialogService, ctx, evt) { + const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; + + return new Promise(async function(resolve, reject) { + let promptResult = await _askTrackMeasurements( + UIViewportDialogService, + viewportIndex + ); + + if (promptResult === RESPONSE.SET_STUDY_AND_SERIES) { + promptResult = await _askSaveDiscardOrCancel( + UIViewportDialogService, + viewportIndex + ); + } + + // TODO: Hook into @JamesAPetts createReport + if (promptResult === RESPONSE.CREATE_REPORT) { + window.alert('CREATE REPORT'); + } + + resolve({ + userResponse: promptResult, + StudyInstanceUID, + SeriesInstanceUID, + }); + }); +} + +function _askTrackMeasurements(UIViewportDialogService, viewportIndex) { + return new Promise(function(resolve, reject) { + const message = 'Track measurements for this series?'; + const actions = [ + { type: 'cancel', text: 'No', value: RESPONSE.CANCEL }, + { + type: 'primary', + text: 'Yes', + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportIndex, + type: 'info', + message, + actions, + onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + }); + }); +} + +function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) { + return new Promise(function(resolve, reject) { + const message = + 'Measurements cannot span across multiple studies. Do you want to save your tracked measurements?'; + const actions = [ + { type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: 'No, discard previosuly tracked series & measurements', + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + { + type: 'primary', + text: 'Yes', + value: RESPONSE.CREATE_REPORT, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportIndex, + type: 'warning', + message, + actions, + onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + }); + }); +} + +export default promptUser; diff --git a/extensions/measurement-tracking/src/getViewportModule.js b/extensions/measurement-tracking/src/getViewportModule.js index b494f418b..65203872e 100644 --- a/extensions/measurement-tracking/src/getViewportModule.js +++ b/extensions/measurement-tracking/src/getViewportModule.js @@ -12,8 +12,19 @@ const OHIFCornerstoneViewport = props => { ); }; -function getViewportModule({ commandsManager }) { - return [{ name: 'cornerstone-tracked', component: OHIFCornerstoneViewport }]; +function getViewportModule({ servicesManager }) { + const ExtendedOHIFCornerstoneSRViewport = props => { + const { ToolBarService } = servicesManager.services; + + return ( + + ); + }; + + return [{ name: 'cornerstone-tracked', component: ExtendedOHIFCornerstoneSRViewport }]; } export default getViewportModule; diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index 10184832a..b7cf5ae8f 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -24,7 +24,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { measurementChangeTimestamp, 200 ); - const { MeasurementService } = servicesManager.services; + const { MeasurementService, DisplaySetService } = servicesManager.services; const [ trackedMeasurements, sendTrackedMeasurementsEvent, @@ -105,7 +105,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { const activeMeasurementItem = 0; - const onExportClick = () => { + const exportReport = () => { const measurements = MeasurementService.getMeasurements(); const trackedMeasurements = measurements.filter( m => @@ -117,7 +117,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { DICOMSR.downloadReport(trackedMeasurements, dataSource); }; - const onCreateReportClick = () => { + const createReport = () => { const measurements = MeasurementService.getMeasurements(); const trackedMeasurements = measurements.filter( m => @@ -125,13 +125,18 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { trackedSeries.includes(m.referenceSeriesUID) ); - const dataSources = extensionManager.getDataSources(); // TODO -> Eventually deal with multiple dataSources. // Would need some way of saying which one is the "push" dataSource const dataSource = dataSources[0]; - DICOMSR.storeMeasurements(trackedMeasurements, dataSource); + DICOMSR.storeMeasurements( + trackedMeasurements, + dataSource, + naturalizedReport => { + DisplaySetService.makeDisplaySets([naturalizedReport]); + } + ); }; return ( @@ -154,8 +159,8 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index 2f4b279ce..a3ef5f777 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -1,8 +1,11 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; +import { utils } from '@ohif/core'; import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui'; import { useTrackedMeasurements } from '../../getContextModule'; +const { formatDate } = utils; + /** * * @param {*} param0 @@ -68,7 +71,7 @@ function PanelStudyBrowserTracking({ const actuallyMappedStudies = mappedStudies.map(qidoStudy => { return { studyInstanceUid: qidoStudy.StudyInstanceUID, - date: qidoStudy.StudyDate, + date: formatDate(qidoStudy.StudyDate), description: qidoStudy.StudyDescription, modalities: qidoStudy.ModalitiesInStudy, numInstances: qidoStudy.NumInstances, @@ -198,10 +201,10 @@ function PanelStudyBrowserTracking({ ); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? [ - ...expandedStudyInstanceUIDs.filter( - stdyUid => stdyUid !== StudyInstanceUID - ), - ] + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); @@ -288,8 +291,11 @@ function _mapDisplaySets( const firstViewportIndexWithMatchingDisplaySetUid = viewports.findIndex( vp => vp.displaySetInstanceUID === ds.displaySetInstanceUID ); + const viewportIdentificator = - _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid] || ''; + viewports.length > 1 + ? _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid] + : ''; const array = componentType === 'thumbnailTracked' diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 7058bb506..1b639282f 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import cornerstone from 'cornerstone-core'; import cornerstoneTools from 'cornerstone-tools'; import CornerstoneViewport from 'react-cornerstone-viewport'; -import OHIF, { DicomMetadataStore } from '@ohif/core'; +import OHIF, { DicomMetadataStore, utils } from '@ohif/core'; import { Notification, ViewportActionBar, @@ -12,6 +12,10 @@ import { } from '@ohif/ui'; import { useTrackedMeasurements } from './../getContextModule'; +import ViewportOverlay from './ViewportOverlay'; + +const { formatDate } = utils; + // TODO -> Get this list from the list of tracked measurements. // TODO -> We can now get a list of tool names from the measurement service. // Use the toolnames to check which tools we have instead, using the @@ -40,6 +44,7 @@ function TrackedCornerstoneViewport({ dataSource, displaySet, viewportIndex, + ToolBarService, }) { const [trackedMeasurements] = useTrackedMeasurements(); const [{ activeViewportIndex, viewports }] = useViewportGrid(); @@ -234,16 +239,21 @@ function TrackedCornerstoneViewport({ setIsTracked(!isTracked); } + const label = + viewports.length > 1 + ? _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid] + : ''; + return ( <> alert(`Series ${direction}`)} showNavArrows={viewportIndex === activeViewportIndex} studyData={{ - label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid], + label, isTracked: trackedSeries.includes(SeriesInstanceUID), isLocked: false, - studyDate: SeriesDate, // TODO: This is series date. Is that ok? + studyDate: formatDate(SeriesDate), // TODO: This is series date. Is that ok? currentSeries: SeriesNumber, seriesDescription: SeriesDescription, modality: Modality, @@ -254,7 +264,7 @@ function TrackedCornerstoneViewport({ patientSex: PatientSex || '', patientAge: PatientAge || '', MRN: PatientID || '', - thickness: `${SliceThickness}mm`, + thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '', spacing: PixelSpacing && PixelSpacing.length ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed( @@ -277,7 +287,15 @@ function TrackedCornerstoneViewport({ isStackPrefetchEnabled={true} // todo isPlaying={false} frameRate={24} - isOverlayVisible={false} + isOverlayVisible={true} + viewportOverlayComponent={props => { + return ( + + ); + }} />
{viewportDialogState.viewportIndex === viewportIndex && ( @@ -286,6 +304,7 @@ function TrackedCornerstoneViewport({ type={viewportDialogState.type} actions={viewportDialogState.actions} onSubmit={viewportDialogState.onSubmit} + onOutsideClick={viewportDialogState.onOutsideClick} /> )}
diff --git a/extensions/measurement-tracking/src/viewports/ViewportOverlay.js b/extensions/measurement-tracking/src/viewports/ViewportOverlay.js new file mode 100644 index 000000000..b042743cf --- /dev/null +++ b/extensions/measurement-tracking/src/viewports/ViewportOverlay.js @@ -0,0 +1,81 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cornerstone from 'cornerstone-core'; +import classnames from 'classnames'; + +const ViewportOverlay = ({ + imageId, + scale, + windowWidth, + windowCenter, + imageIndex, + stackSize, + activeTools +}) => { + const topLeft = 'top-viewport left-viewport'; + const topRight = 'top-viewport right-viewport-scrollbar'; + const bottomRight = 'bottom-viewport right-viewport-scrollbar'; + const bottomLeft = 'bottom-viewport left-viewport'; + const overlay = 'absolute pointer-events-none'; + + const isZoomActive = activeTools.includes('Zoom'); + const isWwwcActive = activeTools.includes('Wwwc'); + + if (!imageId) { + return null; + } + + const generalImageModule = cornerstone.metaData.get('generalImageModule', imageId) || {}; + const { instanceNumber } = generalImageModule; + + return ( +
+
+ {isZoomActive && ( +
+ Zoom: + {scale.toFixed(2)}x +
+ )} + {isWwwcActive && ( +
+ W: + {windowWidth.toFixed(0)} + L: + {windowCenter.toFixed(0)} +
+ )} +
+
+ {stackSize > 1 && ( +
+ I: + + {`${instanceNumber}/${stackSize}`} + +
+ )} +
+
+
+
+
+
+ ); +}; + +ViewportOverlay.propTypes = { + scale: PropTypes.number.isRequired, + windowWidth: PropTypes.number.isRequired, + windowCenter: PropTypes.number.isRequired, + imageId: PropTypes.string.isRequired, + imageIndex: PropTypes.number.isRequired, + stackSize: PropTypes.number.isRequired, + activeTools: PropTypes.arrayOf(PropTypes.string) +}; + +ViewportOverlay.defaultProps = { + activeTools: [] +}; + +export default ViewportOverlay; diff --git a/modes/example/src/index.js b/modes/example/src/index.js index 33aae7ad1..597f179ca 100644 --- a/modes/example/src/index.js +++ b/modes/example/src/index.js @@ -1,4 +1,5 @@ import toolbarButtons from './toolbarButtons.js'; +import { hotkeys } from '@ohif/core'; export default function mode({ modeConfiguration }) { return { @@ -68,6 +69,9 @@ export default function mode({ modeConfiguration }) { ], extensions: ['org.ohif.default', 'org.ohif.cornerstone'], sopClassHandlers: ['org.ohif.default.sopClassHandlerModule.stack'], + hotkeys: [ + ...hotkeys.defaults.hotkeyBindings + ] }; } diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index 71c249341..2bf100c09 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -1,4 +1,5 @@ import toolbarButtons from './toolbarButtons.js'; +import { hotkeys } from '@ohif/core'; const ohif = { layout: 'org.ohif.default.layoutTemplateModule.viewerLayout', @@ -94,6 +95,9 @@ export default function mode({ modeConfiguration }) { 'org.ohif.dicom-sr', ], sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler], + hotkeys: [ + ...hotkeys.defaults.hotkeyBindings + ] }; } diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index c2e5cd5e4..b0a271638 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -1,8 +1,11 @@ // TODO: torn, can either bake this here; or have to create a whole new button type // Only ways that you can pass in a custom React component for render :l -import { ExpandableToolbarButton, ListMenu } from '@ohif/ui'; import React from 'react'; import classnames from 'classnames'; +import { ExpandableToolbarButton, ListMenu } from '@ohif/ui'; +import { defaults } from '@ohif/core'; + +const { windowLevelPresets } = defaults; export default [ // Divider @@ -39,36 +42,36 @@ export default [ commandName: 'setToolActive', commandOptions: { toolName: 'Wwwc' }, commands: { - 'windowLevelPreset1': { - commandName: 'windowLevelPreset1', - commandOptions: {}, + 1: { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[1], }, - 'windowLevelPreset2': { - commandName: 'windowLevelPreset2', - commandOptions: {}, + 2: { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[2], }, - 'windowLevelPreset3': { - commandName: 'windowLevelPreset3', - commandOptions: {}, + 3: { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[3], }, - 'windowLevelPreset4': { - commandName: 'windowLevelPreset4', - commandOptions: {}, + 4: { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[4], }, - 'windowLevelPreset5': { - commandName: 'windowLevelPreset5', - commandOptions: {}, + 5: { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[5], } }, type: 'primary', content: ListMenu, contentProps: { options: [ - { value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' }, - { value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' }, - { value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' }, - { value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' }, - { value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' }, + { value: 1, title: 'Soft tissue', subtitle: '400 / 40' }, + { value: 2, title: 'Lung', subtitle: '1500 / -600' }, + { value: 3, title: 'Liver', subtitle: '150 / 90' }, + { value: 4, title: 'Bone', subtitle: '80 / 40' }, + { value: 5, title: 'Brain', subtitle: '2500 / 480' }, ], renderer: ({ title, subtitle, isActive, index }) => ( <> diff --git a/platform/core/package.json b/platform/core/package.json index a14313b13..34aa934cc 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -44,6 +44,7 @@ "isomorphic-base64": "^1.0.2", "lodash.clonedeep": "^4.5.0", "lodash.merge": "^4.6.1", + "moment": "^2.24.0", "mousetrap": "^1.6.3", "validate.js": "^0.12.0" } diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js index 2b3cfe172..8f86f743c 100644 --- a/platform/core/src/DICOMSR/dataExchange.js +++ b/platform/core/src/DICOMSR/dataExchange.js @@ -43,45 +43,6 @@ const retrieveMeasurements = server => { 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 storeMeasurementsOld = 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 saved successfully', - }; - } catch (error) { - log.error( - `[DICOMSR] Error while saving the measurements: ${error.message}` - ); - throw new Error('Error while saving the measurements.'); - } -}; - /** * * @param {object[]} measurementData An array of measurements from the measurements service @@ -119,7 +80,7 @@ const generateReport = measurementData => { * that you wish to serialize. * @param {object} dataSource The dataSource that you wish to use to persist the data. */ -const storeMeasurements = async (measurementData, dataSource) => { +const storeMeasurements = async (measurementData, dataSource, onSuccess) => { // TODO -> Eventually use the measurements directly and not the dcmjs adapter, // But it is good enough for now whilst we only have cornerstone as a datasource. log.info('[DICOMSR] storeMeasurements'); @@ -136,7 +97,11 @@ const storeMeasurements = async (measurementData, dataSource) => { await dataSource.store.dicom(naturalizedReport); if (StudyInstanceUID) { - studies.deleteStudyMetadataPromise(StudyInstanceUID); + dataSource.deleteStudyMetadataPromise(StudyInstanceUID); + } + + if (onSuccess) { + onSuccess(naturalizedReport); } return { diff --git a/platform/core/src/DataSources/IWebApiDataSource.js b/platform/core/src/DataSources/IWebApiDataSource.js index 10285d364..2ba208ea5 100644 --- a/platform/core/src/DataSources/IWebApiDataSource.js +++ b/platform/core/src/DataSources/IWebApiDataSource.js @@ -17,6 +17,7 @@ function create({ retrieve, store, retrieveSeriesMetadata, + deleteStudyMetadataPromise, getImageIdsForDisplaySet, }) { const defaultQuery = { @@ -59,6 +60,7 @@ function create({ store: store || defaultStore, getImageIdsForDisplaySet, retrieveSeriesMetadata, + deleteStudyMetadataPromise, }; } diff --git a/platform/core/src/classes/HotkeysManager.js b/platform/core/src/classes/HotkeysManager.js index ee65f08a3..0848e0437 100644 --- a/platform/core/src/classes/HotkeysManager.js +++ b/platform/core/src/classes/HotkeysManager.js @@ -1,3 +1,4 @@ +import objectHash from 'object-hash'; import hotkeys from './../utils/hotkeys'; import log from './../log.js'; @@ -6,6 +7,7 @@ import log from './../log.js'; * * @typedef {Object} HotkeyDefinition * @property {String} commandName - Command to call + * @property {Object} commandOptions - Command options * @property {String} label - Display name for hotkey * @property {String[]} keys - Keys to bind; Follows Mousetrap.js binding syntax */ @@ -18,7 +20,7 @@ export class HotkeysManager { if (!commandsManager) { log.warn( - 'HotkeysManager instantiated without a commandsManager. Hotkeys will be unable to find and run commands.' + '[hotkeys] HotkeysManager instantiated without a commandsManager. Hotkeys will be unable to find and run commands.' ); } @@ -141,28 +143,30 @@ export class HotkeysManager { * When a hotkey combination is triggered, the command name and active contexts * are used to locate the correct command to call. * - * @param {HotkeyDefinition} commandName + * @param {HotkeyDefinition} command * @param {String} extension * @returns {undefined} */ - registerHotkeys({ commandName, keys, label } = {}, extension) { + registerHotkeys({ commandName, commandOptions = {}, keys, label } = {}, extension) { if (!commandName) { - log.warn(`No command was defined for hotkey "${keys}"`); + log.warn(`[hotkeys] No command was defined for hotkey "${keys}"`); return; } - const previouslyRegisteredDefinition = this.hotkeyDefinitions[commandName]; + const commandHash = objectHash({ commandName, commandOptions }); + const options = Object.keys(commandOptions).length ? JSON.stringify(commandOptions) : 'no'; + const previouslyRegisteredDefinition = this.hotkeyDefinitions[commandHash]; if (previouslyRegisteredDefinition) { const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys; this._unbindHotkeys(commandName, previouslyRegisteredKeys); - log.info(`Unbinding ${commandName} from ${previouslyRegisteredKeys}`); + log.info(`[hotkeys] Unbinding ${commandName} with ${options} options from ${previouslyRegisteredKeys}`); } // Set definition & bind - this.hotkeyDefinitions[commandName] = { keys, label }; - this._bindHotkeys(commandName, keys); - log.info(`Binding ${commandName} to ${keys}`); + this.hotkeyDefinitions[commandHash] = { keys, label }; + this._bindHotkeys(commandName, commandOptions, keys); + log.info(`[hotkeys] Binding ${commandName} with ${options} options to ${keys}`); } /** @@ -191,7 +195,7 @@ export class HotkeysManager { * @param {string[]} keys - One or more key combinations that should trigger command * @returns {undefined} */ - _bindHotkeys(commandName, keys) { + _bindHotkeys(commandName, commandOptions = {}, keys) { const isKeyDefined = keys === '' || keys === undefined; if (isKeyDefined) { return; @@ -203,7 +207,7 @@ export class HotkeysManager { hotkeys.bind(combinedKeys, evt => { evt.preventDefault(); evt.stopPropagation(); - this._commandsManager.runCommand(commandName, { evt }); + this._commandsManager.runCommand(commandName, { evt, ...commandOptions }); }); } diff --git a/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js new file mode 100644 index 000000000..cbce9728b --- /dev/null +++ b/platform/core/src/defaults/hotkeyBindings.js @@ -0,0 +1,103 @@ +import windowLevelPresets from "./windowLevelPresets"; + +/* + * Supported Keys: https://craig.is/killing/mice + */ +export default [ + /** Global */ + { + commandName: 'incrementActiveViewport', + label: 'Next Viewport', + keys: ['right'], + }, + { + commandName: 'decrementActiveViewport', + label: 'Previous Viewport', + keys: ['left'], + }, + /** Viewport */ + { commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] }, + { commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] }, + { commandName: 'invertViewport', label: 'Invert', keys: ['i'] }, + { + commandName: 'flipViewportVertical', + label: 'Flip Horizontally', + keys: ['h'], + }, + { + commandName: 'flipViewportHorizontal', + label: 'Flip Vertically', + keys: ['v'], + }, + { commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] }, + { commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] }, + { commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] }, + { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, + { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, + { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, + { + commandName: 'previousViewportDisplaySet', + label: 'Previous Series', + keys: ['pagedown'], + }, + { + commandName: 'nextViewportDisplaySet', + label: 'Next Series', + keys: ['pageup'], + }, + /** Window level presets */ + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[1], + label: 'W/L Preset 1', + keys: ['1'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[2], + label: 'W/L Preset 2', + keys: ['2'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[3], + label: 'W/L Preset 3', + keys: ['3'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[4], + label: 'W/L Preset 4', + keys: ['4'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[5], + label: 'W/L Preset 5', + keys: ['5'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[6], + label: 'W/L Preset 6', + keys: ['6'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[7], + label: 'W/L Preset 7', + keys: ['7'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[8], + label: 'W/L Preset 8', + keys: ['8'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[9], + label: 'W/L Preset 9', + keys: ['9'], + }, +]; diff --git a/platform/core/src/defaults/index.js b/platform/core/src/defaults/index.js new file mode 100644 index 000000000..f8c065d7b --- /dev/null +++ b/platform/core/src/defaults/index.js @@ -0,0 +1,4 @@ +import hotkeyBindings from './hotkeyBindings'; +import windowLevelPresets from './windowLevelPresets'; +export { hotkeyBindings, windowLevelPresets }; +export default { hotkeyBindings, windowLevelPresets }; diff --git a/platform/core/src/defaults/windowLevelPresets.js b/platform/core/src/defaults/windowLevelPresets.js new file mode 100644 index 000000000..83a7321ba --- /dev/null +++ b/platform/core/src/defaults/windowLevelPresets.js @@ -0,0 +1,12 @@ +export default { + 1: { description: 'Soft tissue', window: '400', level: '40' }, + 2: { description: 'Lung', window: '1500', level: '-600' }, + 3: { description: 'Liver', window: '150', level: '90' }, + 4: { description: 'Bone', window: '2500', level: '480' }, + 5: { description: 'Brain', window: '80', level: '40' }, + 6: { description: 'Trest', window: '1', level: '1' }, + 7: { description: '', window: '', level: '' }, + 8: { description: '', window: '', level: '' }, + 9: { description: '', window: '', level: '' }, + 10: { description: '', window: '', level: '' }, +}; diff --git a/platform/core/src/index.js b/platform/core/src/index.js index acfdd3fbf..a16604cc0 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -20,7 +20,8 @@ import studies from './studies/'; import ui from './ui'; import user from './user.js'; import { ViewModelProvider, useViewModel } from './ViewModelContext'; -import utils, { hotkeys } from './utils/'; +import utils from './utils/'; +import defaults from './defaults'; import { UIDialogService, @@ -37,6 +38,11 @@ import { import IWebApiDataSource from './DataSources/IWebApiDataSource'; +const hotkeys = { + ...utils.hotkeys, + defaults: { hotkeyBindings: defaults.hotkeyBindings } +}; + const OHIF = { MODULE_TYPES, // @@ -45,6 +51,7 @@ const OHIF = { HotkeysManager, ServicesManager, // + defaults, utils, hotkeys, studies, @@ -88,6 +95,7 @@ export { HotkeysManager, ServicesManager, // + defaults, utils, hotkeys, studies, diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js index 47ee09488..d7aef6f69 100644 --- a/platform/core/src/index.test.js +++ b/platform/core/src/index.test.js @@ -15,6 +15,7 @@ describe('Top level exports', () => { 'UIDialogService', 'MeasurementService', // + 'defaults', 'utils', 'hotkeys', 'studies', diff --git a/platform/core/src/redux/reducers/preferences.js b/platform/core/src/redux/reducers/preferences.js index 2ec01c917..a9eca66e3 100644 --- a/platform/core/src/redux/reducers/preferences.js +++ b/platform/core/src/redux/reducers/preferences.js @@ -1,16 +1,7 @@ +import { windowLevelPresets } from '../../defaults'; + const defaultState = { - windowLevelData: { - 1: { description: 'Soft tissue', window: '550', level: '40' }, - 2: { description: 'Lung', window: '150', level: '-600' }, - 3: { description: 'Liver', window: '150', level: '90' }, - 4: { description: 'Bone', window: '2500', level: '480' }, - 5: { description: 'Brain', window: '80', level: '40' }, - 6: { description: 'Trest', window: '1', level: '1' }, - 7: { description: '', window: '', level: '' }, - 8: { description: '', window: '', level: '' }, - 9: { description: '', window: '', level: '' }, - 10: { description: '', window: '', level: '' }, - }, + windowLevelData: windowLevelPresets, generalPreferences: { // language: 'en-US' }, diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js index 8fb176959..7768905fa 100644 --- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js @@ -72,16 +72,16 @@ const BaseImplementation = { study = _model.studies[_model.studies.length - 1]; } - // TODO: Worth identifying why this is being called many times with series - // that are already "added"? - const didAddSeries = study.addSeries(instances); + study.addSeries(instances); - if (didAddSeries) { - this._broadcastEvent(EVENTS.INSTANCES_ADDED, { - StudyInstanceUID, - SeriesInstanceUID, - }); - } + // Broadcast an event even if we used cached data. + // 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, { + StudyInstanceUID, + SeriesInstanceUID, + }); }, addStudy(study) { const { StudyInstanceUID } = study; diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index 2cabbe54d..a9f7d34b4 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -27,6 +27,15 @@ export default class ToolBarService { return this.buttons; } + getActiveTools() { + return Object.keys(this.buttons).filter(key => { + const button = this.buttons[key]; + if (button && button.props && button.props.isActive) { + return button; + } + }); + } + setButtons(buttons) { this.buttons = buttons; this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); diff --git a/platform/core/src/services/UIViewportDialogService/index.js b/platform/core/src/services/UIViewportDialogService/index.js index 8fdb91437..12cc526c5 100644 --- a/platform/core/src/services/UIViewportDialogService/index.js +++ b/platform/core/src/services/UIViewportDialogService/index.js @@ -26,13 +26,21 @@ const serviceImplementation = { * * @param {ViewportDialogProps} props { content, contentProps, viewportIndex } */ -function _show({ viewportIndex, type, message, actions, onSubmit }) { +function _show({ + viewportIndex, + type, + message, + actions, + onSubmit, + onOutsideClick, +}) { return serviceImplementation._show({ viewportIndex, type, message, actions, onSubmit, + onOutsideClick, }); } diff --git a/platform/core/src/utils/formatDate.js b/platform/core/src/utils/formatDate.js new file mode 100644 index 000000000..a2a9a9bab --- /dev/null +++ b/platform/core/src/utils/formatDate.js @@ -0,0 +1,12 @@ +import moment from 'moment'; + +/** + * Format date + * + * @param {string} date Date to be formatted + * @param {string} format Desired date format + * @returns {string} Formatted date + */ +export default (date, format = 'DD-MMM-YYYY') => { + return moment(date).format(format); +}; diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index a69889262..fdf6974cc 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -15,6 +15,7 @@ import makeCancelable from './makeCancelable'; import hotkeys from './hotkeys'; import Queue from './Queue'; import isDicomUid from './isDicomUid'; +import formatDate from './formatDate'; import formatPN from './formatPN'; import resolveObjectPath from './resolveObjectPath'; import * as hierarchicalListUtils from './hierarchicalListUtils'; @@ -27,6 +28,7 @@ const utils = { addServers, sortBy, writeScript, + formatDate, formatPN, b64toBlob, StackManager, @@ -50,6 +52,7 @@ export { absoluteUrl, addServers, sortBy, + formatDate, writeScript, b64toBlob, StackManager, diff --git a/platform/ui/src/components/InputLabelWrapper/InputLabelWrapper.jsx b/platform/ui/src/components/InputLabelWrapper/InputLabelWrapper.jsx index f80dfccdb..e9b17b429 100644 --- a/platform/ui/src/components/InputLabelWrapper/InputLabelWrapper.jsx +++ b/platform/ui/src/components/InputLabelWrapper/InputLabelWrapper.jsx @@ -21,13 +21,21 @@ const InputLabelWrapper = ({ className, children, }) => { + const onClickHandler = e => { + if (!isSortable) { + return; + } + + onLabelClick(e); + }; + return (