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 (